Contents

Shell命令扩展 - 从此成为Shell达人【2】

本想总结写Shell的技巧,但是内容太多写不动了,改为主要写Shell的命令扩展。命令扩展主要包含Shell命令执行扩展,在命令执行前Shell对命令进行预处理;Shell历史命令扩展,对历史命令进行引用修改。总的来说都是为了减少输入,提高效率。主要参考《Linux Shell核心编程指南》1、《bash shell脚本编程经典实例(第2版)》2、Bash手册。

此次系统学习总结Shell扩展功能时,发现《Linux Shell核心编程指南》对这一部分的基本的概念都没有描述清楚。给我的启示是这块需要完整看下bash命令的手册而不是看这些二手的信息,另外这本书在我心目中的评分减一颗星!


Shell脚本支持八种类型的扩展功能,在命令被拆分成单词后执行前先进行扩展替换再执行,扩展展开的顺序为: 花括号扩展(brace expansion)、波浪号扩展(tilde expansion)、参数与变量替换(parameter and variable expansion)、命令替换(command substitution)从左到右执行替换、算术扩展(arithmetic expansion)、单词切割(word splitting)和路径替换(pathname expansion)。还有一种进程替换(process subtitution)也被一般Shell所支持,其中命令替换算数扩展在上一篇博客中已经介绍。

tip

man手册bash命令中写道: Only brace expansion, word splitting, and pathname expansion can change the number of words of the expansion; other expansions expand a single word to a single word. The only exceptions to this are the expansions of “$@” and “${name[@]}” as explained above (see PARAMETERS).

只有花括号扩展、单词扩展和路径替换在扩展前后的词数会发生改变,其他扩展总是将一个词扩展为一个词。唯一的例外是上面提到的参数与变量替换 $@${name[@]}

但在进行单词分割扩展时会按照IFS变量对命令进行再一次分割,所以其实命令替换如果输出以空格分隔的字符串也会被造成词数增加。

在Shell中可以在使用花括号扩展来获取以空格分隔的字符串,用于循环遍历或者命令参数。花括号扩展不可被单引号或双引号引用,支持嵌套,内部不能用空格间隔。注意和一组命令{}的语法区分,表示执行一组命名的花括号内需要空格间隔,具体可以看前一篇博客

花括号扩展有如下使用方式。

shell

bash-5.1$ echo {1,2,3}
1 2 3
bash-5.1$ echo t{i,o}p
tip top
bash-5.1$ echo t{o,e{a,m}}p
top teap temp
bash-5.1$ echo {1..10}
1 2 3 4 5 6 7 8 9 10
bash-5.1$ echo {1..10..2} # 2是步长
1 3 5 7 9

波浪号~在Shell中默认代表当前用户的Home路径,~user表示某个指定用户的Home路径,~+表示当前工作路径,~-表示前一个工作路径。一般常用的就是cd ~/Documents。注意波浪号扩展不会在引号中执行

tip

可以使用cd -回到上次cd进入的路径。不过和~不同的是-不属于Shell扩展,属于cd命令的支持。

man bash中cd命令的说明: An argument of - is equivalent to $OLDPWD.

