Bash Shell
2019-01-11 22:09:23 阿炯

GNU Bourne Again Shell (Bash) ,这是大多数 Linux 发行版上的默认 shell。系Unix Shell的一种,在1987年由布莱恩·福克斯为了GNU计划而编写。首发于1989年6月,原先是计划用在GNU操作系统上,但能运行于大多数类Unix系统的操作系统之上,是Linux操作系统上的官方Shell。Bash 是一个完整的POSIX 规范项目,除了有交互式命令列编辑功能,还在基架构上支援工作控制,并提供类似csh的功能等。包括Linux与Mac OS X v10.4起至macOS Mojave都将它作为默认shell,而自macOS Catalina,默认Shell以zsh取代,主要是其新版本的Bash-v5升级为GPLv3授权。


Bash是Bourne shell的后继兼容版本与开放源代码版本,它的名称来自Bourne shell(sh)的一个双关语(Bourne again / born again):Bourne-Again SHell。

Bash是一个命令处理器,通常运行于文本窗口中,并能执行用户直接输入的命令;还能从文件中读取命令,这样的文件称为脚本。和其他Unix shell 一样,它支持文件名替换(通配符匹配)、管道、here文档、命令替换、变量,以及条件判断和循环遍历的结构控制语句。包括关键字、语法在内的基本特性全部是从sh借鉴过来的。其他特性,例如历史命令,是从csh和ksh借鉴而来。总的来说,Bash虽然是一个满足POSIX规范的shell,但有很多扩展。

一个名为Shellshock的安全漏洞在2014年9月初被发现,并迅速导致互联网上的一系列攻击。这个漏洞可追溯到1989年发布的1.03版本。该漏洞非常严重,因为使用Bash的CGI脚本会变得脆弱,使得攻击者可以执行任意的代码。这个漏洞与Bash通过环境变量把函数定义传递给shell子进程的方式相关。

语法与特性

bash的命令语法是Bourne shell命令语法的超集。数量庞大的Bourne shell脚本大多不经修改即可以在bash中执行,只有那些引用了Bourne特殊变量或使用了Bourne的内置命令的脚本才需要修改。其命令语法很多来自Korn shell(ksh)和C Shell(csh),例如命令行编辑,命令历史,目录栈,$RANDOM和$PPID变量,以及POSIX的命令置换语法:$(...)。作为一个交互式的shell,按下TAB键即可自动补全已部分输入的程序名,文件名,变量名等等。

使用'function'关键字时,Bash的函数声明与Bourne/Korn/POSIX脚本不兼容(Korn shell 有同样的问题)。不过Bash也接受Bourne/Korn/POSIX的函数声明语法。因为许多不同,Bash脚本很少能在Bourne或Korn解释器中运行,除非编写脚本时刻意保持兼容性。然而,随着Linux的普及,这种方式正变得越来越少。不过在POSIX模式下,Bash更加符合POSIX。

bash的语法针对Bourne shell的不足做了很多扩展,其中的一些列举如下。

1、花括号扩展

花括号扩展是一个从C shell借鉴而来的特性,它产生一系列指定的字符串(按照原先从左到右的顺序)。这些字符串不需要是已经存在的文件。
$ echo a{p,c,d,b}e
ape ace ade abe
$ echo {a,b,c}{d,e,f}
ad ae af bd be bf cd ce cf

花括号扩展不应该被用在可移植的shell脚本中,因为Bourne shell产生的结果不同。
#! /bin/sh
# 传统的shell并不产生相同结果
echo a{p,c,d,b}e # a{p,c,d,b}e

当花括号扩展和通配符一起使用时,花括号扩展首先被解析,然后正常解析通配符。因此可以用这种方法获得当前目录的一系列JPEG和PNG文件。
ls *.{jpg,jpeg,png}    # 首先扩展为*.jpg *.jpeg *.png,然后解析通配符
echo *.{png,jp{e,}g}   # echo显示扩展结果;花括号扩展可以嵌套。

