第七章:以Shell的眼光看待世界

在本章中,我们将研究当我们按 Enter 键时在命令行上发生的一些“魔法”。虽然我们将研究shell的几个有趣而复杂的功能,但我们将只使用一个新命令。

第七章:以Shell的眼光看待世界扩展路径名扩展隐藏文件的路径名扩展波浪号扩展算术扩展大括号扩展参数扩展命令替换引号双引号单引号转义字符反斜杠转义序列总结

扩展

expansion —— 扩展

每次我们键入命令并按 Enter 键时, bash 都会在执行命令之前对文本执行几次替换。我们已经看到了几个例子,说明一个简单的字符序列,例如 * ,对shell有很多意义。实现这一点的过程称为 expansion ,扩展。通过扩展,我们输入一些东西,然后在shell对其进行操作之前将其展开为其他东西。为了演示我们的意思,让我们看看 echo 命令。 echo 是一个内置的shell,可以执行非常简单的任务。它在标准输出上打印其文本参数。

这很简单。传递给echo的任何参数都会显示出来。让我们再举一个例子:

那么,刚才发生了什么?为什么 echo 没有打印 * ?正如我们在通配符工作中回忆的那样, * 字符表示匹配文件名中的任何字符,但我们在最初的讨论中没有看到的是shell是如何做到这一点的。简单的答案是,在执行 echo 命令之前,shell会将 * 扩展为其他内容(在本例中,是当前工作目录中的文件名)。当按下 Enter 键时,shell会在执行命令之前自动展开命令行上的任何限定字符,因此 echo 命令从未看到 * ,只看到其展开的结果。知道这一点后,我们可以看到 echo 的表现符合预期。

路径名扩展

通配符工作的机制称为 pathname expansion ,路径名扩展。如果我们尝试前几章中使用的一些技术,我们会发现它们实际上是扩展。给定一个如下所示的主目录:

我们可以进行以下扩展:

和:

或者:

超越我们的主目录,我们可以这样做:

隐藏文件的路径名扩展

众所周知,以句点字符开头的文件名是隐藏的。路径名扩展也尊重这种行为。以下展开不会显示隐藏的文件。

echo *

乍一看,我们可以通过以前导句点开始模式来在扩展中包含隐藏文件,如下所示:

echo .*

它几乎奏效了。然而,如果我们仔细检查结果,我们会看到这些名字 . 以及 .. 也将出现在结果中。由于这些名称指向当前工作目录及其父目录,因此使用此模式可能会产生不正确的结果。如果我们尝试以下命令,我们可以看到这一点:

ls -d .* | less

为了在这种情况下更好地执行路径名扩展,我们必须采用更具体的模式。

echo .[!.]*

此模式扩展到每个仅以一个句点开头,后跟任何其他字符的文件名。这将适用于大多数隐藏文件(尽管它仍然不包括带有多个前导句点的文件名)。带有 -A 选项(“几乎所有”)的 ls 命令将提供隐藏文件的正确列表。

ls -A

波浪号扩展

正如我们在介绍 cd 命令时可能记得的那样,波浪号字符( ~ )具有特殊含义。当在单词开头使用时,它会扩展到指定用户的主目录名称,或者如果没有指定用户,则扩展到当前用户的主文件夹名称。

如果用户“bob”有一个账号,然后它扩展进入:

算术扩展

shell允许通过扩展执行算术运算。这允许我们将shell提示符用作计算器。

算术扩展,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),我们可以使用以下方法:

单个括号可用于对多个子表达式进行分组。使用此技术,我们可以重写前面的示例,并使用单个扩展而不是两个扩展获得相同的结果:

这是一个使用除法和余数运算符的示例。请注意整数除法的效果:

算术扩展在【第34章】中有更详细的介绍。

大括号扩展

也许最奇怪的扩展叫做 brace expansion ,大括号扩展。有了它,我们可以从包含大括号的模式中创建多个文本字符串。这里有一个例子:

【sh没有大括号扩展】

要用大括号扩展的模式可能包含称为前导码(preamble)的前导部分和称为后记(postscript)的尾随部分。大括号表达式本身可以包含逗号分隔的字符串列表,也可以包含整数或单个字符的范围。该模式不能包含未加引号的空格。以下是一个使用整数范围的示例:

在bash 4.0及更高版本中,整数也可以像这样进行零填充(zero-padded):

以下是按相反顺序排列的一系列字母:

大括号扩展可以嵌套:

那么,这有什么好处呢?最常见的应用程序是创建要创建的文件或目录列表。例如,如果我们是摄影师,并且有大量图像要按年和月组织,我们可能要做的第一件事就是创建一系列以数字“年-月”格式命名的目录。这样,目录名称将按时间顺序排序。我们可以键入一个完整的目录列表,但这需要大量的工作,而且容易出错。相反,我们可以这样做:

Pretty slick,很丝滑!

参数扩展

在本章中,我们将只简要地讨论参数扩展,但稍后我们将广泛地介绍它。这是一个在shell脚本中比直接在命令行上更有用的功能。它的许多功能都与系统存储小块数据并为每个块命名的能力有关。许多这样的块,更恰当地称为 variables ,变量,可供我们检查。例如,名为 USER 的变量包含我们的用户名。要调用参数展开并显示 USER 的内容,我们可以这样做:

要查看可用变量的列表,请尝试以下操作:

