shell 展开之大括号展开(brace expansion)

1,095 阅读3分钟

语法

大括号展开很难用一种语法形式表示,但是它由三部分组成,一个前缀,一对大括号,一个后缀。大括号中的内容可以是一个由逗号分隔的字符串序列,或者是一个表达式序列。

首先看一个大括号内是一个逗号分隔的字符串序列的例子

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_wsshell_ws} 当作了 ls 命令的参数!

如果我们去掉这个空格,那么 shell 会把 ~/{make_ws, shell_ws} 这个整体当作 ls 命令的参数,并且 shell 会先执行两个展开,先是执行大括号开展,再执行波浪线展开,最后把展开的结果作为 ls 命令的参数。

那么我们再把这个错误的命令纠正下,正确命令如下

ls -ld ~/{make_ws,shell_ws}

输出结果为

drwxrwxr-x 3 david david 4096 625 16:01 /home/david/make_ws
drwxrwxr-x 2 david david 4096 718 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手册

参考

www.gnu.org/software/ba…