变量替换主要是对引用的变量直接进行一些字符串的处理。有如下几种有趣的用法。

  • 变量测试

    变量测试可以在某些条件下将变量的值替换。例如echo ${input:?'not input'}在没有定义变量inputinput为空时会在标准错误输出not input并且结束命令;如果定义了input并且input不为空,则返回input变量的值。

    在Shell中支持以下几种变量测试的功能。

    语法格式功能描述
    ${var:-str}如果var未定义或值为空,则返回str。否则,返回变量的值
    ${var:=str}如果var未定义或值为空,则将str赋值给变量,并返回结果。否则,直接返回变量的值
    ${var=str}如果var未定义,则将str赋值给变量,并返回结果。否则,直接返回变量的值
    ${var:?str}如果var未定义或值为空,则通过标准错误显示包含str的错误信息。否则,直接返回变量的值
    ${var:+str}如果var未定义或值空,就直接返回空。否则,返回str

    另外用于替换的值会经过波浪号扩展、参数扩展、命令替换以及算数扩展处理,也就是:

    • 参数扩展: 可以使用其它变量,如${BASE:=${HOME}}
    • 波浪号扩展: 可以扩展用户的HOME目录,如${BASE:=~}
    • 命令替换: 可以使用命令替换其它命令的输出,如${BASE:="$(pwd)"}
    • 算数扩展: 可以使用$(())语法执行整数运算,如${BASE:=/home/uid$((ID+1))}
  • 变量切片

    变量切片可以对变量进行切片、替换操作,不会改变变量本身的值

    语法格式功能描述
    ${变量:偏移量}从变量的偏移量位置开始,切割截取变量的值到结尾。变量的偏移量起始值沩0
    ${变量:偏移量:长度}从变量的偏移量位置开始,截取特定长度的变量值。变量的偏移量起始值为0
    ${变量#关键字}使用关键字对变量进行模式匹配,从左往右删除匹配到的内容,关键字可以使用*符号,使用#匹配时为最短匹配(最短匹配掐头)
    ${变量##关键字}使用关键字对变量进行模式匹配,从左往右删除匹配到的内容,关键字可以使用*符号,使用##匹配时为最长匹配(最长匹配掐头)
    ${变量%关键字}使用关键字对变量进行模式匹配,从右往左删除匹配到的内容,关键字可以使用*符号,使用%匹配时为最短匹配(最短匹配去尾)
    ${变量%%关键字}使用关键字对变量进行模式匹配,从右往左删除匹配到的内容,关键字可以使用*符号,使用%%匹配时为最长匹配(最长匹配去尾)

    掐头#和去尾%分别从开头和结尾开始匹配,如果前面匹配失败就不会继续匹配,例如:

    shell

    bash-3.2$ val=path.txt.exe
    bash-3.2$ echo ${val%.exe}
    path.txt
    bash-3.2$ echo ${val%.txt} # 不会被替换
    path.txt.exe
  • 变量统计和替换

    下面是查找变量、统计变量内容的长度以及对变量内容进行替换操作的功能语法。

    语法格式功能描述
    ${!前缀字符*}查找以指定字符开头的变量名称,变量名之间使用IFS分隔
    ${!前缀字符@}查找以指定字符开头的变量名称,@在引号中将被扩展为独立的单词
    ${!数组名称[*]}列出数组中所有下标,*在引号中会被扩展为一个整体
    ${!数组名称[@]}列出数组中所有下标,@在引号中会被扩展为独立的单词
    ${#变量}统计变量的长度,变量可以是数组
    ${变量/旧字符串/新字符串}将变量中的旧字符串替换为新字符串,仅替换第一个
    ${变量//旧字符串/新字符串}将变量中的旧字符串替换新字符串,替换所有
    ${变量^匹配字符}将变量中的小写替换为大写,仅替换第一个
    ${变量^匹配字符}将变量中的小写替换大写,替换所有
    ${变量,匹配字符}将变量中的大写替换为小写,仅替换第一个
    ${变量,匹配字符}将变量中的大写替换小写,替换所有
  • 间接引用

    在变量名前使用感叹号!,可以实现对变量的间接引用,而不是返回变量本身的值,只能对变量实现一层的间接引用。感觉比较鸡肋,如下示例。

    shell

    bash-3.2$ player=faker
    bash-3.2$ mvp=player
    bash-3.2$ echo ${mvp}
    player
    bash-3.2$ echo ${!mvp} # 间接引用
    faker

进程替换可以将命令的输出或输入作为文件名传递给Shell命令,对该文件名所代表的文件读取或写入相当于和命令进行管道通信。例如,用进程替换对比两个命令的输出,如下。

shell

vimdiff <(ll) <(cd -;ll)

简单讲Shell会将<(ll)<(cd -;ll)替换成文件名,对该文件进行读取就是读取命令的输出。原理涉及到目录/dev/fd管道,可以参考之前的博客从文件描述符和打开文件之间的关系重新理解Shell重定向。当然也可以用>(cmd)的形式写入该文件时,相当于对命令的标准输入写入内容。

Shell支持*.[]符号进行Shell模式匹配文件路径。和波浪号扩展一样不可加引号。如下示例。

shell

bash-3.2$ touch {a,b,c}.txt
bash-3.2$ ls
a.txt   b.txt   c.txt
bash-3.2$ echo ?.txt
a.txt b.txt c.txt
bash-3.2$ echo a*
a.txt
bash-3.2$ echo [a-z].txt
a.txt b.txt c.txt
bash-3.2$ echo [!a].txt
b.txt c.txt
bash-3.2$ echo [^a].txt
b.txt c.txt

Shell的历史扩展被称为Bang(!)命令,在当前输入的命令中引用历史命令,减少重复的输入。

首先,可以对历史命令的整体进行引用。学名为Event Designators

语法格式功能描述
!n引用历史编号为n的命令,编号可以查看history
!-n引用之前的第n条命令,排序列表查看history
!!引用最近一条命令,和!-1Ctrl-P作用一样
!str引用最近以str开头的命令
!?str?引用最近包含str的命令
^a^b把上一条命令的a替换成b,相当于命令!!:s/a/b
^abc删除上一条命令中的abc
!#引用目前输入的所有字串,如: more a !#,这个最终的命令是more a more a

进一步,可以对历史命令中的某个词进行引用。学名为Word Designators。一般用:分割历史命令整体和单词的引用。下面示例仅针对引用编号为n命令整体的某个词进行引用。

如果没有指定整体命令的引用则将最近引用最近一条命令。

语法格式功能描述
!n:0引用历史编号为n命令的第0个参数,也就是可执行文件名
!n:^引用历史编号为n命令的第1个参数
!n:$引用历史编号为n命令的最后一个参数
!n:x-y引用历史编号为n命令的第x到第y参数
!n:*引用历史编号为n命令的除第0个参数外的所有参数,和1-$同义
!n:x*x-$同义

在选定历史命令的词后又可以对词进行修改。学名为Word Designators。同样也是使用:分割。

花活很多,根本记不住,用处不大。只举个替换命令例子,在上个命令的所有参数中进行查找替换。

shell

!:*:s/old/new