您可能已经注意到,对于其他类型的扩展,如果我们键入了错误的模式,扩展将不会发生, echo 命令只会显示键入错误的模式。使用参数扩展时,如果我们拼错了变量的名称,扩展仍然会发生,但会导致一个空字符串:

命令替换

命令替换允许我们将命令的输出用作扩展。

我最喜欢的一个是这样的:

在这里,我们将 which cp 的结果作为参数传递给 ls 命令,从而获得 cp 程序的列表,而无需知道其完整路径名。我们不仅限于简单的命令。可以使用整个管道(此处仅显示部分输出):

在这个例子中,管道的结果变成了 file 命令的参数列表。

bash 也支持旧shell程序使用的命令替换语法。它使用后引号(backquotes)而不是美元符号和括号。

引号

quote —— 引号

现在我们已经看到了shell可以执行扩展的多种方式,是时候学习如何控制它了。例如:

或者这个:

在第一个示例中,shell的单词分割从 echo 命令的参数列表中删除了额外的空格。在第二个示例中,参数扩展用空字符串替换了值 $1 ,因为它是一个未定义的变量。shell提供了一种称为引用(quoting)的机制,可以选择性地抑制不需要的扩展。

双引号

我们将讨论的第一种引用是双引号(double quotes)。如果我们把文本放在双引号内,shell使用的所有特殊字符都会失去其特殊含义,被视为普通字符。例外情况是 $\ (反斜杠)和 ```(后引号)。这意味着单词分割、路径名扩展、波浪号扩展和大括号扩展被抑制,但参数扩展、算术扩展和命令替换仍在进行。使用双引号,我们可以处理包含嵌入式空格的文件名。假设我们是一个名为 two words.txt 的文件的不幸受害者。如果我们试图在命令行上使用它,分词会导致它被视为两个单独的参数,而不是所需的单个参数。

通过使用双引号,我们停止了单词分割,得到了预期的结果;此外,我们甚至可以修复损坏:

那里!现在我们不必一直输入那些讨厌的双引号。

记住,参数展开、算术展开和命令替换仍然发生在双引号内。

我们应该花点时间看看双引号对命令替换的影响。首先,让我们更深入地了解一下分词是如何工作的。在前面的例子中,我们看到了分词是如何删除文本中的多余空格的。

默认情况下,分词会查找空格、制表符和换行符(换行符)的存在,并将其视为单词之间的分隔符。这意味着未加引号的空格、制表符和换行符不被视为文本的一部分。它们仅作为分隔物。由于它们将单词分成不同的参数,我们的示例命令行包含一个命令,后面跟着四个不同的参数。如果我们添加双引号:

单词分割被抑制,嵌入的空格不被视为分隔符;相反,他们成为了参数(argument)的一部分。添加双引号后,我们的命令行包含一个命令,后面跟着一个参数。

换行符被分词机制视为分隔符,这一事实对命令替换产生了有趣但微妙的影响。请考虑以下示例:

首先,未加引号的命令替换导致命令行包含49个参数。在第二种情况下,它产生了一个带有一个参数的命令行,其中包括嵌入的空格和换行符。

单引号

如果我们需要抑制所有展开,我们使用单引号,single quotes 。以下是无引号、双引号和单引号的比较:

正如我们所看到的,随着引用的每一个后续级别,越来越多的扩展都被抑制了。

转义字符

escaping characters——转义字符

有时我们只想引用一个字符。为此,我们可以在字符前加一个反斜杠(backslash),在这种情况下称为转义字符。通常,这是在双引号内完成的,以选择性地防止扩展。

使用转义来消除文件名中字符的特殊含义也很常见。例如,可以在文件名中使用通常对shell具有特殊含义的字符。这将包括 $!& 、空格等。要在文件名中包含特殊字符,我们可以这样做:

要允许反斜杠字符出现,请键入两个连续的反斜杠—— \\ 将其转义。请注意,在单引号内,反斜杠失去了其特殊含义,被视为普通字符。

反斜杠转义的另一个用途是抑制别名。例如,假设 ls 命令别名为 ls='ls --color=auto' ,这是许多Linux发行版的默认值,我们可以在命令前加一个反斜杠,别名将被忽略, ls 命令将在没有颜色选项的情况下执行。

反斜杠转义序列

除了作为转义符的作用外,反斜杠还用作表示法的一部分,表示某些称为控制代码(control codes)的特殊字符。ASCII编码方案中的前32个字符用于向电传打字机类设备传输命令。其中一些代码是熟悉的(制表符——tab、退格——backspace、换行——linefeed和回车——carriage return),而另一些则不是(null、传输结束——end-of-transmission和确认——acknowledge)。

转义序列含义
\aBell(使计算机发出蜂鸣声的警报)
\b退格
\n换行——newline。
在类Unix系统中,这会发生换行——linefeed。
\r回车——carriage return
\t制表符——tab

上表列出了一些常见的反斜杠转义序列。使用反斜杠表示的想法起源于C编程语言,并被许多其他语言采用,包括shell。

echo 中添加 -e 选项将能够解释转义序列。您也可以将它们放在 $' ' 内。在这里,使用 sleep 命令,一个只需等待指定秒数然后退出的简单程序,我们可以创建一个基本的倒计时器:

也可以这样写:

总结

随着我们继续使用shell,我们会发现扩展和引用的使用频率会越来越高,因此很好地理解它们的工作方式是有意义的。事实上,可以说它们是学习shell最重要的科目。如果不正确理解扩展,shell将永远是神秘和混乱的根源,其大部分潜在的能量都会被浪费。

官方文档: Bash Reference Manual