Skip to content

BASH

参考1

参考2

bash 是一种 lax interpreter, 有太多的 pitfalls, 所以它本身只适合物尽其用得用来编写简单的脚本,并不适合用来做太多事:当做一件事情的 bash 代码量开始变庞大的时候,应该考虑换用其他较正式的语言编写。

概述

bash 在接收到命令后,将以空格把命令分割成一个个 token,并对每个 token 执行扩展(glob)。扩展之后,才根据结果来进行命令的实际执行。这里的扩展只执行一次,也就是说如果扩展后的结果还有一些特殊的 $, * 符号,bash 是不会递归扩展他们的,这样太复杂。

扩展可以是:

  • 单纯的替换
  • 根据已有文件扩展
  • 字符串扩展(类似正则,但不一样)
  • 根据 bash 变量扩展

扩展的行为可以用过 shopt 命令来控制。

扩展(expansion)

针对特殊字符的单纯替换

波浪线 ~string 将被字符串替换为对应的用户目录:

sh
pwd # /home/me/anywhere
echo ~ # /home/me
echo ~xiaoyan13 # /home/xiaoyan13
echo ~root # /root

echo ~+ # /hoem/me/anywhere. `~+` 被特别处理为当前目录,即 `pwd`

根据已有文件

? / * / [] 属于根据已有文件扩展,其中 ?[] 匹配单个字符,* 匹配 0 或多个字符。

sh
ll # a.txt b.txt
ls [abc].txt # a.txt b.txt
ls ?.txt # a.txt b.txt
ls *.txt # a.txt b.txt

# [] 内部可以使用 `-` 和 `!/^` 表达更丰富的语义
ls [a-c].txt # a.txt. b.txt  `a-c` 被展开为 abc
ls [-abc].txt # `-` 位于首部或尾部就不会展开了,所以这样就可以匹配到 `-` 了

ls [!ac].txt # b.txt. `!` 和 `^` 都表示排除。

[[:xxx:]][] 的另一种语法,扩展成某一类(xxx)所代表的字符之中的一个。

sh
echo [[:upper:]]* # 命令输出所有大写字母开头的文件的名字

最后,文件名扩展在不匹配时,会原样输出。警惕这一点:

sh
ls # 假设没有任何文件的话
echo * # 则输出 `*`

根据字符串处理的替换 {}

{,} 属于字符串替换(类似于正则替换,与现有文件无关)。注意,内部不要含有空格。

sh
echo {1,2,3,233} # 等价于 echo 1 2 3 233
echo d{a,b,c}g # echo dag dbg dcg

cp a.log{,.bak} # cp a.log a.log.bak

大括号和其他模式混合在一起的时候,它的优先级最高;大括号支持嵌套,类似于循环的嵌套,不过这样写很复杂了,少用:

sh
echo a{A{1,2},B{3,4}}b # aA1b aA2b aB3b aB4b

echo /bin/{cat,b*} # 等价于 echo /bin/cat /bin/b*, 再进一步解析

大括号有 .. 语法,支持范围匹配。

sh
echo {c..a} # c b a

量词语法

基于上面几种匹配模式,引入量词语法。

  • ?(pattern-list):模式匹配零次或一次。
  • *(pattern-list):模式匹配零次或多次。
  • +(pattern-list):模式匹配一次或多次。
  • @(pattern-list):只匹配一次模式。
  • !(pattern-list):匹配给定模式以外的任何内容。

基于 $ 的扩展

$ 开头类型的 token 将被匹配为 bash 的一类特殊模式,有 shell 变量、命令、甚至运算。

sh
echo $SHELL # echo /bin/bash

${}{} 内部有自己的语法。其内部的具体语法稍后讨论。这涉及到 bash 脚本。

想要输出 $ 而不是进行扩展,使用 \$ 来转义一下。

sh
echo \$date # $date

command substitution

bash
$ echo 'Hello world.' > hello.txt
$ cat hello.txt
Hello world.
$ echo "The file <hello.txt> contains: $(cat hello.txt)"
The file <hello.txt> contains: Hello world.

