Bash-编程高级教程-一-

83 阅读24分钟

Bash 编程高级教程(一)

原文:Pro Bash Programming

协议:CC BY-NC-SA 4.0

一、你好世界:你的第一个 Shell 程序

一个 shell 脚本是一个包含一个或多个您可以在命令行上输入的命令的文件。本章描述了如何创建这样的文件并使其可执行。它还涵盖了围绕 shell 脚本的一些其他问题,包括如何命名文件、将文件放在哪里以及如何运行它们。

我将从每种计算机语言中传统演示的第一个程序开始:一个打印“Hello,World!”在你的终端。这是一个简单的程序,但它足以演示许多重要的概念。代码本身是本章最简单的部分。命名文件并决定将它放在哪里并不是复杂的任务,但却很重要。

在本章的大部分时间里,您将在一个终端中工作。它可以是一个虚拟终端,一个终端窗口,甚至是一个哑终端。在您的终端中,shell 将立即执行您键入的任何命令(当然是在您按 Enter 键之后)。

你应该在你的主目录中,你可以在变量$ HOME 中找到:

echo "$HOME"

你可以用pwd命令或PWD变量找到当前目录:

pwd
echo "$PWD"

如果您不在您的主目录中,您可以通过键入cd并在 shell 提示符下按 Enter 键到达那里。

Image 注意如果你在 Mac 上尝试本书中的代码,请注意,当前版本的 Mac OS X,Yosemite,官方支持 Bash 版本 3.2.53(1)。Bash 的当前版本是 4.3,它修复了 Shellshock 漏洞。Bash 4.3 适用于大多数 Linux 发行版。一些代码/功能可能在 Mac OS X 系统上不可用,因为它是特定于 Bash 4.x 的

《守则》

代码无非是这样的:

echo Hello, World!

这个命令行有三个词:命令本身和两个参数。命令echo打印其参数,用一个空格分隔,并以换行符结束。

文件

在将代码转换成脚本之前,您需要做出两个决定:将文件命名为什么,以及将它放在哪里。该名称应该是唯一的(也就是说,它不应该与任何其他命令冲突),并且应该放在 shell 可以找到它的地方。

脚本的命名

初学者经常犯的错误是把一个测试脚本叫做test。要了解为什么这样不好,请在命令提示符下输入以下内容:

type test

命令告诉你对于任何给定的命令,shell 将执行什么(如果它是一个外部文件,还可以在哪里找到它)。在bashtype -a test会显示所有与名称test,匹配的命令:

$ type test
test is a shell builtin
$ type -a test
test is a shell builtin
test is /usr/bin/test

如你所见,名为test的命令已经存在;它用于测试文件类型和比较值。如果你调用你的脚本test,当你在 shell 提示符下键入test时,它将不会运行;将运行由type标识的第一个命令。(关于typetest,我将在后面的章节中详细讨论。)

通常,Unix 命令名尽可能短。它们通常是描述性词语的前两个辅音(例如,mv代表 m o v e 或ls代表 l i s t)或描述性短语的第一个字母(例如,ps代表pprocessstatus 或sed代表sstreameditor)

在这个练习中,调用脚本hw。很多 shell 程序员都会加一个后缀,比如*。* sh,以表示该程序是一个 shell 脚本。脚本不需要它,我只对正在开发的程序使用一个。我的后缀是-sh,当程序结束时,我删除它。shell 脚本成为另一个命令,不需要与任何其他类型的命令区分开来。

为脚本选择一个目录

当 shell 被赋予要执行的命令的名称时,它会在PATH变量中列出的目录中查找该名称。此变量包含以冒号分隔的目录列表,这些目录包含可执行命令。这是$PATH的典型值:

!"
/bin:/usr/bin:/usr/local/bin:/usr/games

如果你的程序不在PATH目录中,你必须给出一个路径名,无论是绝对的还是相对的,以便bash找到它。一个绝对路径名给出了文件系统根目录的位置,比如/home/chris/bin/hw;一个相对路径名是相对于当前工作目录(当前应该是你的主目录)给出的,如bin/hw

命令通常存储在名为bin的目录中,用户的个人程序存储在$HOME目录下的bin子目录中。要创建该目录,请使用以下命令:

mkdir bin

现在它已经存在,必须添加到PATH变量中:

PATH=$PATH:$HOME/bin

要将这一更改应用到您打开的每个 shell,请将它添加到一个文件中,当 shell 被调用时,它将。这将是.bash_profile.bashrc.profile,取决于bash是如何被调用的。这些文件仅用于交互式 shells,不用于脚本。

创建文件并运行脚本

通常您会使用文本编辑器来创建您的程序,但是对于这样一个简单的脚本,没有必要调用编辑器。您可以使用重定向从命令行创建该文件:

echo echo Hello, World! > bin/hw

大于号(>)告诉 shell 将命令的输出发送到指定的文件,而不是发送到终端。你将在第二章中了解更多关于重定向的内容。

现在,可以通过将该程序作为 shell 命令的参数调用来运行该程序:

bash bin/hw

那行得通,但并不完全令人满意。您希望能够键入hw,而不必在前面加上bash,并执行命令。为此,授予文件执行权限:

chmod +x bin/hw

现在,只需使用其名称就可以运行该命令:

!"
$ hw
Hello, World!

选择和使用文本编辑器

对许多人来说,电脑软件中最重要的一部分是文字处理器。虽然我用一个来写这本书(LibreOffice 作家 ),但我不经常用它。我最后一次使用文字处理器是在五年前,当时我写了这本书的第一版。另一方面,文本编辑器是不可或缺的工具。我用它来写电子邮件、新闻组文章、shell 脚本、PostScript 程序、网页等等。

文本编辑器对纯文本文件进行操作。它只存储您键入的字符;它没有添加任何隐藏的格式代码。如果我在文本编辑器中键入A并按回车键,然后保存它,文件将包含两个字符:一个和一个换行符。包含相同文本的文字处理文件要大几千倍。(带 ,文件包含 2526 字节;LibreOffice.org 文件包含 7579 个字节。)

您可以在任何文本编辑器中编写脚本,从基本的e3nano到全功能的emacsnedit。更好的文本编辑器允许你一次打开多个文件。例如,它们通过语法突出显示、自动缩进、自动完成、拼写检查、宏、搜索和替换以及撤消来简化代码编辑。最终,你选择哪个编辑器是个人喜好的问题。我使用 GNU emacs(见图 1-1 )。

9781484201220_Fig01-01.jpg

图 1-1 。GNU emacs文本编辑器中的 Shell 代码

Image 注意在 Windows 文本文件中,!“行以两个字符结束:一个回车符 (CR)和一个换行符 (LF)。在 Unix 系统上,比如 Linux,行以一个换行符结束。如果在 Windows 文本编辑器中编写程序,则必须用 Unix 行尾保存文件,或者在保存后删除回车。

建设一个更好的“你好,世界!”

在本章的前面,您使用重定向创建了一个脚本。至少可以说,那个剧本是极简主义的。所有的程序,甚至是一行程序,都需要文档。信息至少应该包括作者、日期和命令描述。在文本编辑器中打开文件bin/ hw ,使用注释添加清单 1-1 中的信息。

清单 1-1hw

#!/bin/bash
#: Title       : hw
#: Date        : 2008-11-26
#: Author      : "Chris F.A. Johnson" <shell@cfajohnson.com>
#: Version     : 1.0
#: Description : print Hello, World!
#: Options     : None

printf "%s\n" "Hello, World!" !"

注释在一个单词的开头以一个八叉符或者散列 开始,一直持续到行尾。Shell 会忽略它们。我经常在散列后添加一个字符来表示注释的类型。然后,我可以在文件中搜索我想要的类型,忽略其他注释。

第一行是一种特殊类型的注释,称为 shebanghash- bang 。它告诉系统使用哪个解释器来执行文件。字符#! 必须出现在第一行的最开头;换句话说,它们必须是文件的前两个字节才能被识别。

摘要

以下是您在本章中学到的命令、概念和变量。

命令

  • pwd:打印当前工作目录的名称
  • cd:改变 shell 的工作目录
  • echo:打印它的参数,用空格分开,用换行符结束
  • type:显示命令的信息
  • mkdir:新建一个目录
  • chmod:修改文件的权限
  • source: a.k.a. . (dot):在当前 shell 环境中执行脚本
  • printf:打印格式字符串指定的参数

