在本章中,我们将研究当我们按 Enter 键时在命令行上发生的一些“魔法”。虽然我们将研究shell的几个有趣而复杂的功能,但我们将只使用一个新命令。
echo
—— 显示一行文本expansion —— 扩展
每次我们键入命令并按 Enter 键时, bash
都会在执行命令之前对文本执行几次替换。我们已经看到了几个例子,说明一个简单的字符序列,例如 *
,对shell有很多意义。实现这一点的过程称为 expansion ,扩展。通过扩展,我们输入一些东西,然后在shell对其进行操作之前将其展开为其他东西。为了演示我们的意思,让我们看看 echo
命令。 echo
是一个内置的shell,可以执行非常简单的任务。它在标准输出上打印其文本参数。
xxxxxxxxxx
[me@linuxbox ~]$ echo this is a test
this 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 ~]$ ls
Desktop ls-output.txt Pictures Templates
Documents Music Public Videos
我们可以进行以下扩展:
xxxxxxxxxx
[me@linuxbox ~]$ echo D*
Desktop Documents
和:
xxxxxxxxxx
[me@linuxbox ~]$ echo *s
Documents 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/bob
shell允许通过扩展执行算术运算。这允许我们将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}-Back
Front-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}}b
aA1b aA2b aB3b aB4b
那么,这有什么好处呢?最常见的应用程序是创建要创建的文件或目录列表。例如,如果我们是摄影师,并且有大量图像要按年和月组织,我们可能要做的第一件事就是创建一系列以数字“年-月”格式命名的目录。这样,目录名称将按时间顺序排序。我们可以键入一个完整的目录列表,但这需要大量的工作,而且容易出错。相反,我们可以这样做:
xxxxxxxxxx
[me@linuxbox ~]$ mkdir Photos
[me@linuxbox ~]$ cd Photos
[me@linuxbox Photos]$ mkdir {2007..2009}-{01..12}
[me@linuxbox Photos]$ ls
2007-01 2007-07 2008-01 2008-07 2009-01 2009-07
2007-02 2007-08 2008-02 2008-08 2009-02 2009-08
2007-03 2007-09 2008-03 2008-09 2009-03 2009-09
2007-04 2007-10 2008-04 2008-10 2009-04 2009-10
2007-05 2007-11 2008-05 2008-11 2009-05 2009-11
2007-06 2007-12 2008-06 2008-12 2009-06 2009-12
Pretty slick,很丝滑!
在本章中,我们将只简要地讨论参数扩展,但稍后我们将广泛地介绍它。这是一个在shell脚本中比直接在命令行上更有用的功能。它的许多功能都与系统存储小块数据并为每个块命名的能力有关。许多这样的块,更恰当地称为 variables ,变量,可供我们检查。例如,名为 USER
的变量包含我们的用户名。要调用参数展开并显示 USER
的内容,我们可以这样做:
xxxxxxxxxx
[me@linuxbox ~]$ echo $USER
me
要查看可用变量的列表,请尝试以下操作:
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/cp
quote —— 引号
现在我们已经看到了shell可以执行扩展的多种方式,是时候学习如何控制它了。例如:
xxxxxxxxxx
[me@linuxbox ~]$ echo this is a test
this is a test
或者这个:
xxxxxxxxxx
[me@linuxbox ~]$ echo The total is $100.00
The total is 00.00
在第一个示例中,shell的单词分割从 echo
命令的参数列表中删除了额外的空格。在第二个示例中,参数扩展用空字符串替换了值 $1
,因为它是一个未定义的变量。shell提供了一种称为引用(quoting)的机制,可以选择性地抑制不需要的扩展。
我们将讨论的第一种引用是双引号(double quotes)。如果我们把文本放在双引号内,shell使用的所有特殊字符都会失去其特殊含义,被视为普通字符。例外情况是 $
、\
(反斜杠)和 ```(后引号)。这意味着单词分割、路径名扩展、波浪号扩展和大括号扩展被抑制,但参数扩展、算术扩展和命令替换仍在进行。使用双引号,我们可以处理包含嵌入式空格的文件名。假设我们是一个名为 two words.txt 的文件的不幸受害者。如果我们试图在命令行上使用它,分词会导致它被视为两个单独的参数,而不是所需的单个参数。
xxxxxxxxxx
[me@linuxbox ~]$ ls -l two words.txt
ls: cannot access two: No such file or directory
ls: 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 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
我们应该花点时间看看双引号对命令替换的影响。首先,让我们更深入地了解一下分词是如何工作的。在前面的例子中,我们看到了分词是如何删除文本中的多余空格的。
xxxxxxxxxx
[me@linuxbox ~]$ echo this is a test
this 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 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
首先,未加引号的命令替换导致命令行包含49个参数。在第二种情况下,它产生了一个带有一个参数的命令行,其中包括嵌入的空格和换行符。
如果我们需要抑制所有展开,我们使用单引号,single quotes 。以下是无引号、双引号和单引号的比较:
xxxxxxxxxx
[me@linuxbox ~]$ echo text ~/*.txt {a,b} $(echo foo) $((2+2)) $USER
text /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
命令,一个只需等待指定秒数然后退出的简单程序,我们可以创建一个基本的倒计时器:
xxxxxxxxxx
sleep 10; echo -e "Time's up\a"
也可以这样写:
xxxxxxxxxx
sleep 10; echo "Time's up" $'\a'
随着我们继续使用shell,我们会发现扩展和引用的使用频率会越来越高,因此很好地理解它们的工作方式是有意义的。事实上,可以说它们是学习shell最重要的科目。如果不正确理解扩展,shell将永远是神秘和混乱的根源,其大部分潜在的能量都会被浪费。
官方文档: Bash Reference Manual