$() 将被替换为括号内部的命令的标准输出结果。

重定向(Redirection)

文件描述符

  • input: 0
  • standard ouput: 1
  • error output: 2

描述符的继承与传递

  1. 命令的描述符继承自 bash,即默认输入 0 为 keyboard, 输出 1 和 2 为 terminal display;
  2. 命令之间通过管道 | 传递描述符。

更多有用的描述符使用

bash
# exec 空命令,可以用于更改描述符
# 3>&1: 由于 fd3 不存在,所以创建了描述符 3,并将其指向标准输出 1
# >mylog: 将 fd1 指向 mylog
exec 3>&1 >mylog # fd1:mylog fd2:terminal fd3:terminal
echo moo
# 3>&- 用于关闭 fd3. `>&-` 用于关闭输出符。
# 而 `<&-0` 用于关闭输入符,它很少被用到。
exec 1>&3 3>&-
bash
ping 127.0.0.1 &>results # &>results 等价于将 fd1 和 fd2 都指向 results: >results 2>&1
bash
echo Hello >~/world
echo World >>~/world # appending 符号 `>>` 将在末尾添加而非覆写,用于在避免覆写文件内容
echo World &>>~/world # &> 的 appending 版本
bash
# <<<"..\n allowed..." 将后方字符串作为 fd0 的输入。
cat <<<"Hello world.
Since I started learning bash, you suddenly seem so much bigger than you were before."
bash
# 打开 fd5,该文件描述符同时读(如果对方给了输出)和写对方。常用于网络。
exec 5<>/dev/tcp/ifconfig.me/80
echo "GET /ip HTTP/1.1
Host: ifconfig.me
" >&5
cat <&5

需要保持明白的是,一个程序可以同时持有多个文件描述符,但是同一时间最多只有 2 个描述符在工作,即一个输入和一个输出。

其他必要知识

文件名可以使用通配符

他们被视为普通的字符处理。他们在显示和使用中被引号引住即可。bash 规定,任何被单引号引住的 token 都不会再被解析和模式匹配。

sh
touch 'fo*'
ls # 'fo*'

?* 都不能匹配 / 字符

由于这两个字符是基于文件替换的,所以,默认情况下 ?* 都不能匹配 / 字符,即不匹配当前目录更深的子目录下的文件。如果要匹配子目录的文件,则像下面这样写:

sh
ls */*.txt # 显式的指出 `/`

ls **/*.txt # `**` 将作为 `*` 的增强版,它能够匹配到 `/`,所以 `**` 可以匹配任意多层次的目录

转义字符在命令行中的处理

由前面介绍的那样,很多本来是普通字符的字符,被 bash 当做语法特殊处理了,所以他们本身就不能表达原来的普通字符了。所以转义字符就是用来解决这一点的,所有转义字符处理后将会被原样输出,而不会再被特殊处理:

  • \\n 表示 '' (用作换行)
  • \\ 表示 \
  • \$ 表示 $

引号

  • ''(单引号)内的东西,均不会被 bash 解释,即单纯的字符串。
  • $'' 内的东西,只有 \ 会被识别。也就是说,此字符串的识别和 bash 无关,完全取决于环境。它不会识别 bash 内置的转义字符和 $ 等特殊语法,只会识别通用的转义字符。
  • ""(双引号)在我们需要借助 bash 提供的变量等功能的时候使用。它内部,bash 内置的三个特殊字符 $, \, ` 会被识别;\ 也只会识别 bash 内置的那部分转义字符,不会识别我们通用的转义字符,比如 \n, \t 之类。也就是说,双引号内部的识别完全取决于 bash 的实现,而与外部无关,这和 $'' 刚好对立。

最后,只要被引号引住,就意味着其本身作为一个整体 token 存在,所以,空格不会被删除:

sh
a="1 2  3"
echo $a # 1 2 3
echo "$a" # 1 2  3

而这也是是否用引号引住的差别。是否用引号引住的主要差别是:

  • 是否会根据空格拆开变量
  • 是否会去处理通配符:使用引号默认导致通配符失效,bash 不再处理他们。