Shell expansion学习
Bash在执行输入的命令前,会对输入的字符进行处理,这个过程叫(字符)展开。
我们可以通过shell命令echo查看展开过程。
$ echo this is a test
this is a test #a与test之间只保留了一个空格
$ echo *
Desktop LauncherFolder MyDocuments
shell命令在执行前将*
展开成了其它东西。
shell展开类型
下面通过简单的例子来学习shell命令展开。
比如一个常见的需求:打印出指定目录中每一个文件用于后续处理。
$ cd ~/sample #其中'b c.txt'文件名有空格
a.txt 'b c.txt' d.txt
下面是示例脚本:
#!/bin/bash
num_files=0
for file in `ls ~/sample/*.txt`
do
echo $file
num_files=$((num_files + 1))
done
echo num of files is $num_files
波浪线展开
~/sample/*.txt
Tilde expansion, ~
会展开成指定用户的家目录$HOME
,如果没有指定用户,则展开成当前用户家目录。
$ echo ~foo
~foo
$ echo ~
/home/user
$ echo ~/foo
/home/user/foo
文件名展开
单词分割后,除非设置了 -f 选项,否则 Bash 会扫描每个单词,查看是否有字符 *
、?
或 [
。如果出现这些字符之一,则该单词将被视为PATTERN
,并替换为与模式匹配的按字母顺序排序的文件名列表。以上例子会查找sample目录下匹配后缀名为.txt
的文件名。
$ echo *.txt
命令替换
`ls ~/sample/*.txt`
命令替换就是将命令的输出结果展开使用。主要有两种形式,一种是$(command)
,另一种是(old-style)倒引号圈住命令。以上要替换命令是ls ~/sample/*.txt
。
Bash 通过执行 COMMAND 并将命令替换替换为命令的标准输出来执行展开,并删除任何尾随换行符。嵌入的换行符不会被删除,但可能会在单词分割期间删除。
$ echo $(ls)
$ echo `ls`
#观测以下两者区别
$ echo $(ls -l ) #只输出了一行
$ ls -l
命令替换可以嵌套。比如:
$ a=$(file $(ls *.txt))
$ echo $a
LICENSE.txt: ASCII text
如果替换出现在双引号内,则不会对结果执行单词分割和文件名展开。
shell参数和变量展开
也就是变量引用,参数展开为实际值。例子中是for循环文件列表的每一个文件。
$ echo $file
算术表达式展开
num_files=$((num_files + 1))
$((expr))
算术表达式展开,可以用作整数计算。在算术表达式中空格并不重要,并且表达式可以嵌套。
表达式被视为在双引号内,但括号内的双引号不被特殊处理。表达式中的所有标记都会经历参数扩展、命令替换和引号删除。算术替换可以嵌套。
运算符与 C 编程语言中的运算符大致相同。按优先级递减顺序,列表如下所示:
在可能的情况下,Bash 用户应尝试使用带方括号的语法:
$[ EXPRESSION ]
注意因为展开只是支持整数除法,所以结果是整数(地板除)
$ echo $((1+4))
$ echo $(($((5**2))*3))
$ echo $((5/2))
单词分割
shell 扫描参数扩展、命令替换和算术扩展的结果,这些结果未在双引号内发生,以进行单词拆分。
shell 将$IFS
的每个字符视为分隔符,并将其他扩展的结果拆分为这些字符上的单词。如果未设置 IFS,或者其值恰好为默认值“<空格><制表符><换行符>”,则任何 IFS 字符序列都用于分隔单词。如果 IFS 的值不是默认值,则只要空格字符位于 IFS(IFS 空格字符)的值中,单词开头和结尾就会忽略空格字符“空格”和“Tab”的序列。IFS 中任何非 IFS 空格的字符以及任何相邻的 IF 空格字符都会分隔字段。IFS 空格字符序列也被视为分隔符。如果 IFS 的值为 null,则不会发生分词。
如果没有展开,就不执行单词分割。
除了上述例子展示的几种展开,还有其它展开,包括:
花括号展开
花括号展开(Brace expansion)是一种可以生成任意字符串的机制。花括号表达式本身可能包含一个由逗号分开的字符串列表,或者一系列整数,或者单个的字符串。这种模式不能嵌入空白字符。花括号展开模式可能包含一个开头部分叫做报头,一个结尾部分叫做附言.
花括号展开可以嵌套。
$ echo a{A,B,C}b # a是报头,b是附言
aAb aBb aCb
$ echo {1..10} # 整数序列
1 2 3 4 5 6 7 8 9 10
$ echo {A..Z} # 字符序列
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
$ echo {Z..A} #支持倒序排列
Z Y X W V U T S R Q P O N M L K J I H G F E D C B A
$ echo {10..1}
10 9 8 7 6 5 4 3 2 1
$ echo a{A{1,2},B{1,2}}b #嵌套
aA1b aA2b aB1b aB2b
花括号展开在任何其他展开之前执行,并且结果中将保留其他扩展所特有的任何字符。
$ echo re{*,?}md
readme.md re?md
进程替换
<(LIST)
or
>(LIST)
展开执行顺序
如果系统支持进程替换展开,与波浪号、参数变量、算术展开、命令替换同时执行。
示例脚本分析
文章开头的脚本示例执行结果:
/home/user/sample/a.txt
/home/user/sample/b
c.txt
/home/user/sample/d.txt
num of files is 4
我们看到输出的结果和预料的有差异,问题就出在单词分割上。
脚本展开顺序:
- 波浪号展开
~/sample/*.txt
展开为/home/user/sample/*.txt
- 文件名展开
单词分割后,有*
星号,文件名展开为/home/user/sample/a.txt
,/home/user/sample/b c.txt
,/home/user/sample/d.txt
- 命令替换展开
/home/user/sample/a.txt /home/user/sample/b c.txt /home/user/sample/d.txt
- 单词分割
执行完命令替换后再进行单词分割,由于/home/user/sample/b c.txt
有空格,单词分割时将其展开为两个单词,分别为/home/user/sample/b
和c.txt
,最终输出错误的结果。
脚本修正
将脚本第3行替换为
for file in ~/sample/*.txt
不需要使用ls
命令列出文件列表,而是通过文件名展开即可,其在单词分割后面执行。输出结果为:
/home/user/sample/a.txt
/home/user/sample/b c.txt
/home/user/sample/d.txt
num of files is 3
如何控制展开
shell 提供了一种叫做引用的机制,来有选择地禁止不需要的展开
- 双引号引用
使用双引号,我们可以禁止以下展开
- 单词分割
- 文件名展开
- 波浪号展开
- 花括号展开
以下几种仍然展开
- 参数展开
- 算术展开
- 命令替换
$
,\
,倒引号
在默认情况下,单词分割机制会在单词中寻找空格,制表符,和换行符,并把它们看作单词之间的界定符。它们只作为分隔符使用。单词分割被禁止,内嵌的空格也不会被当作界定符,它们成为参数的一部分。 一旦加上双引号,我们的命令行就包含一个带有一个参数的命令。
- 单引号
禁止所有展开。
参考资料
https://tldp.org/LDP/Bash-Beginners-Guide/html/sect_03_04.html
https://www.gnu.org/software/bash/manual/html_node/Shell-Expansions.html