除了列举备选项,还可以用“..”在花括号扩展中指定字符或数字范围。较新的Bash版本接受一个整数作为第三个参数,指定增量。
$ echo {1..10}
1 2 3 4 5 6 7 8 9 10
$ echo file{1..4}.txt
file1.txt file2.txt file3.txt file4.txt
$ echo {a..e}
a b c d e
$ echo {1..10..3}
1 4 7 10
$ echo {a..j..3}
a d g j

当花括号扩展和变量扩展一起使用时,变量扩展解析于花括号扩展之后。有时有必要使用内置的eval函数
$ start=1; end=10
$ echo {$start..$end}  # 由于解析顺序,无法得到想要的结果
{1..10}
$ eval echo {$start..$end} # 首先进行变量扩展的解析
1 2 3 4 5 6 7 8 9 10

2、使用整数

与Bourne shell不同的是bash不用另外生成进程即能进行整数运算。bash使用((...))命令和$[...]变量语法来达到这个目的:
 VAR=55    # 将整数55赋值给变量VAR
 ((VAR = VAR + 1))  # 变量VAR加1。注意这里没有'$'
 ((++VAR))    # 另一种方法给VAR加1。使用C语言风格的前缀自增
 ((VAR++))    # 另一种方法给VAR加1。使用C语言风格的后缀自增
 echo $((VAR * 22)) # VAR乘以22并将结果送入命令
 echo $[VAR * 22]   # 同上,但为过时用法

((...))命令可以用于条件语句,因为它的退出状态是0或者非0(大多数情况下是1),可以用于是与非的条件判断:
 if((VAR == Y * 3 + X * 2))
 then
 echo Yes
 fi

 ((Z > 23)) && echo Yes

((...))命令支持下列比较操作符:'==', '!=', '>', '<', '>=',和'<='。

bash不能在自身进程内进行浮点数运算。当前有这个能力的unix shell只有KornShell和Z shell。

3、输入输出重定向

bash拥有传统Bourne shell缺乏的I/O重定向语法。bash可以同时重定向标准输出和标准错误,这需要使用下面的语法:
command &> file

这比等价的Bourne shell语法"command > file 2>&1"来的简单。2.05b版本以后,bash可以用下列语法重定向标准输入至字符串(称为here string):
command <<< "string to be read as standard input"

如果字符串包括空格就需要用引号包裹字符串。

例子: 重定向标准输出至文件,写数据,关闭文件,重置标准输出。
# 生成标准输出(文件描述符1)的拷贝文件描述符6
 exec 6>&1
 # 打开文件"test.data"以供写入
 exec 1>test.data
 # 产生一些内容
 echo "data:data:data"
 # 关闭文件"test.data"
 exec 1>&-
 # 使标准输出指向FD 6(重置标准输出)
 exec 1>&6
 # 关闭FD6
 exec 6>&-

打开及关闭文件
 # 打开文件test.data以供读取
 exec 6<test.data
 # 读文件直到文件尾
 while read -u 6 dta
 do
   echo "$dta"
 done
 # 关闭文件test.data
 exec 6<&-

抓取外部命令的输出
  # 运行'find'并且将结果存于VAR
  # 搜索以"h"结尾的文件名
  VAR=$(find . -name "*h")

4、进程内的正则表达式

bash 3.0支持进程内的正则表达式,使用下面的语法:
 [[ string =~ regex ]]

正则表达式语法同regex(7) man page所描述的一致。正则表达式匹配字符串时上述命令的退出状态为0,不匹配为1。正则表达式中用圆括号括起的子表达式可以访问shell变量BASH_REMATCH,如下:
 if [[ abcfoobarbletch =~ 'foo(bar)bl(.*)' ]]
 then
 echo The regex matches!
 echo $BASH_REMATCH      -- outputs: foobarbletch
 echo ${BASH_REMATCH[1]} -- outputs: bar
 echo ${BASH_REMATCH[2]} -- outputs: etch
 fi

使用这个语法的性能要比生成一个新的进程来运行grep命令优越,因为正则表达式匹配在bash进程内完成。如果正则表达式或者字符串包括空格或者shell 关键字,(诸如'*'或者'?'),就需要用引号包裹。Bash 4 开始的版本已经不需要这么做了。