概念

  • 脚本:这是一个包含 shell 要执行的命令的文件。
  • Word :单词是 shell 认为是一个单元的字符序列。
  • 输出重定向:您可以使用> FILENAME 将命令的输出发送到一个文件而不是终端。
  • 变量:这些是存储值的名字。
  • 注释:它们由一个未加引号的单词组成,以#开头。该行的所有剩余字符构成一个注释,将被忽略。
  • Shebang 或 hash-bang :这是一个散列和一个感叹号(#!),后跟应该执行文件的解释器的路径。
  • 解释器:这是一个读取文件并执行其中包含的语句的程序。它可能是一个 shell 或另一种语言解释器,如awkpython

变量

  • PWD包含 shell 当前工作目录的路径名。
  • HOME存储用户主目录的路径名。
  • PATH是一个用冒号分隔的目录列表,其中存储了命令文件。shell 在这些目录中搜索它需要执行的命令。

练习

  1. 编写一个脚本,在$HOME *中创建一个名为bpl的目录。*用两个子目录binscripts填充这个目录。
  2. 编写一个脚本来创建“Hello,World!”$HOME/bpl/bin/中的脚本hw;使其可执行;然后执行它。

二、输入、输出和吞吐

我们在第一章的中使用的两个命令是 shell 脚本程序的核心:echoprintf。两者都是bash内置的命令。两者都以标准输出流打印信息,但是printf要强大得多,而echo也有它的问题。

在这一章中,我将介绍echo及其问题、printf的功能、read命令以及标准的输入和输出流。然而,我将从参数和变量的概述开始。

参数和变量

引用bash手册(在命令提示符下键入man bash以阅读它),“参数是存储值的实体。”有三种类型的参数:位置参数、特殊参数和变量。位置参数是命令行上出现的参数,它们由一个数字引用。特殊参数由 shell 设置,用于存储关于其当前状态的信息,例如参数的数量和最后一个命令的退出代码。它们的名字是非字母数字字符(例如,*#_)。变量由一个名称标识。名称又能代表什么呢我将在“变量”部分解释这一点。

通过在参数名称、数字或字符前加上美元符号来访问参数值,如$3$#$HOME。这个名字可以用大括号括起来,如${10}${PWD}${USER}

位置参数

命令行上的参数可以作为编号参数供 shell 程序使用。第一个参数是$1,第二个是$2,以此类推。

您可以通过使用位置参数使第一章中的hw脚本更加灵活。清单 2-1 称之为hello

清单 2-1hello

#: Description: print Hello and the first command-line argument
printf "Hello, %s!\n" "$1"

现在,您可以调用带有参数的脚本来更改其输出:

$ hello John
Hello, John!
$ hello Susan
Hello, Susan!

Bourne shell 最多只能处理九个位置参数。如果一个脚本使用了$10,它将被解释为$1后跟一个零。为了能够运行旧的脚本,bash保持这种行为。要访问大于9的位置参数,数字必须用大括号括起来:${15}

脚本被传递给参数,这些参数可以通过它们的位置、00、1、2等等来访问。函数shiftN将位置参数移动N个位置,如果运行shift(N的默认值为1),那么2 等等来访问。函数`shift N`将位置参数移动`N`个位置,如果运行`shift`(`N`的默认值为 1),那么`0将被丢弃,1将变成1`将变成`0,2将变成2`将变成`1`,以此类推:它们都将被移动 1 个位置。shift 有一些非常聪明和简单的用法来遍历未知长度的参数列表。

Image 移位功能是破坏性的:即被丢弃的参数不见了,不能再取回。

特殊 *@#0$?_!- 参数

前两个特殊参数$*$@扩展为所有位置参数的组合值。$#扩展到位置参数的个数。$0包含当前运行脚本的路径,如果没有脚本正在执行,则包含 shell 本身的路径。

$$包含当前进程的进程标识号(PID),$?被设置为最后执行的命令的退出代码,$_被设置为该命令的最后一个参数。$!包含后台执行的最后一个命令的 PID,$-设置为当前有效的选项标志。

我将在编写脚本的过程中更详细地讨论这些参数。

变量

变量是用名称表示的参数;名称是一个只包含字母、数字或下划线,并以字母或下划线开头的单词。

可以按以下形式将值赋给变量:

name=VALUE

Image 注意 Bash 对间距非常讲究:注意=前面没有空格,后面也没有。如果有空格,该命令将不起作用。

许多变量是由 shell 自己设置的,包括您已经看到的三个:HOMEPWDPATH。除了两个小的例外,auto_resumehistchars,shell 设置的所有变量都是大写字母。

参数和选项

在命令后输入的单词是它的参数。这些单词由空格分隔(一个或多个空格或制表符)。如果空格被转义或引用,它不再分隔单词,而是成为单词的一部分。

以下命令行都有四个参数:

echo'2   3'   4 5
echo  -n  Now\ is  the  time
printf "%s %s\n" one two three

在第一行中,23之间的空格被引用了,因为它们被单引号包围了。在第二个例子中,now后面的空格用反斜杠进行转义,这是 shell 的转义字符。

在最后一行,空格用双引号引起来。

在第二个命令中,第一个参数是一个选项。传统上,Unix 命令的选项是前面带连字符的单个字母,有时后面跟一个参数。Linux 发行版中的 GNU 命令通常也接受长选项。这些单词前面有一个双连字符。例如,大多数 GNU 实用程序都有一个名为--version的选项来打印版本:

$ bash --version
GNU bash, version 4.3.11(1)-release (x86_64-unknown-linux-gnu)

Copyright (C) 2013 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

回声,以及为什么你应该避免它

当我开始编写 shell 脚本时,我很快就了解了 Unix 的两个主要分支:美国电话电报公司的 System V 和 BSD 。他们的不同之处之一是echo的行为。所有现代 shells 中的一个内部命令,echo将其参数打印到标准输出流中,参数之间有一个空格,后跟一个换行符:

$ echo The quick brown fox
The quick brown fox

根据 shell 的不同,默认换行符可以通过以下两种方式取消:

$ echo -n No newline
No newline$ echo "No newline\c"
No newline$

echo的 BSD 变体接受了选项 -n,抑制了换行符。T 的版本使用了一个转义序列\c来做同样的事情。还是反过来了?我很难记住哪个是哪个,因为尽管我使用的是 AT & T 系统(硬件操作系统),它的echo命令接受 AT & T 和 BSD 语法。

当然,这是历史。在这本书里,我们要讨论的是bash,那么这有什么关系呢?bash-e选项来激活转义序列,比如\c,但是默认情况下使用-n来防止换行被打印。(除了\c之外,echo -e识别的转义序列与下一节描述的相同)。

Image 提示如果您想让转义序列被识别,请在 echo 命令中添加–e。

问题是,bash有一个xpg_echo选项(XPG 代表 X/Open Portability Guide,Unix 系统的一个规范),使得echo的行为像另一个版本。这可以在 shell 中打开或关闭(在命令行或脚本中使用shopt -s xpg_echo),也可以在编译 shell 时打开。换句话说,即使在bash中,你也不能绝对确定你会得到什么样的行为。

如果您将echo的使用限制在不存在冲突的情况下,也就是说,您确定参数不是以-n开始并且不包含转义序列,那么您将会相当安全。对于其他一切(或者如果你不确定),使用printf

printf :格式化和打印数据

shell 命令printf源自 C 编程语言的同名函数,目的相似,但在一些细节上有所不同。像 C 函数一样,它使用一个格式字符串来指示如何表示其余的参数:

printf FORMAT ARG ...

FORMAT字符串可以包含普通字符、转义序列和格式说明符。普通字符按标准输出原样打印。转义序列被转换成它们所代表的字符。格式说明符被替换为命令行中的参数。

转义序列

转义序列是以反斜杠开头的单个字母:

  • \a::警报(铃声)
  • \b:退格
  • \e:转义字符
  • \f:表格进给
  • \n:换行符
  • \r:回车
  • \t:水平标签
  • \v:垂直制表符
  • \\:反斜杠
  • \nnn:由一至三个八进制数字指定的字符
  • \xHH:由一个或两个十六进制数字指定的字符

必须用引号或另一个反斜杠来保护反斜杠不受 shell 的影响:

$  printf "Q\t\141\n\x42\n"
Q       a
B

格式规范

格式说明符是以百分号开头的字母。可选的修饰语可以放在两个字符之间。说明符被相应的参数替换。当参数多于说明符时,格式字符串将被重用,直到所有的参数都用完为止。最常用的说明符有%s``%d``%f%x

%s说明符打印参数中的文字字符:

$ printf "%s\n" Print arguments on "separate lines"
Print
arguments
on
separate lines

除了参数中的转义序列被转换之外,%b%s相似:

$ printf "%b\n" "Hello\nworld" "12\tword"
Hello
world
12      word

整数打印有%d 。该整数可以指定为十进制、八进制(使用前导 0)或十六进制(在十六进制数前面加上0x)数。如果该数字不是有效的整数,printf会打印一条错误消息:

$ printf "%d\n" 23 45 56.78 0xff 011
23
45
bash: printf: 56.78: invalid number
0
255
9

对于小数或浮点数,使用%f 。默认情况下,它们将以六位小数打印:

$ printf "%f\n" 12.34 23 56.789 1.2345678
12.340000
23.000000
56.789000
1.234568

浮点数可以使用%e 以指数(也称为科学符号)表示:

$ printf "%e\n" 12.34 23 56.789 123.45678
1.234000e+01
2.300000e+01
5.678900e+01
1.234568e+02

整数可以用十六进制打印,小写字母用%x表示,大写字母用%X表示。例如,当指定网页的颜色时,它们是用十六进制表示法指定的。我从 X Window 系统包含的rgb.txt文件中知道,皇家蓝的红绿蓝值分别是 65、105 和 225。要将它们转换为网页的样式规则,请使用:

$ printf "color: #%02x%02x%02x;\n" 65 105 225
color: #4169e1;

宽度规格

您可以通过在百分号后加上宽度规格来修改格式。参数将在该宽度的字段中右对齐打印,如果数字为负数,则左对齐打印。这里我们有宽度为 8 个字符的第一个字段;这些字将被打印在右边。然后是一个 15 个字符宽的字段,它将左对齐打印:

$ printf "%8s %-15s:\n" first second third fourth fifth sixth
   first second         :
   third fourth         :
   fifth sixth          :

如果宽度规格以 0 开头,则数字以前导零填充,以填充宽度:

$ printf "%04d\n" 12 23 56 123 255
0012
0023
0056
0123
0255

带小数的宽度说明符指定浮点数的精度或字符串的最大宽度:

$  printf "%12.4s %9.2f\n" John 2 Jackson 4.579 Walter 2.9
        John      2.00
        Jack      4.58
        Walt      2.90

中显示的脚本。清单 2-2 使用printf输出一个简单的销售报告。

清单 2-2 。报告

#!/bin/bash
#: Description : print formatted sales report

## Build a long string of equals signs
divider=====================================
divider=$divider$divider

## Format strings for printf
header="\n %-10s %11s %8s %10s\n"
format=" %-10s %11.2f %8d %10.2f\n"

## Width of divider
totalwidth=44

## Print categories
printf "$header" ITEM  "PER UNIT" NUM TOTAL

## Print divider to match width of report
printf "%$totalwidth.${totalwidth}s\n" "$divider"

## Print lines of report
printf "$format" \
    Chair 79.95 4 319.8 \
   Table  209.99 1 209.99 \
   Armchair 315.49 2 630.98

生成的报告如下所示:

 ITEM          PER UNIT      NUM      TOTAL
============================================
 Chair            79.95        4     319.80
 Table           209.99        1     209.99
 Armchair        315.49        2     630.98

注意在第二个totalwidth变量名:${totalwidth}周围使用了大括号。在第一个实例中,名称后面跟一个句点,句点不能是变量名的一部分。在第二个中,它后面跟有字母s,可能是,所以totalwidth名称必须用大括号与它分开。

打印到变量

在 3.1 版本中,bash增加了一个-v选项,将输出存储在一个变量中,而不是打印到标准输出:

$ printf -v num4 "%04d" 4
$ printf "%s\n" "$num4"
0004

行延续

report脚本的结尾,使用行继续符,最后四行作为一行读取。行尾的反斜杠告诉 shell 忽略换行符,有效地将下一行连接到当前行。

标准输入/输出流和重定向

在 Unix 中(Linux 是其中的一个变种),一切都是字节流。这些流可以作为文件访问,但是有三个流很少通过文件名访问。这些是附加到每个命令的输入/输出(I/O)流:标准输入、标准输出和标准错误。默认情况下,这些流连接到您的终端。

当一个命令读取一个字符或一行时,它从标准输入流中读取,标准输入流就是键盘。当它打印信息时,它被发送到标准输出,你的显示器。第三个流,标准误差,也连接到您的监视器;顾名思义,它用于错误消息。这些流用数字来指代,称为文件 描述符 (FDs )。它们分别是 0、1 和 2。流名也常常缩写为 stdinstdoutstderr

I/O 流可以重定向到(或来自)一个文件或一个管道。

重定向 : >、>、??、<

在第一章中,您使用>重定向操作符将标准输出重定向到一个文件。

使用>重定向时,如果文件不存在,则创建该文件。如果它确实存在,在向它发送任何内容之前,该文件会被截断为零长度。您可以通过将空字符串(即空字符串)重定向到文件来创建空文件:

printf "" > FILENAME

或者简单地用这个:

> FILENAME

重定向是在执行该行上的任何命令之前执行的。如果您重定向到您正在读取的同一个文件,该文件将被截断,并且该命令将没有可读取的内容。

>>操作符不会截断目标文件;它附加到它上面。通过执行以下操作,您可以在第一章的hw命令中添加一行:

echo exit 0 >> bin/hw

重定向标准输出不会重定向标准错误。错误信息仍会显示在您的显示器上。要将错误消息发送到一个文件,换句话说,要重定向 FD2,重定向操作符前面要有 FD。

标准输出和标准错误都可以重定向到同一行。下一个命令向FILE发送标准输出,向ERRORFILE发送标准误差:

$ printf '%s\n%v\n' OK? Oops! > FILE 2> ERRORFILE
$ cat ERRORFILE
bash4: printf: `v': invalid format character

在这种情况下,错误消息将被保存到一个特殊的文件/dev/null。有时称为比特桶 ,任何写入其中的东西都被丢弃。

printf '%s\n%v\n' OK? Oops! 2>/dev/null

可以使用>&N将输出重定向到另一个 I/O 流,而不是将输出发送到文件,其中N是文件描述符的编号。该命令将标准输出和标准误差发送到FILE:

printf '%s\n%v\n' OK? Oops! > FILE 2>&1

在这里,顺序很重要。标准输出被发送到FILE,然后标准错误被重定向到标准输出要去的地方。如果顺序反过来,效果就不一样了。重定向将标准错误发送到标准输出当前所在的位置,然后更改标准输出所在的位置。标准误差仍然指向标准输出最初指向的地方:

printf '%s\n%v\n' OK? Oops! 2>&1 > FILE

bash还有一个非标准语法,用于将标准输出和标准错误重定向到同一个位置:

&> FILE

要将标准输出和标准误差附加到FILE,使用以下命令:

&>> FILE

从标准输入读取的命令可以将其输入从文件重定向:

tr, H wY < bin/hw

您可以使用exec命令为脚本的其余部分重定向 I/O 流,或者直到它再次被更改。

exec 1>tempfile
exec 0<datafile
exec 2>errorrfile

所有标准输出现在都将转到文件tempfile ,输入将从datafile 中读取,错误信息将转到errorfile ,而无需为每个命令指定。

阅读输入

read命令是一个从标准输入读取的内置命令。默认情况下,它会一直读取,直到收到一个换行符。输入存储在一个或多个作为参数给出的变量中:

read var

如果给定了多个变量,则第一个单词(直到第一个空格或制表符的输入)被分配给第一个变量,第二个单词被分配给第二个变量,依此类推,剩余的单词被分配给最后一个变量:

$ read a b c d
January February March April May June July August
$ echo $a
January
$ echo $b
February
$ echo $c
March
$ echo $d
April May June July August

readbash版本有几个选项。POSIX 标准只认可-r选项。它告诉 shell 逐字解释转义序列。

默认情况下,read从输入中去掉反斜杠,后面的字符按字面理解。这种默认行为的主要效果是允许行的延续。使用-r选项,一个反斜杠后跟一个换行符被读为一个字面反斜杠和输入的结束。

我将在第十五章中讨论其他选项。

像任何其他读取标准输入的命令一样,read可以通过重定向从文件中获取输入。例如,要从FILENAME中读取第一行,请使用以下命令:

read var < FILENAME

管道

管道将一个命令的标准输出直接连接到另一个命令的标准输入。管道符号(|)用于命令之间:

$ printf "%s\n" "$RANDOM" "$RANDOM" "$RANDOM" "$RANDOM"tee FILENAME
618
11267
5890
8930

tee命令读取标准输入,并将其传递给一个或多个文件以及标准输出。$RANDOM 是一个bash变量,每次被引用时返回 0 到 32,767 之间的不同整数。

$ cat FILENAME
618
11267
5890
8930

命令替换

使用命令替换可以将命令的输出存储在变量中。有两种形式可以做到这一点。第一个源自《谍影重重》,使用了反斜杠:

date=`date`

较新的(也是推荐的)语法如下:

date=$( date )

命令替换通常应该保留给外部命令。当与内置命令一起使用时,它非常慢。这就是为什么printf中增加了-v选项。

摘要

以下是您在本章中学到的命令和概念。

命令

  • cat:将一个或多个文件的内容打印到标准输出
  • tee:将标准输入复制到标准输出以及一个或多个文件中
  • read:从标准输入中读取一行的内置 shell 命令
  • date:打印当前日期和时间

概念

  • 标准 I/O 流:这些是字节流,命令从这些字节流中读取,输出发送到这些字节流。
  • 自变量:这些是跟随命令的字;参数可能包括选项以及其他信息,如文件名。
  • 参数:这些是存储值的实体;这三种类型是位置参数、特殊参数和变量。
  • 管道:管道是由|分隔的一个或多个命令的序列;管道符号之前的命令的标准输出被提供给其后的命令的标准输入。
  • 行继续符:这是一行末尾的反斜杠,用于移除新行并将该行与下一行合并。
  • 命令替换:这意味着将命令的输出存储在变量中或命令行上。

练习

  1. 这个命令有什么问题?

    tr A Z < $HOME/temp > $HOME/temp
    
  2. 使用$RANDOM编写一个脚本,将以下输出写入文件和变量。以下数字仅用于显示格式;你的脚本应该产生不同的数字:

     1988.2365
    13798.14178
    10081.134
     3816.15098
    

三、循环和分支

任何编程语言的核心都是迭代和条件执行。迭代是一段代码的重复,直到条件发生变化。条件执行是基于一个条件在两个或多个动作(其中一个可能是什么都不做)之间做出选择。

在 shell 中,有三种类型的循环(whileuntilfor)和三种类型的条件执行(ifcase,以及条件运算符&&和||,分别表示ANDOR)。除了forcase之外,命令的退出状态控制行为。

退出状态

您可以直接使用 shell 关键字whileuntilif或者使用控制操作符&&||来测试命令是否成功。退出代码存储在特殊参数$?中。

如果命令执行成功(或真),则$?的值为零。如果命令由于某种原因失败,$?将包含一个介于 1 和 255 之间的正整数,包括 1 和 255。失败的命令通常返回 1。零和非零退出代码也分别称为

命令可能会因为语法错误而失败:

$ printf "%v\n"
bash: printf: `v': invalid format character
$ echo $?
1

或者,失败可能是命令无法完成其任务的结果:

$ mkdir /qwerty
bash: mkdir: cannot create directory `/qwerty': Permission denied
$ echo $?
1

测试表达式

表达式由test命令或两个非标准 shell 保留字之一[[((判断为真或假。test命令比较字符串、整数和各种文件属性;((测试算术表达式,而[[ ... ]]做的和test一样,增加了比较正则表达式的特性。

测试,又名[ … ]

命令评估多种表达式,从文件属性到整数到字符串。它是一个内置命令,因此它的参数会像其他命令一样进行扩展。(详见第五章。)另一个版本(``)在末尾需要一个右括号。

![Image 注意正如前面在第二章中提到的,bash 对间距很讲究,要求括号周围有空格。这也很重要,因为没有空格的命令[ test[test与预期的不同。

文件测试

几个操作符测试一个文件的状态。可以用-e(或者非标准的-a)来测试文件的存在性。文件类型可用-f检查常规文件,用-d检查目录,用-h-L检查符号链接。其他运算符测试特殊类型的文件以及设置了哪些权限位。

以下是一些例子:

test -f /etc/fstab    ## true if a regular file
test -h /etc/rc.local ## true if a symbolic link
[ -x "$HOME/bin/hw" ]   ## true if you can execute the file
[[ -s $HOME/bin/hw ]]  ## true if the file exists and is not empty

整数测试

整数之间的比较使用-eq-ne-gt-lt-ge-le运算符。

-eq测试整数的相等性:

$ test 1 -eq 1
$ echo $?
0
$ [ 2 -eq 1 ]
$ echo $?
1

不等式测试用-ne:

$ [ 2 -ne 1 ]
$ echo $?
0

其余的运算符测试大于、小于、大于或等于以及小于或等于。

字符串测试

字符串是零个或多个字符的串联,可以包含除NUL (ASCII 0)之外的任何字符。可以测试它们是否相等,是否为非空字符串或空字符串,以及在bash中是否按字母顺序排序。=操作符测试相等性,换句话说,它们是否相同;!=不平等测试。bash也接受==的等式,但是没有理由使用这个非标准操作符。

以下是一些例子:

test "$a""$b"
[ "$q" != "$b" ]

如果-z-n运算符的参数为空或非空,则它们会成功返回:

$ [ -z "" ]
$ echo $?
0
$ test -n ""
$ echo $?
1

大于和小于符号在bash中用于比较字符串的词汇位置,必须进行转义以防止它们被解释为重定向操作符:

$ str1=abc
$ str2=def
$ test "$str1" \< "$str2"
$ echo $?
0
$ test "$str1" \> "$str2"
$ echo $?
1

前面的测试可以用-a(逻辑AND)和-o(逻辑OR)操作符组合成一个对test的调用:

test -f /path/to/file -a $test -eq 1
test -x bin/file -o $test -gt 1

test通常与if或条件运算符&&||结合使用。

[[ … ]]:计算表达式

test一样,[[ ... ]]评估一个表达式。与test不同,它不是一个内置命令。它是 shell 语法的一部分,不像内置命令那样接受解析。参数被扩展,但是在[[]]之间的单词不进行分词和文件名扩展。

它支持所有与test相同的操作符,并有一些增强和补充。然而,它是非标准的,所以当test可以执行相同的功能时,最好不要使用它。

测试增强

=!=右侧的参数未被引用时,它被视为一个模式,并复制case命令的功能。

在 shell 中没有复制的[[ ... ]]的特性是使用=~操作符匹配扩展正则表达式的能力:

$ string=whatever
$ [[ $string =~ h[aeiou] ]]
$ echo $?
0
$ [[ $string =~ h[sdfghjkl] ]]
$ echo $?
1

正则表达式在第八章中有解释。

((…)):计算算术表达式

一个非标准特性,如果算术表达式的值为零,则(( arithmetic expression ))返回false,否则返回true。可移植的等价物使用test和 POSIX 语法进行 shell 运算:

test $(( a - 2 )) -ne 0
[ $a != 0 ]

但是因为(( expression ))是 shell 语法而不是内置命令,表达式的解析方式与命令的参数不同。这意味着,例如,大于符号(>)的或小于符号(<)的不会被解释为重定向运算符:

if (( total > max )); then : ...; fi

测试裸变量是否为零,如果变量不为零,则成功退出:

((verbose)) && command ## execute command if verbose != 0

非数字值相当于 0:

$ y=yes
$ ((y)) && echo $y || echo n
$ nLists

一个列表是一个或多个命令的序列,由分号、“与”符号、控制操作符或换行符分隔。列表可以用作whileuntil循环中的条件、 if 语句或任何循环的主体。列表的退出代码是列表中最后一个命令的退出代码。

条件执行

条件构造使脚本能够决定是执行一个代码块,还是选择执行两个或多个代码块中的哪一个。

如果

基本的 if 命令评估一个或多个命令的列表,如果<condition list>的执行成功,则执行列表:

if <condition list>
then
   <list>
fi

通常,<condition list>是一个单独的命令,通常是test或者它的同义词,,或者,在bash中,[[。在清单 3-1 的[中,test-z操作数检查是否输入了一个名字。

清单 3-1 。读取并检查输入

read name
if [[ -z $name ]]
then
   echo "No name entered" >&2
   exit 1  ## Set a failed return code
fi

使用else 关键字,如果<condition list>失败,可以执行一组不同的命令,如清单 3-2 所示。请注意,在数值表达式中,变量不需要前导$。

清单 3-2 。提示输入一个数字,并检查它是否不大于 10

printf "Enter a number not greater than 10: "
read number
if (( number > 10 ))
then
    printf "%d is too big\n" "$number" >&2
    exit 1
else
    printf "You entered %d\n" "$number"
fi

可以给出不止一个条件,使用elif关键字,这样如果第一个测试失败,就尝试第二个,如清单 3-3 所示。

清单 3-3 。提示输入一个数字,并检查它是否在给定的范围内

printf "Enter a number between 10 and 20 inclusive: "
read number
if (( number < 10 ))
then
    printf "%d is too low\n" "$number" >&2
    exit 1
elif (( number > 20 ))
then
    printf "%d is too high\n" "$number" >&2
    exit 1
else
    printf "You entered %d\n" "$number"
fi

Image 注意在实际使用中,在比较数值之前,会检查前面例子中输入的数字是否有无效字符。“案例”一节给出了实现这一点的代码。

通常使用&&||<condition list>中给出一个以上的测试。

条件运算符、&&和||

包含 ANDOR条件操作符的列表从左到右计算。如果前一条命令成功,则执行AND操作符 ( &&)之后的命令。如果前面的命令失败,则执行OR操作符 ( ||)之后的部分。

例如,要检查一个目录并cd进入它,如果它存在,使用这个:

test -d "$directory" && cd "$directory"

如果cd失败,要更改目录并出错退出,请使用以下命令:

cd "$HOME/bin" || exit 1

下一个命令试图创建一个目录并cd到它。如果mkdircd失败,它将出错退出:

mkdir "$HOME/bin" && cd "$HOME/bin" || exit 1

条件运算符通常与if一起使用。在本例中,如果两个测试都成功,则执行echo命令:


if [ -d "$dir" ] && cd "$dir"
then
    echo "$PWD"
fi

情况

一个case语句将一个单词(通常是一个变量)与一个或多个模式进行比较,并执行与该模式相关的命令。这些模式是使用通配符(*?)以及字符列表和范围([...])的路径名扩展模式。语法如下:

case WORD in
  PATTERN) COMMANDS ;;
  PATTERN) COMMANDS ;; ## optional
esac

case的一个常见用途是确定一个字符串是否包含在另一个字符串中。它比使用grep要快得多,后者创建了一个新的进程。这个简短的脚本通常会被实现为一个 shell 函数(参见第六章),这样它将在不创建新进程的情况下被执行,如清单 3-4 所示。

清单 3-4 。一个字符串包含另一个字符串吗?

case $1 in
    *"$2"*) true ;;
    *) false ;;
esac

命令truefalse只能分别成功或失败。

另一个常见任务是检查字符串是否是有效数字。同样,清单 3-5 通常会被实现为一个函数。

清单 3-5 。这是有效的正整数吗?

case $1 in
    *[!0-9]*) false;;
    *) true ;;
esac

许多脚本在命令行上需要一个或多个参数。为了检查是否有正确的数字,常用case:

case $# in
    3) ;; ## We need 3 args, so do nothing
    *) printf "%s\n" "Please provide three names" >&2
       exit 1
       ;;
esac

当一个命令或一系列命令需要重复时,它被放入一个循环中。shell 提供了三种类型的循环:whileuntilfor。前两个执行,直到条件为真或假;第三个循环遍历一个值列表。

正在…

while循环 的条件是一个或多个命令的列表,条件为真时要执行的命令放在关键字dodone之间:

while <list>
do
  <list>
done

通过在每次执行循环时增加一个变量,命令可以运行特定的次数:

n=1
while$n -le 10 ]
do
  echo "$n"
  n=$(( $n1 ))
done

true命令可用于创建一个无限循环:

while true ## ':' can be used in place of true
do
  read x
done

一个while循环可用于从文件中逐行读取:

while IFS= read -r line
do
  : do something with "$line"
done < FILENAME?

直到

until很少使用,循环只要条件失败。与while相反:

n=1
until [ $n -gt 10 ]
do
  echo "$n"
  n=$(( $n1 ))
done

for循环 的顶端,一个变量被赋予一个来自单词列表的值。在每次迭代中,分配列表中的下一个单词:

for var in Canada USA Mexico
do
  printf "%s\n" "$var"
done

bash也有一种非标准形式,类似于 C 编程语言中的形式。第一个表达式在 for 循环开始时计算,第二个是测试条件,第三个在每次迭代结束时计算:

for (( n=1; n<=10; ++n ))
do
  echo "$n"
done

破裂

使用break命令可以在任何点退出循环:

while :
do
  read x
  [ -z "$x" ] && break
done

使用数值参数,break可以退出多个嵌套循环:

forin a b c d e
do
  while true
  do
    if$RANDOM -gt 20000 ]
    then
      printf .
      break## break out of both while and for loops
    elif$RANDOM -lt 10000 ]
    then
      printf '"'
      break ## break out of the while loop
    fi
  done
done
echo

继续

在循环内部, continue命令通过传递任何剩余命令,立即开始循环的新迭代:

forin {1..9} ## See Brace expansion in Chapter 4
do
  x=$RANDOM
  [ $x -le 20000 ] && continue
  echo "n=$n x=$x"
done

摘要

循环和分支是计算机程序的主要组成部分。在本章中,您学习了用于这些任务的命令和运算符。

命令

  • test:对表达式求值并返回成功或失败
  • if:如果命令列表成功,则执行一组命令,如果不成功,则可选地执行另一组命令
  • case:将单词与一个或多个模式进行匹配,并执行与第一个匹配模式相关的命令
  • while:当一列命令执行成功时,重复执行一组命令
  • until:重复执行一组命令,直到一列命令执行成功
  • for:对列表中的每个单词重复执行一组命令
  • break:退出循环
  • continue:立即开始下一次循环迭代

概念

  • 退出状态:命令的成功或失败,在特殊参数$?中存储为 0 或正整数
  • 列表:由;&&&||或换行符分隔的一个或多个命令的序列

练习

  1. 编写一个脚本,要求用户输入一个介于 20 和 30 之间的数字。如果用户输入了无效的数字或非数字,请再次询问。重复操作,直到输入满意的数字。
  2. 编写一个脚本,提示用户输入文件名。重复操作,直到用户输入一个存在的文件。

四、命令行解析和扩展

作为一种编程语言,shell 的优势之一是它对命令行参数的解析,以及它对命令行中的单词执行的各种扩展。当使用参数调用命令时,shell 在调用命令之前会做几件事。

为了帮助可视化所发生的事情,清单 4-1 中的简短脚本ba将显示 shell 在处理完所有参数后传递给它的内容。它的每个参数都打印在单独的一行上,前面是$pre的值,后面是$post的值。

清单 4-1ba;显示命令行参数

pre=:
post=:
printf "$pre%s$post\n" "$@"

注意:用清单 4-1 中的文本创建一个名为 sa 的脚本。这是在本章的代码示例中使用的。

特殊参数$@扩展为所有命令行参数的列表,但是结果会根据它是否被引用而不同。当被引用时,它扩展为位置参数"$1""$2""$3""$4"等等,包含空格的参数将被保留。如果$@未加引号,则只要有空白,就会发生拆分。

当一行被执行时,无论是在命令提示符下还是在脚本中,只要有未加引号的空格,shell 就会将该行拆分成单词。然后bash检查生成的单词,根据需要对它们进行多达八种扩展。扩展的结果作为参数传递给命令。本章考察了整个过程,从基于未加引号的空格的单词的初始解析,到按执行顺序的每个扩展:

  1. 支撑膨胀
  2. 波状符号展开
  3. 参数和变量扩展
  4. 算术扩展
  5. 命令替换
  6. 单词拆分
  7. 路径名扩展
  8. 过程替代

本章以一个 shell 程序结束,该程序演示了如何在命令行上使用内置命令getopts解析选项(以连字符开头的参数)。

引用

shell 对命令行的初始解析使用不带引号的空格,即空格、制表符和换行符来分隔单词。单引号或双引号之间的空格或转义字符(\)前面的空格被视为周围单词的一部分(如果有的话)。分隔引号从参数中去除。

下面的代码有五个参数。第一个是单词this,前面有一个空格(反斜杠去掉了它的特殊含义)。第二个参数是'is a';整个参数用双引号括起来,再次删除了空格的特殊含义。短语demonstration of用单引号括起来。接下来是一个单一的,逃逸的空间。最后,字符串quotes and escapes通过转义空格连接在一起。

$ sa \ this "is a" 'demonstration of' \  quotes\ and\ escapes
: this:
:is a:
:demonstration of:
: :
:quotes and escapes:

引号可以嵌入单词中。在双引号里面,单引号并不特殊,但是双引号必须转义。在单引号内,双引号并不特殊。

$ sa "a double-quoted single quote, '" "a double-quoted double quote, \""
:a double-quoted single quote, ':
:a double-quoted double quote, ":
$ sa 'a single-quoted double quotation mark, "'
:a single-quoted double quotation mark, ":

单引号内的所有字符都按字面理解。单引号单词即使经过转义也不能包含单引号;引号将被视为关闭前一个,另一个单引号打开一个新的引用部分。中间没有任何空格的连续引用单词被视为单个参数:

$ sa "First argument "'still the first argument'
:First argument still the first argument:

bash中,如果被转义,单引号可以包含在$'string'形式的单词中。此外,第二章对printf的描述中列出的转义序列被替换为它们所代表的字符:


$ echo $'\'line1\'\n\'line2\''
'line1'
'line2'

引用的参数可以包含文字换行符:

$ sa "Argument containing 
`> a newline"`
`:Argument containing`
`a newline:`

![Image](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8e90042a0f8d460a95ac3797438eef3a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771586970&x-signature=50dn51v5jMT4ir4FH8vCthccT3g%3D) **注意**![Image](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f7e0c7d9ef7e4c44812941a8820a76c5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771586970&x-signature=HQ9JDwBHUyULAKCKkmx36z6eo0c%3D)是回车键,不是要在终端上键入的东西。因为 shell 确定命令不完整,所以它会显示一个>`提示,让您完成命令。

支撑膨胀

执行的第一个扩展(大括号扩展)是非标准的(也就是说,它不包含在 POSIX 规范中)。它对包含逗号分隔列表或序列的无引号大括号进行操作。每个元素都成为一个单独的参数。

$ sa {one,two,three}
:one:
:two:
:three:
$ sa {1..3} ## added in bash3.0
:1:
:2:
:3:
$ sa {a..c}
:a:
:b:
:c:

大括号表达式之前或之后的字符串将包含在每个扩展参数中:

$ sa pre{d,l}ate
:predate:
:prelate:

大括号可以嵌套:

$ sa {{1..3},{a..c}}
:1:
:2:
:3:
:a:
:b:
:c:

同一个单词中的多个大括号被递归展开。扩展第一个大括号表达式,然后为下一个大括号表达式处理每个结果单词。使用单词{1..3}{a..c},扩展了第一个术语,给出以下内容:

1{a..c} 2{a..c} 3{a..c}

然后对这些单词中的每一个进行扩展,得到最终结果:

$ sa {1..3}{a..c}
:1a:
:1b:
:1c:
:2a:
:2b:
:2c:
:3a:
:3b:
:3c:

在第 4 版的bash中,更多的功能被添加到了大括号扩展中。数字序列可以用零填充,并且可以指定序列中的增量:

$ sa {01..13..3}
:01:
:04:
:07:
:10:
:13:

增量也可用于字母序列:

$ sa {a..h..3}
:a:
:d:
:g:

波状符号展开

不带引号的波浪号扩展到用户的主目录:

$ sa ~
:/home/chris:

后跟登录名,它会扩展到该用户的主目录:

$ sa ~root ~chris
:/root:
:/home/chris:

在命令行或变量赋值中引用时,波浪号不会展开:

$ sa "~" "~root"
:~:
:~root:
$ dir=~chris
$ dir2="~chris"
$ sa "$dir" "$dir2"
:/home/chris:
:~chris:

如果波浪号后面的名称不是有效的登录名,则不执行扩展:

$ sa ~qwerty
:~qwerty:

参数和变量扩展

参数扩展用变量的内容替换变量;它由一个美元符号($)引入。其后是要展开的符号或名称:

$ var=whatever
$ sa "$var"
:whatever:

参数可以用大括号括起来:

$ var=qwerty
$ sa "${var}"
:qwerty:

在大多数情况下,大括号是可选的。当引用大于九的位置参数时,或者当变量名后紧跟可能是名称一部分的字符时,它们是必需的:

$ first=Jane
$ last=Johnson
$ sa "$first_$last" "${first}_$last"
:Johnson:
:Jane_Johnson:

因为first_是一个有效的变量名,所以 shell 试图扩展它而不是first;添加大括号可以消除歧义。

大括号也用在扩展中,不仅仅是返回参数值。这些经常是神秘的扩展(例如,${var##*/}${var//x/y})给 shell 增加了大量的功能,在下一章中将详细讨论。

没有用双引号括起来的参数扩展要经过分词路径名扩展

算术扩展

当 shell 遇到$(( expression ))时,对expression求值,并将结果放在命令行上;expression是一个算术表达式。除了加、减、乘、除这四种基本的算术运算外,它使用最多的运算符是%(模,除法后的余数)。

$ sa "$(( 1 + 12 ))" "$(( 12 * 13 ))" "$(( 16 / 4 ))" "$(( 6 - 9 ))"
:13:
:156:
:4:
:-3:

算术运算符(参见表 4-1 和 4-2 )的优先级与您在学校学到的相同(基本上,乘法和除法在加法和减法之前执行),它们可以用括号分组以改变求值顺序:

$ sa "$(( 3 + 4 * 5 ))" "$(( (3 + 4) * 5 ))"
:23:
:35:

表 4-1 。算术运算符

|

操作员

|

描述

| | --- | --- | | -  + | 一元减号和加号 | | !  ~ | 逻辑与按位求反 | | *  /  % | 乘法、除法、余数 | | + - | 加法、减法 | | <<  >> | 向左和向右按位移位 | | <=  >=  < > | 比较 | | == != | 平等和不平等 | | & | 按位AND | | ^ | 按位异或OR | | &#124; | 按位OR | | && | 逻辑AND | | &#124;&#124; | 逻辑OR | | =  *=  /=  %=  +=  -=  <<=  >>=  &=  ^=  &#124;= | 分配 |

表 4-2 。bash扩展

|

操作员

|

描述

| | --- | --- | | ** | 指数运算 | | id++  id-- | 可变后增量和后减量 | | ++id  –-id | 可变预增量和预减量 | | expr ? expr1 : expr2 | 条件运算符 | | expr1 , expr2 | 逗号 |

模运算符%返回除法运算后的余数:

$ sa "$(( 13 % 5 ))"
:3:

将秒(这是 Unix 系统存储时间的方式)转换成日、小时、分钟和秒涉及除法和模运算符,如清单 4-2 所示。

清单 4-2secs2dhms,将秒(在参数$1中)转换为日、小时、分钟和秒

secs_in_day=86400
secs_in_hour=3600
mins_in_hour=60
secs_in_min=60

days=$(( $1$secs_in_day ))
secs=$(( $1$secs_in_day ))
printf "%d:%02d:%02d:%02d\n" "$days" "$(($secs / $secs_in_hour))" \
        "$((($secs / $mins_in_hour) %$mins_in_hour))" "$(($secs % $secs_in_min))"

如果没有用双引号括起来,算术展开的结果以分词为准。

命令替换

命令替换用命令的输出替换命令。该命令必须放在反斜杠(command)之间,或者放在以美元符号($( command ))开头的括号之间。例如,为了计算名称中包含今天日期的文件的行数,该命令使用date命令的输出:

$ wc -l $( date +%Y-%m-%d ).log
61 2009-03-31.log

命令替换的旧格式使用反斜杠。该命令与上一个命令相同:

$ wc -l `date +%Y-%m-%d`.log
2 2009-04-01.log

这并不完全相同,因为我在午夜前不久运行了第一个命令,在午夜后不久运行了第二个命令。因此,wc处理了两个不同的文件。

如果命令替换没有加引号,则对结果进行分词路径名扩展

单词拆分

如果参数和算术展开以及命令替换的结果没有被加上引号,则它们会被拆分:


$ var="this is a multi-word value"
$ sa $var "$var"
:this:
:is:
:a:
:multi-word:
:value:
:this is a multi-word value:

分词基于Iinternalffields分隔符变量IFS的值。IFS的默认值包含空格、制表符和换行符(IFS=$' \t\n')。当IFS有默认值或未设置时,任何默认的IFS字符序列被读取为单个分隔符。

$ var='   spaced
   out   '
$ sa $var
:spaced:
:out:

如果IFS包含另一个字符(或多个字符)以及空白,那么任何空白字符加上该字符的序列将界定一个字段,但是每个非空白字符的实例界定一个字段:

S IFS=' :'
$ var="qwerty  : uiop :  :: er " ## :  :: delimits 2 empty fields
$ sa $var
:qwerty:
:uiop:
::
::
:er:

如果IFS只包含非空白字符,那么IFS中每个字符的每一次出现都限定了一个字段,空白被保留:

$ IFS=:
$ var="qwerty  : uiop :  :: er "
$ sa $var
:qwerty  :
: uiop :
:  :
::
: er :

路径名扩展

命令行中包含字符*?[的未加引号的单词被视为文件分块模式,并被替换为与该模式匹配的文件的字母列表。如果没有文件匹配该模式,则该单词保持不变。

星号匹配任何字符串。h*匹配当前目录中所有以h开头的文件,*k匹配所有以k结尾的文件。shell 用按字母顺序排列的匹配文件列表替换通配符模式。如果没有匹配的文件,通配符模式保持不变。

$ cd "$HOME/bin"
$ sa h*
:hello:
:hw:
$ sa *k
:incheck:
:numcheck:
:rangecheck:

问号匹配任何单个字符;以下模式匹配第二个字母是a的所有文件:

$ sa ?a*
:rangecheck:
:ba:
:valint:
:valnum:

方括号匹配任何一个括起来的字符,可以是一个列表,一个范围,或者一类字符:[aceg]匹配ace或者g中的任何一个;[h-o]匹配从ho的任意字符;而[[:lower:]]匹配所有小写字母。

您可以使用set -f命令禁用文件名扩展。bash有许多影响文件扩展名的选项。我会在第八章的中详细介绍它们。

过程替代

进程替换为命令或命令列表创建一个临时文件名。您可以在任何需要文件名的地方使用它。表单<(command)使得command的输出可以作为文件名使用;>(command)是一个可以写入的文件名。

$ sa <(ls -l) >(pr -Tn)
:/dev/fd/63:
:/dev/fd/62:

Image 注意pr命令通过插入页眉来转换文本文件进行打印。可以用-T选项关闭标题,用-n选项给行编号。

当命令行上的文件名被读取时,它产生命令的输出。进程替换可以用来代替管道,允许循环中定义的变量对脚本的其余部分可见。在这个代码片段中,totalsize对循环外的脚本不可用:

$ ls -l |
> while read perms links owner group size month day time file
> do
>   printf "%10d %s\n" "$size" "$file"
>   totalsize=$(( ${totalsize:=0}${size:-0} ))
> done
$  echo ${totalsize-unset} ## print "unset" if variable is not set
unset

通过使用进程替换,变量totalsize在循环之外变得可用:

$ while read perms links owner group size month day time file
> do
>   printf "%10d %s\n" "$size" "$file"
>   totalsize=$(( ${totalsize:=0}${size:-0} ))
> done < <(ls -l *)
$ echo ${totalsize-unset}
12879

解析选项

shell 脚本的选项,前面有连字符的单个字符,可以用内置命令getopts解析。某些选项可能有参数,并且选项必须在非选项参数之前。

多个选项可以用一个连字符连接,但是任何带参数的选项都必须是字符串中的最后一个选项。它的参数如下,中间有或没有空格。

在下面的命令行中,有两个选项,-a-f。后者接受一个文件名参数。John是第一个非选项参数,-x不是选项,因为它在非选项参数之后。

myscript -a -f filename John -x Jane

getopts的语法如下:

getopts OPTSTRING var

OPTSTRING包含所有选项的字符;那些带参数的函数后跟一个冒号。对于清单 4-3 中的脚本,字符串是f:v。每个选项都放在变量$var中,选项的参数(如果有的话)放在$OPTARG中。

通常用作while循环的条件,getopts成功返回,直到它已经解析了命令行上的所有选项,或者直到它遇到单词--。命令行上所有剩余的单词都是传递给脚本主要部分的参数。

一个经常使用的选项是-v打开详细模式,它显示的不仅仅是关于脚本运行的默认信息。其他选项—例如-f—需要文件名参数。

这个示例脚本处理-v-f选项,并且在详细模式下显示一些信息。

清单 4-3parseopts,解析命令行选项

progname=${0##*/} ## Get the name of the script without its path

## Default values
verbose=0
filename=

## List of options the program will accept;
## those options that take arguments are followed by a colon
optstring=f:v

## The loop calls getopts until there are no more options on the command line
## Each option is stored in $opt, any option arguments are stored in OPTARG
while getopts $optstring opt
do
  case $opt in
    f) filename=$OPTARG ;; ## $OPTARG contains the argument to the option
    v) verbose=$(( $verbose1 )) ;;
    *) exit 1 ;;
  esac
done

## Remove options from the command line
## $OPTIND points to the next, unparsed argument
shift "$(( $OPTIND - 1 ))"

## Check whether a filename was entered
if [ -n "$filename" ]
then
   if$verbose -gt 0 ]
   then
      printf "Filename is %s\n" "$filename"
   fi
else
   if$verbose -gt 0 ]
   then
     printf "No filename entered\n" >&2
   fi
   exit 1
fi

## Check whether file exists
if [ -f "$filename" ]
then
  if$verbose -gt 0 ]
  then
    printf "Filename %s found\n" "$filename"
  fi
else
  if$verbose -gt 0 ]
  then
    printf "File, %s, does not exist\n" "$filename" >&2
  fi
  exit 2
fi

## If the verbose option is selected,
## print the number of arguments remaining on the command line
if$verbose -gt 0 ]
then
  printf "Number of arguments is %d\n" "$#"
fi

不带任何参数运行脚本除了生成一个失败的返回代码之外没有任何作用:

$ parseopts
$ echo $?
1

使用 verbose 选项,它还会打印一条错误消息:

$ parseopts -v
No filename entered
$ echo $?
1

对于非法选项(即不在$optstring中的选项),shell 会打印一条错误消息:

$ parseopts -x
/home/chris/bin/parseopts: illegal option – x

如果输入了一个文件名,但该文件不存在,它会产生以下结果:

$ parseopts -vf qwerty; echo $?
Filename is qwerty
File, qwerty, does not exist
2

为了允许非选项参数以连字符开始,选项可以以--明确结束:

$ parseopts -vf ~/.bashrc -– -x
Filename is /home/chris/.bashrc
Filename /home/chris/.bashrc found
Number of arguments is 1

摘要

shell 在将命令行传递给命令之前对其进行预处理,这为程序员节省了大量工作。

命令

  • head:从文件中提取前N行;N默认为 10
  • cut:从文件中提取列

练习

  1. 这个命令行上有多少个参数?

    sa $# $(date "+%Y %m %d") John\ Doe
    
  2. 以下代码片段存在什么潜在问题?

    year=$( date +%Y )
    month=$( date +%m )
    day=$( date +%d )
    hour=$( date +%H )
    minute=$( date +%M )
    second=$( date +%S )
    

五、参数和变量

自从 30 多年前 Unix shell 诞生以来,变量就一直是它的一部分,但是这些年来,它们的功能不断发展。标准的 Unix shell 现在有了参数扩展,可以对其内容执行复杂的操作。bash增加了更多的扩展能力以及索引和关联数组。

本章涵盖了您可以对变量和参数做什么,包括它们的范围。换句话说,在定义了一个变量之后,在哪里可以访问它的值呢?本章简要介绍了程序员可以使用的 shell 使用的 80 多个变量。它讨论了如何命名变量,以及如何用参数扩展来区分它们。

位置参数是传递给脚本的参数。它们可以用shift命令操作,按数字单独使用或循环使用。

数组为一个名称分配多个值。bash既有数字索引数组,也有从bash-4.0开始的关联数组,它们由字符串而不是数字来赋值和引用。

变量的命名

变量名只能包含字母、数字和下划线,并且必须以字母或下划线开头。除了这些限制,你可以自由地建立你认为合适的名字。然而,使用一致的方案来命名变量是一个好主意,选择有意义的名称对使您的代码自文档化大有帮助。

也许最常引用的(尽管很少实现)惯例是环境变量应该用大写字母,而局部变量应该用小写字母。鉴于bash本身在内部使用了超过 80 个大写变量,这是一种危险的做法,冲突并不少见。我见过诸如PATHHOMELINESSECONDSUID这样的变量被误用,带来潜在的灾难性后果。没有一个bash的变量以下划线开头,所以在我的第一本书 Shell 脚本编写方法:一个问题解决方法 (Apress,2005)中,我使用大写名称加下划线来表示 Shell 函数设置的值。

单字母的名字应该很少使用。它们适合作为循环中的索引,其唯一的功能是作为计数器。传统上用于这个目的的字母是i,但我更喜欢n。(在教室里教编程的时候,黑板上的字母I太容易和数字 1 混淆,所以我开始用n代表“数字”,25 年后我还在用它)。

我使用单字母变量名的另一个地方是从文件中读取一次性材料的时候。例如,如果我只需要文件中的一个或两个字段,我可以这样使用:

while IFS=: read login a b c name e
do
  printf "%-12s %s\n" "$login" "$name"
done < /etc/passwd

我推荐使用两种命名方案中的任何一种。第一个是 Heiner Steven 在http://www.shelldorado.com/``. He capitalizes the first letter of all variables and also the first letters of further words in the name: ConfigFileLastDirFastMath在他的 Shelldorado 网站上使用的。在某些情况下,他的用法更接近我的。

我用的都是小写字母:configfilelastdirfastmath。当组合在一起的单词含糊不清或难以阅读时,我用下划线将它们分开:line_widthbg_underlineday_of_week`。

无论您选择什么系统,重要的是名称给出了变量包含的内容的真实指示。但是不要忘乎所以,用这样的东西:

long_variable_name_which_may_tell_you_something_about_its_purpose=1

一个变量的范围:你能从这里看到它吗?

默认情况下,变量的定义只有定义它的 shell(以及该 shell 的子 shell)知道。调用当前脚本的脚本不会知道这个变量,被当前脚本调用的脚本也不会知道这个变量,除非它被导出到环境

环境是一个形式为name=value的字符串数组。每当执行外部命令(创建子进程)时,无论是编译后的二进制命令还是解释后的脚本,该数组都会在后台传递给它。在 shell 脚本中,这些字符串可以作为变量使用。

可以使用 shell 内置命令export将脚本中分配的变量导出到环境中:

var=whatever
export var

bash中,这可以缩写成这样:

export var=whatever

没有必要导出一个变量,除非你想让当前脚本调用的脚本(或其他程序)可以使用它...).导出变量不会使它在除子进程之外的任何地方可见。

清单 5-1 告诉你变量$x是否在环境中,如果有的话,它包含什么。

清单 5-1showvar,打印变量x的值

if [[ ${x+X} = X ]] ## If $x is set
then
  if [[ -n $x ]] ## if $x is not empty
  then
    printf "  \$x = %s\n" "$x"
  else
    printf "  \$x is set but empty\n"
  fi
else
  printf " %s is not set\n" "\$x"
fi

一旦变量被导出,它将一直保留在环境中,直到被取消设置:

$ unset x
$ showvar
  $x is not set
$ x=3
$ showvar
  $x is not set
$ export x
$ showvar
  $x = 3
$ x= ## in bash, reassignment doesn't remove a variable from the environment
$ showvar
  $x is set but empty

Image 注意 showvar不是一个 bash 命令,而是一个如清单 5-1 所示的脚本,它使用x的值。

子 shell 中设置的变量对于调用它的脚本是不可见的。子 Subshells 包括命令替换,如在$(command)command中;管道的所有元素,以及括号中的代码,如( command )

关于 shell 编程,可能最常被问到的问题是,“我的变量去哪里了?我知道我设置了它们,为什么它们是空的?”通常,这是由于将一个命令的输出通过管道传输到一个分配变量的循环中造成的:

printf "%s\n" ${RANDOM}{,,,,,} |
  while read num
  do
    (( num > ${biggest:=0} )) && biggest=$num
  done
printf "The largest number is: %d\n" "$biggest"

biggest被发现为空时,在所有的 shell 论坛中都可以听到关于在while循环中设置的变量在它们之外不可用的抱怨。但问题不在于循环;这是因为循环是管道的一部分,因此在 subshell 中执行。

在 bash-4.2 中,一个新选项lastpipe使管道中的最后一个进程能够在当前 shell 中执行。通过以下方式调用它:

shopt -s lastpipe

Shell 变量

shell 要么设置要么使用 80 多个变量。其中许多是由bash内部使用的,对 shell 程序员来说用处不大。有些用于调试,有些常用于 shell 程序。大约一半是由 shell 自己设置的,其余的是由操作系统、用户、终端或脚本设置的。

在 shell 设置的那些中,您已经看到了RANDOM,它返回 0 到 32,767 之间的随机整数,以及PWD,它包含当前工作目录的路径。您看到了在解析命令行选项时使用的OPTINDOPTARG(第四章)。有时,BASH_VERSION(或BASH_VERSINFO)用于确定正在运行的 shell 是否能够运行脚本。本书中的一些脚本至少需要bash-3.0,并且可能使用其中一个变量来确定当前的 shell 是否足够新以运行该脚本:

case $BASH_VERSION in
  [12].*) echo "You need at least bash3.0 to run this script" >&2; exit 2;;
