正则表达式
基本正则表达式(BRE)集合
符号 | 含义 |
---|---|
^ | ^a,匹配以a开头的行 |
$ | x$,匹配以x结尾的行 |
^$ | 匹配空行 |
. | 匹配任意一个且只有一个字符 |
\ | 转义字符 |
* | 匹配前一个字符0次或1次以上 |
.* | 匹配所有内容 |
^.* | 匹配多个字符开头的所有内容 |
.*$ | 匹配以多个字符结尾的内容 |
[abc] | 匹配集合内的任意一个字符 |
[^abc] | 表示对[abc]的取反 |
扩展正则表达式
扩展正则表达式符号在基础正则表达式的基础上增加了5个,分别是:+ ? | {} ()
符号 | 含义 |
---|---|
+ | 匹配前一个字符一次或多次 |
[abc]+ | 匹配方括号内的a或b或c一次或多次 |
? | 匹配前一个字符串0次或1次 |
| | 表示或者,同时过滤多个字符串 |
() | 分组过滤,被括起来的内容表示一个整体 |
a{n,m} | 匹配前一个字符最少n次,最多m次 |
a{n} | 匹配前一个字符串正好n次 |
a{n,} | 匹配前一个字符串最少n次 |
a{,m} | 匹配前一个字符串最多m次 |
[[-]]-的用法
比较字符串
最常用的功能之一是比较字符串,这也是我们一直在用的功能。
# 匹配
% [[ abc == abc ]] && echo good
good
# = 和 == 是一样的,最好统一使用一种
% [[ abc = abc ]] && echo good
good
# 不匹配
% [[ abc != abd ]] && echo good
good
# 正则表达式匹配 一个.代表一个随机字符
% [[ abc =~ a.c ]] && echo good
good
# 前者字符序比后者小
% [[ abc < bcd ]] && echo good
good
# 前者字符序比后者大
% [[ cde > bcd ]] && echo good
good
# 没有 >= 和 <=
% [[ cde >= bcd ]] && echo good
zsh: parse error near `bcd'
注意:有>、<、=,但没有没有 >= 和 <=!
除了在里边用等号、不等号之类比较外,还可以判断字符串是否为空:
% str=achg
# 判断字符串内容长度是否大于 0,等同于 (($#str))
% [[ -n $str ]] && echo good
good
% str=""
# 判断字符串是否为空,等同于 ((! $#str))
% [[ -z $str ]] && echo good
good
记:-n 不为空;-z 为空。
判断文件
另一类很重要的功能是判断文件,比如判断某一个文件是否存在、是否是目录、是否可读等等。
判断 /usr/bin/env zsh 文件是否存在:
% [[ -e /usr/bin/zsh ]] && echo good
good
% [[ -e /usr/bin/zshh ]] && echo good
-e 可以替换成如下的选项,用法是一致的:
选项 | 符合条件的文件 |
---|---|
-b | 块设备文件 |
-c | 字符设备文件 |
-d | 目录 |
-e | 存在的任何文件 |
-f | 普通文件,含符号链接,不含目录、设备文件、socket、FIFO |
-g | 设置了 setgid 的文件 |
-h | 符号链接 |
-k | 设置了粘滞位(sticky bit)的文件 |
-p | FIFO 文件 |
-r | 对当前进程可读的文件 |
-s | 非空文件 |
-u | 设置了 setuid 的文件 |
-x | 对当前进程可执行的文件 |
-w | 对当前进程可写的文件 |
-L | 符号链接(同 -h) |
-O | 被当前进程的用户拥有的文件 |
-G | 被当前进程的用户组拥有的文件 |
-S | socket 文件 |
-N | atime 和 mtime 一样的文件 |
还有一个比较特殊的 -t 选项:
# $$ 是当前的进程 id
% ls /proc/$$/fd
0 1 10 11 2
% [[ -t 10 ]] && echo good
good
% [[ -t 3 ]] && echo good
-t 后要接数字(如果不是,相当于 0),判断当前进程是否打开了对应的 fd(进程默认会打开 0、1、2 这三个 fd,分别对应标准输入、标准输出和错误输出,此外每打开一个文件、管道或者网络连接,都会对应一个 fd,关掉后对应 fd 会消失)。
比较文件
除了判断单个文件是否符合条件外, 还可以用来比较两个文件。
# file1 比 file2 新
% [[ file1 -nt file2 ]]
# file1 比 file2 旧
% [[ file1 -ot file2 ]]
# file1 和 file2 是否对应同一个文件(路径相同或者互为硬连接)
% [[ file1 -ef file2 ]]
比较数值
也可以用来比较数值,注意不是用等号、大于号、小于号等等比较,有一系列专门的符号。通常我们没必要用来比较数值,用 (( )) 更方便一些。
# -eq 是判断两个数值是否相等
% [[ 12 -eq 12 ]] && echo good
good
-eq 可以替换成下列符号,用法一样:
符号 | 含义 |
---|---|
-eq | 相等 |
-ne | 不相等 |
-lt | < |
-gt | > |
-le | <= |
-ge | >= |
组合使用
# && 是逻辑与
% [[ a == a && b == b ]] && echo good
good
# || 是逻辑或
% [[ a == a || a == b ]] && echo good
good
# ! 是逻辑非
% [[ ! a == b ]] && echo good
good
# 可以一起用,! 优先级最高,其次 &&,再次 ||
% [[ ! a == b && b == a || b == b ]] && echo good
good
# 如果不确定优先级,可以加小括号
% [[ ((! a == b) && b == a) || b == b ]] && echo good
good
需要注意一下空格, 内侧和内容之间需要空格隔开,== 两边也需要空格。如果是在 zsh 中直接敲入,! 后边也要加一个空格,不然会被解析成历史命令。
管道和重定向
这是和其他进程、文件系统等交互的基础。
管道
管道是类 Unix 系统中的一个比较基础也特别重要的概念,它用于将一个程序的输出作为另一个程序的输入,进而两个程序的数据可以互通。
管道的基本用法:
% ls
git tmp
# wc -l 功能是计算输入内容的行数
% ls | wc -l
2
| 即管道,如果只输入 wc -l,wc 会等待用户输入,这时可以输入字符串,然后回车继续输入,直到按 ctrl + d 结束输入。然后 wc 会统计用户一共输入了多少行,然后输出行数。
# 敲 wc -l 回车后,依次按 a 回车 b 回车 ctrl + d
% wc -l
a
b
2
但如果前边有个管道符号,ls | wc -l,那么 wc 就不等待用户输入了,而是直接将 ls 的结果作为输入读取过来,然后统计行数,输出结果。
关于管道的更多细节
我们再运行一个简单的例子:
% cat | wc -l
# 查看 cat 进程打开的 fd
% ls -l /proc/$(pidof cat)/fd
total 0
lrwx------ 1 goreliu goreliu 0 2017-08-30 21:15 0 -> /dev/pts/1
l-wx------ 1 goreliu goreliu 0 2017-08-30 21:15 1 -> pipe:[2803]
lrwx------ 1 goreliu goreliu 0 2017-08-30 21:15 2 -> /dev/pts/1
# 查看 wc 进程打开的 fd
% ls -l /proc/$(pidof wc)/fd
total 0
lr-x------ 1 goreliu goreliu 0 2017-08-30 21:16 0 -> pipe:[2803]
lrwx------ 1 goreliu goreliu 0 2017-08-30 21:16 1 -> /dev/pts/1
lrwx------ 1 goreliu goreliu 0 2017-08-30 21:16 2 -> /dev/pts/1
cat 命令的效果是等待用户输入,等用户输入一行,它就把这行再输出来,直到用户按 ctrl + d。所以 cat | wc -l 也会等待用户输入。
我们看下 fd 的指向,/dev/ps1/1 是指向伪终端设备文件的,进程就是通过这个来读取用户的输入和输出自己的内容。0 是标准输入(即用户输入端),1 是标准输出(即正常情况的输出端),2 是错误输出(即异常情况的输出端)。但是 cat 的输出端指向了 一个管道,并且 wc 的 输入端指向了一个相同的管道,这代表两个进程的输入输出端是通过管道连接的。这种管道是匿名管道,即只在内核中存在,是没有对应的文件路径的。
重定向
重定向,指的便是 fd 的重定向,管道也是重定向的一种方法。但用得更多的是将进程的 fd 重定向到文件。
一个最简单的例子是输出内容到文件。
% echo abce > test.txt
% cat test.txt
abce
更多重定向的用法
一个 fd 只能重定向到一个文件,一一对应。但在 zsh 中,我们可以把一个 fd 对应到多个文件。
% cat >0.txt >1.txt >2.txt
输入完成后,3 个文件的内容都更新了, zsh 进程做了中介。
cat 的标准输出是重定向到管道了,管道对面是 zsh 进程!实际将内容写入文件的是 zsh,而不是 cat。
给 cat 的标准输出重定向 3 个文件,它将 3 个文件的内容全部读取了出来。
除了能同时重定向 fd 到多个文件外,还可以同时重定向到管道和文件。
命名管道
除了匿名管道,我们还可以使用命名管道,这样更容易控制。命名管道所使用的文件即 FIFO(First Input First Output,先入先出)文件。
# mkfifo 用来创建 FIFO 文件
% mkfifo fifo
% ls -l
prw-r--r-- 1 goreliu goreliu 0 2017-08-30 21:29 fifo|
# cat 写入 fifo
% cat > fifo
# 打开另一个 zsh,运行 wc -l 读取 fifo
% wc -l < fifo
然后在 cat 那边输入一些内容,按 ctrl + d 退出,wc 这边就会统计输入的行数。
exec 命令的用法
说起重定向,就不得不提 exec 命令。exec 命令主要用于启动新进程替换当前进程以及对 fd 做一些操作。
用 exec 启动新进程:
% exec cat
看上去效果和直接运行 cat 差不多。但如果运行 ctrl + d 退出 cat,终端模拟器就关闭了,因为在运行 exec cat 的时候,zsh 进程将已经被 cat 取代了,回不去了。
但在脚本中很少直接这样使用 exec,更多情况是用它来操作 fd:
# 将当前 zsh 的错误输出重定向到 test.txt
% exec 2>test.txt
# 随意敲入一个不存在的命令,错误提示不出现了
% fdsafds
# 错误提示被重定向到 test.txt 里
% cat test.txt
zsh: command not found: fdsafds
更多用法:
用法 | 功能 |
---|---|
n>filename | 重定向 fd n 的输出到 filename 文件 |
n<filename | 重定向 fd n 的输入为 filename 文件 |
n<>filename | 同时重定向 fd n 的输入输出为 filename 文件 |
n>&m | 重定向 fd n 的输出到 fd m |
n<&m | 重定向 fd n 的输入为 fd m |
n>&- | 关闭 fd n 的输出 |
n<&- | 关闭 fd n 的输入 |
更多例子:
# 把错误输出关闭,这样错误内容就不再显示
% exec 2>&-
% fsdafdsa
% exec 3>test.txt
% echo good >&3
% exec 3>&-
# 关闭后无法再输出
% echo good >&3
zsh: 3: bad file descriptor
% exec 3>test.txt
# 将 fd 4 的输出重定向到 fd 3
% exec 4>&3
% echo abcd >&4
# 输出内容到 fd 4,test.txt 内容更新了
% cat test.txt
abcd
通常情况我们用 exec 主要为了重定向输出和关闭输出,比较少操作输入。
文件读写
写文件
写文件要比读文件简单一些,最常用的用法是使用 > 直接将命令的输出重定向到文件。如果文件存在,内容会被覆盖;如果文件不存在,会被创建。
% echo abc > test.txt
如果不想覆盖之前的文件内容,可以追加写入:
% echo abc >> test.txt
这样追加写入自动换行!
这样如果文件存在,内容会被追加写入进去;如果文件不存在,也会被创建。
创建文件
有时我们只想先创建个文件,等以后需要的时候再写入。
touch 命令用于创建文件(普通文件):
% touch test1.txt test2.txt
# 或者用 echo 输出重定向,效果和 touch 一样
# 加 -n 是因为不加的话 echo 会输出一个换行符
% echo -n >>test1.txt >>test2.txt
# 或者使用输入重定向
% >>test1.txt >>test2.txt </dev/null
# mkdir 用来创建目录,如果需要在新目录创建文件
% mkdir dir1 dir2
如果文件已经存在,touch 命令会更新它的时间(mtime、ctime、atime 一起更新,其余两种方法不会)到当前时间。另外下边的清空文件方法,也都可以用来创建文件。touch 命令的使用比较方便,但如果想尽量少依赖外部命令,可以使用后两种方法。
因为文件创建过程通常不存在性能瓶颈,不用过多考虑性能因素。如果需要创建大量文件,可以在自己的环境分别用这几种方法试验几次,看需要多少时间。
另外如果文件数量太多的话,方法二、三要按批次创建,因为一个进程能打开的 fd 总数是有上限的。
清空文件
有时我们需要清空一个现有的文件:
# 使用 echo 输出重定向
% echo -n >test.txt
# 使用输入重定向
% >test.txt </dev/null
# 也可以使用 truncate 命令清空文件
% truncate -s 0 test.txt
通常使用第一种方法即可,比较简单易懂。非特殊场景尽量不要用像 truncate 这样不常见的命令。
删除文件
删除文件的方法比较单一,用 rm 命令即可。
% rm test1.txt test2.txt
# -f 参数代表即使文件不存在也不报错
% rm -f test1.txt test2.txt
# -r 参数可以递归删除目录和文件
% rm -r dir1 dir2 test*.txt
# -v 参数代表 rm 会输出删除文件的过程
% rm -v test*.txt
removed 'test1.txt'
removed 'test2.txt'
多行文本写入
通常我们写文件时不会每一行都单独写入,这样效率太低。
可以先把字符串拼接起来,然后一次性写入,这样比多次写入效率更高:
% str=ab
% str+="\ncd"
% str+="\n$str"
echo $str > test.txt
可以直接把数组写入到文件,每行一个元素:
% array=(aa bb cc)
% print -l $array > test.txt
如果是将一段内容比较固定的字符串写入到文件,可以这样:
# 在脚本中也是如此,第二行以后的行首 > 代表换行,非输入内容
# <<EOF 代表遇到 EOF 时会终止输入内容
# 里边也可以使用变量
% > test.txt <<EOF
> aa
> bb
> cc dd
> ee
> EOF
% cat test.txt
aa
bb
cc dd
ee
用 mapfile 读写文件
如果不喜欢使用重定向符号,还可以用哈希表来操作文件。Zsh 有一个 zsh/mapfile 模块,用起来很方便:
% zmodload zsh/mapfile
# 这样就可以创建文件并写入内容,如果文件存在则会被覆盖
% mapfile[test.txt]="ab cd"
% cat test.txt
ab cd
# 判断文件是否存在
% (($+mapfile[test.txt])) && echo good
good
# 读取文件
% echo $mapfile[test.txt]
ab cd
# 删除文件
% unset "mapfile[test.txt]"
# 遍历文件
% for i (${(k)mapfile}) {
> echo $i
> }
test1.txt
test2.txt
从文件中间位置写入
有时我们需要从一个文件的中间位置(比如从第 100 的字符或者第三行开始)继续写入,覆盖之后的内容。Zsh 并不直接提供这样的方法,但我们可以迂回实现,先用 truncate 命令把文件截断,然后追加写。如果文件后边的内容还需要保留,可以在截断之前先读取进来(见下文读文件部分的例子),最后再写回去。
% echo 1234567890 > test.txt
# 只保留前 5 个字符
% truncate -s 5 test.txt
% cat test.txt
12345
% echo abcde >> test.txt
% cat test.txt
12345abcde
读文件
读取整个文件
读取整个文件比较容易: str=$(<test.txt)
% str=$(<test.txt)
% echo $str
aa
bb
cc dd
ee
按行遍历文件
如果文件比较大,那读取整个文件会消耗很多资源,可以按行遍历文件内容:
% while {read i} {
> echo $i
> } <test.txt
aa
bb
cc dd
ee
read 命令是从标准输入读取一行内容,把标准输入重定向后,就变成了从文件读取。
读取指定行 (f)n
如果只需要读取指定的某行或者某些行,不需要用上边的方法加自己计数。
# (f)2 是读取第二行
% echo ${"$(<test.txt)"[(f)2]}
bb
读取文件到数组
读取文件内容到数组中,每行是数组的一个元素:
% array=(${(f)"$(<test.txt)"})
读取指定数量的字符
有时我们需要按字节数来读取文件内容,而不是按行读取。
% cat test.txt
1234567890
# -k5 是只最多读取 5 个字节,-u 0 是从 fd 0 读取,不然会卡住
% read -k 5 -u 0 str <test.txt
% echo $str
12345