5、转义字符

$'string' 形式的字符串会被特殊处理。字符串会被展开成string,并像C语言那样将反斜杠及紧跟的字符进行替换。反斜杠转义序列的转换方式如下:
转义字符     扩展为...
\a     响铃符
\b     退格符
\e     ANSI转义符,等价于\033
\f     馈页符
\n     换行符
\r     回车符
\t     水平制表符
\v     垂直制表符
\\     反斜杠
\'     单引号
\nnn     十进制值为nnn的8-bit字符(1-3位)
\xHH     十六进制值为HH的8-bit字符(1或2位)
\cx     control-X字符

扩展后的结果将被单引号包裹,就好像美元符号一直就不存在一样。

双引号包裹的字符串前若有一个美元符号($"...")将会使得字符串被翻译成符合当前locale的语言。如果当前locale是C或者POSIX,美元符号会被忽略。如果字符串被翻译并替换,替换后的字符串仍被双引号包裹。

6、关联数组

Bash 4.0 开始支持关联数组,通过类似AWK的方式,对于多维数组提供了伪支持。

$ declare -A a    # 声明一个名为a的伪二位数组
$ i=1; j=2
$ a[$i,$j]=5    # 将键 "$i,$j" 与值 5 对应
$ echo ${a[$i,$j]}

7、移植性

调用Bash时指定 --posix 或者在脚本中声明 set -o posix ,可以使得Bash几乎遵循 POSIX 1003.2 标准。若要保证一个Bash脚本的移植性,至少需要考虑到 Bourne shell,即Bash取代的shell。Bash有一些传统的 Bourne shell 所没有的特性,包括以下这些:
某些扩展的调用选项
命令替换(即$())(尽管这是 POSIX 1003.2 标准的一部分)
花括号扩展
某些数组操作、关联数组
扩展的双层方括号判断语句
某些字符串生成操作
进程替换
正则表达式匹配符
Bash特有的内置工具
协进程

8、键盘快捷键

Bash默认使用Emacs的快捷键,可以通过 set -o vi 让它使用Vi的快捷键。

9、进程管理

Bash有两种执行命令的模式:批处理模式、并发模式。

批处理模式
要以 批处理模式 执行命令(即按照顺序),必须用;分隔,例子如下:
command1 ; command2

在这个例子中,当command1执行完毕,即执行command2。

并发模式
使command1后台运行,则单独在结尾处使用&,例子如下:
command1 &

要并发执行两个命令,它们必须用&分隔,例子如下:
command1 & command2

在这种情况下,command1 在后台执行(通过&),从而立即将控制返回到shell,以执行command2。

通过 Control+Z 可以将当前进程挂起(放置后台并暂停运行),可通过 fg 命令恢复至前台,也通过bg将挂起的进程后台运行。

查看进程状态
所有进程的情况,我们可以通过jobs命令查看,包含正在后台运行和停止了的。
$ jobs
[1]-  Running        command1 &
[2]+  Stopped        command2

上例的输出结果中,中括号包围的数字(如: "[1]" 和 "[2]" )为job的ID号。加号(+)用于指定fg和bg命令的默认对象job。减号( - )用于指定,若当前的默认对象job退出后,下一个默认对象job是谁。"Running" 和 "Stopped" 表示进程状态。 最后一个字段为命令。

小结:
一般命令在前台执行(fg),执行完毕后,控制返回给用户。
在命令后面加上&,它会在后台执行(bg),并将特殊的环境变量$!设置为该任务的进程ID。这时shell可以并发执行其他命令。
后台程序试图写入数据到终端设备时(与写入标准输出不同)可能被阻塞。
shell可以等待一个后台任务执行完成,只需使用wait命令,加上进程ID或者任务序号;也可以等待所有的后台任务,只需使用不加参数的wait。


启动过程

bash启动的时候会运行各种不同的脚本。

当bash作为一个登录的交互shell被调用,或者作为非交互shell但带有--login参数被调用时,它首先读入并执行文件/etc/profile。然后它会依次寻找~/.bash_profile,~/.bash_login,和~/.profile,读入并执行第一个存在且可读的文件。--noprofile参数可以阻止bash启动时的这种行为。

