本文介绍 ZSH 参数扩展的另一个重要内容——参数扩展标记(Parameter Expansion Flags)。

参数扩展标记是 zsh 独有的内容,由紧跟在大括号之后的一对圆括号指定,也就是形如 ${(flags)var} 的形式,flag 之间也可以进行组合。

这个部分比较复杂,表格里无法三言两语讲清楚。本来打算不用表格,但是考虑到表格方便查阅,因此还是决定以表格形式为主辅以一些例子帮助理解。

首先是一些比较常用的,数组和字符串都能使用的 flag。它们用于数组时会单独应用到每个元素上。

flag 作用
# 将字符串作为数学表达式求值,并将结果作为 ASCII/Unicode 码替换为对应字符,相当于 chr 函数
% 对结果进行 Prompt Expansion,相当于 print -P
A 赋值时将结果转换为数组,写成 AA 则转换为关联数组
D 上文中提到的 ${~var} 展开的逆运算,不要再用 ${var/$HOME/\~}
e 对结果进行参数扩展、命令替换、算术扩展,有点类似 eval
g:opts: 将结果中的转义序列进行展开,opts 为空时行为类似不带参数的 echo,可以使用 oce 开关来允许忽略八进制前导0、转义 ^M 格式、转义 \M-t 格式的转义序列
P 将展开结果作为变量名再次展开,类似 bash 的 ${!var}
b 将结果中模式匹配的元字符进行转义
q 将展开结果转义,可以重复最多四次来达到不同的转义效果
Q 移除一层转义
V 将特殊字符转义为 ^M 的形式
t 输出变量的类型
C 将每个单词的首字母大写
L 将结果转换为小写
U 将结果转换为大写
c 用于修饰 ${#var},可以统计字符串或数组中所有元素的长度,相邻元素之间视为填充空格
w 用于修饰 ${#var},统计数组或字符串中词的个数,可以使用 s flag 来指定空格以外的分隔符
W w 类似,不过两个连续分隔符之间会视为存在一个空字符串,也会统计到长度中
X Qe# 时遇到错误则立即报错
f 将结果按行分割为数组,相当于 ps:\n:
s:str: 以 str 为分隔符将字符串分割为数组
z 以 shell 解析规则将字符串分隔为数组
0 以 NULL 为分隔符分割字符串
p 用于与其他 flag 组合,允许对其他 flag 的参数进行 \n 等转义序列的展开
~ 用于与其他 flag 组合,将其他 flag 的参数视为 pattern。
Z:opts: z 类似但允许更多参数,带 c 参数可以正确解析注释,带 C 参数可以将注释去掉,n 参数可以让换行符被视为空白
l:expr::s1::s2 在左侧填充 s1(默认空格)直到长度为 expr,如果指定了 s2 ,则 s2 会被填充一次
r:expr::s1::s2 l 类似,不过是在右侧填充
m 用在 l, r, # 中,判断长度时不使用字符数而是实际占用的长度

注:flag 中的冒号可以替换为任意字符,或者成对的括号例如 (...){...}[...]<...>

例子:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# 相当于 python 代码 `chr(32+33)`
var=32+33
echo ${(#)var}
A

# 会输出白底红字的「Hello」
var="%F{red}%K{white}Hello%f%k"
echo ${(%)var}

# `A` flag 的主要用途是搭配 `${...=...}`、`${...:=...}` 等一起使用
# 允许用它们为数组/关联数组初始化
❯ : ${(AA)=var=a 1 b 2 c 3}
echo ${(t)var}
association

# 无 flag  - 统计元素数
# `c`      - 统计字符数(元素间视为一个空格)
# `w`      - 统计词数(以空格为分隔符)
# `ws:|:   - 统计词数(以'|'为分隔符)
# `Ws:|:`  - 统计词数(以'|'为分隔符,但是允许空字符串存在)
var=("a||||b" "c d" "e f")
echo $#var ${(c)#var} ${(w)#var} ${(ws:|:)#var} ${(Ws:|:)#var}
3 14 5 4 7

# 大小写相关
var="zSH is SO coOl"
echo ${(C)var}
Zsh Is So Cool
echo ${(L)var}
zsh is so cool
echo ${(U)var}
ZSH IS SO COOL

# `D` flag 一般用于将路径还原成对用户来说更易读的格式
var=$HOME/.local/share/zsh
echo ${(D)var}
~/.local/share/zsh
# 注意此处可以正确处理 named directory,比 ${var/#$HOME/\~} 不仅简单,兼容性也好
hash -d share=~/.local/share
echo ${(D)var}
~share/zsh

# 类似 $(echo $var)
var='abc\ndef'
❯ print -r $var
abc\ndef
❯ print -r ${(g::):var}
abc
def

# 这个操作类似于 eval,也和 eval 同样存在风险
var='$((1+1)) $HOME $(echo hello)'
echo ${(e)var}
2 /home/aloxaf hello

# `X` flag 可以让 `e` flag 等在遇到错误时正确设置错误码而不是忽略
var='${var/$(}'
❯ : ${(e)var} && echo No error
zsh: parse error near `}'
No error
❯ : ${(Xe)var} && echo No error
zsh: parse error near `}'
zsh: parse error

# 变量的嵌套展开,就和 php 里的 $$var 一样
bar=1 foo=bar var=foo
echo ${(P)var}
bar
echo ${(P)${(P)var}}
1

# `q` 用于转义特殊字符,但是有多种转义形式
# `Q` 是 `q` 的逆运算
var="'123'"$'\n"456 789\b0"'
❯ print -r ${(q)var}    # 用反斜杠 + $'' 转义
\'123\'$'\n'\"456\ 789$'\b'0\"
❯ print -r ${(qq)var}   # 包裹在单引号中
'''123''
"456 780"'
❯ print -r ${(qqq)var}  # 包裹在双引号中
"'123'
\"456 780\""
❯ print -r ${(qqqq)var} # 包裹在 $'' 中
$'\'123\'\n"456 789\b0"'
❯ print -r ${(q-)var}   # 最小化转义,通常这个形式是可读性最好的
\'123\''
"456 780"'
❯ print -r ${(q+)var}   # 另一种形式的最小化转义,会用 $'' 包裹不可打印字符
$'\'123\'\n"456 789\C-H0"'
❯ print -r ${(V)var}    # 转义为 ^H 的格式,我也忘了这种格式叫啥了
'123'\n"456 789^H0"

# 此处使用了 `f` 来按行分割,使用 `@` 来允许保留空行
# 这就是在 zsh 中按行读取小文件的标准形式
❯ print -l "${(@f)"$(<~/.zshenv)"}"

# zsh 允许变量中存在 NULL
# 因此 zsh 脚本常常使用 NULL 来作为一些数据的分隔符,因为正常数据中绝不会出现 NULL
var=$'abc\0def\0hij'
❯ print -l ${(0)var}
abc
def
hij

# `p` flag 常搭配 `s` 和 `j` flag 使用,允许你在指定分隔符时使用转义序列
var=$'123\n456\\n789'
# \n 被视为一个普通字符串,分割点是第二处
# 为了便于观察结果,使用了 `q` flag 来转义换行符
❯ print -rl ${(q)${(s:\n:)var}}
123$'\n'456
789
# \n 被视为换行符,分割点是第一处
❯ print -rl ${(q)${(ps:\n:)var}}
123
456\\n789

# 此处的 `~` flag 将 `j` flag 的参数视为了 pattern,其余部分仍然视为纯字符串
# 注意这个效果和 ${~var} 不同,后者会将整个展开结果作为 pattern
var=("?" "*")
[[ "?" == ${(~j:|:)var} ]]; echo $?
0
[[ "." == ${(~j:|:)var} ]]; echo $?
1

# 左填充,右填充也同理
var="1"
echo ${(l:5::0)var}
00001
echo ${(l:5::0::_:)var}
000_1

# z flag 常用来保留引号内容的内容为单独的元素
var='a "b c" # I am comment'
❯ print -l ${(z)var}    # 引号内被作为单独的元素,但是注释被分割了
a
"b c"
#
I
am
comment
❯ print -l ${(Z:C:)var} # 注释被去掉了
a
"b c"
❯ print -l ${(Z:c:)var} # 注释也被作为一个整体
a
"b c"
# I am comment

以下 flag 仅用于数组

flag 含义
@ "${(@)var}" 等效于 "${var[@]}""${(@)var[1,2]}" 等效于 "${var[1]}" "${var[2]}"
a O flag 一起使用可以将数组逆序
i 排序时忽略大小写
n 排序时按数字大小比较
o 递增排序,可以与 ‘a’, ‘i’, ’n’ 组合使用
O 递减排序,可以与 ‘a’, ‘i’, ’n’ 组合使用
F 将数组按行合并为字符串,相当于 pj:\n:
j:str: 用 str 作为分隔符拼接字符串,此处的 : 可以替换为其他字符
k 取出关联数组的 key
v 取出关联数组的 value
u 数组去重

继续举几个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# `@` flag 最常见的用法是搭配引号保留数组中的空元素
var=$'1\n\n2'
echo ${#${(f)var}}
2
echo "${#${(@f)var}}"
3

# 数组排序
arr=(1 2 3 11 12 13)
echo ${(o)arr}   # 按字典序递增
1 11 12 13 2 3
echo ${(on)arr}  # 按算术大小递增
1 2 3 11 12 13

# 数组拼接
arr=(a b c)
echo ${(j:,:)arr} # 以 , 为分隔符拼接数组
a,b,c
echo ${(j.:.)arr} # 以 : 为分隔符拼接数组
a:b:c

# 关联数组
local -A arr=(a 1 b 2 c 3)
echo $arr
1 2 3
echo ${(k)arr}
a b c
echo ${(v)arr}
1 2 3

以下 flag 仅用于 ${...#..}${...%...},除了 Sl 也可以用于 ${.../...}

flag 含义
S 用于 #% 时允许搜索子字符串,用于 / 时启用非贪婪匹配
I:expr: 搜索第 expr 个匹配
B 展开为匹配结果开头的下标
E 展开为匹配结果结尾的下标
M 展开为匹配部分
N 展开为匹配部分的长度
R 展开为未匹配的部分

例子例子:

1
2
3
4
5
6
7
8
9
var='123,456,789,abc,def'
echo ${var/,*,/,}    # 贪婪
123,def
echo ${(S)var/,*,/,} # 非贪婪
123,789,abc,def
echo ${(M)var#*,}    # 输出匹配部分,而不是进行替换
123,
echo ${(N)var#*,}    # 输出匹配部分的长度
4