Bash学习 - 第3章:Basic Shell Features,第5节:Shell Expansions

本文为 Bash Reference Manual第3章:Basic Shell Features 第5节:Shell Expansions 的读书笔记。

扩展是在命令行拆分为标记后执行的。Bash 执行以下扩展:

  • 大括号扩展
  • 波浪号扩展
  • 参数和变量扩展
  • 命令替换
  • 算术扩展
  • 单词拆分
  • 文件名扩展
  • 引号去除

扩展的顺序是:大括号扩展;波浪号扩展、参数和变量扩展、算术扩展以及命令替换(按从左到右的顺序进行);单词拆分;文件名扩展;以及引号去除。

在支持的系统上,还有一种额外的扩展可用:进程替换。它与波浪号扩展、参数、变量、算术扩展及命令替换同时进行。

引号去除总是最后进行。它会去掉原始单词中存在的引号字符,而不是其他扩展产生的引号字符,除非这些字符本身已经被引用。更多信息请参见引号去除。

只有大括号扩展、单词拆分和文件名扩展可以增加 扩展后的单词数量;其他扩展都是将一个单词扩展为一个单词。唯一的例外是 "$@"$* 的扩展(参见特殊参数),以及 "${name[@]}"${name[*]}的扩展(参见数组)。

3.5.1 Brace Expansion

大括号扩展是一种生成具有共同前缀和后缀的任意字符串的机制,其中前缀或后缀可以为空。此机制类似于文件名扩展(参见文件名扩展),但生成的文件名不一定存在。用于大括号扩展的模式由可选的前导部分组成,其后是用逗号分隔的字符串序列或位于一对大括号之间的序列表达式,然后是可选的后缀。前导部分会添加到大括号内的每个字符串前面,然后后缀会附加到每个生成的字符串末尾,从左到右进行扩展。

大括号扩展可以嵌套。每个扩展字符串的结果不会排序;大括号扩展会保持从左到右的顺序。例如,

bash 复制代码
bash$ echo a{d,c,b}e
ade ace abe

序列表达式的形式为 x ...y [...incr],其中 x 和 y 可以是整数或字母,incr 是可选的增量,为整数。当提供整数时,该表达式会展开为 x 和 y 之间的每个数字(包括 x 和 y)。如果 x 或 y 以零开头,则生成的每一项将包含相同位数,必要时补零。提供字母时,该表达式会以字典顺序(使用 C 语言环境)展开为 x 和 y 之间的每个字符(包括 x 和 y)。注意,x 和 y 必须是相同类型(整数或字母)。当提供增量时,它作为各项之间的差值。默认增量根据情况为 1 或 -1。

大括号扩展在其他任何扩展之前进行,其他扩展中具有特殊意义的字符在结果中会被保留。这是严格的文本操作。Bash 不会对扩展的上下文或大括号之间的文本进行任何语法解释。

正确格式的大括号扩展必须包含未加引号的开括号和闭括号,以及至少一个未加引号的逗号或有效的序列表达式。任何格式不正确的大括号扩展都会保持不变。

可以使用反斜杠对'{'或','进行引用,以防其被视为大括号表达式的一部分。为了避免与参数扩展冲突,字符串'${'不会被视为可进行大括号扩展的内容,并且会抑制大括号扩展,直到遇到闭合的'}'。

当要生成的字符串的公共前缀比上面的例子更长时,这种结构通常作为简写使用:

bash 复制代码
mkdir /usr/local/src/bash/{old,new,dist,bugs}
# 或
chown root /usr/{ucb/{ex,edit},lib/{ex?.?*,how_ex}}

大括号扩展与历史版本的 sh 存在轻微的不兼容。sh 在大括号作为单词的一部分出现时不会对其进行特殊处理,并会在输出中保留它们。Bash 由于大括号扩展会从单词中移除大括号。例如,在 sh 中输入的单词 'file{1,2}' 在输出中保持不变。Bash 在大括号扩展后将该单词输出为 'file1 file2'。使用 B 选项启动 Bash,或通过 set 命令的 B 选项禁用大括号扩展(参见 Shell 内建命令),以实现严格的 sh 兼容性。

示例:

bash 复制代码
$ echo {1..10..2}
1 3 5 7 9
$ for i in {1..10..2}; do echo $i; done
1
3
5
7
9

$ echo {a..z..2}
a c e g i k m o q s u w y

$ echo {05..10}
05 06 07 08 09 10
$ echo {0A..0C}
{0A..0C}

$ echo '{2..5}'
{2..5}
$ echo "{2..5}"
{2..5}

# quoting using \ or ''
$ echo a{\{,\},\,}
a{ a} a,