当一个登录shell退出时,bash读取并执行~/.bash_logout文件,如果此文件存在。

当一个交互的非登录shell启动后,bash读取并执行~/.bashrc文件。这个行为可以用--norc参数阻止。--rcfile file参数强制bash读取并执行指定的file而不是默认的~/.bashrc。

如果用sh来调用bash,bash在启动后进入posix模式,它会尽可能模仿sh历史版本的启动行为,以便遵守POSIX标准。用sh名字调用的非交互shell不会去读取其他启动脚本,--rcfile参数无效。

当bash以POSIX模式启动时(例如带有--posix参数)它使用POSIX标准来读取启动文件。在此模式下,交互shells扩展变量ENV,从以此为文件名的文件中读取命令并执行。

bash会探测自己是不是被远程shell守护程序运行(通常是rshd)。如果是,它会读取并执行~/.bashrc中的命令。但是rshd一般不会用rc相关参数调用shell,也不会允许指定这些参数。

Bourne shell和csh启动脚本的比较

Bash的特性是从Bourne shell和csh发展而来,因此一定程度上允许同Bourne shell的启动文件共享,并提供一些csh用户熟悉的启动特性。

1、设置可继承的环境变量

Bourne shell登陆时使用 ~/.profile 来设置环境变量,这些环境变量可以被子进程继承。Bash可以以兼容的方式使用~/.profile ,只需在Bash自有的脚本中显式执行下面这行代码。通过在~/.profile 中避免使用Bash特有的语法,就可以和Bourne shell保持兼容性。

. ~/.profile

2、别名和函数

更通用的函数以及借鉴自csh的“别名(alias)”很大程度上取代了Bourne shell的别名(alias)和函数。然而这两个特性一般不能从登录式shell中继承,在该登录式shell的子shell中,它们必须被重新定义。尽管有个环境变量ENV可以被用于这个问题,不过 csh 和 Bash 都可以用子shell的启动脚本直接处理。在Bash当中,~/.bashrc 是交互式子shell启动时执行的脚本。如果想要在登录式shell中使用 ~/.bashrc 定义的函数,可以在 ~/.bash_login 的环境变量后面加上这样一行:

. ~/.bashrc

3、登录与退出时执行的命令

最初登录时,csh 执行 ~/.login ,可以执行一些只在登录时执行的操作,例如显示系统负载、硬盘状态、是否收到新邮件、在日志中记录登录时间,等等。Bourne shell 可以在 ~/.profile 文件中模拟这种行为,但并没有预先定义文件名。可以在 ~/.bash_profile 文件的环境变量设置和函数定义的后面添加这样一行:

. ~/.bash_login

与之类似,csh还有一个文件 ~/.logout ,这个文件只在登录式shell退出时执行。Bash与之对应的文件是 ~/.bash_logout ,并且不需要专门的设置。在 Bourne shell 中,trap 这个内置工具可以实现类似的效果。

4、兼容旧环境的Bash启动脚本示例

下面这个 ~/.bash_profile 的框架与 Bourne shell 兼容,并且为 ~/.bashrc 和 ~/.bash_login 提供类似于 csh 的语义。[ -r 文件名] 测试指定文件是否存在,如果不存在,跳过 && 后面的部分

[ -r ~/.profile ] && . ~/.profile    # 只使用Bourne shell的语法设置环境变量
if [ -n "$PS1" ] ; then    # 判断是否是交互式shell
   [ -r ~/.bashrc     ] && . ~/.bashrc    # 加载~/.bashrc(tty、prompt、函数设置等)
   [ -r ~/.bash_login ] && . ~/.bash_login    # 执行登录式shell登录时的任务
fi    # if块的结束标志


操作系统相关的问题

