6. 字符串操作进阶

实际上,在用 Bash 编程时,大部分时间都是在处理字符串,因此把这一节熟练掌握非常重要。

正则表达式

范例:处理 URL 地址

URL 地址(URL(Uniform Resoure Locator:统一资源定位器)是WWW页的地址)几乎是我们日常生活的玩伴,我们已经到了无法离开它的地步啦,对它的操作很多,包括判断 URL 地址的有效性,截取地址的各个部分(服务器类型、服务器地址、端口、路径等)并对各个部分进行进一步的操作。

下面我们来具体处理这个URL地址:ftp://anonymous:ftp@mirror.lzu.edu.cn/software/scim-1.4.7.tar.gz

$ url="ftp://anonymous:ftp@mirror.lzu.edu.cn/software/scim-1.4.7.tar.gz"

匹配URL地址,判断URL地址的有效性

$ echo $url | grep "ftp://[a-z]*:[a-z]*@[a-z\./-]*"

截取服务器类型

$ echo ${url%%:*}
ftp
$ echo $url | cut -d":" -f 1
ftp

截取域名

$ tmp=${url##*@} ; echo ${tmp%%/*}
mirror.lzu.edu.cn

截取路径

$ basename $url
scim-1.4.7.tar.gz
$ echo ${url##*/}
scim-1.4.7.tar.gz

截取文件名

$ basename $url
scim-1.4.7.tar.gz
$ echo ${url##*/}
scim-1.4.7.tar.gz

截取文件类型(扩展名)

$ echo $url | sed -e 's/.*[0-9].\(.*\)/\1/g'
tar.gz

范例:匹配某个文件中的特定范围的行

先准备一个测试文件README

Chapter 7 -- Exercises
7.1 please execute the program: mainwithoutreturn, and print the return value
of it with the command "echo $?", and then compare the return of the printf
function, they are the same.
7.2 it will depend on the exection mode, interactive or redirection to a file,
if interactive, the "output" action will accur after the \n char with the line
buffer mode, else, it will be really "printed" after all of the strings have
been stayed in the buffer.
7.3 there is no another effective method in most OS. because argc and argv are
not global variables like environ.

然后开始实验,打印出答案前指定行范围:第 7 行到第 9 行,刚好找出了第 2 题的答案

$ sed -n 7,9p README
7.2 it will depend on the exection mode, interactive or redirection to a file,
if interactive, the "output" action will accur after the \n char with the line
buffer mode, else, it will be really "printed" after all of the strings have

其实,因为这个文件内容格式很有特色,有更简单的办法

$ awk '/7.2/,/^$/ {printf("%s\n", $0);}' README
7.2 it will depend on the exection mode, interactive or redirection to a file,
if interactive, the "output" action will accur after the \n char with the line
buffer mode, else, it will be really "printed" after all of the strings have
been stayed in the buffer.

有了上面的知识,就可以非常容易地进行这些工作啦:修改某个文件的文件名,比如调整它的编码,下载某个网页里头的所有 pdf 文档等。这些就作为练习自己做吧。

处理格式化的文本

平时做工作,大多数时候处理的都是一些“格式化”的文本,比如类似 /etc/passwd 这样的有固定行和列的文本,也有类似 tree 命令输出的那种具有树形结构的文本,当然还有其他具有特定结构的文本。

关于树状结构的文本的处理,可以参考我早期写的另外一篇博客文章:源码分析:静态分析 C 程序函数调用关系图

实际上,只要把握好特性结构的一些特点,并根据具体的应用场合,处理起来就不会困难。

下面来介绍具体文本的操作,以 /etc/passwd 文件为例。关于这个文件的帮忙和用法,请通过 man 5 passwd 查看。下面对这个文件以及相关的文件进行一些有意义的操作。

范例:选取指定列

选取/etc/passwd文件中的用户名和组ID两列

$ cat /etc/passwd | cut -d":" -f1,4

选取/etc/group文件中的组名和组ID两列

$ cat /etc/group | cut -d":" -f1,3

范例:文件关联操作

如果想找出所有用户所在的组,怎么办?

$ join -o 1.1,2.1 -t":" -1 4 -2 3 /etc/passwd /etc/group
root:root
bin:bin
daemon:daemon
adm:adm
lp:lp
pop:pop
nobody:nogroup
falcon:users

说明: join 命令用来连接两个文件,有点类似于数据库的两个表的连接。 -t 指定分割符,-1 4 -2 3 指定按照第一个文件的第 4 列和第二个文件的第 3 列,即组 ID 进行连接,-o 1.1,2.1 表示仅仅输出第一个文件的第一列和第二个文件的第一列,这样就得到了我们要的结果,不过,可惜的是,这个结果并不准确,再进行下面的操作,你就会发现:

$ cat /etc/passwd | sort -t":" -n -k 4 > /tmp/passwd
$ cat /etc/group | sort -t":" -n -k 3 > /tmp/group
$ join -o 1.1,2.1 -t":" -1 4 -2 3 /tmp/passwd /tmp/group
halt:root
operator:root
root:root
shutdown:root
sync:root
bin:bin
daemon:daemon
adm:adm
lp:lp
pop:pop
nobody:nogroup
falcon:users
games:users

可以看到这个结果才是正确的,所以以后使用 join 千万要注意这个问题,否则采取更保守的做法似乎更能保证正确性,更多关于文件连接的讨论见参考后续资料。

上面涉及到了处理某格式化行中的指定列,包括截取(如 SQLselect 用法),连接(如 SQLjoin 用法),排序(如 SQLorder by 用法),都可以通过指定分割符来拆分某个格式化的行,另外,“截取”的做法还有很多,不光是 cutawk,甚至通过 IFS 指定分割符的 read 命令也可以做到,例如:

$ IFS=":"; cat /etc/group | while read C1 C2 C3 C4; do echo $C1 $C3; done

因此,熟悉这些用法,我们的工作将变得非常灵活有趣。

到这里,需要做一个简单的练习,如何把按照列对应的用户名和用户 ID 转换成按照行对应的,即把类似下面的数据:

$ cat /etc/passwd | cut -d":" -f1,3 --output-delimiter=" "
root 0
bin 1
daemon 2

转换成:

$ cat a
root    bin     daemon
0       1       2

并转换回去,有什么办法呢?记得诸如 trpastesplit 等命令都可以使用。

参考方法:

  • 正转换:先截取用户名一列存入文件 user,再截取用户 ID 存入 id,再把两个文件用 paste -s 命令连在一起,这样就完成了正转换
  • 逆转换:先把正转换得到的结果用 split -1 拆分成两个文件,再把两个拆分后的文件用 tr 把分割符 \t 替换成 \n,只有用 paste 命令把两个文件连在一起,这样就完成了逆转换。

参考资料