esac

提示字符串变量PS1PS2,在命令行交互 shells 中使用;PS3select内置命令一起使用,在执行跟踪模式下PS4打印在每一行之前(详见第十章)。

Shell 变量

以下变量由 shell 设置:

Taba

shell 使用以下变量,可能会为其中一些变量设置默认值(例如,IFS):

Tabb

参见附录 A 了解所有 Shell 变量的描述。

参数扩展

现代 Unix shell 的大部分功能来自于它的参数扩展。在 Bourne shell 中,这些主要涉及测试参数是已设置还是为空,以及用默认值或替代值进行替换。合并到 POSIX 标准中的 KornShell additions 增加了字符串操作。KornShell 93 增加了更多扩展,这些扩展还没有被纳入标准,但是bash已经采用了。bash-4.0增加了两个新的资料片。

伯恩·谢尔

Bourne shell 和它的后继程序有一些扩展,可以用缺省值替换一个空的或未设置的变量,如果一个变量是空的或未设置的,就给它分配一个缺省值,如果一个变量是空的或未设置的,就停止执行并打印一条错误消息。

var:default{var:-default}和{var-default}:使用默认值

最常用的扩展${var:-default}检查变量是否未设置或为空,如果是,则扩展为默认字符串:

$ var=
$ sa "${var:-default}"  ## The sa script was introduced in Chapter 4
:default:

如果省略冒号,则扩展只检查变量是否未设置:

$ var=
$ sa "${var-default}" ## var is set, so expands to nothing
::
$ unset var
$ sa "${var-default}" ## var is unset, so expands to "default"
:default:

如果选项未提供默认值或环境中未继承默认值,则此代码片段会将默认值分配给$filename:

defaultfile=$HOME/.bashrc
## parse options here
filename=${filename:-"$defaultfile"}

var:+alternate{var:+alternate}、{var+alternate}:使用替代值

如果参数不为空,或者如果设置了不带冒号的参数,则前一个扩展的补码将替换替代值。仅当$var被设置且不为空时,第一次扩展才会使用alternate:

$ var=
$ sa "${var:+alternate}" ## $var is set but empty
::
$ var=value
$ sa "${var:+alternate}" ## $var is not empty
:alernate:

如果没有冒号,如果设置了变量,则使用alternate,即使变量为空:

$ var=
$ sa "${var+alternate}" ## var is set
:altername:
$ unset var
$ sa "${var+alternate}" ## $var is not set
::
$ var=value
$ sa "${var:+alternate}" ## $var is set and not empty
:alternate:

向变量添加字符串时经常使用这种扩展。如果变量为空,您不想添加分隔符:

$ var=
$ forin a b c d e f g
> do
>   var="$var $n"
> done
$ sa "$var"
: a b c d e f g:

