专业 Bash 语法,成为更高级的 Shell 艺术家
bash 和 Shell 艺术
作为 Unix 系列操作系统的用户,你可能每天都在使用 Shell。在众多 Unix 系列的 Shell 中,被认为是最为普及的 bash,本文我们将介绍一些似乎不太常见的语法。
操作系统环境
- Ubuntu 20.04.3 LTS
- Kernel: 6.5.0-060500-generic
- GNU bash,版本 5.1.16(1)-release (x86_64-pc-linux-gnu)
管道的语法
整个管道的定义如下:
管道(pipeline)是由一个或多个由控制运算符 | 或 |& 分隔的命令组成的序列。 以下是管道的格式:
[time [-p]] [ ! ] command [ [|||&] command2 … ]
command 的标准输出通过管道连接到 command2 的标准输入。
例子
┌──(linuxmi㉿linuxmi)-[~]
└─$ seq 10 | wc -l
10

因此,通过这样的方式,seq 10 的标准输出被连接到 wc -l 的标准输入,因此 seq 10 输出的1到10的10行将被读取到 wc -l 中,并 wc -l 输出的10将显示在终端上。
command |& command2
使用 |& 时,command 的标准错误输出也通过管道连接到 command2 的标准输入。这是 2>&1 | 的简写形式。这个标准错误输出的隐式重定向是在命令指定的所有重定向之后执行的。
如果使用 | 作为管道的分隔符,则仅将左侧命令的标准输出连接到右侧命令的标准输入。
例如:

这样做的话,seq 会将1到10的10行写入标准输出,dd 读取这10行并将其直接写入标准输出,tail -n 5 从读取的10行中读取最后的5行并将其写入标准输出。
在这种情况下,dd 会将关于读写的数据量等统计信息写入标准错误输出,并且这些信息会直接显示在终端上。
如果使用 |& 作为管道分隔符,那么左侧命令的标准错误输出也会连接到右侧命令的标准输入。
例如:
┌──(linuxmi㉿linuxmi)-[~/www.linuxmi.com]
└─$ seq 10 | dd |& tail -n 5
9
10
输入了 0+1 块记录
输出了 0+1 块记录
21 字节已复制,4.2766e-05 s,491 kB/s┌──(linuxmi㉿linuxmi)-[~/www.linuxmi.com]
└─$ seq 10 | dd |& tail -n 5

这样做的话,dd 写入标准错误输出的统计信息也会连接到 tail -n 5 的标准输入。tail -n 5 会从标准输入中读取1到10的10行以及统计信息的全部内容,并将其中的最后5行写入标准输出。
这个过程是
┌──(linuxmi㉿linuxmi)-[~/www.linuxmi.com]
└─$ seq 10 | dd 2>&1 | tail -n 5
9
10
输入了 0+1 块记录
输出了 0+1 块记录
21 字节已复制,0.00715583 s,2.9 kB/s

这等效于:
! command | command2
除非启用了pipefail选项,否则管道的返回状态将成为最后一个命令的退出状态。如果在管道之前有感叹号 !,那么该管道的退出状态将是上述退出状态的逻辑否定。
Unix系列操作系统的进程可以通过 exit(2) 系统调用等方式返回退出状态。Bash可以利用执行命令时的退出状态进行各种控制。此外,对于管道和复合命令,也定义了退出状态,并可以同样使用。
管道的退出状态取决于shell选项pipefail的设置,但在默认设置下(pipefail未启用),管道的最后(最右边)命令的退出状态将成为整个管道的退出状态。
例如:
┌──(linuxmi㉿linuxmi)-[~/www.linuxmi.com]
└─$ seq 5 | grep 7; echo $?
1

这样做的话,seq 5 会写出1到5的5行,grep 7 会在其中搜索7,但由于没有匹配的行,它以退出状态1结束。整个管道的退出状态也是1。echo $? 用于显示刚刚执行的命令的退出状态,因此终端上将显示1。
在这里,
┌──(linuxmi㉿linuxmi)-[~/www.linuxmi.com]
└─$ ! seq 5 | grep 7; echo $?
0

这样做的话,整个管道的退出状态将被逻辑否定,变为0。
复合命令的语法
在bash中,提供了用于组合多个命令以执行的语法,例如管道、列表、复合命令等。
列表(list)是指使用操作符;、&、&&、||分隔的一个或多个管道。
这里所说的管道是指由|分隔的一个或多个命令的序列。
因此,简单地写一个命令也是管道的一种。
例如:
┌──(linuxmi㉿linuxmi)-[~/www.linuxmi.com]
└─$ seq 5 | grep 3
3

┌──(linuxmi㉿linuxmi)-[~/www.linuxmi.com]
└─$ echo www.linuxmi.com
www.linuxmi.com

考虑到这两个管道,可以使用seq 5 | grep 3, echo hoge 这个顺序,用四种不同的列表操作符将这两个管道排列成四种不同的列表。

