语法
大括号展开很难用一种语法形式表示,但是它由三部分组成,一个前缀,一对大括号,一个后缀。大括号中的内容可以是一个由逗号分隔的字符串序列,或者是一个表达式序列。
首先看一个大括号内是一个逗号分隔的字符串序列的例子
echo a{b,c,e}d
输出结果为
abd acd aed
从输出结果可以看出,{b,c,e}
展开的结果有三个,分别为 b, c, e,并且每一个结果与前缀 a 以及后缀 d 结合。
现在来看下如何在大括号内使用表达式序列,这种用法有一个固定形式,如下
{x..y[..incr]}
x, y 可以为整数,也可以为一个单一字符,x..y
表示一个区间。例如 {9..1}
,表示从9到1这个区间的所有整数,包括9和1。又例如,{a..f}
表示 a 到 f 区间所有的字符,包括 a 和 f。
语法中的[..incr]
表示一个增量,默认为1或者-1。例如 {9..1}
,这个大括号展开没有指定增量,但是我们可以猜到这个增量的值是 -1。又例如 {a..f}
,这个大括号展开也没有指定增量,但是我们也可以猜到这个增量的值是1。
但是,如果默认增量不满足我们的要求,我们可以特别指定这个增量的值,例如
echo {1..7..2}
输出如下
1 3 5 7
零填充
我们再来看一个比较有意思的事情,零填充。
echo {9..11}
输出结果如下
9 10 11
从输出结果可以发现,执行大括号展开后,每一项的宽度并不是一样的。有时候在格式化输出时,需要结果的每一项的宽度一样,那么就需要进行零填充。
零填充使用的大括号展开语法为
{x..y[..inc]}
只要在 x 或者 y 之前添加一个0,当输出结果的宽度不一样时,就会进行零填充。
现在改写一下上面的例子
echo {09..11}
输出结果为
09 10 11
从输出看,每一项的宽度是2,但是我如果想让它的宽度变为3,怎么办呢?那我们就再加一个0,命令如下
echo {009..11}
输出结果为
009 010 011
内嵌
大括号展开可以内嵌到另一个大括号开展中,并且展开顺序是由外向内。
手册上有一个很有借鉴意义的例子,如下
chown root /usr/{ucb/{ex,edit},lib/{ex?.?*,how_ex}}
首先展开最外层的大括号开展,结果如下
chown root /usr/ucb/{ex,edit} /usr/lib/{ex?.?*,how_ex}
然后再次执行大括号展开
chown root /usr/ucb/ex /usr/usb/edit /usr/lib/ex?.? /usr/lib/how_ex
陷阱
shell 脚本中充满了各种陷阱,归根结底是你不了解这些语法,因此下面列举几个我在写脚本遇到的陷阱。
首先,不要在单引号或者双引号中使用大括号展开,因为单/双引号中不支持这个展开。
echo "{1..9}"
输出结果为
{1..9}
单引号中任何字符都保持原义。双引号中有特殊意义的字符只有
$
和`
和\
,$
用于参数展开、命令替换、以及算术展开,`
用于命令替换,\
用于转换特殊字符,例如\$
,使$
失去特殊意义,保持原义。
其次,大括号展开的前缀不能是 $
,因为这是参数展开的形式
echo ${1..3}
输出结果为
-bash: ${1..3}: 错误的替换
可以看出系统不支持参数展开和大括号展开的混用。
最后,如果一对大括号内,使用的是以逗号分隔的字符串,那么这些字符串是不允许出现空格的
ls -ld ~/{make_ws, shell_ws}
执行结果为
ls: 无法访问'/home/david/{make_ws,': 没有那个文件或目录
ls: 无法访问'shell_ws}': 没有那个文件或目录
不知道大家有没有看明白为何是这样?由于在第一个逗号后出现了空格,因此 shell 把 ~/{make_ws
和 shell_ws}
当作了 ls 命令的参数!
如果我们去掉这个空格,那么 shell 会把 ~/{make_ws, shell_ws}
这个整体当作 ls 命令的参数,并且 shell 会先执行两个展开,先是执行大括号开展,再执行波浪线展开,最后把展开的结果作为 ls 命令的参数。
那么我们再把这个错误的命令纠正下,正确命令如下
ls -ld ~/{make_ws,shell_ws}
输出结果为
drwxrwxr-x 3 david david 4096 6月 25 16:01 /home/david/make_ws
drwxrwxr-x 2 david david 4096 7月 18 08:07 /home/david/shell_ws
例子
举一个工作中遇到的很经典的例子,我现在有一个名为 demo.png 图片,现在我要把这个 demo.png 图片复制成37张图片,并且名字依次是 000.png, 001.png, ..., 037png。
我当初的第一想法是执行38次 cp 命令,如下
cp -v demo.png 000.png
cp -v demo.png 001.png
...
cp -v demo.png 037.png
这太疯狂了,你应该感觉到你此时在做重复的事,因此我立马想到使用脚本命令来实现,如下
for name in {000..37};do cp -v demo.png ${name}.png; done
这个命令使用了大括号展开 {000..37}
,展开后的每一项宽度为3,例如 001
。
优先级
shell 中有很多种展开,但是大括号展开优先级是最高的。如果遇到混合使用多个 shell 展开,并且不确定展开顺序,可以查询 Bash手册 。