文本处理是一个非常基础和重要的操作,因为很多配置文件和脚本都是以文本的形式进行存储和编辑的。其中管道(pipe)是类 Unix 系统中一种非常重要的机制,可以将多个命令通过特殊符号“|”连接起来,形成一个管道链,使得每个命令的输出作为下一个命令的输入。
一、Unix文本工具
这里有一些在类 Unix 系统中经常使用到的标准文本处理工具:
1、没有使用正则表达式:
- cat(1) 连接文件并输出全部的内容;
- tac(1) 连接文件并反向输出;
- cut(1) 选择行的一部分并输出;
- head(1) 输出文件的开头;
- tail(1) 输出文件的末尾;
- sort(1) 对文本文件的行进行排序;
- uniq(1) 从已排序的文件中移除相同的行;
- tr(1) 转换或删除字符;
- diff(1) 对文件的行进行对比。
2、默认使用基础正则表达式( BRE ):
- ed(1) 是一个原始行编辑器;
- sed(1) 是一个流编辑器;
- grep(1) 匹配满足 pattern 的文本;
- vim(1) 是一个屏幕编辑器;
- emacs(1) 是一个屏幕编辑器。(有些扩展的 BRE )
3、使用扩展的正则表达式( ERE ):
- awk(1) 进行简单的文本处理;
- egrep(1) 匹配满足多个 pattern 的文本;
- tcl(3tcl) 可以进行任何想得到的文本处理,经常与 tk(3tk) 一起使用;
- perl(1) 可以进行任何想得到的文本处理;
- pcregrep 软件包中的 pcregrep(1) 可以匹配满足 Perl 兼容正则表达式(PCRE) 模式的文本;
- 带有 re 模块的 python(1) 可以进行任何想得到的文本处理。
如果不确定这些命令究竟做了什么,请使用“man command” 来自己把它搞清楚吧。
注意:
- 排序的顺序和表达式的范围取决于语言环境。如果想要获得一个命令的传统行为,可以使用 “LANG=C” 或 C.UTF-8 语言环境代替原来的 UTF-8 语言环境。
- Perl 正则表达式( perlre(1) )、Perl 兼容正则表达式(PCRE) 和 Python 的re 模块提供的正则表达式与一般的 ERE 相比多了许多通用的扩展。
二、正则表达式
正则表达式被使用在许多文本处理工具中。它们类似 shell 的通配符,但更加复杂和强大。正则表达式描述要匹配的模式,它是由文本字符和元字符构成的。
元字符仅仅是带有特殊含义的字符。它们有两种主要的形式,BRE 和 ERE ,使用哪种取决于上述的文本工具。
BRE 和 ERE 中的元字符:
BRE | ERE | 正则表达式的描述 |
---|---|---|
\ . [ ] ^ $ * |
\ . [ ] ^ $ * |
通用的元字符 |
\+ \? \( \) \{ \} \| |
BRE 独有的“\ ”转义元字符 |
|
+ ? ( ) { } | |
ERE 独有的不需要“\ ”转义的元字符 |
|
c |
c |
匹配非元字符 “c ” |
\c |
\c |
匹配一个字面意义上的字符 “c ”,即使 “c ” 本身是元字符 |
. |
. |
匹配任意字符,包括换行符 |
^ |
^ |
字符串的开始位置 |
$ |
$ |
字符串的结束位置 |
\< |
\< |
单词的开始位置 |
\> |
\> |
单词的结束位置 |
[abc…] |
[abc…] |
匹配在 “abc... ” 中的任意字符 |
[^abc…] |
[^abc…] |
匹配除了 “abc... ” 中的任意字符 |
r* |
r* |
匹配零个或多个 “r ” |
r\+ |
r+ |
匹配一个或多个 “r ” |
r\? |
r? |
匹配零个或一个 “r ” |
r1\|r2 |
r1|r2 |
匹配一个 “r1 ” 或 “r2 ” |
\(r1\|r2\) |
(r1|r2) |
匹配一个 “r1 ” 或 “r2 “ ,并作为括号内的正则表达式 |
emacs 中的正则表达式基本上是 BRE 但含有 ERE 中的元字符 “+” 和 “?” 。因此,在 emacs 中没必要使用 “\” 来转义它们。
grep(1) 可以使用正则表达式来进行文本搜索。
尝试下列例子:
$ egrep 'GNU.*LICENSE|Yoyodyne' /usr/share/common-licenses/GPL GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE Yoyodyne, Inc., hereby disclaims all copyright interest in the program
三、替换表达式
对于替换表达式,一些字符有特殊的含义。
替换表达式:
替换表达式 | 替换表达式替换的文本 |
---|---|
& |
正则表达式所匹配的内容(在 emacs 中使用 \& ) |
\n |
前 n 个括号的正则表达式匹配的内容( “n” 是数字) |
对 Perl 替换字符串来说,应使用“$&”而非“&”,应使用“$n”而非“\n”。尝试下列例子:
$ echo zzz1abc2efg3hij4 | \ sed -e 's/\(1[a-z]*\)[0-9]*\(.*\)$/=&=/' zzz=1abc2efg3hij4= $ echo zzz1abc2efg3hij4 | \ sed -e 's/\(1[a-z]*\)[0-9]*\(.*\)$/\2===\1/' zzzefg3hij4===1abc $ echo zzz1abc2efg3hij4 | \ perl -pe 's/(1[a-z]*)[0-9]*(.*)$/$2===$1/' zzzefg3hij4===1abc $ echo zzz1abc2efg3hij4 | \ perl -pe 's/(1[a-z]*)[0-9]*(.*)$/=$&=/' zzz=1abc2efg3hij4=
请特别注意这些括号正则表达式的格式,以及这些被匹配的文本在不同的工具中是如何被替换的。这些正则表达式在一些编辑器中也可以用来移动光标和替换文本。
在 shell 命令行行末的反斜杠 “\” 会跳脱一个换行符(作为空白符),并将光标移动到下一行的行首。
四、全局替换
ed(1) 命令可以在 “file” 中将所有的 “FROM_REGEX” 替换成 “TO_TEXT” 。
$ ed file <<EOF ,s/FROM_REGEX/TO_TEXT/g w q EOF
sed(1) 命令可以在 “file” 中将所有的 “FROM_REGEX” 替换成 “TO_TEXT” 。
$ sed -i -e 's/FROM_REGEX/TO_TEXT/g' file
vim(1) 命令可以通过使用 ex(1) 命令在 “file” 中将所有的 “FROM_REGEX” 替换成 “TO_TEXT” 。
$ vim '+%s/FROM_REGEX/TO_TEXT/gc' '+w' '+q' file
注意:上面的 “c” 标志可以确保在每次替换时都进行交互式的确认。
多个文件( “file1”,“file2” 和 “file3” )可以使用 vim(1) 或 perl(1) 通过正则表达式进行类似的处理。
$ vim '+argdo %s/FROM_REGEX/TO_TEXT/ge|update' '+q' file1 file2 file3
上面的 “e” 标志是为了防止 “No match” 错误中断替换。
$ perl -i -p -e 's/FROM_REGEX/TO_TEXT/g;' file1 file2 file3
在 perl(1)例子中 , “-i” 是在每一个目标文件的原处编辑,”-p” 是表示循环所有给定的文件。
注意:
- 使用参数 “-i.bak” 代替 “-i” ,可以在文件名后添加 “.bak” 再保存。对于复杂的替换,这使得从错误中恢复变得容易;
- ed(1) 和 vim(1) 使用 BRE ; perl(1) 使用 ERE 。
五、提取数据
下面有一个文本文件 “DPL” ,里面含有 2004 年以前 Debian 项目的领导者名字和起始日期,并以空格分隔。
Ian Murdock August 1993 Bruce Perens April 1996 Ian Jackson January 1998 Wichert Akkerman January 1999 Ben Collins April 2001 Bdale Garbee April 2002 Martin Michlmayr March 2003
Awk 经常被用来从这种类型的文件中提取数据。尝试下列例子:
$ awk '{ print $3 }' <DPL # month started August April January January April April March $ awk '($1=="Ian") { print }' <DPL # DPL called Ian Ian Murdock August 1993 Ian Jackson January 1998 $ awk '($2=="Perens") { print $3,$4 }' <DPL # When Perens started April 1996
Shell (例如 Bash )也可以用来分析这种文件。尝试下列例子:
$ while read first last month year; do echo $month done <DPL ... same output as the first Awk example
内建命令 read 使用 “$IFS” (内部域分隔符)中的字符来将行分隔成多个单词。如果将 “$IFS” 改变为 “:” ,可以很好地使用 shell 来分析 “/etc/passwd”。
$ oldIFS="$IFS" # save old value $ IFS=':' $ while read user password uid gid rest_of_line; do if [ "$user" = "bozo" ]; then echo "$user's ID is $uid" fi done < /etc/passwd bozo's ID is 1000 $ IFS="$oldIFS" # restore old value
(如果要用 Awk 做到相同的事,使用 “FS=’:’” 来设置域分隔符。)
IFS 也被 shell 用来分割参数扩展、命令替换和算术扩展的结果。这不会出现在双引号或单引号中。 IFS 的默认值为 空格、tab 和换行符。请谨慎使用 shell 的 IFS 技巧。当 shell 将脚本的一部分解释为对它的输入时,会发生一些奇怪的事。
$ IFS=":," # use ":" and "," as IFS $ echo IFS=$IFS, IFS="$IFS" # echo is a Bash builtin IFS= , IFS=:, $ date -R # just a command output Sat, 23 Aug 2003 08:30:15 +0200 $ echo $(date -R) # sub shell --> input to main shell Sat 23 Aug 2003 08 30 36 +0200 $ unset IFS # reset IFS to the default $ echo $(date -R) Sat, 23 Aug 2003 08:30:50 +0200
六、小片段脚本
下面的脚本作为管道的一部分,可以做一些细致的事情。
管道命令的小片段脚本列表:
脚本片段(在一行内输入) | 命令效果 |
---|---|
find /usr -print |
找出”/usr “下的所有文件 |
seq 1 100 |
显示 1 到 100 |
| xargs -n 1 command |
把从管道过来的每一项作为参数,重复执行命令 |
| xargs -n 1 echo |
把从管道过来的,用空格隔开的项,分隔成多行 |
| xargs echo |
把从管道过来的所有行合并为一行 |
| grep -e regex_pattern |
从管道过来,包含有 regex_pattern的行,提取出来 |
| grep -v -e regex_pattern |
把从管道过来,不包含有 regex_pattern的行,提取出来 |
| cut -d: -f3 - |
把从管道过来,用 “: “分隔的第三列提取出来 (passwd 文件等。) |
| awk '{ print $3 }' |
把用空格隔开的第三列提取出来 |
| awk -F'\t' '{ print $3 }' |
把用 tab 键隔开的第三列提取出来 |
| col -bx |
删除退格键,扩展 tab 键为空格键 |
| expand - |
扩展 tab 键到空格键 |
| sort| uniq |
排序并删除重复行 |
| tr 'A-Z' 'a-z' |
将大小字母转换为小写字母 |
| tr -d '\n' |
将多行连接为一行 |
| tr -d '\r' |
删除换行回车符 |
| sed 's/^/# /' |
在每行行首增加一个”# “符 |
| sed 's/\.ext//g' |
删除 “.ext “ |
| sed -n -e 2p |
显示第二行 |
| head -n 2 - |
显示最前面两行 |
| tail -n 2 - |
显示最后两行 |
使用 find(1) 和 xargs(1),单行 shell 脚本能够在多个文件上循环使用,可以执行相当复杂的任务。当使用 shell 交互模式变得太麻烦的时候,请考虑写一个 shell 脚本。