为了防止前导空格,可以使用参数扩展:

$ var=
$ forin a b c d e f g
> do
>   var="${var:+"$var "}$n"
> done
$ sa "$var"
:a b c d e f g:

这是对n的每个值执行以下操作的一种简化方法:

if [ -n "$var" ]
then
  var="$var $n"
else
  var=$n
fi

或者:

[ -n "$var" ] && var="$var $n" || var=$n

var:=default{var:=default},{var=default}:分配默认值

${var:=default}扩展的行为方式与${var:-default}相同,只是它也将默认值赋给变量:

$ unset n
$ while :
> do
>  echo :$n:
>  [ ${n:=0} -gt 3 ] && break ## set $n to 0 if unset or empty
>  n=$(( $n1 ))
> done
::
:1:
:2:
:3:
:4:

var:?消息{var:?消息},{var?message}:如果为空或未设置,则显示错误消息

如果var为空或未设置,message将被打印到标准错误,脚本将以状态 1 退出。如果message为空,将打印parameter null or not set。清单 5-2 需要两个非空的命令行参数,并在它们缺失或为空时使用这个扩展来显示错误消息。

清单 5-2checkarg,如果参数未设置或为空,退出

## Check for unset arguments
: ${1?An argument is required} \
  ${2?Two arguments are required}

