shell-骨干流程3——命令展开
总体流程图镇楼:
Figure 1: shell执行流程
命令展开
命令展开是shell变成语言灵活性的最佳体现,如图中绿色部分所示,包含5-11步。展开是对每一个标记(token)分别进行的,共有以下7步,对应总体流程的5-11步。
第5-7步都是各式各样的命令展开。基本的命令展开包括3种,分别为:大括号展开、波浪符号展开、参数与变量展开。这三种命令展开本质上是shell语法糖的展开。
第8,9两步实际上是子命令执行,并非语法糖,原始命令将启动子进程(子shell)来执行子命令,执行的结果作为标记,嵌入到原命令中。5-7步和8-9步的区别在于,5-7步本质是查找语法糖对应的内容进行展开,无需使用子进程;8-9步本质是采纳子进程执行的结果,而非简单的查找替换。
经过5-9步的处理,原始命令已经能够被shell直接执行,因此我们需要第10步将这些替换过后的内容重组起来,根据系统分隔符(Internal Field Seperator,IFS)再次分割(因为命令展开过程中会带来新的内容)。
最后我们还需要第11步,展开路径和文件名,这一步和之前5-7步展开又是不同的,之前是语法糖替换,而这次是使用shell模式匹配方式(通配符)替换。第11步的shell模式匹配是正则表达式简化版,主要是利用通配符,并非完整的正则表达式规则。
当命令展开完成后,造成展开的引号将会被全部移除。作为单词整体引用的引号保持原样。例如,hello "$USER"
的引号在变量展开后会移除,而"hello"
的引号则不会。(这里理解存疑)
shell的模式匹配
在介绍shell中各种各样的展开之前,先说下shell的模式匹配。它是标准正则匹配的简化版,尽量保证了正则匹配中的精华,兼顾了匹配的速度。可用于shell流程中的大括号展开和路径与文件名展开。为了方便说明,我们在/tmp/
文件夹下新建patternMatching
文件夹,并在其中新建a.txt aaa.txt abc.txt a.md b.md c.html cd.html ddd.html 001.txt
这9个文件,作为演示示例。
注意,shell中默认使用自带的模式匹配,而非正则匹配。模式匹配将在命令展开的各方面的到应用。
1$ cd /tmp/ && mkdir patternMatching && cd patternMatching
2$ touch a.txt aaa.txt abc.txt a.md b.md c.html cd.html ddd.html 001.txt
3$ ls
4001.txt aaa.txt abc.txt a.md a.txt b.md cd.html c.html ddd.html
shell的模式匹配主要是使用了通配符,通用的有3种,当启用“extglob” shell选项时,则可以使用另外5个扩展模式匹配运算符。除了通配符,模式匹配中的其他内容都保留其字面意思,如果要匹配通配符的字面意思,则需要使用反斜杠\
进行转义。另外,NULL
字符不允许出现在模式匹配中。
通用的三种通配符有*, ?, [ ]
:
- “*”:匹配任意字符串,包括空字符。
- “?”:匹配任何单个字符,必须是一个字符,不匹配空字符。
- “ ”:匹配中括号中给定的一个字符。例如
[abdgAD134]
就是匹配中括号中的六个字母和三个数字。如果是连续的字符,可以使用连字符分隔的一对字符,来表示匹配范围内的任一字符。在默认环境中,[a-dx-z]
相当于[abcdxyz]
。如果是不想匹配中括号中的内容,可以在开头加上!
或^
,表示取反,如[!a-z]
或[^a-z]
则只匹配表示不匹配字母'a-z'。-
在'['和']'中,可以使用语法指定字符类
[[:class:]]
, 这里的class
是下列类型之一,在 POSIX 标准中定义的类型:alnum alpha ascii blank cntrl digit graph lower print punct space upper word xdigit
-
用法示例:
1$ ls * # 显示任意内容
2001.txt aaa.txt abc.txt a.md a.txt b.md cd.html c.html ddd.html
3$ ls a* # a开头的所有文件
4aaa.txt abc.txt a.md a.txt
5$ ls a? # ?只匹配一个字符,因此那个都无法匹配上
6ls: cannot access 'a?': No such file or directory
7$ ls c?.html # ?匹配一位,所以c.html没有匹配
8cd.html
9$ ls [a-z].* # 匹配所有单个字母为名的文件
10a.md a.txt b.md c.html
11$ $ ls [[:digit:]]* # 匹配文件名第一位是数字的文件
12001.txt
13$ ls [[:alpha:]]* # # 匹配文件名第一位是字母的文件
14aaa.txt abc.txt a.md a.txt b.md cd.html c.html ddd.html
如果在shell选项中开启了extglob
选项(shopt -s extglob
),那么shell在模式匹配中可以使用五种扩展模式。
?(pattern-list)
:匹配零个或一个的给定模式。*(pattern-list)
:匹配零个或多个的给定模式。+(pattern-list)
:匹配一个或多个的给定模式。@(pattern-list)
:匹配一个给定的模式。!(pattern-list)
:匹配除了给定的模式的其他模式。
根据上述给出的文件,例子如下:
1$ ls c?([a-z]).html # c后面匹配0或1个字母
2cd.html c.html
3$ ls a*([a-z]).* # 匹配以a开头的任意文件(文件后缀由.*匹配)
4aaa.txt abc.txt a.md a.txt
5$ ls a+([a-z]).* # 匹配以a开头,并且不是单个a的文件,也就是说a后面必须有别的字母(文件后缀由.*匹配)
6aaa.txt abc.txt
7$ ls c@([a-z]).html # 严格仅匹配一次
8cd.html
9$ ls !([a-z]).* # 不匹配任何单个字母为名的文件(文件后缀由.*匹配)
10001.txt aaa.txt abc.txt cd.html ddd.html
11$ ls +([[:digit:]]).* # 匹配名称全是数字的文件(文件后缀由.*匹配)
12001.txt
大括号展开
大括号是shell所有展开中的第一步,这意味着在执行大括号展开的时候,其他展开的标识符都存在与其中,这导致大括号展开相对严格且固定的格式,任何不正确的格式都会让shell无法识别大括号展开而原样输出。大括号扩展的语法有两种形式:
其中,前导preamble
和后缀postscript
都是可选的,表示大括号左右的内容。
大括号展开本质上是一种生成多个字符串的机制(由一变多),可用于生成路径和文件名称的字符串、参数的字符串甚至用户自定义输入的字符串。先举几个例子:
1$ echo a{a,b}b # {a,b}生成两个
2aab abb
3$ echo a{xy,yx,yz} # 生成3个字符串,无后缀
4axy ayx ayz
5$ echo a{a..d}b # 正向序列,生成a,b,c,d四个
6aab abb acb adb
7$ echo a{d..a}b # 反向序列,生成d,c,b,a四个
8adb acb abb aab
9$ echo a{d..a..2}b # 反向序列,步长为2的方式生成,d,b两个
10adb abb
可以看出,大括号的实际效果是将大括号外的内容和大括号内的每一个内容(用逗号或序列区分)进行组合,生成字符串。在使用逗号进行分割的语法中,大括号内至少只要有一个逗号。若没有逗号,shell将不认为是大括号展开,原样输出内容。这同时意味大括号至少会生成两个字符串,如果为大括号内有空内容,生成的时候也是将空内容和大括号左右组合起来。
而使用序列进行大括号展开,一定要使用..
符号,其左边是起始字符,右边是终止字符,默认包括起始和终止字符。如果需要指定步长(INCR),则使用两次..
符号,步长必须是一个整数。。如果不给定步长,shell会根据起始、终止内容自动判断步长为+1或-1。
大括号展开可以复合,即大括号展开可以内嵌到另一个大括号开展中,并且展开顺序是由外向内。《Bash参考手册》上有一个很有借鉴意义的例子,如下:
首先将最外层的大括号开展,结果如下:
然后再次执行大括号展开,最终结果如下:
最后需要强调一点:大括号展开又是优先级最高的展开,因此语法结构固定且严格。单、双引号内的大括号不进行展开(参考《Shell 骨干流程1——形成初步命令》的引号章节);为避免与参数扩展冲突,字符串'${'
亦不被认为是大括号扩展。
波浪符号展开
如果一个单词(word)以不带引号的波浪字符(~)开始,那么所有直到第一个非加引号的斜杠(或所有字符,如果没有未加引号的斜杠出现)的字符都被认为是波浪前缀。一般情况下,波浪字符与shell的环境变量有关(如$HOME,$PWD,$OLDPWD
),如果波浪字符与数字组合,那么会涉及到shell的dirs
内置命令,我们将会单独说明。
-
波浪字符最常用方法是表示当前用户的HOME目录,即
~
等同于$HOME
。如果波浪字符后面加特定用户名,则表示该用户名的HOME目录。 -
波浪字符与
'+,-'
组合,与当前路径有关,~+
表示当前路径($PWD
),~-
表示上个路径。'-'
用在路径名中通常表示上次所在目录。 -
波浪字符、数字N以及可选的
'+,-'
组合,则与目录堆栈有关。目录堆栈可用dirs
命令查看。~N
等同于~+N
表示从目录堆栈顶部开始数,~-N
表示从目录堆栈底部开始数。我们单开一个小节来说明。
shell文件目录堆栈
shell为了方便用户在多个目录之间直接切换,提供了目录堆栈功能。目录堆栈是将若干个目录放到一起,目录的增删遵循后入先出的规则(和堆栈一样),并且用户可以直接访问到目录堆栈中的任一目录,与目录堆栈相关的命令有:pushd, popd, dirs
。使用dirs
命令可以看到目录堆栈的内容。使用pushd
命令切换到目录同时,会将该目录添加到堆栈顶部,而使用popd
命令会删除堆栈顶部目录。默认情况下,目录堆栈底部总是保留当前目录位置。
1$ dirs
2~
3# dirs 使用指南
4dirs [-clpv] [+n] [-n]
5 当命令不带任何参数时,显示当前目录堆栈的内容,默认情况下,所有内容显示在一行,并以空格分隔。我们使用 pushd 命令向目录堆栈压入新的目录项,使用 popd 删除目录项。当前目录项始终放在目录堆栈的底部。
6 -c 清除目录堆栈所有条目。
7 -l 生成一个带有全路径名的列表;默认情况下用波浪线代表用户HOME目录。
8 -p 一个目录一行的方式显示。
9 -v 每行一个目录来显示目录栈的内容,每个目录前加上的编号。
10 +n 显示从左到右的第n个目录,数字从0开始。
11 -n 显示从右到左的第n个日录,数字从0开始。
当前所在目录作为目录堆栈的底部常驻目录,无法将其popd
出来。
shell通过psuhd
和popd
两个命令操作目录堆栈,不加任何参数的情况下,使用pushd
命令切换到目录同时,会将该目录添加到堆栈顶部,而使用popd
命令会删除堆栈顶部目录。若pushd
命令不加任何参数,则会将位于记录栈最上面的2个目录对换位置。当然,psuhd
和popd
有带参数的使用方式。
1# 注意下面的N是数字,n是字母n参数
2
3popd +N # 删除栈中(从左边数)第N个元素,由0开始计。
4popd -N # 删除栈中(从右边数)第N个元素,由0开始计。
5pop -n # 不改变当前目录(也就是不改变栈顶元素,操作除了栈顶外栈内其他元素)
6
7# usage: pushd [-n] [+N | -N | dir]
8pushd # 不加参数时,交换栈顶前两个元素
9pushd +N # 将栈内元素循环左移,直到将(从左边数)第N个元素移动到栈顶,由0开始计。
10pushd -N # 将栈内元素循环左移,直到将(从右边数)第N个元素移动到栈顶,由0开始计。
11pushd -n dir # 将目录入栈,但不改变当前元素,即将目录插入栈中作为第二个元素。 注意-n参数要在目录之前。
借由pushd
和popd
的特性,可以用shell实现目录间的快速定位。
那么目录堆栈和波浪符号有什么关系呢?答案是波浪符号与加减号、数字的组合是dirs
与加减号、数字的组合的简写。
举个例子,我们先将/home /usr /etc /tmp ~
反向压入目录堆栈。
1$ dirs
2/home /usr /etc /tmp ~
3$ dirs +1
4/usr
5$ echo ~1 ~+1 # 二者效果等同于dirs +1
6/usr /usr
7$ dirs -1
8/tmp
9$ echo ~-1 # 效果等同于dirs -1
10/tmp
参数与变量展开
$
在shell中是一个非常重要的符号,参数与变量展开、命令替换、算术表达式计算都与$
相关。接下来三个小结,我们将逐个进行介绍。
首先,$
最常见的功能是组成参数/变量名称,标准模式为${...}
,在不会产生误解的情形下,大括号可以省略。在大于等于10的位置参数、特殊模式变量以及名称中带有特殊符号导致变量名称有二义性时,必须要有大括号包裹。shell中的变量和一般编程语言中变量作用类似,但是在设置变量是不需要使用$
符合,只有在引用变量时才需要,例子如下:
除了常规的使用方法,shell还为变量添加了各种各样便利的功能。这些拓展功能都需要使用大括号包裹。
参数间接扩展
如果shell变量中,$
符号后面的第一个字符是感叹号!
,且后面的内容是一个变量的名称,那么引用的参数并不是“名称”而是该名称对应的实际的值。这就构成了变量间的间接引用。举个例子:
在上面例子中,${}
中第一个字符是感叹号,而且parameter
是一个变量的名称,其值为name
,因此在执行时shell把!parameter
转换成其变量的值,即为name
,因此扩展后的结果为${name}
,最终显示name
变量的值linux
。当然,如果感叹号后面跟的内容不是变量的名称,那么就会报-bash: parameters: invalid indirect expansion
错误。
参数匹配扩展
参数的匹配扩展与参数间接扩展外表相似,但是用法完全不同。参数匹配扩展的更像是查找变量名前缀相同的变量,因此还引入了通配符*,@
,所以匹配扩展不会涉及变量实际的值。我们在前一个例子基础上增加几个变量:
1$ name1=ubuntu
2$ name2=redhat
3$ name3=slackware
4$ echo ${!name*}
5name name1 name2 name3
6$ echo ${!name@}
7name name1 name2 name3
可以看出,参数间接扩展获取的是参数值,参数匹配扩展获取的符合前缀的参数名。如果我们将二者组合起来,就可以获取有相同前缀变量名的值:
1$ for var in ${!name*};do echo "$var--${!var}";done;
2name--linux
3name1--ubuntu
4name2--redhat
5name3--slackware
这个例子中,${!name*}
使用的是参数匹配扩展,匹配所有以name
为前缀的变量,共找到name, name1, name2, name3
四个,然后在for循环遍历中,再用参数间接扩展${!var}
将var
所指向的变量名称的实际值打印出来。
我们在上文中提到参数匹配扩展可以用*,@
两种符合,那么使用这两种符号有什么区别吗?区别在于:当表达式被双引号包裹时,@
会扩展成独立的几个变量,而*
则会扩展成变量组合而成的字符串。还是以上面的几个两边为例:
1$ for var in "${!name@}";do echo "$var--${!var}";done;
2name--linux
3name1--ubuntu
4name2--redhat
5name3--slackware
6$ for var in "${!name*}";do echo "$var--${!var}";done;
7-bash: name name1 name2 name3: invalid variable name
在双引号中使用@
符号生成的是一个序列,可以通过for
循环遍历;而通过*
符号生成的是一个字符串,for
循环会直接读取整个字符串name name1 name2 name3
作为$var
的值,因此无法找到对应的变量,从而报invalid variable name
错误。
空参数处理
由于shell是一个弱类型且语法较为宽松的编程语言,因此不会对变量是否存在、变量是否为NULL
以及变量类型进行检测。如果使用了一个未设置的变量,shell直接返回空。这让我们在写程序时很容易携程难以查找的bug,因此shell提供了空参数处理的扩展。
空参数处理的格式为:${变量名[:][-+=?]变量值}
。其中起不起用冒号有区别,后面[-+=?]
四选一。
我们首先来解释有没有冒号:
的区别。当没有冒号的时候,即${变量名[-+=?]变量值}
时,shell只检测变量是否存在,相当于test -v 变量名
;而使用冒号的时候,即${变量名:[-+=?]变量值}
时,shell不仅检测变量时候存在,而且检测变量是否为空,相当于test -v 变量名 && test -z 变量名
。
test -v
用于检测变量是否存在,变量存在返回结果0,不存在返回结果1;test -z
用于检测变量长度是否为0,如果为0返回0,否则返回1。
${parameter:-word}
,如果变量parameter未设置或者值为空,那么给出word的值;否则给出$parameter
的值。${parameter:-word}
,如果变量parameter未设置或者值为空,那么给出word的值同时把word的值赋给$parameter
,否则给出$parameter
的值;注意无法给位置参数和特殊参数赋值;${parameter:?word}
,用于交互式shell,如果变量parameter未设置或者值为空,那么将word内容作为标准错误返回给shell界面,否则给出$parameter
的值。${parameter:+word}
,${parameter:-word}
的相反操作。如果parameter为空或未设置,则不进行任何替换,否则给出word的值。
下面举几个简单的例子来帮助理解空参数处理。
1$ name=linux
2$ name1=
3$ echo ${name:-windows} --- ${name1:-windows} --- ${name-windows} --- ${name1-windows}
4linux --- windows --- linux ---
5# name 存在且不为空,因此${name:-windows},${name-windows}给出$name的值,
6# name1存在但是为空,因此${name1:-windows}给出替代值windows,
7# 而${name1-windows}只检测到$name1存在,因此直接输出$name1,结果为空值
8$ echo name1
9
10# 无论是${name1:-windows}还是${name1-windows}都不会给$name1赋值。
11$ echo ${name:=windows} --- ${name1:=windows}
12linux --- windows
13$ echo $name1
14windows
15# 可以看出使用:=会给原变量赋值。
16$ name1=
17$ echo ${name=windows} --- ${name1=windows}
18linux ---
19$ echo $name1
20
21# 这里是空值,因为$name1存在,因此${name1=windows}不进行赋值操作
子串扩展
shell的子串扩展是从shell变量中切割出一部分(字串)的快捷方法,功能和python的切片类似。格式为:
其中,offset
代表偏移值,为开始切割字符的序数,从0开始算,length
为切割的长度,如果不指定,默认截取到最后。例子如下
1$ string=01234567890abcdefgh
2$ echo ${string:7} # 不指定length默认到末尾,第一位索引序数为0
37890abcdefgh
4$ echo ${string:7:0} # length长度为0,截取长度也为0
5
6$ echo ${string:7:2} # length长度为2
778
- 如果
offset
值为负数,那么子串是从最后开始计算起始点,-1表示最后一位,-2表示倒数第二位。 - 如果
length
值大于剩余的字符数,那么子串只截取到末尾。 - 如果
length
值为负数,那么length
表示的是原字符串截取到所在位序数,不包括length
所在的那一位。
需要指出,冒号
:
和负号-
之间至少要用一个空格隔开,否在会被当成是空参数处理:-
。
例子如下所示:
1$ string=01234567890abcdefgh
2$ echo ${string:7:100} # 当length大于剩余字符串长度时,只截取到末尾
37890abcdefgh
4$ echo ${string: -7:2} # 注意中间有个空格
5bc
6$ echo ${string:0: -5} # -5(d)是原变量倒数第5个索引,但是不包括-5(d)
701234567890abc
8$ echo ${string: -2: -1} # 这个例子更明显的表现出只包含开头,不包含结尾
9g
一般情况下,子串扩展都是从0开头,-1结尾,但是有一种例外,位置参数是按1开始从头计算偏移值。
1# 设置位置参数
2$ set -- 0 1 2 3 4 5 6 7 8 9 0 a b c d e f g h
3$ echo ${@:7} # 位置参数开头从1开始计算
46 7 8 9 0 a b c d e f g h
5$ echo ${string:7} # 其他参数都是从0开始计算
67890abcdefgh
参数长度计算
bash shell在参数扩展中还提供了一种计算参数长度的便捷方式,即为
对于一般变量,${#parameter}
会计算字符串长度,对于位置参数,它会计算位置参数的个数,如果parameter是一个数组名,并且下标为*或者@,表达式扩展为数组的元素个数。例子如下:
1$ set -- a b c d
2$ var=123456789
3$ arr=(1 2 3)
4$ echo ${#@} ${#*}
54 4
6$ echo ${#var}
79
8$ echo ${#arr[@]} ${#arr[*]}
93 3
参数匹配删除
shell可以通过自带的模式匹配来识别特定的“关键字”,并对识别出来的内容进行操作,例如删除、替换等。这一小节,我们先说参数匹配删除,它可以实现一些很常见的操作,如获取文件名、文件后缀、文件路径、数字等。
匹配删除共有四种模式,分别是从头最短检索、从头最长检索、从尾最短检索、从尾最长检索。从头、从尾比较容易理解,就是匹配的时候是从前往后找还是从后往前找。最长、最短通常是对*
通配符而言,如果遇到连续多个合适的匹配内容,最短匹配是匹配尽量少的字符、最长匹配是尽量多的匹配字符。按照这个分类方式,我们可以列出参数匹配删除的用法表格:
从头 | 从尾 | |
---|---|---|
最短 | ${parameter#word} |
${parameter%word} |
最长 | ${parameter##word} |
${parameter%%word} |
最短最长比较好记,使用一个符号#,%
比较短,是最短匹配;使用两个符号##,%%
比较长,是最长匹配。至于从头从尾,我的记忆方法是通常#
符号在bash shell中放在开头比较多(注释),因此是从头匹配。
下面还是通过几个例子来方便理解参数匹配删除:
1# 先设置一些位置参数
2$ set -- ab-cd-ef== uv-wx-yz==
3# 使用从头最短匹配,*-匹配到的是ab- ,然后将其删除
4$ echo ${1#*-}
5cd-ef==
6# 使用从头最长匹配,*-匹配到的是ab-cd- ,然后将其删除
7$ echo ${1##*-}
8ef==
9# 使用从尾最短匹配,-*=匹配到的是-ef== ,然后删除
10$ echo ${1%-*=}
11ab-cd
12# 使用从尾最长匹配,-*=匹配到的是-cd-ef== ,然后删除
13echo ${1%%-*=}
14ab
此外,和计算参数长度的扩展一样,如果是使用@,*
的位置参数或数组,那么参数匹配删除将对其中的每一个元素进行操作。类似于for ... in...
的效果。
1# 在以下几个例子中,表示位置参数或数组的#,@符合可以互换。
2# 对于使用@,*的位置参数,将会对其中每一个元素进行匹配删除操作
3$ echo ${@#*-}
4cd-ef== wx-yz==
5$ echo ${*%%-*=}
6ab uv
7$ arr=(--a --b --c)
8# 对数组中每一个元素进行从头最短匹配
9$ echo ${arr[*]#-?(-)} # ?(-)需要启用“extglob” shell选项
10-a -b -c
11# 对数组中每一个元素进行从头最长匹配
12$ echo ${arr[@]##-?(-)} # ?(-)需要启用“extglob” shell选项
13a b c
14$ arr=(a-- b-- c--)
15# 对数组中每一个元素进行从尾最短匹配
16$ echo ${arr[*]%-?(-)}
17a- b- c-
18# 对数组中每一个元素进行从尾最长匹配
19$ echo ${arr[@]%%-?(-)}
20a b c
21
22# 几个实用的例子
23# 获取文件名,文件后缀
24$ FILENAME=linux_bash.sh
25$ echo ${FILENAME%.*}
26linux_bash
27$ echo ${FILENAME##*.}
28sh
29# 判断某字符串是否以某字符开头
30$ OPT='-option'
31$ if [ ${OPT#-} != ${OPT} ];
32> then
33> echo "start with -"
34> else
35> echo "not start with -"
36> fi
37start with -
参数匹配替换
前面介绍了参数的匹配删除,bash还提供了匹配替换的功能,也是通过自带的模式匹配来识别特定的“关键字”再进行替换。其格式为:
其中,parameter
是待替换的原变量,pattern
是匹配的模式,即需要匹配的内容,string
是替换的内容。如果pattern
匹配到了符合的字符子串,那么就用string
替换匹配到的内容;如果pattern
未匹配到相关内容,则不做任何操作。需要指出的是,参数匹配替换都是用的是最长匹配。参数匹配替换的详细用法有以下6点:
- 默认情况下,即
${parameter/pattern/string}
,是从头匹配一个pattern
内容并替换。 - 当
pattern
以/
符号开头时,即${parameter//pattern/string}
,将会匹配所有符合pattern
的内容。 - 当
pattern
以#
符号开头时,即${parameter/#pattern/string}
,将会匹配以pattern
开头的内容。 - 当
pattern
以%
符号开头时,即${parameter/%pattern/string}
,将会匹配以pattern
结尾的内容。 - 如果启用了
nocasematch
shell选项,则不考虑字母的大小写。 - 和参数长度计算、参数匹配删除一样,如果是使用
@,*
的位置参数或数组,那么参数匹配删除将对其中的每一个元素进行操作。类似于for ... in...
的效果。
我们以字符串string=abceddabceddabcedd
为例说明参数匹配替换的用法。
1$ string=abceddabceddabcedd
2$ echo ${string/d?a/+} # 匹配到abce "dda" bceddabcedd
3abce+bceddabcedd
4# 匹配替换都是使用最长匹配
5$ echo ${string/d*a/+} # 匹配到abce "ddabcedda" bcedd
6abce+bcedd
7# 替换所有d?a
8$ echo ${string//d?a/f} # 匹配到abce "dda" bce "dda" bcedd
9abce+bce+bcedd
10# 替换以a开头的内容
11$ echo ${string/#a??/+} # 匹配到 "abc" eddabceddabcedd
12+eddabceddabcedd
13$ echo ${string/#b??/+} # 没有b开头的内容,因此未匹配到任何内容
14abceddabceddabcedd
15# 替换以b*d结尾的内容
16$ echo ${string/%b*d/+} # 匹配到a "bceddabceddabcedd"
17a+
18# 对于位置参数的处理
19$ set -- abc abd abe
20$ echo ${@/a/+}
21+bc +bd +be
22# 对于数组的处理
23$ arr=(abc abd abe)
24$ echo ${arr[@]/a/+}
25+bc +bd +be
大小写修改
bash shell在匹配的基础上还提供了一个小功能,就是将匹配出来的内容进行大小写修改,实际上用的不多,可以作为了解。注:此操作仅适用于bash4.0往上版本。
其基本格式为:
1# 小写转大写: ^会把开头的小写字母转换成大写,^^会转换所有小写成大写
2${parameter^pattern}
3${parameter^^pattern}
4
5# 大写转小写: ,会把开头的大写转换成小写,,,会把所以大写转换成小写
6${parameter,pattern}
7${parameter,,pattern
举几个简单的例子:
1# 小写转大写
2$ par='abc'
3$ echo ${par^} # 首字母
4Abc
5$ echo ${par^^} # 全部
6ABC
7# 大写转小写
8$ par='ABC'
9$ echo ${par,}
10aBC
11$ echo ${par,,}
12abc
此外,如果是使用@,*
的位置参数或数组,那么大小写修改将对其中的每一个元素进行操作。类似于for ... in...
的效果。
变量操作
在bash4.0 版本以后,添加了一些方便使用的小功能,这些被统合为变量操作,格式为${parameter@operator}
,作用为根据操作符(operator)执行参数转换或者,操作符如下:
- Q:将字符串使用引号包裹。
- E:对于使用反斜线
\
后的字符一律按转义处理。 - P:如果
parameter
含有prompt string
时,按照prompt解释。 - A:拓展成参数赋值的语句。
- a:由参数属性值组成的字符串。
参数扩展做为bash中最多样,最灵活的扩展为shell提供了多样和简洁的处理方式,其本质上是多种小工具的聚合体,有的是为了弥补bash shell本身的弱点,有的是为了综合了使用率最高的小工具。同时这也是bash shell中非常容易出错的部分,需要我们谨慎地使用。
命令替换
shell通过命令替换能够完成一些运行时的动态变化。命令替换,就是在执行bash shell是用命令的输出替换命令本身。有两种格式:
Bash通过在子shell环境中执行命令替换的内容来进行扩展,并将命令替换为命令执行后的标准输出,并删除任何结尾的换行符。嵌入的换行符不会被删除,但在后面再次分词过程中可能会被删除。简单的例子如下:
1$ echo `whoami`
2lelouch
3$ echo $(whoami)
4lelouch
5$ echo "hello,`whoami`"
6hello,lelouch
7$ echo "hello,$(whoami)"
8hello,lelouch
那为什么用新版的$()
替代旧版的反引号和呢?第一个原因就是反引号"`"太容易和单引号"'"搞混了,二者长得太像不利于排错。第二个原因就是反引号不容易复合,需要使用转义符号\
而使用$()
则直接嵌套就能使用。举个例子,我们要查看当前用户home目录中的一个test.sh
文件:
1# 使用反引号命令替代,反引号需要转义
2$ cat `ls /home/\`whoami\`/test.sh`
3#!/bin/bash
4read filename
5read url
6if test -w $filename && test -n $url
7then
8 echo $url > $filename
9 echo "写入成功"
10else
11 echo "写入失败"
12fi
13# 使用$()命令替代
14$ cat $(ls /home/$(whoami)/test.sh)
15#!/bin/bash
16read filename
17read url
18if test -w $filename && test -n $url
19then
20 echo $url > $filename
21 echo "写入成功"
22else
23 echo "写入失败"
24fi
命令替代在shell自适应编程里非常重要。因为面向不同用户编程时,各个用户的环境变量,配置文件路径都有差异,bash shell可以通过命令替代了解当前用户的上下文,为shell脚本提供灵活处理不同环境的能力。
算术表达式扩展
算术表达式扩展允许计算算术表达式和替换结果,广义上也属于一种命令替换。算术表达式扩展使用$
和两个小括号,格式是:
特别地,bash shell中的算术表达式扩展只支持固定长度的证书运算,任何浮点数运算都会被认为是一个错误而不做任何替换操作。此外,算术表达式扩展也是可以复合的。
1$ echo $((1+1))
22
3$ echo $((10/4))
42
5$ echo $((6%5))
61
7$ echo $((6-15))
8-9
9# 不支持浮点数运算
10echo $((1.1+3))
11-bash: 1.1+3: syntax error: invalid arithmetic operator (error token is ".1+3")
12# 复合的算法表达式
13$ echo $((10+$((3*2)) ))
1416
bash shell的计算能力只是辅助作用,虽然除0错误被捕获并标记为错误,但是bash shell没有溢出检查,不支持浮点运算,也没有强大的数学公式库,因此并不推荐使用bash shell做复杂的数学运算。
再次单词分割
在《shell骨干流程1——形成初步命令》一文中,我们了解到,shell命令处理流程的第一步就是使用元字符把输入内容分割为标记(token),以便bash shell进行保留字识别和流程控制。但是经过大括号展开、波浪符号展开、命令替换、算数运算扩展这些步骤,会带来新的内容。因此,作为shell处理流程的第10步。我们需要将这些替换过后的内容重新整肃,根据系统分隔符(Internal Field Seperator,IFS)再次将命令分割成一个个便于处理的标记(token,包括控制符和单词)。也是说,如果没有前面几步的扩展,也就不需要再次进行分割。
默认的IFS有space, tab, newline
以及他们的组合,例如连续的空格,tab
或者多个连续的空行。
在这次处理中,显式的空参数("" 或 '')被保留并作为空字符串传递给命令,未加引号的隐式空参数将被删除。
路径与文件名展开
如果bash中没有设置-f
选项,就会支持路径与文件名扩展,其扩展模式也是通过bash shell自带的模式匹配。比如,显示/etc
目录下的所有配置文件:
或是列出所有以字母a
或b
开头的配置文件:
或者是显示所有的以image
开头的后面跟着一个字符格式为.jpg
的文件:
在路径与文件名扩展中,字符.
在文件名开头或斜线后必须显式匹配,除非设置了shell选项dotglob
。匹配文件名时,斜杠字符必须始终显示匹配。更多文件匹配的例子可以看文章开头shell的模式匹配小节。
shell命令展开作为四大步骤中最复杂的步骤,是我花时间精力最多的部分,其多样性和灵活性为shell脚本提供了强大的生命力。经过上述11步的操作,shell命令终于可以真正进入执行阶段了。