美文网首页
ZSH 你不知道那些事儿-004-展开和替换基础

ZSH 你不知道那些事儿-004-展开和替换基础

作者: FFCP | 来源:发表于2017-08-25 17:46 被阅读88次

所谓“展开”或“替换”,从最简单的角度,可以理解为“如何从一个变量中取值”,又或者是 “如何将一个命令的输出保存到一个变量中”等等。

zsh 一共提供了 9 种类型的展开功能,本文对这 9 种类型的展开功能做一个简单的介绍。

1 参数展开(Parameter Expansion)

参数展开有的地方也叫变量替换(Variable Substitution),用简单的方式说就是用于从一个变量中取值的语法,当然 Shell 的参数展开功能除了最基本的取值之外,还有诸如:在取值的同时对值做一些变换操作等功能,比如:字符串替换等等。

在 zsh 中使用 echoprint 命令打印一个字符串,如:

echo HelloWorld

将会输出: HelloWorld

在 zsh 中使用如下语法将一个字符串赋值给一个变量 s:

s=hello

要取变量 s 的值,使用语法: ${s} ,在没有歧义的情况下可以省略花括号,而使用: $s ;要打印 s 的值:

echo $s

要将 s 的值赋给另一个变量 s2:

s2=$s

${s} / $s 就是最基本的参数展开的语法。

参数展开还有很多高级用法,这里简单介绍 3 个参数展开的高级用法:

1.1 字符串替换

使用如下语法对变量中存储的字符串进行替换: ${NAME/PATTERN/REPL} ,其中NAME 为变量名, PATTERN 为要替换的部分, REPL 为替换的内容。示例:

s=HelloWorld
echo ${s/l/L}

输出: HeLloWorld

