BASH坑杀无算
原文见:https://gitee.com/laokz/OS-kernel-test/blob/master/memo-bash.md
本文只讨论非交互模式的脚本编程,只记录作者认为重要、迷糊、有用的东西,以便理解、避坑、备忘,最终还是要以manpage为权威参考。本文属于BASH高级编程。
BASH有很多小技巧和捷径,能用通常理解的形式就别用这些小技巧,自找麻烦。
用词说明:
- 模式、模式串、模式匹配,均指shell通配符模式,以区别于正则表达式
- 引起来、引号,除特别说明外,单引号、双引号和
\
转义都算 - 字、word,字符外最小的shell处理单元、单词、token(不是很准确),由未引起来的
| & ; ( ) < > 空白符
分隔。除特别说明外,可以进行各种展开 - 本文不加区分地交替使用shell、bash,都指的是本文主角–BASH
第0节 对抗各种坑的思想方法
- Shell是个宏处理器。宏意味着文本和符号能展开组合成新的文本和符号
- 展开无处不在
- 引号约束展开
- 象BASH一样思考:
- 遵从引号规则,处理字符,读入字
- 遵从引号规则,执行各种各样的展开
- 遵从引号规则,再拆成一个个字
- 重定向
- 执行命令
第1节 引号规则
引号规则用于去除字符的特殊含义,回归字面本意,也影响字拆分效果。不象C语言中的引号,BASH没有字符、字符串的引号区分。引号规则有三种机制:
1. 转义字符\
用法类同C语言
2. 单引号去除所有字符的特殊含义,也就不会有任何展开拆分
- 无例外,
\'
也不行 - 有特殊用法
$'string'
,string里允许使用C语言的转义序列
3. 双引号去除大部分字符的特殊含义,包括通配符
- 绝对例外:
$ `
仍保持展开含义,因此双引号内允许$
类展开
if [[ abc == "a*" ]]; then echo yes; else echo no; fi # 输出:no
- 相对例外:
\
后跟$ ` " \ newline
时仍保持转义含义
echo "\n" # 输出:\n 这里\是个普通字符
echo "\\n" # 输出:\n 这里\\转义成普通字符\
第2节 命令
由简至繁分为三类:
1. 简单命令
除了内建命令、外部命令外,还有:
- 函数算简单命令。定义时还可以指定重定向,调用时也可以前缀变量赋值
- 管道算简单命令。与管道中命令重定向的关系:管道建立在前,重定向在后,因此命令重定向起决定作用(数据流动方向)。取反管道的
!
要置于第一条命令前。|&
将stderr也进行pipe。管道中的每个命令都在子shell中执行,其中shopt的lastpipe选项可以使最后一个命令运行在当前环境。set的pipefail选项可以让管道提前失败,而不是默认的最后一条命令失败才失败
ls >/dev/null |wc # 管道建立在前,重定向在后,因此wc什么也得不到
2. 系列命令
简单命令用; & && ||
进行的排列,; &
的优先级低,符合直觉。;
用于在一行中书写系列命令,否则回车换行即可。
# 分号前面是一整个命令,然后它又分成前后两个命令,因此显示b,而不是什么也不显示
false && echo a; echo b
3. 复合命令
复合命令是对前述命令更复杂的组合,组合后相当于单条命令,因此可以进行重定向,作用于其中的每条命令。重定向无意义的命令会忽略掉这个重定向,要小心那些有意义的,特别是输入重定向时。有5种:
1)(小括号命令分组),在子shell中执行
2){ 大括号命令分组; },在当前环境中执行
注意 {} 与命令间的空格和分号(或回车)是必须的。
3)((算术表达式))运算命令
算术运算符及其规则同C语言,包括用括号改变运算优先级、逗号分隔多个表达式、常量的各种进制表示等。几个注意事项:
- 这是个shell命令,相当于C的statement而不是expression,计算结果为0时命令执行状态为1–失败,因此必要时要前缀上
!
,或后缀上|| true
,又或者表达式缀上,1
,以防止脚本退出 - 变量不需要前缀
$
,unset或为null时按0计算。所有接受算术运算的地方都如此,比如:数组下标计算、整数变量赋值时 - 表达式里可以使用命令替换
- BASH不支持浮点数
set -e
i=0
((++i)) # 看看++
echo prefix: i=$i exit-code=$?
i=0
((i++))
echo postfix i=$i exit-code=$?
4)[[ 条件表达式 ]]测试命令
表达式可以测试文件(符号链接测试的是目标文件)、变量、字符串属性,还可以测试set的选项是否设置,算术比较最好由上面的命令来测试。注意[[]]
与表达式间的空格、操作符与操作数间的空格是必须的。表达式可用()
改变优先级,! && ||
进行逻辑运算。不要用过时的test、[]命令。
- 表达式内不进行3种展开:{}大括号展开、字拆分、路径展开
v="a b"; if [[ $v == a ]]; then echo yes; else echo no; fi # 输出:no
- == 和 != 的右侧有通配符时进行模式匹配
v="a b"; if [[ $v == a* ]]; then echo yes; else echo no; fi # 输出:yes
- =~ 匹配右侧的正则表达式。注意这种匹配同shell模式一样,受引号规则约束,且
\
在正则表达式和shell展开都有特殊意义,shell优先,因此别写得太复杂!必须要写时,用变量存储正则表达式是个简单方法。这种匹配也受shopt的nocasematch选项影响,默认大小写敏感
shopt -s nocasematch
if [[ "abcxyZ" =~ ^a.*z$ ]]; then echo yes; fi # 输出:yes
- 正则表达式可以进行分组匹配,并用BASH_REMATCH数组变量存储匹配结果,第0个是整个匹配的串,其它的是各组匹配的。匹配是最大匹配
v="Zabc123+-Z"
if [[ $v =~ ([^Z].*)([0-9]+)([+-]{2}) ]]; then
for ((i=0; i<4; i++)); do
echo -n ${BASH_REMATCH[i]}' '
done
fi # 输出:abc123+- abc12 3 +-
5)控制块命令
包括for while until if case select
。这里只说下case用法:
case word in pattern[|pattern] ...) list;; ... esac
- word和pattern不进行3种展开:{}大括号展开、字拆分、路径展开
- pattern是shell模式,为
*
时可以当作default,当然应作为最后一个分支 ;;
相当于break,也可用;&
相当于fall through,;;&
指示在后面的分支上继续匹配
第3节 命令执行
命令解析执行流程
- 处理转义字符
- 从左至右解析成字
- 确定命令类型
- 收集赋值和重定向,待用
- 执行其它各种展开
展开后确有命令 | 展开后没有命令 |
---|---|
5.创建命令执行环境(不确定,但必在此或前发生) | |
6.展开重定向并执行 | 5.展开赋值并执行,加入当前环境 |
7.展开赋值并执行,不加入当前环境 | 6.展开重定向并执行 |
8.执行命令 | 7.如果上述没出错也无命令展开,就成功退出 |
注意:命令前缀的赋值是临时性的,只影响命令运行时,不影响当前环境;这个临时赋