如果列表运算符是&,则 seq 5 | grep 3 将在后台运行,而不等待其完成,然后立即执行 echo linuxmi。因此,两个管道的输出可能会交错。
如果列表运算符是;,则首先执行 seq 5 | grep 3,等待其完成,然后 (必然) 执行 echo linuxmi。
如果列表运算符是&&,则首先执行 seq 5 | grep 3,只有在其退出状态为0时才执行 echo linuxmi。
如果列表运算符是||,则首先执行 seq 5 | grep 3,只有在其退出状态不为0时才执行 echo linuxmi。
{ list; }
{ list; }
list 仅在当前shell环境中执行。
list 的结尾必须是换行符或分号。
这称为组合命令(group command)。
返回状态是 list 的退出状态。
请注意,与元字符(例如 ( 和 ))不同,{ 和 } 是保留字,并且必须出现在被视为保留字的地方。由于它们不会被划分为单词,因此在 { 和 } 与列表之间必须用空格或shell元字符分隔。
如前所述
管道是由使用|分隔的一个或多个命令组成的序列, 而列表是由使用;, &, &&, ||之一的运算符分隔的一个或多个管道的组合。
如果您想要将整个列表视为单个命令,将其嵌入到管道的元素中或使用重定向,您可以使用两种类型的复合命令来实现。
┌──(linuxmi㉿linuxmi)-[~/www.linuxmi.com]
└─$ (seq 5 | grep 3 && echo linuxmi) | wc -l
2
┌──(linuxmi㉿linuxmi)-[~/www.linuxmi.com]
└─$ { seq 5 | grep 3 && echo linuxmi; } | wc -l
2

(list) 的形式的复合命令会在子shell(子进程)中执行列表。 而 { list; } 的形式的复合命令会在当前shell进程中直接执行列表(没有fork的开销)。
由于 ( ) 是shell的元字符,所以前后不加空格也会被解释为单词分隔。 而 { } 是shell的保留字,如果前后的单词无法分隔,则不加空格会导致语法错误。
┌──(linuxmi㉿linuxmi)-[~/www.linuxmi.com]
└─$ {seq 5 | grep 3 && echo linuxmi; } | wc -l
bash: 未预期的记号 "}" 附近有语法错误
# { 和 seq 无法进行单词分隔。 ┌──(linuxmi㉿linuxmi)-[~/www.linuxmi.com] └─$ { seq 5 | grep 3 && echo linuxmi;}| wc -l 2 # ; 和 | 都是用于单词分隔的元字符,所以即使紧挨着也没关系

引号的语法
|、&、;、(、)、<、>、$ 以及空格等字符在Shell中具有特殊意义,因此在表示原始字符本身时,可以使用引号来禁用特殊含义和功能。
bash 中有三种引号:
- 在想要引号的字符前加上反斜杠
/。 - 用单引号括起来(所有字符都失去特殊含义)。
- 用双引号括起来(除了
$、`、!之外的字符都失去特殊含义)。
$’str’
如果在用 ASCII 文本编写的 Shell 脚本中表示换行符、退格符、转义字符等控制字符或 ASCII 范围之外的 Unicode 字符,仅仅使用引号来禁用特殊含义是不够的,还需要使用某种 ASCII 表示方法来表示这些字符。
可以使用 bash 的内建命令或外部命令,如 echo 或 printf,以及 bash 的命令替换功能,例如:
┌──(linuxmi㉿linuxmi)-[~/www.linuxmi.com]
└─$ touch "$(echo -e '/U1F363')"; ls
🍣 1 93139 linuxmi--- linuxmi.com linuxmi.py linuxmi.sh
┌──(linuxmi㉿linuxmi)-[~/www.linuxmi.com]
└─$ touch "$(printf '/U1F363')"; ls
🍣 1 93139 linuxmi--- linuxmi.com linuxmi.py linuxmi.sh

虽然也有类似下面这样的方法,但是由于命令在子shell中执行会带来一些额外的开销,并且代码会变得冗长。在这里,可以使用 $'str' 这种引号的语法:
┌──(linuxmi㉿linuxmi)-[~/www.linuxmi.com]
└─$ touch $'/U1F363'; ls
🍣 1 93139 linuxmi--- linuxmi.com linuxmi.py linuxmi.sh

这样的表达方式更为简洁,且无需执行子shell导致的额外开销。
总结
介绍的语法仅仅是bash语法的冰山一角。Bash还有许多有趣且实用的语法,如果你有兴趣的话,不妨阅读一下man bash。
如果你对本文还有什么疑问与建议,请在Linux迷 www.linuxmi.com 的评论给我们留言,如果你觉得本文对你有所启发与帮助,请分享给你的朋友与同事。
相关:
使用 bash 脚本创建 Linux 健康检查工具 https://www.linuxmi.com/linux-health-check-tool-using-bash-script.html
The post 专业 Bash 语法,成为更高级的 Shell 艺术家 first appeared on Linux迷.

共有 0 条评论