在本章中,我们将研究当我们按 Enter 键时在命令行上发生的一些“魔法”。虽然我们将研究shell的几个有趣而复杂的功能,但我们将只使用一个新命令。
echo —— 显示一行文本expansion —— 扩展
每次我们键入命令并按 Enter 键时, bash 都会在执行命令之前对文本执行几次替换。我们已经看到了几个例子,说明一个简单的字符序列,例如 * ,对shell有很多意义。实现这一点的过程称为 expansion ,扩展。通过扩展,我们输入一些东西,然后在shell对其进行操作之前将其展开为其他东西。为了演示我们的意思,让我们看看 echo 命令。 echo 是一个内置的shell,可以执行非常简单的任务。它在标准输出上打印其文本参数。
xxxxxxxxxx[me@linuxbox ~]$ echo this is a testthis is a test这很简单。传递给echo的任何参数都会显示出来。让我们再举一个例子:
xxxxxxxxxx[me@linuxbox ~]$ echo *Desktop Documents ls-output.txt Music Pictures Public Templates Videos那么,刚才发生了什么?为什么 echo 没有打印 * ?正如我们在通配符工作中回忆的那样, * 字符表示匹配文件名中的任何字符,但我们在最初的讨论中没有看到的是shell是如何做到这一点的。简单的答案是,在执行 echo 命令之前,shell会将 * 扩展为其他内容(在本例中,是当前工作目录中的文件名)。当按下 Enter 键时,shell会在执行命令之前自动展开命令行上的任何限定字符,因此 echo 命令从未看到 * ,只看到其展开的结果。知道这一点后,我们可以看到 echo 的表现符合预期。
通配符工作的机制称为 pathname expansion ,路径名扩展。如果我们尝试前几章中使用的一些技术,我们会发现它们实际上是扩展。给定一个如下所示的主目录:
xxxxxxxxxx[me@linuxbox ~]$ lsDesktop    ls-output.txt  Pictures TemplatesDocuments  Music          Public   Videos我们可以进行以下扩展:
xxxxxxxxxx[me@linuxbox ~]$ echo D*Desktop Documents和:
xxxxxxxxxx[me@linuxbox ~]$ echo *sDocuments Pictures Templates Videos或者:
xxxxxxxxxx[me@linuxbox ~]$ echo [[:upper:]]*Desktop Documents Music Pictures Public Templates Videos超越我们的主目录,我们可以这样做:
xxxxxxxxxx[me@linuxbox ~]$ echo /usr/*/share/usr/kerberos/share /usr/local/share众所周知,以句点字符开头的文件名是隐藏的。路径名扩展也尊重这种行为。以下展开不会显示隐藏的文件。
echo *
乍一看,我们可以通过以前导句点开始模式来在扩展中包含隐藏文件,如下所示:
echo .*
它几乎奏效了。然而,如果我们仔细检查结果,我们会看到这些名字 . 以及 .. 也将出现在结果中。由于这些名称指向当前工作目录及其父目录,因此使用此模式可能会产生不正确的结果。如果我们尝试以下命令,我们可以看到这一点:
ls -d .* | less
为了在这种情况下更好地执行路径名扩展,我们必须采用更具体的模式。
echo .[!.]*
此模式扩展到每个仅以一个句点开头,后跟任何其他字符的文件名。这将适用于大多数隐藏文件(尽管它仍然不包括带有多个前导句点的文件名)。带有 -A 选项(“几乎所有”)的 ls 命令将提供隐藏文件的正确列表。
ls -A
正如我们在介绍 cd 命令时可能记得的那样,波浪号字符( ~ )具有特殊含义。当在单词开头使用时,它会扩展到指定用户的主目录名称,或者如果没有指定用户,则扩展到当前用户的主文件夹名称。
xxxxxxxxxx[me@linuxbox ~]$ echo ~/home/me如果用户“bob”有一个账号,然后它扩展进入:
xxxxxxxxxx[me@linuxbox ~]$ echo ~bob/home/bobshell允许通过扩展执行算术运算。这允许我们将shell提示符用作计算器。
xxxxxxxxxx[me@linuxbox ~]$ echo $((2 + 2))4算术扩展,arithmetic expansion,使用以下形式:
$((expression))
其中 expression ,表达式,是由值和算术运算符组成的算术表达式。
算术扩展只支持整数(only integers,whole numbers,no decimals——整数,没有小数),但可以执行许多不同的操作。下表描述了一些支持的运算符。
| 运算符 | 描述 | 
|---|---|
| + | Addition,加 | 
| - | Subtraction,减 | 
| * | Multiplication,乘 | 
| / | Division,除 但请记住,由于扩展只支持整数运算,因此结果是整数  | 
| % | Modulo,模除,意思就是“remainder”,剩余 | 
| ** | Exponentiation,指数 【注意,sh没有**运算符】  | 
空格在算术表达式中不重要,表达式可能嵌套。例如,要将5的平方乘以3(to multiply 5 squared by 3),我们可以使用以下方法:
xxxxxxxxxx[me@linuxbox ~]$ echo $(($((5**2)) * 3))75单个括号可用于对多个子表达式进行分组。使用此技术,我们可以重写前面的示例,并使用单个扩展而不是两个扩展获得相同的结果:
xxxxxxxxxx[me@linuxbox ~]$ echo $(((5**2) * 3))75这是一个使用除法和余数运算符的示例。请注意整数除法的效果:
xxxxxxxxxx[me@linuxbox ~]$ echo Five divided by two equals $((5/2))Five divided by two equals 2[me@linuxbox ~]$ echo with $((5%2)) left over.with 1 left over.算术扩展在【第34章】中有更详细的介绍。
也许最奇怪的扩展叫做 brace expansion ,大括号扩展。有了它,我们可以从包含大括号的模式中创建多个文本字符串。这里有一个例子:
xxxxxxxxxx[me@linuxbox ~]$ echo Front-{A,B,C}-BackFront-A-Back Front-B-Back Front-C-Back【sh没有大括号扩展】
要用大括号扩展的模式可能包含称为前导码(preamble)的前导部分和称为后记(postscript)的尾随部分。大括号表达式本身可以包含逗号分隔的字符串列表,也可以包含整数或单个字符的范围。该模式不能包含未加引号的空格。以下是一个使用整数范围的示例:
xxxxxxxxxx[me@linuxbox ~]$ echo Number_{1..5}Number_1 Number_2 Number_3 Number_4 Number_5在bash 4.0及更高版本中,整数也可以像这样进行零填充(zero-padded):
xxxxxxxxxx[me@linuxbox ~]$ echo {01..15}01 02 03 04 05 06 07 08 09 10 11 12 13 14 15[me@linuxbox ~]$ echo {001..15}001 002 003 004 005 006 007 008 009 010 011 012 013 014 015以下是按相反顺序排列的一系列字母:
xxxxxxxxxx[me@linuxbox ~]$ echo {Z..A}Z Y X W V U T S R Q P O N M L K J I H G F E D C B A大括号扩展可以嵌套:
xxxxxxxxxx[me@linuxbox ~]$ echo a{A{1,2},B{3,4}}baA1b aA2b aB3b aB4b那么,这有什么好处呢?最常见的应用程序是创建要创建的文件或目录列表。例如,如果我们是摄影师,并且有大量图像要按年和月组织,我们可能要做的第一件事就是创建一系列以数字“年-月”格式命名的目录。这样,目录名称将按时间顺序排序。我们可以键入一个完整的目录列表,但这需要大量的工作,而且容易出错。相反,我们可以这样做:
xxxxxxxxxx[me@linuxbox ~]$ mkdir Photos[me@linuxbox ~]$ cd Photos[me@linuxbox Photos]$ mkdir {2007..2009}-{01..12}[me@linuxbox Photos]$ ls2007-01 2007-07 2008-01 2008-07 2009-01 2009-072007-02 2007-08 2008-02 2008-08 2009-02 2009-082007-03 2007-09 2008-03 2008-09 2009-03 2009-092007-04 2007-10 2008-04 2008-10 2009-04 2009-102007-05 2007-11 2008-05 2008-11 2009-05 2009-112007-06 2007-12 2008-06 2008-12 2009-06 2009-12Pretty slick,很丝滑!
在本章中,我们将只简要地讨论参数扩展,但稍后我们将广泛地介绍它。这是一个在shell脚本中比直接在命令行上更有用的功能。它的许多功能都与系统存储小块数据并为每个块命名的能力有关。许多这样的块,更恰当地称为 variables ,变量,可供我们检查。例如,名为 USER 的变量包含我们的用户名。要调用参数展开并显示 USER 的内容,我们可以这样做:
xxxxxxxxxx[me@linuxbox ~]$ echo $USERme要查看可用变量的列表,请尝试以下操作:
xxxxxxxxxx[me@linuxbox ~]$ printenv | less您可能已经注意到,对于其他类型的扩展,如果我们键入了错误的模式,扩展将不会发生, echo 命令只会显示键入错误的模式。使用参数扩展时,如果我们拼错了变量的名称,扩展仍然会发生,但会导致一个空字符串:
x[me@linuxbox ~]$ echo $SUER[me@linuxbox ~]$命令替换允许我们将命令的输出用作扩展。
xxxxxxxxxx[me@linuxbox ~]$ echo $(ls)Desktop Documents ls-output.txt Music Pictures Public Templates Videos我最喜欢的一个是这样的:
xxxxxxxxxx[me@linuxbox ~]$ ls -l $(which cp)-rwxr-xr-x 1 root root 71516 2007-12-05 08:58 /bin/cp在这里,我们将 which cp 的结果作为参数传递给 ls 命令,从而获得 cp 程序的列表,而无需知道其完整路径名。我们不仅限于简单的命令。可以使用整个管道(此处仅显示部分输出):
xxxxxxxxxx[me@linuxbox ~]$ file $(ls -d /usr/bin/* | grep zip)/usr/bin/bunzip2:       symbolic link to `bzip2'/usr/bin/bzip2:         ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.9, stripped/usr/bin/bzip2recover:  ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.9, stripped/usr/bin/funzip:        ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.9, stripped/usr/bin/gpg-zip:       Bourne shell script text executable/usr/bin/gunzip:        symbolic link to `../../bin/gunzip'/usr/bin/gzip:          symbolic link to `../../bin/gzip'/usr/bin/mzip:          symbolic link to `mtools'在这个例子中,管道的结果变成了 file 命令的参数列表。
bash 也支持旧shell程序使用的命令替换语法。它使用后引号(backquotes)而不是美元符号和括号。
xxxxxxxxxx[me@linuxbox ~]$ ls -l `which cp`-rwxr-xr-x 1 root root 71516 2007-12-05 08:58 /bin/cpquote —— 引号
现在我们已经看到了shell可以执行扩展的多种方式,是时候学习如何控制它了。例如:
xxxxxxxxxx[me@linuxbox ~]$ echo this is a     testthis is a test或者这个:
xxxxxxxxxx[me@linuxbox ~]$ echo The total is $100.00The total is 00.00在第一个示例中,shell的单词分割从 echo 命令的参数列表中删除了额外的空格。在第二个示例中,参数扩展用空字符串替换了值 $1 ,因为它是一个未定义的变量。shell提供了一种称为引用(quoting)的机制,可以选择性地抑制不需要的扩展。
我们将讨论的第一种引用是双引号(double quotes)。如果我们把文本放在双引号内,shell使用的所有特殊字符都会失去其特殊含义,被视为普通字符。例外情况是 $ 、\ (反斜杠)和 ```(后引号)。这意味着单词分割、路径名扩展、波浪号扩展和大括号扩展被抑制,但参数扩展、算术扩展和命令替换仍在进行。使用双引号,我们可以处理包含嵌入式空格的文件名。假设我们是一个名为 two words.txt 的文件的不幸受害者。如果我们试图在命令行上使用它,分词会导致它被视为两个单独的参数,而不是所需的单个参数。
xxxxxxxxxx[me@linuxbox ~]$ ls -l two words.txtls: cannot access two: No such file or directoryls: cannot access words.txt: No such file or directory通过使用双引号,我们停止了单词分割,得到了预期的结果;此外,我们甚至可以修复损坏:
xxxxxxxxxx[me@linuxbox ~]$ ls -l "two words.txt"-rw-rw-r-- 1 me me 18 2016-02-20 13:03 two words.txt[me@linuxbox ~]$ mv "two words.txt" two_words.txt那里!现在我们不必一直输入那些讨厌的双引号。
记住,参数展开、算术展开和命令替换仍然发生在双引号内。
xxxxxxxxxx[me@linuxbox ~]$ echo "$USER $((2+2)) $(df -h)"me 4 Filesystem     Size    Used    Avail   Use%    Mounted ontmpfs               1.6G    2.0M    1.6G     1%     /run/dev/sda2            94G     19G     71G    21%     /tmpfs               7.8G       0    7.8G     0%     /dev/shmtmpfs               5.0M    4.0K    5.0M     1%     /run/lock/dev/sda1           975M    6.1M    969M     1%     /boot/efi/dev/sdb1           907G    574G    287G    67%     /hometmpfs               1.6G    1.8M    1.6G     1%     /run/user/1000我们应该花点时间看看双引号对命令替换的影响。首先,让我们更深入地了解一下分词是如何工作的。在前面的例子中,我们看到了分词是如何删除文本中的多余空格的。
xxxxxxxxxx[me@linuxbox ~]$ echo this is a    testthis is a test默认情况下,分词会查找空格、制表符和换行符(换行符)的存在,并将其视为单词之间的分隔符。这意味着未加引号的空格、制表符和换行符不被视为文本的一部分。它们仅作为分隔物。由于它们将单词分成不同的参数,我们的示例命令行包含一个命令,后面跟着四个不同的参数。如果我们添加双引号:
xxxxxxxxxx[me@linuxbox ~]$ echo "this is a    test"this is a    test单词分割被抑制,嵌入的空格不被视为分隔符;相反,他们成为了参数(argument)的一部分。添加双引号后,我们的命令行包含一个命令,后面跟着一个参数。
换行符被分词机制视为分隔符,这一事实对命令替换产生了有趣但微妙的影响。请考虑以下示例:
xxxxxxxxxx[me@linuxbox ~]$ echo $(df -h)Filesystem Size Used Avail Use% Mounted on tmpfs 1.6G 2.0M 1.6G 1% /run /dev/sda2 94G 19G 71G 21% / tmpfs 7.8G 0 7.8G 0% /dev/shm tmpfs 5.0M 4.0K 5.0M 1% /run/lock /dev/sda1 975M 6.1M 969M 1% /boot/efi /dev/sdb1 907G 574G 287G 67% /home tmpfs 1.6G 1.8M 1.6G 1% /run/user/1000[me@linuxbox ~]$ echo "$(df -h)"Filesystem  Size    Used    Avail   Use%    Mounted ontmpfs       1.6G    2.0M     1.6G     1%    /run/dev/sda2    94G     19G      71G    21%    /tmpfs       7.8G       0     7.8G     0%    /dev/shmtmpfs       5.0M    4.0K     5.0M     1%    /run/lock/dev/sda1   975M    6.1M     969M     1%    /boot/efi/dev/sdb1   907G    574G     287G    67%    /hometmpfs       1.6G    1.8M     1.6G     1%    /run/user/1000首先,未加引号的命令替换导致命令行包含49个参数。在第二种情况下,它产生了一个带有一个参数的命令行,其中包括嵌入的空格和换行符。
如果我们需要抑制所有展开,我们使用单引号,single quotes 。以下是无引号、双引号和单引号的比较:
xxxxxxxxxx[me@linuxbox ~]$ echo text ~/*.txt {a,b} $(echo foo) $((2+2)) $USERtext /home/me/ls-output.txt a b foo 4 me[me@linuxbox ~]$ echo "text ~/*.txt {a,b} $(echo foo) $((2+2)) $USER"text ~/*.txt {a,b} foo 4 me[me@linuxbox ~]$ echo 'text ~/*.txt {a,b} $(echo foo) $((2+2)) $USER'text ~/*.txt {a,b} $(echo foo) $((2+2)) $USER正如我们所看到的,随着引用的每一个后续级别,越来越多的扩展都被抑制了。
escaping characters——转义字符
有时我们只想引用一个字符。为此,我们可以在字符前加一个反斜杠(backslash),在这种情况下称为转义字符。通常,这是在双引号内完成的,以选择性地防止扩展。
xxxxxxxxxx[me@linuxbox ~]$ echo "The balance for user $USER is: \$5.00"The balance for user me is: $5.00使用转义来消除文件名中字符的特殊含义也很常见。例如,可以在文件名中使用通常对shell具有特殊含义的字符。这将包括 $ 、! 、& 、空格等。要在文件名中包含特殊字符,我们可以这样做:
xxxxxxxxxx[me@linuxbox ~]$ mv bad\&filename good_filename要允许反斜杠字符出现,请键入两个连续的反斜杠—— \\ 将其转义。请注意,在单引号内,反斜杠失去了其特殊含义,被视为普通字符。
反斜杠转义的另一个用途是抑制别名。例如,假设 ls 命令别名为 ls='ls --color=auto' ,这是许多Linux发行版的默认值,我们可以在命令前加一个反斜杠,别名将被忽略, ls 命令将在没有颜色选项的情况下执行。
除了作为转义符的作用外,反斜杠还用作表示法的一部分,表示某些称为控制代码(control codes)的特殊字符。ASCII编码方案中的前32个字符用于向电传打字机类设备传输命令。其中一些代码是熟悉的(制表符——tab、退格——backspace、换行——linefeed和回车——carriage return),而另一些则不是(null、传输结束——end-of-transmission和确认——acknowledge)。
| 转义序列 | 含义 | 
|---|---|
| \a | Bell(使计算机发出蜂鸣声的警报) | 
| \b | 退格 | 
| \n | 换行——newline。 在类Unix系统中,这会发生换行——linefeed。  | 
| \r | 回车——carriage return | 
| \t | 制表符——tab | 
上表列出了一些常见的反斜杠转义序列。使用反斜杠表示的想法起源于C编程语言,并被许多其他语言采用,包括shell。
在 echo 中添加 -e 选项将能够解释转义序列。您也可以将它们放在 $' ' 内。在这里,使用 sleep 命令,一个只需等待指定秒数然后退出的简单程序,我们可以创建一个基本的倒计时器:
xxxxxxxxxxsleep 10; echo -e "Time's up\a"也可以这样写:
xxxxxxxxxxsleep 10; echo "Time's up" $'\a'随着我们继续使用shell,我们会发现扩展和引用的使用频率会越来越高,因此很好地理解它们的工作方式是有意义的。事实上,可以说它们是学习shell最重要的科目。如果不正确理解扩展,shell将永远是神秘和混乱的根源,其大部分潜在的能量都会被浪费。
官方文档: Bash Reference Manual