shell-骨干流程3——命令展开 Jan 4, 2022 · shell CLI处理流程 · 分享到: shell的模式匹配 大括号展开 波浪符号展开 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无法识别大括号展开而原样输出。大括号扩展的语法有两种形式: 1preamble{pattern1,pattern2,...}postscript 2或者 3preamble{start..end[..incr]}postscript 其中,前导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将不认为是大括号展开,原样输出内容。这同时意味大括号至少会生成两个字符串,如果为大括号内有空内容,生成的时候也是将空内容和大括号左右组合起来。 1$ echo a{b} # 没有逗号则原样输出 2a{b} 3$ $ echo a{a,,c} # 大括号展开对空内容的处理 4aa a ac 而使用序列进行大括号展开,一定要使用..符号,其左边是起始字符,右边是终止字符,默认包括起始和终止字符。如果需要指定步长(INCR),则使用两次..符号,步长必须是一个整数。。如果不给定步长,shell会根据起始、终止内容自动判断步长为+1或-1。 大括号展开可以复合,即大括号展开可以内嵌到另一个大括号开展中,并且展开顺序是由外向内。《Bash参考手册》上有一个很有借鉴意义的例子,如下: 1chown root /usr/{ucb/{ex,edit},lib/{ex?.?*,how_ex}} 首先将最外层的大括号开展,结果如下: 1chown root /usr/ucb/{ex,edit} /usr/lib/{ex?.?*,how_ex} 然后再次执行大括号展开,最终结果如下: 1chown root /usr/ucb/ex /usr/usb/edit /usr/lib/ex?.?* /usr/lib/how_ex 最后需要强调一点:大括号展开又是优先级最高的展开,因此语法结构固定且严格。单、双引号内的大括号不进行展开(参考《Shell 骨干流程1——形成初步命令》的引号章节);为避免与参数扩展冲突,字符串'${'亦不被认为是大括号扩展。 波浪符号展开 如果一个单词(word)以不带引号的波浪字符(~)开始,那么所有直到第一个非加引号的斜杠(或所有字符,如果没有未加引号的斜杠出现)的字符都被认为是波浪前缀。一般情况下,波浪字符与shell的环境变量有关(如$HOME,$PWD,$OLDPWD),如果波浪字符与数字组合,那么会涉及到shell的dirs内置命令,我们将会单独说明。 波浪字符最常用方法是表示当前用户的HOME目录,即~等同于$HOME。如果波浪字符后面加特定用户名,则表示该用户名的HOME目录。 1$ echo ~ # 波浪线单独表示当前用户HOME目录 2/home/lelouch 3$ echo ~root # 波浪线后面跟用户名表示该用户名的HOME目录 4/root 5$ echo ~test 6/home/test 波浪字符与'+,-'组合,与当前路径有关,~+表示当前路径($PWD),~-表示上个路径。'-'用在路径名中通常表示上次所在目录。 1$ pwd # 原始目录 2/home/lelouch 3$ cd /tmp # 切换到/tmp目录 4$ echo ~+ # 显示当前目录 5/tmp 6$ echo ~- # 显示前一个目录 7/home/lelouch 波浪字符、数字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与加减号、数字的组合的简写。 1〜N 2等同于'dirs +N'显示的字符串 3 4〜+N 5等同于'dirs +N'显示的字符串 6 7〜-N 8等同于'dirs -N'显示的字符串 举个例子,我们先将/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中的变量和一般编程语言中变量作用类似,但是在设置变量是不需要使用$符合,只有在引用变量时才需要,例子如下: 1$ a=hello 2$ echo $a 3hello 4$ echo ${a} 5hello 6$ a=world 7$ echo $a 8world 除了常规的使用方法,shell还为变量添加了各种各样便利的功能。这些拓展功能都需要使用大括号包裹。 参数间接扩展 如果shell变量中,$符号后面的第一个字符是感叹号!,且后面的内容是一个变量的名称,那么引用的参数并不是“名称”而是该名称对应的实际的值。这就构成了变量间的间接引用。举个例子: 1$ name=linux 2$ paramter=name 3$ echo ${!parameter} # 这里实际等同于${name} 4linux 在上面例子中,${}中第一个字符是感叹号,而且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的切片类似。格式为: 1${parameter:offset} 2${parameter:offset:length} 其中,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在参数扩展中还提供了一种计算参数长度的便捷方式,即为 1${#parameter} 对于一般变量,${#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还提供了匹配替换的功能,也是通过自带的模式匹配来识别特定的“关键字”再进行替换。其格式为: 1${parameter/[/#%]pattern/string} 其中,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是用命令的输出替换命令本身。有两种格式: 1# 旧版使用反引号 2`command` 3# 新版使用$() 4$(command) 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脚本提供灵活处理不同环境的能力。 算术表达式扩展 算术表达式扩展允许计算算术表达式和替换结果,广义上也属于一种命令替换。算术表达式扩展使用$和两个小括号,格式是: 1$(( expression )) 特别地,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目录下的所有配置文件: 1ls /etc/*.conf 或是列出所有以字母a或b开头的配置文件: 1ls /etc/[ab]*.conf 或者是显示所有的以image开头的后面跟着一个字符格式为.jpg的文件: 1ls image?.jpg 在路径与文件名扩展中,字符.在文件名开头或斜线后必须显式匹配,除非设置了shell选项dotglob。匹配文件名时,斜杠字符必须始终显示匹配。更多文件匹配的例子可以看文章开头shell的模式匹配小节。 shell命令展开作为四大步骤中最复杂的步骤,是我花时间精力最多的部分,其多样性和灵活性为shell脚本提供了强大的生命力。经过上述11步的操作,shell命令终于可以真正进入执行阶段了。 参考内容 https://www.gnu.org/software/bash/manual/bash.html https://zhuanlan.zhihu.com/p/65599187