$ echo a{'{','}','\'}
a{ a} a\

由于大括号扩展在参数和变量扩展之前,所以下面的写法是错误的:

bash 复制代码
$ a=5
$ for i in {1..$a}; do echo $i; done
{1..5}

以下是正确写法之一:

bash 复制代码
$ a=5
$ for i in $(seq 1 $a); do echo $i; done
1
2
3
4
5

3.5.2 Tilde Expansion

如果一个单词以未加引号的波浪号字符('~')开头,则从该字符开始直到第一个未加引号的斜杠(如果没有未加引号的斜杠,则包括所有字符)之间的所有字符都被视为波浪号前缀。如果波浪号前缀中的所有字符都未加引号,则波浪号之后的字符将被视为可能的登录名。如果该登录名为空字符串,则波浪号会被替换为 HOME Shell 变量的值。如果 HOME 未设置,则波浪号会扩展为执行 Shell 的用户的主目录。否则,波浪号前缀将被替换为指定登录名关联的主目录。

如果波浪号前缀是 '~',则 shell 变量 PWD 的值会替换波浪号前缀。如果波浪号前缀是 '~-',则 shell 会用变量 OLDPWD 的值进行替换(如果已设置)。

如果波浪号前缀中波浪号后面的字符是一个数字 N,前面可以可选地带一个 '+' 或 '-',则波浪号前缀会被目录栈中对应的元素替换,就像使用波浪号前缀中波浪号后的字符作为参数调用内建命令 dirs 时显示的一样(参见目录栈)。如果波浪号前缀(去掉波浪号)的内容是一个没有前导 '+' 或 '-' 的数字,则波浪号扩展会假定为 '+'。

波浪号扩展的结果会被当作引用处理,因此替换内容不会进行单词拆分和文件名扩展。

如果登录名无效,或者波浪号扩展失败,则波浪号前缀保持不变。

Bash 会检查每个变量赋值中':'后或第一个'='后紧跟的未加引号的波浪号前缀,并在这些情况下执行波浪号扩展。因此,可以在 PATH、MAILPATH 和 CDPATH 的赋值中使用带波浪号的文件名,shell 会将其扩展后的值进行赋值。

下表显示了 Bash 如何处理未加引号的波浪号前缀:

$HOME 的值。

~/foo

$HOME/foo

~fred/foo

用户 fred 的主目录中的目录或文件 foo。

~/foo

$PWD/foo

~-/foo

${OLDPWD-'~-'}/foo

~N

'dirs N' 命令显示的字符串。

~+N

'dirs +N' 命令显示的字符串。

~-N

'dirs -N' 命令显示的字符串。

当出现在简单命令的参数中时,Bash 也会对满足变量赋值条件(参见 Shell 参数)的单词执行波浪符扩展。除上述声明命令外,当处于 POSIX 模式时,Bash 不会执行这种扩展。

💡 内置命令dirs是一个目录栈,需要用pushd和popd入栈和出栈

示例:

bash 复制代码
$ echo ~
/home/vagrant
$ echo ~vagrant
/home/vagrant
$ echo ~root
/root
# no user with name abcd 
$ echo ~abcd
~abcd

$ echo ~+
/home/vagrant
$ echo ~-
/home/vagrant/test
$ echo ~
/home/vagrant

3.5.3 Shell Parameter Expansion

'' 字符用于引入参数扩展、命令替换或算术扩展。要扩展的参数名称或符号可以用大括号括起来,括号是可选的,但可以防止紧跟在变量后面的字符被误解为变量名的一部分。例如,如果第一个位置参数的值为 'a',那么 {11} 会展开为第十一位置参数的值,而 $11 会展开为 'a1'。

当使用大括号时,匹配的结束大括号是第一个未被反斜杠转义、未在引号字符串中、也不在嵌入的算术扩展、命令替换或参数扩展中的 '}'。

参数扩展的基本形式是 ${parameter},它会替换为 parameter 的值。parameter 是上面描述的 Shell 参数(见 Shell 参数)或数组引用(见数组)。当 parameter 是多位数的位置参数,或者 parameter 后跟的字符不应被解释为其名称的一部分时,需要使用大括号。

如果 parameter 的第一个字符是感叹号(!),并且参数不是命名引用(nameref),它将引入一个间接级别。Bash 使用通过展开参数其余部分形成的值作为新参数;该新参数然后被展开,并且该值用于后续的展开,而不是原始参数的展开。这个过程称为间接展开 。该值会受到波浪号展开、参数展开、命令替换和算术展开的影响。如果参数是命名引用,这将展开为参数所引用变量的名称,而不是执行完整的间接展开,以确保兼容性。这方面的例外是下面描述的 {!prefix\*} 和 {!name[@]} 的展开。感叹号必须紧跟左大括号后面才能引入间接性。

这段话提到了间接展开(即 ${!word} 形式)和命名引用。

先看一个间接展开的例子:

bash 复制代码
## 错误的写法
set -- a b c d
for ((i=1; i<=$#; i++)); do
	echo $i
done

## 正确的写法,使用间接展开
set -- a b c d
for ((i=1; i<=$#; i++)); do
	echo ${!i}
done

再看一个极简的间接展开示例:

bash 复制代码
$ var=123
$ a=var
$ echo $a
var
$ echo ${!a}
123

接下来是nameref的示例:

bash 复制代码
$ name=grace
$ declare -n nf
$ nf=name
$ echo $nf
grace
$ nf="flora"
$ echo $nf
flora
$ echo $name
flora
$ nf=sophia
$ echo $nf
sophia
$ echo $name
sophia
$ name=tommy
$ echo $nf
tommy

使用间接展开输出nameref指向的变量,但间接展开不支持嵌套:

bash 复制代码
$ echo ${!nf}
name
$ echo ${!${!nf}}
-bash: ${!${!nf}}: bad substitution
$ varname=${!nf}
$ echo ${!varname}
tommy

在下面的每种情况下,单词都会经历波浪号扩展、参数扩展、命令替换和算术扩展。

在不执行子字符串扩展时,使用下面描述的形式(例如 ':-'),Bash 会测试参数是否未设置或为空省略冒号则只测试参数是否未设置

💡 如果包含冒号,操作符会测试参数是否存在且其值不为空;如果省略冒号,操作符只测试参数是否存在。


${parameter:−word}

如果参数未设置或为 null,则替换为单词的展开形式。否则,替换为参数的值。

bash 复制代码
# 此测试设计得不错
$ v=123
$ echo ${v-unset}
123
$ echo ${v:-unset-or-null}
123
$ unset v
$ echo ${v-unset}
unset
$ echo ${v:-unset-or-null}
unset-or-null
$ v=
$ echo ${v-unset}

$ echo ${v:-unset-or-null}
unset-or-null

${parameter:=word}

如果参数未设置或为 null,则将单词的展开结果赋值给该参数,并且展开的结果就是该参数的最终值。位置参数和特殊参数不能以这种方式赋值。

💡 下面示例中的:是一个内置命令,其命令形式为: [参数]。该命令除了展开参数并执行任何指定的重定向之外不做任何操作。

bash 复制代码
$ unset var
$ : ${var=DEFAULT}
$ echo $var
DEFAULT
$ var=
$ : ${var=DEFAULT}
$ echo $var

$ var=
$ : ${var:=DEFAULT}
$ echo $var
DEFAULT
$ unset var
$ : ${var:=DEFAULT}
$ echo $var
DEFAULT

${parameter:?word}

如果参数为 null 或未设置,shell 会将 word 的展开(如果 word 不存在,则会提示相应信息)写入标准错误,并且如果 shell 不是交互式的,则以非零状态退出。交互式 shell 不会退出,但不会执行与展开相关的命令。否则,会替换参数的值。

bash 复制代码
$ var=
$ : ${var:?var is unset or null}
-bash: var: var is unset or null
$ echo ${var?var is unset}

$ unset var
$ : ${var?var is unset}
-bash: var: var is unset
$ : ${var:?var is unset or null}
-bash: var: var is unset or null
$ var=123
$ echo ${var:?var is unset or null}
123

${parameter:+word}

如果参数为 null 或未设置,则不会进行替换,否则会替换为 word 的展开形式。参数的值不会被使用。

bash 复制代码
$ var=123
$ echo ${var:+var is set and not null}
var is set and not null
$ echo $var
123
$ echo ${var+var is set}
var is set
$ var=
$ echo ${var:+var is set and not null}

$ echo ${var+var is set}
var is set
$ unset var
$ echo ${var+var is set}

$ echo ${var:+var is set and not null}

$

{parameter:offset} {parameter:offset:length}

这被称为子字符串扩展。它从offset 偏移量指定的字符开始,将参数的值扩展到最多 length 个字符。如果参数是 @*,或是用 @*下标的索引数组,或是关联数组名,结果会如下面所述有所不同。如果省略 :length (上述第一种形式),则会扩展为从偏移量指定的字符开始直到参数值末尾的子字符串。如果省略 offset ,则视为 0。如果省略 length ,但在 offset 后有冒号,则视为 0。lengthoffset 是算术表达式(参见 Shell 算术运算)。

如果 offset 的计算结果为小于零的数字,则该值将被用作从参数值末尾开始的字符偏移量。如果 length 的计算结果为小于零的数字,则将其解释为从参数值末尾开始的字符偏移量,而不是字符数,并且展开结果为 offset 与该结果之间的字符。

注意,负的 offset 必须与冒号至少有一个空格隔开,以避免与 ':-' 展开混淆。

示例:

bash 复制代码
$ string=01234567890abcdefgh
$ echo ${string:7}
7890abcdefgh
$ echo ${string:0}
01234567890abcdefgh
$ echo ${string:7:0}

# from position 7, length 2
$ echo ${string:7:2}
78
# from position 7 to positon -2
$ echo ${string:7:-2}
7890abcdef
# 注意冒号后有无空格的区别
$ echo ${string:-7}
01234567890abcdefgh
$ echo ${string: -7}
bcdefgh
$ echo ${string: -7:0}

$ echo ${string: -7:2}
bc
$ echo ${string: -7:-2}
bcdef

$ set -- 01234567890abcdefgh
$ echo $1
01234567890abcdefgh
$ echo ${1:7}
7890abcdefgh
$ echo ${1:7:0}

$ echo ${1:7:2}
78
$ echo ${1:7:-2}
7890abcdef
$ echo ${1: -7}
bcdefgh
$ echo ${1: -7:0}

$ echo ${1: -7:2}
bc
$ echo ${1: -7:-2}
bcdef

$ array[0]=01234567890abcdefgh
$ echo ${array[0]:7}
7890abcdefgh
$ echo ${array[0]:((3+4))}
7890abcdefgh
$ echo ${array[0]:7:0}

$ echo ${array[0]:7:2}
78
$ echo ${array[0]:7:-2}
7890abcdef
$ echo ${array[0]: -7}
bcdefgh
$ echo ${array[0]: -7:0}

$ echo ${array[0]: -7:2}
bc
$ echo ${array[0]: -7:-2}
bcdef

如果参数是@*,结果就是从offset 开始,长度为length的参数。负偏移量是相对于比最大位置参数大一的数来计算的,因此偏移量为 -1 时对应最后一个位置参数(如果没有位置参数则为 0)。如果长度的计算结果小于零,则会发生扩展错误。

💡 最左边的位置为0,最右边的位置为-1;offset位置处的字符总是包含在结果中,length位置处(即其为负数)的字符总是不包含在结果中。

示例:

bash 复制代码
# 其实也是从0开始计数的,只不过此时$0就是command
$ set -- 1 2 3 4 5 6 7 8 9 0 a b c d e f g h
$ echo ${@:7}
7 8 9 0 a b c d e f g h
$ echo ${@:7:2}
7 8
# 如果参数为@, offset 不能为负数
$ echo ${@:7:-2}
-bash: -2: substring expression < 0
$ echo ${@: -7:2}
b c
$ echo ${@:0}
-bash 1 2 3 4 5 6 7 8 9 0 a b c d e f g h
$ echo ${@:0:2}
-bash 1
$ echo ${@: -7:0}

$

💡 在示例中,位置参数0有时为-bash,有时有为./bash。这指示了shell是登录式还是非登录式。bash 以登录 shell 启动时,会在进程名称前自动添加一个短横线-作为标识

💡 在示例中有很多set --,set是一个内置命令,如果此选项后没有参数,则位置参数将被重置。否则,位置参数将被设置为这些参数(--后的参数),即使其中有些参数以 - 开头。

如果参数是一个由'@'或'*'下标索引的索引数组名称,则结果是从 ${parameter[offset]} 开始的数组成员长度。负偏移是相对于指定数组的最大索引加一来计算的。如果 length 的计算结果小于零,则会发生扩展错误。

示例:

bash 复制代码
$ array=(0 1 2 3 4 5 6 7 8 9 0 a b c d e f g h)
$ echo ${array[@]:7}
7 8 9 0 a b c d e f g h
$ echo ${array[@]:7:2}
7 8
$ echo ${array[@]: -7:2}
b c
$ echo ${array[@]: -7:-2}
-bash: -2: substring expression < 0
$ echo ${array[@]:0}
0 1 2 3 4 5 6 7 8 9 0 a b c d e f g h
$ echo ${array[@]:0:2}
0 1
$ echo ${array[@]: -7:0}

$

对子关联数组应用子字符串扩展会产生未定义的结果。

💡 子字符串索引是从零开始 的,位置参数索引则从1开始。如果偏移量为0,并且使用了位置参数,则会在列表前加上$0。


{!prefix\*}** **{!prefix@}

展开为以前缀开头的变量名,这些变量名之间用 IFS 特殊变量的第一个字符分隔。当使用'@'且展开出现在双引号内时,每个变量名会展开为一个单独的词。

示例:

bash 复制代码
$ declare abc1
$ declare abc2=100
$ echo ${!abc*}
abc2
$ set | grep abc
_=abc2
abc2=100
$ unset abc2
$ echo ${!abc*}

$ declare abc1=1
$ declare abc2=2
$ declare abc3=3
$ for i in "${!abc*}"; do echo $i; done
abc1 abc2 abc3
$ for i in "${!abc@}"; do echo $i; done
abc1
abc2
abc3

💡 变量必须是已经赋值的。unset的变量不会匹配。


{!name\[@\]} {!name[*]}

如果 name 是一个数组变量,则展开为在 name 中分配的数组索引(键)列表。如果 name 不是数组,则如果 name 已设置则展开为 0,否则展开为 null。当使用 '@' 且展开出现在双引号内时,每个键会展开为一个独立的单词。

💡 有!输出的是key,无!则输出的是value

示例:

bash 复制代码
$ a=(jan feb mar apr may)
$ echo ${a[0]}
jan
$ echo ${!a[@]}
0 1 2 3 4
$ echo ${a[@]}
jan feb mar apr may

${#parameter}

用参数值的字符长度进行替换。如果参数为*@,则替换的值为位置参数的数量。如果参数是带有*@下标的数组名称,则替换的值为数组中的元素数量。如果参数是带负数下标的索引数组名称,该负数会被解释为相对于参数最大下标加一的偏移量,因此负数下标从数组末尾向前计数,-1下标引用最后一个元素。

示例:

bash 复制代码
$ a=banana
$ echo ${#a}
6
$ a=(jan feb mar apr)
$ echo ${#a}
3
$ set -- a b c d e
$ echo ${#@}
5

{parameter#word} {parameter##word}

单词会被展开以生成一个模式,并根据下面描述的规则(参见模式匹配)与参数的展开值进行匹配。如果模式匹配参数展开值的开头 ,那么展开的结果就是删除最短 匹配模式('#'情况)或最长 匹配模式('##'情况)后的参数展开值。如果参数是*@,模式删除 操作会依次应用于每个位置参数,并且展开结果是生成的列表。如果参数是带有*@下标的数组变量,模式删除操作会依次应用于数组的每个成员,并且展开结果是生成的列表。


{parameter%word} {parameter%%word}

单词会被展开以生成一个模式,并根据下面描述的规则(参见模式匹配)与参数的展开值进行匹配。如果模式匹配参数展开值的尾部,那么展开的结果就是将参数值中最短 匹配模式('%' 情况)或最长 匹配模式('%%' 情况)删除后的值。如果参数是*@,模式删除 操作会依次应用于每个位置参数,展开结果就是得到的列表。如果参数是一个使用*@下标的数组变量,模式删除操作会依次应用于数组的每个成员,展开结果就是得到的列表。

示例:

bash 复制代码
$ filepath="/home/user/docs/work/report.pdf"
$ echo ${filepath#*/}
home/user/docs/work/report.pdf
$ echo ${filepath##*/}
report.pdf

$ filename=a.bak.bak
$ echo ${filename%.bak}
a.bak
$ echo ${filename%%.bak}
a.bak
$ echo ${filename%.*bak}
a.bak
$ echo ${filename%%.*bak}
a

💡 无论是#,##,还是%,%%,操作都是删除。只不过1个字符时时最短匹配(非贪婪模式),2个字符时是最长匹配(贪婪模式)。


{parameter/pattern/string} {parameter//pattern/string}
{parameter/#pattern/string} {parameter/%pattern/string}

模式会被展开以生成一个模式,并与参数的展开值进行匹配,如下所述(见模式匹配)。在展开值中匹配模式的最长部分会被字符串替换。字符串会经历波浪符展开、参数和变量展开、算术展开、命令和进程替换以及引号移除。

在上面的第一种形式中,仅替换第一个匹配项。如果参数和模式之间有两个斜杠(上面的第二种形式),则所有模式匹配项都会被字符串替换。如果模式前面有'#'(上面的第三种形式),它必须匹配参数展开值的开头 。如果模式前面有'%'(上面的第四种形式),它必须匹配参数展开值的结尾

如果字符串展开结果为空,模式匹配项会被删除,并且紧跟在模式后的/可以省略。

示例:

bash 复制代码
$ str="a b a c a d"
$ echo ${str/a/A}
A b a c a d
$ echo ${str//a/A}
A b A c A d
$ echo ${str/#a/A}
A b a c a d
$ echo ${str/%d/A}
a b a c a A
$ echo ${str/%a/A}
a b a c a d
$ echo ${str/a}
b a c a d
$ echo ${str//a}
b c d

如果使用 shopt 启用了 patsub_replacement shell 选项(参见 The Shopt Builtin),字符串中任何未加引号的 & 实例都会被模式中匹配的部分替换。这旨在复制一个常见的 sed 用法。

💡 需要注意,patsub_replacement是bash 5.2新引入的选项,我的bash版本是5.1.8,没有这个选项。

引用字符串的任何部分都会抑制对引用部分展开的替换,包括存储在shell变量中的替换字符串。字符串中的反斜线转义"&";为了允许替换字符串中出现字面的"&",反斜杠被移除。用户应注意字符串是否带有双引号,以避免反斜杠与双引号之间的不良交互,因为反斜杠在双引号中有特殊含义。模式替换在展开字符串后检查未加引号的"&",因此用户应确保在替换中正确引用任何想字面理解的"&"出现,并确保所有想替换的"&"实例都未被引用。

例如:

bash 复制代码
# 第一组
var=abcdef
rep='& '
echo ${var/abc/& }
echo "${var/abc/& }"
echo ${var/abc/$rep}
echo "${var/abc/$rep}"

# 第二组
var=abcdef
rep='& '
echo ${var/abc/\& }
echo "${var/abc/\& }"
echo ${var/abc/"& "}
echo ${var/abc/"$rep"}

💡 按照手册中的说法,第一组的输出为"abc def",第二组的输出为"& def"。但我所有的输出均为"& def"。不知道是否与bash版本有关。

如果启用了 nocasematch shell 选项,匹配时将不区分字母的大小写。

bash 复制代码
$ shopt nocasematch
nocasematch     off
$ shopt -s nocasematch
$ shopt nocasematch
nocasematch     on
$ str="a b A c a d"
$ echo ${str//a/ei}
ei b ei c ei d
$ shopt -u nocasematch
$ shopt nocasematch
nocasematch     off

如果参数是*@,替换操作依次应用于每个位置参数,扩展结果是生成的列表。如果参数是一个带有*@下标的数组变量,替换操作将依次应用于数组的每个成员,扩展结果也是生成的列表。


{parameter\^pattern} {parameter^^pattern}
{parameter,pattern} {parameter,pattern}

此扩展修改参数中字母字符的大小写。首先,模式会被展开,以生成如下在模式匹配中描述的模式。

然后,Bash 根据下述方式检查参数展开值中的字符是否与模式匹配。如果字符匹配模式,其大小写会被转换。模式不应尝试匹配多个字符。

使用^将匹配模式的小写字母转换为大写;,将匹配模式的大写字母转换为小写。单个的^, 变体仅检查展开值的第一个 字符,并在匹配模式时转换其大小写;^^,, 变体会检查展开值中的所有字符,并对每个匹配模式的字符进行大小写转换。如果省略模式,则将其视为?,匹配每个字符。

如果参数是@*,大小写修改操作会依次应用于每个位置参数,展开结果为生成的列表。如果参数是带有@*下标的数组变量,大小写修改操作会依次应用于数组的每个成员,展开结果为生成的列表。

示例:

bash 复制代码
$ word=sophisticate
$ echo ${word^i}
sophisticate
$ echo ${word^s}
Sophisticate
$ echo ${word^^i}
sophIstIcate
$ echo ${word^^s}
SophiSticate

$ word=SOPHISTICATE
$ echo ${word,I}
SOPHISTICATE
$ echo ${word,,I}
SOPHiSTiCATE
$ echo ${word,S}
sOPHISTICATE
$ echo ${word,,S}
sOPHIsTICATE

${parameter@operator}

扩展要么是参数值的变化,要么是关于参数本身的信息,这取决于运算符的取值。每个运算符都是一个字母:

U

展开是一个字符串,它是参数的值,其中小写字母字符被转换为大写。

u

展开是一个字符串,它是参数的值,如果首字符是字母,则将其转换为大写。

L

展开是一个字符串,它是参数的值,其中大写字母字符被转换为小写。

Q

展开是一个字符串,它是参数的值,以可以作为输入重新使用的格式引用。

E

展开是一个字符串,它是参数的值,其中反斜杠转义序列按照 $'...' (参见ANSI-C Quoting)引用机制展开。

P

展开是一个字符串,它是通过将参数的值作为提示字符串展开的结果(参见控制提示)。

A

扩展是一个字符串,形式为赋值语句或 declare 命令,如果进行求值,会重新创建带有其属性和值的参数。

K

生成参数值的可能带引号的版本,但它会将索引数组和关联数组的值打印为带引号的键值对序列(参见数组)。键和值以可重新用作输入的格式加引号。

a

扩展是一个由标志值组成的字符串,表示参数的属性。

k

类似于"K"转换,但会在单词分割后将索引数组和关联数组的键和值扩展为单独的单词。

如果参数是@*,则操作会依次应用于每个位置参数,展开结果是生成的列表。如果参数是带有@*下标的数组变量,则操作会依次应用于数组的每个成员,展开结果是生成的列表。

展开的结果会受到如下所述的单词拆分和文件名扩展的影响。

示例:

bash 复制代码
# 大小写转换
$ a=sophisticate
$ echo ${a@U}
SOPHISTICATE
$ echo ${a@u}
Sophisticate
$ echo ${a@L}
sophisticate

# 转义转换
$ str="hello world \"test\" 'bash'"
$ escaped_str=${str@Q}
# 下例采用了 "单引号断开 + 转义单引号 + 单引号重新开启" 的拼接技巧
$ echo $escaped_str
'hello world "test" '\''bash'\'''

# ANSI C Quoting
$ raw_seq="a\tb\nc\td"
$ echo "$raw_seq"
a\tb\nc\td

# 下面结果的不同,需要参看本节后续Word Splitting部分。
$ echo ${raw_seq@E}
a b c d
$ echo "${raw_seq@E}"
a       b
c       d

# declare命令
$ a=123
$ echo ${a@A}
declare -a a='123'
$ a=(1 2 3)
$ echo ${a@A}
declare -a a='1'

# a option
$ declare -u var='abc'
$ echo $var
ABC
$ echo ${var@a}
u
$ declare -i var=123
$ echo ${var@a}
iu

# K option (k option 在我的版本不支持)
$ arr=(a b c d)
$ echo ${arr[@]@K}
0 "a" 1 "b" 2 "c" 3 "d"

$ user_info=([name]="Tom" [age]="28" [city]="Beijing")
$ echo ${user_info[@]@K}
city "Beijing" age "28" name "Tom"

# option P
$ export PS1='$ '
$ prompt='[\u@\h \W]\$'
$ echo ${prompt@P}
[vagrant@ol9-vagrant ~]$

3.5.4 Command Substitution

命令替换允许命令的输出替换命令本身。命令替换的标准形式是在命令被如下所示地括起来时发生的:

bash 复制代码
$(command)
# 或 (不推荐使用)
`command`.

Bash 通过在子 shell 环境中执行命令来执行命令替换,并用命令的标准输出 替换命令替换,删除任何尾随换行符。嵌入的换行符不会被删除,但在单词分割过程中可能会被移除。命令替换 (cat file) 可以被等效但更快速的 (< file) 所替代。

在旧式反引号替换中,反斜杠保持字面含义,除非后面跟着\$、`````或\。第一个没有反斜线的反引号终止命令替换。使用$(command)形式时,括号内的所有字符组成命令;没有人被特别对待。

指令替代还有另外一种方式:

bash 复制代码
${c command; }

它在当前执行环境中执行命令并捕获其输出,同样会去掉末尾的换行符。

💡 这种形式bash不支持。

紧随左大括号的字符 c 必须是空格、制表符、换行符或 '|',而右大括号必须位于保留字可能出现的位置(即前面由命令终止符如分号隔开)。Bash 允许右大括号与单词中的其他字符连接,而不必跟随 shell 元字符,这通常是保留字所要求的。

命令的任何副作用都会立即在当前执行环境中生效,并在命令完成后保留在当前环境中(例如,exit 内建命令会退出 shell)。

这种类型的命令替换在表面上类似于执行一个未命名的 shell 函数:在执行 shell 函数时会创建局部变量,并且 return 内建命令会强制命令完成;然而,其余的执行环境,包括位置参数,与调用者共享。

如果开括号之后的第一个字符是 '|',该结构在命令执行后会扩展为 REPLY shell 变量的值,而不删除任何尾随换行符,并且命令的标准输出保持与调用 shell 中相同。当命令执行时,Bash 会将 REPLY 创建为一个初始未设置的局部变量,并在命令完成后将 REPLY 恢复为命令替换之前的值,就像处理任何局部变量一样。

例如,该结构会扩展为 '12345',并且在当前执行环境中保持 shell 变量 X 不变:

bash 复制代码
${ local X=12345 ; echo $X; }

(如果不将 X 声明为本地变量,其值将在当前环境中被修改,就像普通的 shell 函数执行一样),而这个结构不需要任何输出就可以扩展为 '12345':

bash 复制代码
${| REPLY=12345; }

并将 REPLY 恢复到命令替换之前的值。

命令替换可以嵌套。使用反引号形式嵌套时,需要用反斜杠转义内层的反引号。

如果替换出现在双引号内,Bash 不会对结果进行单词拆分和文件名扩展。

命令替换在子shell中执行,这点非常关键。以下是一个错误使用命令替换的例子:

bash 复制代码
for i in {1..4}; do echo Line $i; done > test.txt

## 正确输出,最优
cat test.txt | while read -r line; do echo "$line"; done

## 错误输出
cat test.txt | while $(read -r line); do echo "$line"; done

## 正确输出,但不推荐
cat test.txt | while { read -r line; } ; do echo "$line"; done

3.5.5 Arithmetic Expansion

算术扩展会计算算术表达式并替换为结果。算术扩展的格式是:

bash 复制代码
$(( expression ))

表达式会像在双引号内一样进行相同的扩展,但表达式中未转义的双引号字符不会被特殊处理,会被移除。表达式中的所有标记都会进行参数和变量扩展、命令替换以及引号去除。结果将被作为要计算的算术表达式进行处理。由于 Bash 处理双引号的方式可能导致空字符串,算术扩展会将这些视为值为 0 的表达式。算术扩展可以嵌套。

计算是根据下列规则进行的(参见 Shell 算术)。如果表达式无效,Bash 会向标准错误输出显示失败信息,不会进行替换,也不会执行与扩展相关的命令。

示例:

bash 复制代码
# 注意不能写成 a=3 b=4 echo $(( $a + $b ))
# a=1 b=2; a=3 b=4 echo $(( $a + $b ))
$ a=3 b=4; echo $(( $a + $b ))
7

$ echo $((2 + $((3+4)) ))
9

$ echo $(( "3" + 4 ))
7

$ a= b=1; echo $((a+b))
1

3.5.6 Process Substitution

进程替代允许使用文件名来引用进程的输入或输出。它的形式为

bash 复制代码
<(list)
# 或
>(list)

进程列表是异步运行 的,其输入或输出会显示为一个文件名。这个文件名作为扩展的结果被传递给当前命令作为参数

如果使用 >(list) 形式,向该文件写入内容会为 list 提供输入。如果使用 <(list) 形式,读取该文件会获得 list 的输出。注意,<> 与左括号之间不能有空格,否则该结构会被解释为重定向

在支持命名管道(FIFO)或 /dev/fd 方法命名打开文件的系统上,可以使用进程替换。

在可用的情况下,进程替换会与参数和变量扩展、命令替换以及算术扩展同时执行。

示例:

bash 复制代码
$ cat <(echo "Hello, Process Substitution!")
Hello, Process Substitution!

$ diff <(ls /home/vagrant) <(ls /home/oracle)

$ cat > >(wc -l) << EOF
> a
> b
> c
> EOF
3

$ cat << EOF|wc -l
> a
> b
> c
> EOF
3

💡 进程替换的核心是将命令流伪装为文件流,避免临时文件的创建与清理,提升脚本效率和简洁性。

💡 当一些命令需要文件作为参数时,利用进程替换,这些文件参数就可以用某些命令的输出来替代。

bash 复制代码
paste <(echo "OPTION STATE"; set -o) <(echo "CMD"; set +o) | column -t
join -1 1 -2 3 <(set -o) <(set +o)|column -t
comm <(sort file1.txt) <(sort file2.txt)
sort <(cat file1 file2 file3) | uniq
wc -l <(ls /etc) <(ls /usr/bin) <(ls /var/log)

暂时还没有体会到进程替换的优势,不过下面这个例子可参考:

bash 复制代码
# 传统方法(变量修改只在子shell中有效)
var=0
find . -name "*.txt" | while read file; do
    ((var++))
done
echo $var  # 输出 0

# 使用进程替换(变量修改在主shell中保持)
var=0
while read file; do
    ((var++))
done < <(find . -name "*.txt")
echo $var  # 输出正确的文件数量

以上的关键问题,是管道会创建子shell。

3.5.7 Word Splitting

Shell 扫描未在双引号内进行的参数扩展、命令替换和算术扩展的结果以进行单词分割。未扩展的单词不会被分割。

示例:

bash 复制代码
$ raw_seq="a\tb\nc\td"
$ echo ${raw_seq@E}
a b c d
$ echo "${raw_seq@E}"
a       b
c       d

$ a="  aaa
>
> "
$ echo $a
aaa

Shell 将 $IFS 的每个字符视为分隔符,并使用这些字符作为字段终止符将其他扩展的结果拆分为字段。

IFS 空白字符是上述定义(见定义部分)中出现的、出现在 IFS 值中的空白字符。空格、制表符和换行符始终被视为 IFS 空白字符,即使它们不在本地的空格类别中。

如果 IFS 未设置,单词拆分将表现得好像其值为 <空格><制表符><换行符>,并将这些字符视为 IFS 空白字符。如果 IFS 的值为空,则不会进行单词拆分,但隐式空参数(见下文)仍会被移除。

单词拆分开始于从前一次展开结果的开头和结尾移除 IFS 空白字符序列,然后拆分剩余的单词。

如果 IFS 的值仅由 IFS 空白字符组成,那么任何 IFS 空白字符序列都分隔一个字段,因此一个字段由非引用的 IFS 空白字符组成,只有通过引用才会产生空字段。

如果 IFS 包含非空白字符,则 IFS 值中任何不是 IFS 空白的字符,连同相邻的 IFS 空白字符,一起作为字段分隔符。这意味着相邻的非 IFS 空白分隔符会产生一个空字段。一串 IFS 空白字符也会分隔字段。

显式的空参数("" 或 '')会被保留并作为空字符串传递给命令。未加引号的隐式空参数,由没有值的参数扩展产生,会被移除。在双引号中扩展没有值的参数会生成一个空字段,这个空字段会被保留并作为空字符串传递给命令。

当引号括起来的空参数出现在扩展结果非空的单词中时,单词分割会移除空参数部分,仅保留非空扩展部分。也就是说,单词 -d'' 在单词分割和空参数移除后变成 -d。

前面提到的双引号太重要了,来看一个IFS的例子:

bash 复制代码
$ set|grep ^IFS
IFS=$' \t\n'
$ echo $IFS |wc -c
1
$ echo $IFS |od -bc
0000000 012
         \n
0000001
$ echo -n "$IFS" |od -bc
0000000 040 011 012
             \t  \n
0000003
$ echo -n "$IFS" |wc -c
3

另外对于位置参数,"$*"是受IFS影响的,而$@不会:

bash 复制代码
$ set -- 1 2 3 4
$ echo $*
1 2 3 4
$ echo $@
1 2 3 4
$ IFS=
# 无引号时,相当于 "$1 $2 $3 $4"
$ echo $*
1 2 3 4
# 有引号时,相当于 "$1c$2c$3c$4"
$ echo "$*"
1234
$ echo "$@"
1 2 3 4

$ IFS=:
$ echo "$@"
1 2 3 4
$ echo "$*"
1:2:3:4
$ echo $*
1 2 3 4

3.5.8 Filename Expansion

在词拆分之后,除非已设置 -f 选项(见内建命令 set),Bash 会扫描每个单词是否包含字符 *?[。如果其中一个字符出现且没有被引用,则该单词被视为模式,并用匹配该模式的文件名的排序列表替换(见模式匹配),并受 GLOBSORT shell 变量的值影响(见 Bash 变量)。

如果未找到匹配的文件名,并且 shell 选项 nullglob 未启用,则单词保持不变。如果设置了 nullglob 选项,但没有找到匹配项,则该单词将被删除。如果设置了 failglob shell 选项,但未找到匹配项,Bash 将打印错误信息并且不会执行命令。如果启用了 nocaseglob shell 选项,则匹配时不区分字母的大小写。

当模式用于文件名扩展时,文件名开头或紧跟斜杠后的 '.' 字符必须显式匹配,除非设置了 shell 选项 dotglob。为了匹配文件名 . 和 ...,模式必须以 '.' 开头(例如 '.?'),即使设置了 dotglob。如果启用了 globskipdots shell 选项,文件名 . 和 ... 永远不会匹配,即使模式以 '.' 开头。当不匹配文件名时,字符 '.' 不被特别处理。

在匹配文件名时,斜杠字符必须在模式中由斜杠明确匹配,但在其他匹配上下文中,它可以通过下面描述的特殊模式字符进行匹配(见模式匹配)。

请参阅The Shopt Builtin中 shopt 的描述,以了解 nocaseglob、nullglob、globskipdots、failglob 和 dotglob 选项的说明。

GLOBIGNORE shell 变量可用于限制与某个模式匹配的文件名集合。如果设置了 GLOBIGNORE,则每个匹配的文件名中,如果还匹配 GLOBIGNORE 中的某个模式,将从匹配列表中移除。如果设置了 nocaseglob 选项,则与 GLOBIGNORE 中模式的匹配将不区分大小写。当 GLOBIGNORE 被设置且不为空时,文件名 . 和 ... 总是被忽略。然而,将 GLOBIGNORE 设置为非空值的效果是启用 dotglob shell 选项,因此所有以 '.' 开头的其他文件名都会匹配。要恢复旧的行为,即忽略以 '.' 开头的文件名,可以将 '.*' 作为 GLOBIGNORE 中的一个模式。当 GLOBIGNORE 未设置时,dotglob 选项被禁用。GLOBIGNORE 的模式匹配会遵循 extglob shell 选项的设置。

GLOBSORT shell 变量的值控制路径名展开结果的排序方式,如下所述(参见 Bash 变量)。

示例:

bash 复制代码
$ set -o|grep glob
noglob          off
$ ls -F
1.txt  2.txt  3.txt  4.txt  test/
$ echo *
1.txt 2.txt 3.txt 4.txt test
$ echo [1-9].txt
1.txt 2.txt 3.txt 4.txt

$ set -f
$ set -o|grep glob
noglob          on
$ echo *
*
$ echo [1-9].txt
[1-9].txt

除set选项外,shopt还有额外的控制。shopt中并没有对应于noglob的选项:

bash 复制代码
$ set +f
$ shopt nullglob
nullglob        off
$ ls
1.txt  2.txt  3.txt  4.txt  test
$ echo *.pdf
*.pdf
$ shopt -s nullglob
$ echo *.pdf

$
$ shopt failglob
failglob        off
$ shopt -s failglob
$ echo *.pdf
-bash: no match: *.pdf

注意,*默认不会匹配以.开头的文件,即隐含文件。

bash 复制代码
$ > '.1.pdf'
$ ls -a *.pdf
ls: cannot access '*.pdf': No such file or directory
$ ls -a .*pdf
.1.pdf

3.5.8.1 Pattern Matching

除下面描述的特殊模式字符外,模式中出现的任何字符都与其本身匹配。NUL 字符不能出现在模式中。反斜杠用于转义后面的字符;在匹配时,转义用的反斜杠会被舍弃。如果要按字面匹配特殊模式字符,则必须对其进行引用。

特殊模式字符具有以下含义:


*

匹配任何字符串,包括空字符串。当启用 globstar shell 选项,并且在文件名展开上下文中使用 *时,两个相邻的 * 作为单一模式匹配所有文件以及零个或多个目录和子目录。如果后面跟着 /,则两个相邻的 *只匹配目录和子目录。


?

匹配任意单个字符。


[...]

匹配括号中包含的任意一个字符。这被称为括号表达式,并匹配单个字符。由连字符分隔的一对字符表示范围表达式;任何介于这两个字符之间的字符(包含两端字符),根据当前区域设置的排序顺序和字符集,都将匹配。如果'['之后的第一个字符是'!'或'^',则匹配范围之外的任何字符。要匹配'−',请将其放在集合的第一个或最后一个字符。要匹配']',请将其放在集合的第一个字符。

范围表达式中字符的排序顺序以及范围中包含的字符由当前的区域设置以及 LC_COLLATE 和 LC_ALL shell 变量的值决定(如果已设置)。

例如,在默认的 C 区域设置中,'[a-dx-z]' 等同于 '[abcdxyz]'。许多区域设置按字典顺序排序字符,在这些区域设置中,'[a-dx-z]' 通常不等同于 '[abcdxyz]';例如,它可能等同于 '[aBbCcDdxYyZz]'。要获得传统的括号表达式中范围的解释,可以通过将 LC_COLLATE 或 LC_ALL 环境变量设置为 'C' 来强制使用 C 区域设置,或者启用 globasciiranges shell 选项。

在方括号表达式中,可以使用 [:class:] 语法来指定字符类,其中 class 是 POSIX 标准中定义的以下类之一:

bash 复制代码
alnum   alpha   ascii   blank   cntrl   digit   graph   lower
print   punct   space   upper   word    xdigit

字符类匹配属于该类的任何字符。单词字符类匹配字母、数字和字符'_'。

例如,以下模式将匹配当前语言环境中属于空格字符类的任何字符,然后匹配任何大写字母或"!",接着是一个点,最后是任何小写字母或连字符。

bash 复制代码
[[:space:]][[:upper:]!].[-[:lower:]]

在括号表达式中,可以使用语法 [=c=] 来指定一个等价类,它匹配与字符 c 具有相同排序权重(由当前语言环境定义)的所有字符。但是我执行没有结果

bash 复制代码
sudo yum install glibc-langpack-es
export LANG=es_ES.UTF-8
test_string="caña año español"
echo "$test_string" | grep -o '[=n=]'

在括号表达式中,语法 [.symbol.] 匹配排序符号 symbol。

以下代码,可检测哪些字符属于digit字符类。其他字符类,可把digit换为alnum,punct等即可。

bash 复制代码
for code in {1..255}; do
    char=$(printf "\\$(printf '%03o' "$code")")
    if [[ "$char" =~ [[:digit:]] ]]; then
        printf "  %q (U+%04X)\\n" "$char" "$code"
    fi
done

关于字符类的定义,源头在POSIX 标准POSIX.1-2008中。最新的POSIX标准可参见这里。搜索isdigit,或在第7章Locale中,都可以找到字符类的示例。

以下man page也有关于字符类的信息:

bash 复制代码
man 7 regex
man ascii
man grep

💡 [:class:] 是字符类的定义(内部表示),而 [[:class:]] 是字符类的使用。

bash 复制代码
$ ls [[:digit:]].txt
1.txt  2.txt  3.txt  4.txt

$ ls [:digit:].txt
ls: cannot access '[:digit:].txt': No such file or directory

如果使用 shopt 内置命令启用了 extglob shell 选项,shell 将识别多种扩展的模式匹配操作符。在下述说明中,pattern-list 是由一个或多个模式组成的列表,模式之间用"|"分隔。匹配文件名时,dotglob shell 选项决定被测试的文件名集合,如上所述。复合模式可以使用以下一个或多个子模式形成:

?(pattern-list)

匹配给定模式的零次或一次出现。

*(pattern-list)

匹配给定模式的零个或多个出现。

+(pattern-list)

匹配给定模式的一次或多次出现。

@(pattern-list)

匹配给定的某一个模式。

!(pattern-list)

匹配除给定模式之外的任何内容。

extglob 选项会改变解析器的行为,因为括号通常被视为具有语法意义的运算符。为了确保扩展匹配模式被正确解析,请确保在解析包含这些模式的构造(包括 shell 函数和命令替换)之前启用 extglob。

在匹配文件名时,dotglob shell 选项决定了要测试的文件名集合:当启用 dotglob 时,文件名集合包括所有以"."开头的文件,但文件名 . 和 ... 必须由以点开头的模式或子模式匹配;当禁用时,集合中不包括任何以"."开头的文件名,除非模式或子模式以"."开头。如果启用 globskipdots shell 选项,文件名 . 和 ... 永远不会出现在集合中。如前所述,只有在匹配文件名时,"." 才具有特殊含义。

针对长字符串的复杂扩展模式匹配很慢,尤其是当模式包含选择项且字符串包含多次匹配时。对较短的字符串进行单独匹配,或使用字符串数组而不是单个长字符串,可能会更快。

💡 extglob 选项默认是关闭的

3.5.9 Quote Removal

在前述扩展之后,所有未被引用且不是由上述扩展产生的字符\'"都将被移除。

相关推荐
rainbow68896 小时前
C++开源库dxflib解析DXF文件实战
开发语言·c++·开源
deepxuan6 小时前
Day7--python
开发语言·python
firewood20246 小时前
共射三极管放大电路相关情况分析
笔记·学习
zl0_00_06 小时前
美亚2023
学习
AI_56786 小时前
SQL性能优化全景指南:从量子执行计划到自适应索引的终极实践
数据库·人工智能·学习·adb
zl0_00_06 小时前
pctf wp
学习
禹凕6 小时前
Python编程——进阶知识(多线程)
开发语言·爬虫·python
Hello_Embed6 小时前
libmodbus STM32 主机实验(USB 串口版)
笔记·stm32·学习·嵌入式·freertos·modbus
学编程的闹钟6 小时前
98【html的php化】
学习