zsh 的参数扩展相比 bash 而言强大了不止一星半点,它让 zsh 无需借助外部命令就能完成大量操作,是写出一个复杂的流畅的 zsh 插件的必备技能之一。

这也是一个区分“zsh 用户”和“用 zsh 作为交互式 shell 的 bash 用户”的有效手段。

考虑到本节内容很长,而我很懒,我决定将这节拆开。这篇文章里只介绍传统的 ${var%string} 形式的扩展,这方面和 bash 的用法大部分相同,但也有一些区别。


首先是与变量定义相关的

形式 作用
${+var} 若 var 已定义则返回 1,反之返回 0
${var-str} 若 var 未定义则返回 str,反之返回 $var
${var:-str} 若 var 为空则返回 str,反之返回 $var
${var+str} 若 var 已定义则返回 str,反之返回 $var
${var:+str} 若 var 不为空则返回 str,反之返回 $var
${var=str} 若 var 未定义则赋值并返回 str
${var:=str} 若 var 为空则赋值并返回 str
${var::=str} 总是为 var 赋值并返回 str
${var?str} 若 var 未定义则抛出错误,错误信息为 str,否则返回 $var
${var:?str} 若 var 为空则抛出错误,错误信息为 str,否则返回 $var

这部分和 bash 应该是一致的,没啥好讲,只提一个 zsh 里的特殊用法:

包裹字符串字面量

比如我有一个字符串 “~/.local/share”,我想把 share 替换成 bin。

一般的做法是将这个字符串赋给一个变量,然后再替换。但在 zsh 里我们可以直接这样写 ${${:-"~/.local/share"}/share/bin}。这里巧妙地利用了 ${var:-str} 的语法将一个字符串字面量包裹了起来,是一个降低代码可读性节省代码行数的小技巧。


接下来是最常用的字符串替换,仍然与 bash 差不多

形式 作用
${var#pat} 从 $var 的开头删掉匹配模式 pat 的字符串并返回
${var##pat} 同上,不过此处是贪婪匹配
${var%pat} 从 $var 的末尾删掉匹配模式 pat 的字符串并返回
${var%%pat} 同上,不过此处是贪婪匹配
${var:#pat} 如果 $var 完整匹配 pat 则返回空字符串,否则直接返回
${var/pat/repl} 将第一个匹配 pat 的字符串替换为 repl,贪婪匹配
${var//pat/repl} 同上,但是会替换所有出现的位置
${var:/pat/repl} 同上,但是要求 pat 匹配整个字符串(类比一下 ${var:#pat} )

需要注意的是,如果 var 是数组的话,那么规则就会应用到每一个元素。比如 ${var#pat} 变成从数组中每个元素的开头删除匹配模式 pat 的字符串,${var:#pat} 变成将数组中完整匹配 pat 的元素删除。

举例如下,注意只有加了引号并且没有使用 @ 的时候规则才会作用于数组整体:

1
2
3
4
5
local -a var=(1a 1b 1c)
echo "${var#1}"    # => a 1b 1c
echo ${var#1}      # => a b c
echo "${var[@]#1}" # => a b c
echo "${(@)var#1}" # => a b c

关于 ${var/pat/repl} 的形式还有几个需要注意的点:

  • ${var/pat/repl} 的形式中,pat 也可以加 #%#% 来指示从前、从后、或是要求完整匹配字符串。这点同样与 bash 一致。

  • 如果 pat 是一个变量,那么它的展开结果会被当成一个纯字符串而不是模式。这点与 bash 不同需要注意!你需要显式使用 ${~var} 来告诉 zsh 这个地方的展开结果是模式。

  • 这个替换默认是贪婪的,如果想要不贪婪的话,需要使用 S flag:${(S)var/pat/repl} 。关于参数扩展 flag 的内容会在下节介绍。


接下来是数组操作,这部分掺杂的乱七八糟的玩意儿就逐渐多起来了

形式 作用
${var:\|arr} 两数组做差
${var:*arr} 两数组取交集
${var:^arr} zip 两个数组,最终数组长度以较短的为准
${var:^^arr} zip 两个数组,最终数组长度以较长的为准,较短的数组会循环补全到同样长度
${var:ofs} 切片,取下标 ofs 一直到末尾的内容
${var:ofs:len} 切片,从下标 ofs 开始取 len 个元素

注意点:

  • ofs 如果包含负号的话需要加一个空格,否则会与 ${var:-str} 的用法冲突

  • 切片下标会自动进行算术扩展,因此可以直接写 a + b 而不用 $(( a + b ))(当然还是要注意空格)


最后是某些前缀,下面的 spec 可以为上面任一扩展形式,其中 # 如过要与 ^=~ 组合的话,则必须放在它们的右边。特别的,如果 spec 只是一个简单的变量名的话,可以省略大括号:

  • ${#spec} – 取展开结果的长度

  • ${^spec} – 对展开结果启用 RC_EXPAND_PARAM 开关,比如 var=(1 2 3),则 A${^var}B 会展开为 A1B A2B A3B

  • ${=spec} – 对展开结果启用 SH_WORD_SPLIT,也就是根据 IFS 把你的展开结果分割为数组。没错!这就是 bash 的默认行为……

  • ${~spec} – 对展开结果启用 GLOB_SUBST 并视为模式尝试展开。没错!这也是 bash 的默认行为……

后两个都是坑爹 bash 的默认行为,也是导致 bash 里到处都是引号的罪魁祸首。因为 zsh 默认没启用这种行为,所以在写 zsh 的时候大家可以放心地省略引号,不会有任何问题。

最后值得一提的是 ${~spec} 的一个常用用法:echo ${~${:-"~/.zshenv"}} => /home/aloxaf/.zshenv,不要再傻傻地用 ${var/~/$HOME} 了!