一些 Unix 和 Linux 版本常在 /etc 放置 Bash 的系统级启动脚本。Bash在其标准的初始化过程中执行它们,不过其他启动脚本可以按照不同于Bash启动序列文档所述的顺序来读取这些文件。root 用户的文件默认内容,以及新用户被创建时系统提供的默认文件可能有问题。启动 X窗口系统 的启动脚本可能使用用户的 Bash 启动脚本尝试在窗口管理器启动之前设置用户的环境变量。这些问题常常可以通过使用 ~/.xsession 或者 ~/.xprofile 来读取 ~/.profile 而解决。

最新版本:5
2019年1月释出第 5 个主要版本这个版本主要修复了 v4.4 中几个明显的错误,同时也加入了一些新功能。v5.0 主要解決 v4.4的错误,改变对nameref解析的方法,和一系列以模糊测试发现的潜在查出范围的错误。另外5.0中在不执行字符拆解以符合Posix标准解释的情況下,在不同的上下文中,$@和$*的扩充也有许多改变,而且还解決不少极端案例以增加Posix的一致性。而本次最重要的功能更新,是增加了新的环境变量,BASH_ARGV0、EPOCHSECONDS和EPOCHREALTIME。而内建的 History 功能现在可以用来刪除历史数据的范围,并且将负的参数作为历史列表末端的偏移量。Bash 5.0还增加了一个选项,允许区域变量集成前一个作用域中,具有相同名称的变量名。还加入一个新的选项,启用后将允许 shell 只尝试 扩展开始下标(Subscripts)一次,但官方表示,这个功能在数学表达式中仍会出现使用问题。Bash 5.0现在会预设启用globasciiranges 选项,使用者可以在配置阶段將其设定为预设关闭。

官方提醒,Bash 4.4和Bash 5.0之間存在部分不兼容的更新,像是 nameref 解析方法的更新,代表着使用 namerefs 会出现不同的行为,尽管官方提到他们已经尽可能减少这种兼容性问题,但仍有部分无法避免。除此之外,现在在预设情况下,当启用扩充模式的时候, shell 才会启动配置BASH_ARGC和BASH_ARGV,官方表示,这是一个之前的设置定疏忽,在传递大量参数时,会造成效率低下的问题。

官方建议使用者可以将 Bash 连接到已安裝的 Readline 库,而非使用 lib/readline 中的版本,因为只有 Readline 8.0 以及以上版本,才能提供所有Bash 5.0需要的字符,使用早前的 Readline 库版本将无法正常运行。

升级至 Bash 5.0
检查现有的 bash 版本 bash --version,使用 brew 安装 bash brew install bash。

加入软连接
sudo mv /bin/bash /bin/bash.origin
sudo ln -s /usr/local/bin/bash /bin/bash

项目团队宣布 Bash 5.2 正式于2022年9月下旬发行,主要新特性如下:
最大的新特性是重写的命令替换解析代码,新代码递归地调用 bison 解析器。这取代了以前版本中使用的临时解析,提供更好的语法检查,可以迅速捕获语法错误。
另一个变化是改进了数组索引的解析和扩展,以及另外提供了使用内置命令中的 “@” 和 “*” 参数将键重置为给定值的能力,而不是重置整个阵列。
除此之外,还增加了一个新的设置 “patsub_replacement”,启用时,模式替换的替换字符串中的 “&” 被替换为匹配模式的字符串部分,反斜杠将转义 “&” 并插入文字 “&”。
一个新的 readline 等待时间框架,包括新的公共函数,用来设置超时,并查看在超时之前还剩多少时间。

其他变化包括:
扩展了额外进程不分叉的情况,例如使用 “$(” 构造时不再使用分叉。
提供了在编译时启用(配置 --enable-alt-array-implementation)替代数组实现的能力,该实现针对最大访问速度进行了优化,以增加内存消耗为代价。
 “printf” 有一个新的格式说明符 “%Q”,类似于 "%q" ,它将任何指定的精度应用于不带引号的原始参数,然后引用并输出结果。
添加了 noexpand_translations 设置和 “configure --enable-translatable-strings” 构建选项,用来控制是否启用对 $“...” 可本地化替换的支持。
在 posix 模式下,'printf' 现在可以使用 long double 作为浮点转换说明符(如果存在),否则它必须是 double 。

项目主页:https://www.gnu.org/software/bash/