${NAME/PATTERN/REPL} 只会替换第一个匹配到的 PATTERN ,而不是全局替换,所以上例中只有第一个小写 l 被替换为了大写,如需要全局替换,在使用另外一个类似的语法: ${NAME//PATTERN/REPL} 。示例:

s=HelloWorld
echo ${s//l/L}

输出: HeLLoWorLd

1.2 取子串

使用语法: ${NAME:OFFSET:LENGTH} 取得存储在变量 NAME 中的字符串的子串,其中: OFFSET 是子串的起始位置,从 0 开始计数, LENGTH 为子串的长度,:LENGTH 若省略,则表示取从 OFFSET 开始一直到字符串末尾。示例:

s=HelloWorld
echo ${s:5:3}  # => 输出:Wor
echo ${s:6}    # => 输出:orld

1.3 获知一个变量的类型

zsh 的变量可以是整数类型、浮点类型、数组、关联数组(键值对)等等,可以通过 ${(t)NAME} 语法来取得变量的类型。示例:

s=Hello
echo ${(t)s}                    # => 输出: scalar,标量类型,可以理解为字符串类型
arr=(1 2 3)                     # 拥有 3 个值的数组变量 arr
echo ${(t)arr}                  # => 输出:array
integer i                       # 声明一个整数类型的变量 i
i=1
i+=2                            # i 现在的值为 3
echo ${(t)i}                    # => 输出:integer
typeset -A hsh                  # 声明一个关联数组类型的变量 hsh
hsh=(name Jack age 20 gender M) # 现在 hsh 包含 3 对键值对
echo $hsh[name]                 # => 输出:Jack
echo ${(t)hsh}                  # => 输出:association

${(flags)NAME} 这个结构中,圆括号内的部分称为 Parameter Flags, 这是 zsh 的参数展开中相对于其他 Shell 所特有的。 t 就是其中的一个 Flag,表示替换为变量的类型,而不是变量的值,后续会专门对 Parameter Flags 进行介绍。

2 别名展开(Alias Expansion)

别名就是给常用的命令(可以包含参数)创建一个快捷方式,而所谓“别名展开”其实就是指别名被替换为它背后的真正命令的过程。示例:

alias l='ls -lh'

创建了 l 这个别名之后,执行 l 就相当于执行 ls -lh

zsh 支持两种特殊的别名:

2.1 全局别名

通常别名只能用于替换简单的命令名或程序名,你不能创建 alias H='| head'​ 这样的别名企图 cat file H 替换为 cat file | head ;而通过 alias 命令的 -g 选项创建一个全局别名就可以做到这一点:

alias -g H='| head'​

2.2 后缀名别名

使用 alias 命令的 -s 选项可以创建一个后缀名别名:

alias -s tgz='tar -zxf'

创建了这个后缀名别名之后,如果当前目录下存在一个叫做 f.tgz 的文件,那么将文件名 f.tgz 当做一个命令来执行,zsh 就会将命令 f.tgz 替换为 tar -zxf f.tgz ,相当于对文件进行了解压操作。

3 命令替换(Command Substitution)

所谓“命令替换”就是执行指定的命令,并将表达式的值替换为命令的输出。语法为: $(CMD)

示例:

d=$(date)

现在变量 d 的值就是 date 命令的输出,如: 2017年 8月25日 星期五 14时04分26秒 CST

示例 2:

file_content=$(cat ./f.txt)

现在变量 file_content 的值就是文件 ./f.txt 内容。示例 2 还可以简写为:

file_content=$(< ./f.txt)

命令替换还有另外一种形式的语法: `CMD` 。 两种形式基本上是等价的,但是第一种在嵌套时更方便,不需要转义,所以一般推荐使用第一种形式的语法。

4 进程替换(Process Substitution)

进程替换的语法为 <(CMD) ,zsh 会执行 CMD 命令,并将 <(CMD) 替换为一个文件名,从这个文件名所指向的文件中读取数据,就相当于读取 CMD 命令的输出。(注意:你不能向其中写入数据) 示例:

wc -l <(cat ./f.txt)

cat ./f.txt | wc -l 是等价的,进一步也就是直接将 wc -l 作用于文件上: wc -l ./f.txt 上的作用是一样的。那么进程替换一般用在什么场景呢?

进程替换一般用在接受多个文件名作为参数的命令中,例如要比较两个文件的差异,可以使用如下命令:

diff jquery-1.0.js jquery-1.1.js

就会把两个文件的差异打印出来;但是 diff 是按行进行比对的,假如对比的是两个使用 JS 压缩工具压缩过的 JS 文件,因为压缩过的 JS 文件都在同一行,因此直接比对是没有意义的——应该使用 JS 解压缩工具先对 JS 文件进行解压,解压后的 JS 文件中的代码就不会都挤到一行,这样对比起来就容易了。我们假设 JS 解压缩工具名为 js_decompressor ,使用进程替换的话一行命令就可以完成这个比对:

diff <(js_decompressor jquery-1.0.min.js) <(js_decompressor jquery-1.1.min.js)

这一行命令逻辑上等价于:

js_decompressor jquery-1.0.min.js > 1.0-tmp.js
js_decompressor jquery-1.1.min.js > 1.1-tmp.js
diff 1.0-tmp.js 1.1-tmp.js
rm 1.0-tmp.js 1.1-tmp.js

上文已经说过 <(CMD) 只用于读取 CMD 的输出,要向 CMD 喂送数据,则使用 >(CMD)

<(CMD) 语法其实和匿名管道类似,它们都是从流中读取数据,也就是说不能对替换了这个结构的那个文件进行 rewind, seek 等操作。如果程序需要这些操作,zsh 提供了另外一个结构: =(CMD), 它和 <(CMD) 完成的功能是类似的,只不过 zsh 在执行 CMD 的同时会将 CMD 的输出写入到一个临时文件中, =(CMD) 会被替换为这个临时文件的文件名,此时这个文件名指向的是磁盘上的一个确实的文件,而不再是流,因此可以对它做 seek 等操作是可以的。典型的场景是用图形化的工具代替上述示例中的 diff 时, 往往需要使用 =(CMD) 结构,如:

ksdiff =(js_decompressor jquery-1.0.min.js) =(js_decompressor jquery-1.1.min.js)

5 历史展开(History Expansion)

zsh 会记录其中执行过的命令,它支持通过语法结构从记录的历史命令中取出命令。历史展开的语法都以 ! 开头,例如:

  • !N (其中 N 为一个正整数)表示展开为命令历史记录中的第 N 个命令
  • !-N (其中 N 为一个正整数)表示展开为命令历史记录中的倒数第 N 个命令(倒数第 1 个命令即刚刚执行过的上一条命令)
  • !! 展开为上一条命令,等价于: !-1
  • !STR 最近的一条以 STR 打头的命令
  • !?STR? 最近的一条包含 STR 的命令
  • !# 目前为止当前命令行已经输入的内容。

上述称为事件标志(Event Designator),它们会被替换为命令历史中对应的整行命令; zsh 不仅支持整行命令的替换,它还可以替换某个历史命令中的某个参数,语法是在事件标志的后面加一个冒号和一个单词标志(Word Designator)。例如:

  • !!:2 被替换为上一条命令的第一个参数
  • !!:N 被替换为一条命令的第 N 个参数
  • !-2:$ 被替换为上上条命令的最后一个参数
  • !!:X-Y 被替换为上条命令的第 X 个参数直到第 Y 个参数
  • !!:X* 被替换为上条命令的第 X 个参数直到最后一个参数
  • !!:* 被替换为上条命令的全部参数
  • !?zip?:% 被替换为和 zip 相匹配的那个参数(或命令名)
  • !#:1 被替换为当前命令上已经输入的第一个参数

最后一个示例的典型使用场景:对比两个文件,但是对比之前都要对两个文件进行一系列相同的复杂处理步骤:

diff <(cat a.txt | iconv -f GBK | cut -d, -f2,3,4 | sort) <(cat b.txt | iconv -f GBK | cut -d, -f2,3,4 | sort)

a.txtb.txt 所做的处理都是相同的,也就是说 diff 的第一个参数和第二个参数除了文件名之外都是相同的,所以当你输入完第一个参数的时候,也就输入到下面这个状态时:

diff <(cat a.txt | iconv -f GBK | cut -d, -f2,3,4 | sort)

可以输入 !#:1 然后按 TAB 展开,当前命令行就会变为:

diff <(cat a.txt | iconv -f GBK | cut -d, -f2,3,4 | sort) <(cat a.txt | iconv -f GBK | cut -d, -f2,3,4 | sort)

最后把第二个参数中的 a.txt 修改为 b.txt 即可。

实际使用中用到比较多的是取上一条命令的最后一个参数,可以使用历史展开: !!:$ ,另外一个更好的方式使用使用快捷键: ALT - .

历史展开和参数展开、文件名生成都支持修改符功能,修改符的作用是对替换结果做一些变换,这里展示一个示例,后续会对修改符做进一步介绍。

示例:

!!:s/l/L  # 展开为上一条命令,并将上一条命令中第一个出现的字母 l 变为大写
!!:gs/l/L  # 展开为上一条命令,并将上一条命令中字母 l 全部变为大写

通过示例可以看到修改符以冒号开头, s 是修改符中的一个,表示对结果进行字符串替换, g 也是修改符中的一个,它专门用于修饰 s, 表示全局替换。

6 算术展开(Arithmetic Expansion)

算术展开使用语法: $[EXP]$((EXP)) ,其中 EXP 为算术表达式,整个结构被替换为算术运算的结果,EXP 还可以包括逻辑运算,在替换逻辑运算结果时,逻辑真会被替换为数值 1,逻辑假被替换为数值 0。示例:

echo $[1+2]            # => 输出 3
integer i=3
echo $((i*2))          # => 输出 6
echo $(( 3 > 2))       # 输出 1
echo $[3 > 2 && 1 > 2] # 输出 0

此外,结构 ((EXP)) 可以用作 if, while 等控制结构的控制条件部分,如:

if ((1 < 2))
then
    echo true
else
    echo false
fi   # => 输出 true

((10 == 2*5)) && echo yes  # 输出 yes

7 花括号展开(Brace Expansion)

花括号展开有两种用法:

  • hello{01,02,03}world 被展开为 hello01world hello02world hello03world
  • {2..5} 被展开为 2 3 4 5
  • {02..5} 被展开为 02 03 04 05
  • {2..10..3} 被展开为 2 5 8
  • {a..g} 被展开为 a b c d e f g
  • {中..文} 被展开为 中 丮 丯 丰 丱 串 丳 ... 斃 斄 斅 斆 文

8 文件名展开(Filename Expansion)

文件名展开包括:

  • ~ 被展开为当前用户的家目录
  • ~xiaoming 被展开为用户小明的家目录
  • ~+ 被展开为当前目录
  • ~- 被展开为之前的目录

*小提示* 如果使用了 oh-my-zsh, 则有以下小技巧可供使用:

  • 命令 - 切换到前一个目录,相当于 cd -cd ~-
  • 命令 cd 不加任何参数,切换到当前用户的家目录,相当于 cd ~
  • 命令 d 列出最近 10 个访问过的目录,并以序号标记
  • 命令 0, 命令 1 到命令命令 9 : 切换到 d 所输出的对应序号的目录中

9 文件名生成(Filename Generation)

文件名生成其实就是文件名的模式匹配,一个文件模式被替换为匹配到的文件名列表的过程就是“文件名生成”。例如: *.txt 就被替换为当前目录下所有的 .txt 文件的文件名列表,当然如果没有匹配到文件,默认 zsh 就会报错: no macthes found

文件名生成也叫 Globbing,其中的 *, ? 等具有特殊含义的字符叫做通配符(Glob),zsh 支持的通配符包括:

Glob Operator Description
* 匹配任意长度的任意字符,包括空字符串
? 匹配一个任意字符
[...] 匹配一个方括号内的任意字符
[^...] / [!...] 匹配一个不在方括号内的任意字符
<X-Y> 匹配一个在 X 到 Y 范围的数字
(...) 分组,一般用于划定作用范围或强调优先级
X\vertY 匹配 X 或 匹配 Y
^X 不匹配 X
X~Y 匹配 X 但不匹配 Y
X# 匹配 0 个或任意多个 X
X## 匹配 1 个或任意多个 X

大多数的通配符不匹配 /, 这也就导致了它们默认只能在一个目录下进行匹配,而不能夸目录匹配,这也就进一步导致了它们默认都不能进行递归的匹配。

要想使用递归匹配,则需要使用结构 (*/)#PATTERN, 它有一个等价的简写形式: **/PATTERN 。例如递归匹配当前目录下所有 .txt 文件: **/*.txt(*/)#*.txt

zsh 的 Globbing 还支持通过限定符对匹配结果下进行过滤的功能。例如,匹配所有名字中包含 hello 的目录,而不包括普通文件: **/*hello*(/) 。可以看到限定符是在整个通配表达式的末尾加上一对圆括号,在圆括号中填写限定符,其中的 / 就是限定符,表示只匹配目录类型的文件,还有很多限定符可以用于诸如:按文件大小或修改时间筛选,匹配结果的排序等等。因此可以看到,zsh 的文件名生成可以完全替代 find 命令,并且比 find 命令更简短。后续会对 zsh 的通配限定符进行详细介绍。

相关文章

网友评论

      本文标题:ZSH 你不知道那些事儿-004-展开和替换基础

      本文链接:https://www.haomeiwen.com/subject/fkkydxtx.html