## Check for empty arguments
: ${1:?A non-empty argument is required} \
  ${2:?Two non-empty arguments are required}

echo "Thank you."

第一次扩展失败时会打印出message,脚本将在此时退出:

$ checkarg
/home/chris/bin/checkarg: line 10: 1: An argument is required
$ checkarg x
/home/chris/bin/checkarg: line 10: 2: Two arguments are required
$ checkarg '' ''
/home/chris/bin/checkarg: line 13: 1: A non-empty argument is required
$ checkarg x ''
/home/chris/bin/checkarg: line 13: 2: Two non-empty arguments are required
$ checkarg x x
Thank you.

POSIX Shell

除了来自 Bourne shell 的扩展,POSIX shell 还包括许多来自 KornShell 的扩展。这些包括返回长度和从变量内容的开头或结尾删除模式。

${#var}:变量内容的长度

此扩展返回变量扩展值的长度:

read passwd
if${#passwd} -lt 8 ]
then
  printf "Password is too short: %d characters\n" "$#" >&2
  exit 1
fi

${var%PATTERN}:从末尾删除最短的匹配

变量被扩展,匹配PATTERN的最短字符串从扩展值的末尾删除。这里和其他参数扩展中的PATTERN 是文件名扩展(又名文件打包)模式。

给定字符串Toronto和模式o*,最短的匹配模式就是最终的o:

$ var=Toronto
$ var=${var%o*}
$ printf "%s\n" "$var"
Toront

因为被截断的字符串已经被分配给了var,所以现在与模式匹配的最短字符串是ont:

$ printf "%s\n" "${var%o*}"
Tor

这个扩展可以用来替换外部命令dirname,它去掉了路径的文件名部分,留下了到目录的路径(清单 5-3 )。如果字符串中没有斜杠,如果是当前目录中现有文件的名称,则打印当前目录;否则,打印一个点。

清单 5-3dname,打印文件路径的目录部分

case $1 in
  */*) printf "%s\n" "${1%/*}" ;;
  *) [ -e "$1" ] && printf "%s\n" "$PWD" || echo '.' ;;
esac

Image 注意我称这个脚本为dname而不是dirname,因为它不符合dirname命令的 POSIX 规范。在下一章中,有一个名为dirname的 shell 函数实现了 POSIX 命令。

$ dname /etc/passwd
/etc
$ dname bin
/home/chris

${var%%PATTERN}:从末尾删除最长的匹配项

变量被展开,从展开值的末尾开始匹配PATTERN的最长字符串被删除:

$ var=Toronto
$ sa "${var%%o*}"
:t:

${var#PATTERN}:从开头删除最短的匹配

变量被扩展,匹配PATTERN的最短字符串从扩展值的开始处删除:

$ var=Toronto
$ sa "${var#*o}"
:ronto:

${var##PATTERN}:从开头删除最长的匹配

变量被扩展,匹配PATTERN的最长字符串从扩展值的开头删除。这通常用于从$0参数中提取脚本的名称,该参数包含脚本的完整路径:

scriptname=${0##*/} ## /home/chris/bin/script => script

尝试

bash2中引入了 KornShell 93 的两个扩展:搜索和替换以及子串提取。

${var//PATTERN/STRING}:用字符串替换模式的所有实例

因为问号匹配任何单个字符,所以本示例隐藏了一个密码:

$ passwd=zxQ1.=+-a
$ printf "%s\n" "${passwd//?/*}"
*********

使用单斜线时,只替换第一个匹配的字符。

$ printf "%s\n" "${passwd/[[:punct:]]/*}"
zxQ1*=+-a

var:OFFSET:LENGTH:返回{var:OFFSET:LENGTH}:返回var 的子字符串

返回从OFFSET开始的$var的子串。如果指定了LENGTH,则替换该数量的字符;否则,返回字符串的其余部分。第一个字符位于偏移量0:

$ var=Toronto
$ sa "${var:3:2}"
:on:
$ sa "${var:3}"
:onto:

负的OFFSET从字符串的末尾开始计数。如果使用文字减号(与变量中包含的减号相反),则必须在它前面加一个空格,以防止它被解释为default扩展:

$ sa "${var: -3}"
:nto:

${!var}:间接引用

如果一个变量包含另一个变量的名称,例如x=yesa=xbash可以使用间接引用:

$ x=yes
$ a=x
$ sa "${!a}"
:yes:

使用eval builtin 命令可以达到同样的效果,它扩展了它的参数,并将结果字符串作为命令执行:

$ eval "sa \$$a"
:yes:

关于eval的更详细解释,参见第九章。

Bash-4.0

在 4.0 版本中,bash引入了两个新的参数扩展,一个用于转换大写,一个用于转换小写。都有单字符和全局版本。

${var^PATTERN}:转换成大写

var的第一个字符如果匹配PATTERN就转换成大写;用一个双插入符号(^^,它转换所有匹配PATTERN的字符。如果省略PATTERN,则匹配所有字符:

$ var=toronto
$ sa "${var^}"
:Toronto:
$ sa "${var^[n-z]}"
:Toronto:
$ sa "${var^^[a-m]}" ## matches all characters from a to m inclusive
:toronto:
$ sa "${var^^[n-q]}"
:tOrONtO:
$ sa "${var^^}"
:TORONTO:

${var,PATTERN}:转换成小写

除了将大写字母转换为小写字母之外,此扩展的工作方式与上一个扩展相同:

$ var=TORONTO
$ sa "${var,,}"
:toronto:
$ sa "${var,,[N-Q]}"
:ToRonTo:There is also an undocumented expansion that inverts the case:
$ var=Toronto
$ sa "${var~}"
:toronto:
$ sa "${var~~}"
:tORONTO:

位置参数

位置参数可以通过数字($1 ... $9 ${10} ...)单独引用,也可以通过"$@""$*"一次性引用。正如已经提到的,大于9的参数必须用大括号括起来:${10}${11}

不带参数的shift命令删除第一个位置参数,并将剩余的参数向前移动,以便$2变成$1 , $3变成$2,依此类推。有了一个论点,它可以删除更多。要删除前三个参数,请提供一个包含要删除的参数数量的参数:

$ shift 3

要删除所有参数,使用特殊参数$#,它包含位置参数的数量:

$ shift "$#"

要删除最后两个位置参数以外的所有参数,请使用以下命令:

$ shift "$(( $# - 2 ))"

要依次使用每个参数,有两种常用方法。第一种方法是通过展开"$@"来遍历参数值:

for param in "$@"  ## or just:  for param
do
  : do something with $param
done

这是第二个:

while (( $# ))
do
  : do something with $1
  shift
done

数组

迄今为止使用的所有变量都是标量变量;也就是说,它们只包含一个值。相比之下,数组变量可以包含很多值。POSIX shell 不支持数组,但是bash(从版本 2 开始)支持。它的数组是一维的,由整数索引,从bash-4.0开始,也由字符串索引。

整数索引数组

数组变量的单个成员用一个形式为[N]的下标来赋值和访问。第一个元素的索引为0。在bash中,数组是稀疏的;它们不需要被分配连续的索引。一个数组可以有一个索引为0的元素,另一个索引为42的元素,中间没有元素。

显示数组

数组元素由名称和大括号中的下标引用。这个例子将使用 shell 变量BASH_VERSINFO。它是一个数组,包含正在运行的 shell 的版本信息。第一个元素是主要版本号,第二个是次要版本号:

$ printf "%s\n" "${BASH_VERSINFO[0]}"
4
$ printf "%s\n" "${BASH_VERSINFO[1]}"
3

一个数组的所有元素可以用一条语句打印出来。下标@*类似于它们与位置参数的使用:*如果被引用,则扩展为单个参数;如果未加引号,将对结果进行分词和文件名扩展。使用@作为下标并引用展开,每个元素展开为一个单独的自变量,不再对它们进行进一步的展开。

$ printf "%s\n" "${BASH_VERSINFO[*]}"
4 3 30 1 release i686-pc-linux-gnuoldld
$  printf "%s\n" "${BASH_VERSINFO[@]}"
4
3
30
1
release
i686-pc-linux-gnu

各种参数扩展对数组起作用;例如,要从数组中获取第二个和第三个元素,请使用以下命令:

$ printf "%s\n" "${BASH_VERSINFO[@]:1:2}" ## minor version number and patch level
3
30

当下标为*@时,长度扩展返回数组中元素的数量,如果给定了数字索引,则返回单个元素的长度:

$ printf "%s\n" "${#BASH_VERSINFO[*]}"
6
$ printf "%s\n" "${#BASH_VERSINFO[2]}" "${#BASH_VERSINFO[5]}"
2
17

分配数组元素

可以使用索引来分配元素;以下命令创建一个稀疏数组:

name[0]=Aaron
name[42]=Adams

当元素被连续赋值(或者打包)时,索引数组更有用,因为它使得对它们的操作更简单。可以直接对下一个未分配的元素进行分配:

$ unset a
$ a[${#a[@]}]="1 $RANDOM" ## ${#a[@]} is 0
$ a[${#a[@]}]="2 $RANDOM" ## ${#a[@]} is 1
$ a[${#a[@]}]="3 $RANDOM" ## ${#a[@]} is 2
$ a[${#a[@]}]="4 $RANDOM" ## ${#a[@]} is 3
$ printf "%s\n" "${a[@]}"
1 6007
2 3784
3 32330
4 25914

一条命令就可以填充整个数组:

$ province=( Quebec Ontario Manitoba )
$ printf "%s\n" "${province[@]}"
Quebec
Ontario
Manitoba

+=操作符可用于将值追加到索引数组的末尾。这使得下一个未赋值元素的赋值形式更加简洁:

$ province+=( Saskatchewan )
$ province+=( Alberta "British Columbia" "Nova Scotia" )
$ printf "%-25s %-25s %s\n" "${province[@]}"
Quebec                    Ontario                   Manitoba
Saskatchewan              Alberta                   British Columbia
Nova Scotia

关联数组

4.0 版的bash中引入的关联数组使用字符串作为下标,并且必须在使用前声明:

$ declare -A array
$ for subscript in a b c d e
> do
>   array[$subscript]="$subscript $RANDOM"
> done
$ printf ":%s:\n" "${array["c"]}" ## print one element
:c 1574:
$ printf ":%s:\n" "${array[@]}" ## print the entire array
:a 13856:
:b 6235:
:c 1574:
:d 14020:
:e 9165:

摘要

到目前为止,本章最大的主题是参数扩展,而到目前为止,参数扩展的最大部分专门讨论由 KornShell 引入并集成到标准 Unix shell 中的那些扩展。这些工具为 POSIX shell 提供了强大的功能。本章给出的例子相对简单;参数扩展的全部潜力将在本书后面开发严肃的程序时展示。

其次重要的是数组。尽管不是 POSIX 标准的一部分,但它们通过使以逻辑单元收集数据成为可能,为 shell 添加了大量功能。

理解变量的作用域可以省去很多麻烦,而命名良好的变量使程序更容易理解和维护。

操纵位置参数是 shell 编程的一个次要但重要的方面,本章给出的例子将在本书的后面部分重新讨论和扩展。

命令

  • declare:声明变量并设置其属性
  • eval:展开参数并执行结果命令
  • export:将变量放入环境中,以便它们可用于子进程
  • shift:删除位置参数并重新编号
  • shopt:设置 Shell 选项
  • unset:完全删除一个变量

概念

  • 环境:从调用程序继承并传递给子进程的变量集合
  • 数组变量:包含多个值的变量,使用下标访问
  • 标量变量:包含单个值的变量
  • 关联数组:下标为字符串而非整数的数组变量

练习

  1. 默认情况下,可以在哪里访问脚本中赋值的变量?选择所有适用的选项:
    • 在当前脚本中
    • 在当前脚本中定义的函数中
    • 在调用当前脚本的脚本中
    • 在当前脚本调用的脚本中
    • 在当前脚本的子 Shell 中
  2. 我建议不要使用单个字母的变量名,但是给出了几个合理的地方。你能想到它们的其他合法用途吗?
  3. 给定var=192.168.0.123,编写一个使用参数扩展提取第二个数168的脚本。`