格式化输出与文本处理
以下和vim都是程序,但是以下所讲述的都是命令行工具,vim是一个编辑器,是有本质区别的
文本应用程序
到目前为止,我们已经知道了一对文本编辑器(nano 和 vim),看过一堆配置文件,并且目睹了许多命令的输出都是文本格式。
cat
cat 程序许多选项用来帮助更好的可视化文本内容。
-A 选项, 其用来在文本中显示非打印字符。有些时候我们想知道是否控制字符嵌入到了我们的可见文本中。 最常用的控制字符是 tab 字符(而不是空格)和回车字符,在 MS-DOS 风格的文本文件中回车符经常作为结束符出现。
另一种常见情况是文件中包含末尾带有空格的文本行。
$ cat > foo.txtThe quick brown fox jumped over the lazy dog.
$ cat -A foo.txt
^IThe quick brown fox jumped over the lazy dog. $
tab 字符在我们的文本中由^I 字符来表示。意思是 “Control-I”,和 tab 字符是一样的。$字符出现在文本行真正的结尾处, 表明我们的文本包含末尾的空格。
MS-DOS 文本 Vs. Unix 文本
可能你想用 cat 程序在文本中查看非打印字符的一个原因是发现隐藏的回车符。那么隐藏的回车符来自于哪里呢?它们来自于 DOS 和 Windows!Unix 和 DOS 在文本文件中定义每行结束的方式不相同。Unix 通过一个换行符(ASCII 10)来结束一行,然而 MS-DOS 和它的衍生品使用回车(ASCII 13)和换行字符序列来终止每个文本行。文件从 DOS 格式转变为 Unix 格式的过程非常 简单;它只简单地涉及到删除违规的回车符。
cat 程序也包含用来修改文本的选项。最著名的两个选项是-n,其给文本行添加行号和-s, 禁止输出多个空白行。我们这样来说明:
$ cat > foo.txt
The quick brown fox
jumped over the lazy dog.
$ cat -ns foo.txt
1 The quick brown fox
2
3 jumped over the lazy dog.
sort
对标准输入的内容,或命令行中指定的一个或多个文件进行排序,然后把排序 结果发送到标准输出
$ sort > foo.txt
c
b
a
$ cat foo.txt
a
b
c
然后再按下 Ctrl-d 组合键来表示文件的结尾。 随后我们查看生成的文件,看到文本行有序地显示。
因为 sort 程序能接受命令行中的多个文件作为参数,所以有可能把多个文件合并成一个有序的文件。
sort file1.txt file2.txt file3.txt > final_sorted_list.txt
常见的 sort 程序选项
选项 | 长选项 | 描述 |
---|---|---|
-b | --ignore-leading-blanks | 默认情况下,对整行进行排序,从每行的第一个字符开始。这个选项导致 sort 程序忽略 每行开头的空格,从第一个非空白字符开始排序。 |
-f | --ignore-case | 让排序不区分大小写。 |
-n | --numeric-sort | 基于字符串的数值来排序。使用此选项允许根据数字值执行排序,而不是字母值。 |
-r | --reverse | 按相反顺序排序。结果按照降序排列,而不是升序。 |
-k | --key=field1[,field2] | 对从 field1到 field2之间的字符排序,而不是整个文本行。看下面的讨论。 |
-m | --merge | 把每个参数看作是一个预先排好序的文件。把多个文件合并成一个排好序的文件,而没有执行额外的排序。 |
-o | --output=file | 把排好序的输出结果发送到文件,而不是标准输出。 |
-t | --field-separator=char | 定义域分隔字符。默认情况下,域由空格或制表符分隔。 |
我们通过对 du 命令的输出结果排序来说明这个选项,du 命令可以确定最大的磁盘空间用户。通常,这个 du 命令列出的输出结果按照路径名来排序:
$ du -s /usr/share/* | head
252 /usr/share/aclocal
96 /usr/share/acpi-support
8 /usr/share/adduser
196 /usr/share/alacarte
344 /usr/share/alsa
8 /usr/share/alsa-base
12488 /usr/share/anthy
8 /usr/share/apmd
21440 /usr/share/app-install
48 /usr/share/application-registry
在这个例子里面,我们把结果管道到 head 命令,把输出结果限制为前 10 行。我们能够产生一个按数值排序的列表,来显示 10 个最大的空间消费者
$ du -s /usr/share/* | sort -nr | head
509940 /usr/share/locale-langpack
242660 /usr/share/doc
197560 /usr/share/fonts
179144 /usr/share/gnome
146764 /usr/share/myspell
144304 /usr/share/gimp
135880 /usr/share/dict
76508 /usr/share/icons
68072 /usr/share/apps
62844 /usr/share/foomatic
通过使用此 -nr 选项,我们产生了一个反向的数值排序,最大数值排列在第一位。这种排序起作用是 因为数值出现在每行的开头。但是如果我们想要基于文件行中的某个数值排序,又会怎样呢? 例如,命令 ls -l 的输出结果:
$ ls -l /usr/bin | head
total 152948
-rwxr-xr-x 1 root root 34824 2008-04-04 02:42 [
-rwxr-xr-x 1 root root 101556 2007-11-27 06:08 a2p
...
此刻,忽略 ls 程序能按照文件大小对输出结果进行排序,我们也能够使用 sort 程序来完成此任务:
$ ls -l /usr/bin | sort -nr -k 5 | head
-rwxr-xr-x 1 root root 8234216 2008-04-0717:42 inkscape
-rwxr-xr-x 1 root root 8222692 2008-04-07 17:42 inkview
...
指定 -k 5,让 sort 程序使用第五字段作为排序的关键值。
William Shotts
默认情况下,sort 程序把此行看作有两个字段。第一个字段包含字符:
意味着空白字符(空格和制表符)被当作是字段间的界定符,当执行排序时,界定符会被包含在字段当中。再看一下 ls 命令的输出,我们看到每行包含八个字段,并且第五个字段是文件大小:
-rwxr-xr-x 1 root root 8234216 2008-04-07 17:42 inkscape
SUSE 10.2 12/07/2006
Fedora 10 11/25/2008
SUSE 11.04 06/19/2008
Ubuntu 8.04 04/24/2008
Fedora 8 11/08/2007
SUSE 10.3 10/04/2007
...
$ sort distros.txt
Fedora 10 11/25/2008
Fedora 5 03/20/2006
Fedora 6 10/24/2006
Fedora 7 05/31/2007
Fedora 8 11/08/2007
...
恩,大部分正确。问题出现在 Fedora 的版本号上。因为在字符集中 “1” 出现在 “5” 之前,版本号 “10” 在 最顶端,然而版本号 “9” 却掉到底端。
为了解决这个问题,我们必须依赖多个键值来排序。我们想要对第一个字段执行字母排序,然后对 第三个字段执行数值排序。sort 程序允许多个 -k 选项的实例,所以可以指定多个排序关键值。事实上, 一个关键值可能包括一个字段区域。如果没有指定区域(如同之前的例子),sort 程序会使用一个键值, 其始于指定的字段,一直扩展到行尾。
多键值排序的语法:
$ sort --key=1,1 --key=2n distros.txt
Fedora 5 03/20/2006
Fedora 6 10/24/2006
Fedora 7 05/31/2007
...
选项的长格式和 -k 1,1 -k 2n 格式是等价的。在第一个 key 指定了 1,1, 意味着“始于并且结束于第一个字段”。 2n,意味着第二个字段是排序的键值, 并且按照数值排序。一个选项字母可能被包含在一个键值说明符的末尾,其用来指定排序的种类。这些选项字母和 sort 程序的全局选项一样:b(忽略开头的空格),n(数值排序),r(逆向排序),等等。
$ sort -k 3.7nbr -k 3.1nbr -k 3.4nbr distros.txt
Fedora 10 11/25/2008
Ubuntu 8.10 10/30/2008
SUSE 11.0 06/19/2008
...
-k 3.7,指示 sort 程序使用一个排序键值,其始于第三个字段中的第七个字符,对应于年的开头。同样地,我们指定 -k 3.1和 -k 3.4来分离日期中的月和日。 我们也添加了 n 和 r 选项来实现一个逆向的数值排序。这个 b 选项用来删除日期字段中开头的空格( 行与行之间的空格数迥异,因此会影响 sort 程序的输出结果)。
一些文件不会使用 tabs 和空格做为字段界定符;例如,这个 /etc/passwd 文件:
$ head /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin/sh
news:x:9:9:news:/var/spool/news:/bin/sh
这个文件的字段之间通过冒号分隔开,sort 程序提供 了一个 -t 选项来定义分隔符。
通过指定冒号字符做为字段分隔符,我们能按照第七个字段来排序。
$ sort -t ':' -k 7 /etc/passwd | head
me:x:1001:1001:Myself,,,:/home/me:/bin/bash
root:x:0:0:root:/root:/bin/bash
dhcp:x:101:102::/nonexistent:/bin/false
gdm:x:106:114:Gnome Display Manager:/var/lib/gdm:/bin/false
hplip:x:104:7:HPLIP system user,,,:/var/run/hplip:/bin/false
klog:x:103:104::/home/klog:/bin/false
messagebus:x:108:119::/var/run/dbus:/bin/false
polkituser:x:110:122:PolicyKit,,,:/var/run/PolicyKit:/bin/false
pulse:x:107:116:PulseAudio daemon,,,:/var/run/pulse:/bin/false
uniq
当给定一个 排好序的文件(包括标准输出),uniq 会删除任意重复行,并且把结果发送到标准输出。 它常常和 sort 程序一块使用,来清理重复的输出。
$ cat > foo.txt
a
b
c
a
b
c
输入 Ctrl-d 来终止标准输入。现在,如果我们对文本文件执行 uniq 命令:
$ uniq foo.txt
a
b
c
a
b
c
uniq 程序能完成任务,其输入必须是排好序的数据
$ sort foo.txt | uniq
a
b
c
uniq 只会删除相邻的重复行。
常用的 uniq 选项
选项 | 说明 |
---|---|
-c | 输出所有的重复行,并且每行开头显示重复的次数。 |
-d | 只输出重复行,而不是特有的文本行。 |
-f n | 忽略每行开头的 n 个字段,字段之间由空格分隔,正如 sort 程序中的空格分隔符;然而, 不同于 sort 程序,uniq 没有选项来设置备用的字段分隔符。 |
-i | 在比较文本行的时候忽略大小写。 |
-s n | 跳过(忽略)每行开头的 n 个字符。 |
-u | 只输出独有的文本行。这是默认的。 |
这里我们看到 uniq 被用来报告文本文件中重复行的次数,使用这个-c 选项:
$ sort foo.txt | uniq -c2 a2 b2 c
切片和切块
cut
用来从文本行中抽取文本,并把其输出到标准输出。它能够接受多个文件参数或者 标准输入。
cut 程序选择项
选项 | 说明 |
---|---|
-c char_list | 从文本行中抽取由 char_list 定义的文本。这个列表可能由一个或多个逗号 分隔开的数值区间组成。 |
-f field_list | 从文本行中抽取一个或多个由 field_list 定义的字段。这个列表可能 包括一个或多个字段,或由逗号分隔开的字段区间。 |
-d delim_char | 当指定-f 选项之后,使用 delim_char 做为字段分隔符。默认情况下, 字段之间必须由单个 tab 字符分隔开。 |
--complement | 抽取整个文本行,除了那些由-c 和/或-f 选项指定的文本。 |
cut 命令最好用来从其它程序产生的文件中抽取文本,而不是从人们直接输入的文本中抽取。
$ cat -A distros.txt
SUSE^I10.2^I12/07/2006$
Fedora^I10^I11/25/2008$
SUSE^I11.0^I06/19/2008$
Ubuntu^I8.04^I04/24/2008$
Fedora^I8^I11/08/2007$
SUSE^I10.3^I10/04/2007$
Ubuntu^I6.10^I10/26/2006$
Fedora^I7^I05/31/2007$
Ubuntu^I7.10^I10/18/2007$
Ubuntu^I7.04^I04/19/2007$
SUSE^I10.1^I05/11/2006$
Fedora^I6^I10/24/2006$
Fedora^I9^I05/13/2008$
Ubuntu^I6.06^I06/01/2006$
Ubuntu^I8.10^I10/30/2008$
Fedora^I5^I03/20/2006$
字段之间仅仅是单个 tab 字符,没有嵌入空格。因为这个文件使用了 tab 而不是空格, 我们将使用 -f 选项来抽取一个字段:
$ cut -f 3 distros.txt
12/07/2006
11/25/2008
06/19/2008
04/24/2008
11/08/2007
10/04/2007
10/26/2006
05/31/2007
10/18/2007
04/19/2007
05/11/2006
10/24/2006
05/13/2008
06/01/2006
10/30/2008
03/20/2006
抽取字符:
$ cut -f 3 distros.txt | cut -c 7-10
2006
2008
2008
2008
2007
2007
2006
2007
2007
2007
2006
2006
2008
2006
2008
2006
$ cut -d ':' -f 1 /etc/passwd | head
root
daemon
bin
sys
sync
games
man
lp
mail
news
使用-d 选项,我们能够指定冒号做为字段分隔符。
paste
这个 paste 命令的功能正好与 cut 相反。它会添加一个或多个文本列到文件中,而不是从文件中抽取文本列。 它通过读取多个文件,然后把每个文件中的字段整合成单个文本流,输入到标准输出。
从我们之前使用 sort 的工作中,首先我们将产生一个按照日期排序的发行版列表,并把结果 存储在一个叫做 distros-by-date.txt 的文件中:
$ sort -k 3.7nbr -k 3.1nbr -k 3.4nbr distros.txt > distros-by-date.txt
下一步,我们将会使用 cut 命令从文件中抽取前两个字段(发行版名字和版本号),并把结果存储到 一个名为 distro-versions.txt 的文件中:
$ cut -f 1,2 distros-by-date.txt > distros-versions.txt
$ head distros-versions.txt
Fedora 10
Ubuntu 8.10
SUSE 11.0
Fedora 9
Ubuntu 8.04
Fedora 8
Ubuntu 7.10
SUSE 10.3
Fedora 7
Ubuntu 7.04
最后的准备步骤是抽取发行日期,并把它们存储到一个名为 distro-dates.txt 文件中:
[me@linuxbox ~]$ cut -f 3 distros-by-date.txt > distros-dates.txt
[me@linuxbox ~]$ head distros-dates.txt
11/25/2008
10/30/2008
06/19/2008
05/13/2008
04/24/2008
11/08/2007
10/18/2007
10/04/2007
05/31/2007
04/19/2007
现在我们拥有了我们所需要的文本了。为了完成这个过程,使用 paste 命令来把日期列放到发行版名字 和版本号的前面,这样就创建了一个年代列表。通过使用 paste 命令,然后按照期望的顺序来安排它的 参数,就能很容易完成这个任务。
$ paste distros-dates.txt distros-versions.txt
11/25/2008 Fedora 10
10/30/2008 Ubuntu 8.10
06/19/2008 SUSE 11.0
05/13/2008 Fedora 9
04/24/2008 Ubuntu 8.04
11/08/2007 Fedora 8
10/18/2007 Ubuntu 7.10
10/04/2007 SUSE 10.3
05/31/2007 Fedora 7
04/19/2007 Ubuntu 7.04
join
在某些方面,join 命令类似于 paste,它会往文件中添加列,但是它使用了独特的方法来完成。
一个 join 操作通常与关系型数据库有关联,在关系型数据库中来自多个享有共同关键域的表格的 数据结合起来,得到一个期望的结果。这个 join 程序执行相同的操作。它把来自于多个基于共享关键域的文件的数据结合起来。
为了知道在关系数据库中是怎样使用 join 操作的,让我们想象一个很小的数据库,这个数据库由两个表格组成,每个表格包含一条记录。第一个表格,叫做 CUSTOMERS,有三个数据域:一个客户号(CUSTNUM), 客户的名字(FNAME)和客户的姓(LNAME):
CUSTNUM FNAME ME
======== ===== ======
4681934 John Smith
第二个表格叫做 ORDERS,其包含四个数据域:订单号(ORDERNUM),客户号(CUSTNUM),数量(QUAN), 和订购的货品(ITEM)。
ORDERNUM CUSTNUM QUAN ITEM
======== ======= ==== ====
3014953305 4681934 1 Blue Widget
注意两个表格共享数据域 CUSTNUM。这很重要,因为它使表格之间建立了联系。
执行一个 join 操作将允许我们把两个表格中的数据域结合起来,得到一个有用的结果,例如准备 一张发货单。通过使用两个表格 CUSTNUM 数字域中匹配的数值,一个 join 操作会产生以下结果:
FNAME LNAME QUAN ITEM
===== ===== ==== ====
John Smith 1 Blue Widget
为了说明 join 程序,我们需要创建一对包含共享键值的文件。为此,我们将使用我们的 distros.txt 文件。 从这个文件中,我们将构建额外两个文件,一个包含发行日期(其会成为共享键值)和发行版名称:
$ cut -f 1,1 distros-by-date.txt > distros-names.txt
$ paste distros-dates.txt distros-names.txt > distros-key-names.txt
$ head distros-key-names.txt
11/25/2008 Fedora
10/30/2008 Ubuntu
06/19/2008 SUSE
05/13/2008 Fedora
04/24/2008 Ubuntu
11/08/2007 Fedora
10/18/2007 Ubuntu
10/04/2007 SUSE
05/31/2007 Fedora
04/19/2007 Ubuntu
第二个文件包含发行日期和版本号:
$ cut -f 2,2 distros-by-date.txt > distros-vernums.txt
$ paste distros-dates.txt distros-vernums.txt > distros-key-vernums.txt
$ head distros-key-vernums.txt
11/25/2008 10
10/30/2008 8.10
06/19/2008 11.0
05/13/2008 9
04/24/2008 8.04
11/08/2007 8
10/18/2007 7.10
10/04/2007 10.3
05/31/2007 7
04/19/2007 7.04
现在我们有两个具有共享键值( “发行日期” 数据域 )的文件。有必要指出,为了使 join 命令 能正常工作,所有文件必须按照关键数据域排序。
$ join distros-key-names.txt distros-key-vernums.txt | head
11/25/2008 Fedora 10
10/30/2008 Ubuntu 8.10
06/19/2008 SUSE 11.0
05/13/2008 Fedora 9
04/24/2008 Ubuntu 8.04
11/08/2007 Fedora 8
10/18/2007 Ubuntu 7.10
10/04/2007 SUSE 10.3
05/31/2007 Fedora 7
04/19/2007 Ubuntu 7.04
默认情况下,join 命令使用空白字符做为输入字段的界定符,一个空格作为输出字段 的界定符。这种行为可以通过指定的选项来修改。
比较文本
comm
这个 comm 程序会比较两个文本文件,并且会显示每个文件特有的文本行和共有的文把行。
$ cat > file1.txt
a
b
c
d
$ cat > file2.txt
b
c
d
e
下一步,我们将使用 comm 命令来比较这两个文件:
$ comm file1.txt file2.txt
abcde
comm 命令产生了三列输出。第一列包含第一个文件独有的文本行;第二列, 文本行是第二列独有的;第三列包含两个文件共有的文本行。comm 支持 -n 形式的选项,这里 n 代表 1,2 或 3。这些选项使用的时候,指定了要隐藏的列。例如,如果我们只想输出两个文件共享的文本行, 我们将隐藏第一列和第二列的输出结果:
$ comm -12 file1.txt file2.txt
b
c
d
diff
软件开发员经常使用 diff 程序来检查不同程序源码 版本之间的更改,diff 能够递归地检查源码目录,经常称之为源码树。
$ diff file1.txt file2.txt
1d0
< a
4a4
> e
我们看到 diff 程序的默认输出风格:对两个文件之间差异的简短描述。在默认格式中, 每组的更改之前都是一个更改命令,其形式为 range operation range , 用来描述要求更改的位置和类型,从而把第一个文件转变为第二个文件
diff 更改命令
改变 | 说明 |
---|---|
r1ar2 | 把第二个文件中位置 r2 处的文件行添加到第一个文件中的 r1 处。 |
r1cr2 | 用第二个文件中位置 r2 处的文本行更改(替代)位置 r1 处的文本行。 |
r1dr2 | 删除第一个文件中位置 r1 处的文本行,这些文本行将会出现在第二个文件中位置 r2 处。 |
在这种格式中,一个范围就是由逗号分隔开的开头行和结束行的列表。虽然这种格式是默认情况(主要是 为了服从 POSIX 标准且向后与传统的 Unix diff 命令兼容), 但是它并不像其它可选格式一样被广泛地使用。最流行的两种格式是上下文模式和统一模式。
当使用上下文模式(带上 -c 选项),我们将看到这些:
$ diff -c file1.txt file2.txt
*** file1.txt 2008-12-23 06:40:13.000000000 -0500
--- file2.txt 2008-12-23 06:40:34.000000000 -0500
***************
*** 1,4 ****
- abcd
--- 1,4 ----bcd+ e
这个输出结果以两个文件名和它们的时间戳开头。第一个文件用星号做标记,第二个文件用短横线做标记。 纵观列表的其它部分,这些标记将象征它们各自代表的文件。
*** 1,4 ***
在更改组内,文本行以四个指示符之一开头:
diff 上下文模式更改指示符
指示符 | 意思 |
---|---|
blank | 上下文显示行。它并不表示两个文件之间的差异。 |
- | 删除行。这一行将会出现在第一个文件中,而不是第二个文件内。 |
+ | 添加行。这一行将会出现在第二个文件内,而不是第一个文件中。 |
! | 更改行。将会显示某个文本行的两个版本,每个版本会出现在更改组的各自部分。 |
这个统一模式相似于上下文模式,但是更加简洁。通过 -u 选项来指定它:
$ diff -u file1.txt file2.txt
--- file1.txt 2008-12-23 06:40:13.000000000 -0500
+++ file2.txt 2008-12-23 06:40:34.000000000 -0500
@@ -1,4 +1,4 @@
-abcd
+e
上下文模式和统一模式之间最显著的差异就是重复上下文的消除,这就使得统一模式的输出结果要比上下文 模式的输出结果简短。在我们上述实例中,我们看到类似于上下文模式中的文件时间戳,其紧紧跟随字符串 @@ -1,4 +1,4 @@。这行字符串表示了在更改组中描述的第一个文件中的文本行和第二个文件中的文本行。 这行字符串之后就是文本行本身,与三行默认的上下文。每行以可能的三个字符中的一个开头:
diff 统一模式更改指示符
字符 | 意思 |
---|---|
空格 | 两个文件都包含这一行。 |
- | 在第一个文件中删除这一行。 |
+ | 添加这一行到第一个文件中。 |
patch
这个 patch 程序被用来把更改应用到文本文件中。它接受从 diff 程序的输出,并且通常被用来 把较老的文件版本转变为较新的文件版本。
Linux 内核是由大型的,组织松散的贡献者团队开发而成,这些贡献者会提交固定的少量更改到源码包中。一个 diff 文件包含先前的内核版本与带有贡献者修改的新版本之间的差异。 然后一个接受者使用 patch 程序,把这些更改应用到他自己的源码树中。使用 diff/patch 组合提供了 两个重大优点:
- 一个 diff 文件非常小,与整个源码树的大小相比较而言。
- 一个 diff 文件简洁地显示了所做的修改,从而允许程序补丁的审阅者能快速地评估它。
当然,diff/patch 能工作于任何文本文件,不仅仅是源码文件。它同样适用于配置文件或任意其它文本。
准备一个 diff 文件供 patch 程序使用
diff -Naur old_file new_file > diff_file
old_file 和 new_file 部分不是单个文件就是包含文件的目录。这个 r 选项支持递归目录树。
一旦创建了 diff 文件,我们就能应用它,把旧文件修补成新文件。
patch < diff_file
我们将使用测试文件来说明:
$ diff -Naur file1.txt file2.txt > patchfile.txt
$ patch < patchfile.txt
patching file file1.txt
$ cat file1.txt
b
c
d
e
在这个例子中,我们创建了一个名为 patchfile.txt 的 diff 文件,然后使用 patch 程序, 来应用这个补丁。注意我们没有必要指定一个要修补的目标文件,因为 diff 文件(在统一模式中)已经 在标题行中包含了文件名。一旦应用了补丁,我们能看到,现在 file1.txt 与 file2.txt 文件相匹配了。
运行时编辑
tr
一种基于字符的查找和替换操作。
$ echo "lowercase letters" | tr a-z A-Z
LOWERCASE LETTERS
正如我们所见,tr 命令操作标准输入,并把结果输出到标准输出。tr 命令接受两个参数:要被转换的字符集以及相对应的转换后的字符集。字符集可以用三种方式来表示:
- 一个枚举列表。例如, ABCDEFGHIJKLMNOPQRSTUVWXYZ
- 一个字符域。例如,A-Z 。注意这种方法有时候面临与其它命令相同的问题,归因于 语系的排序规则,因此应该谨慎使用。
- POSIX 字符类。例如,[:upper:]
大多数情况下,两个字符集应该长度相同;然而,有可能第一个集合大于第二个,尤其如果我们 想要把多个字符转换为单个字符:
$ echo "lowercase letters" | tr [:lower:] A
AAAAAAAAA AAAAAAA
除了换字之外,tr 命令能允许字符从输入流中简单地被删除。
tr -d '\r' < dos_file > unix_file
这里的 dos_file 是需要被转换的文件,unix_file 是转换后的结果。
tr 也可以完成另一个技巧。使用-s 选项,tr 命令能“挤压”(删除)重复的字符实例:
$ echo "aaabbbccc" | tr -s ab
abccc
注意重复的字符必须是相邻的。 如果它们不相邻:
$ echo "abcabcabc" | tr -s ab
abcabcabc
sed
sed 是 stream editor(流编辑器)的简称。
要不给出单个编辑命令(在命令行中)要不就是包含多个命令的脚本文件名, 然后它就按行来执行这些命令。
$ echo "front" | sed 's/front/back/'
back
在这个例子中,我们使用 echo 命令产生了一个单词的文本流,然后把它管道给 sed 命令。sed,依次对流文本执行指令 s/front/back/,随后输出“back”。我们也能够把这个命令认为是相似于 vi 中的“替换” (查找和替代)命令。
sed 中的命令开始于单个字符。在上面的例子中,这个替换命令由字母 s 来代表,其后跟着查找 和替代字符串,斜杠字符做为分隔符。分隔符的选择是随意的。按照惯例,经常使用斜杠字符,sed 将会接受紧随命令之后的任意字符做为分隔符。
$ echo "front" | sed 's_front_back_'
back
sed 中的大多数命令之前都会带有一个地址,其指定了输入流中要被编辑的文本行。如果省略了地址, 然后会对输入流的每一行执行编辑命令。最简单的地址形式是一个行号。我们能够添加一个地址 到我们例子中:
$ echo "front" | sed '1s/front/back/'
back
给我们的命令添加地址 1,就导致只对仅有一行文本的输入流的第一行执行替换操作。如果我们指定另一 个数字
$ echo "front" | sed '2s/front/back/'
front
我们看到没有执行这个编辑命令,因为我们的输入流没有第二行。地址可以用许多方式来表达。这里是 最常用的:
sed 地址表示法
地址 | 说明 |
---|---|
n | 行号,n 是一个正整数。 |
$ | 最后一行。 |
/regexp/ | 所有匹配一个 POSIX 基本正则表达式的文本行。注意正则表达式通过 斜杠字符界定。选择性地,这个正则表达式可能由一个备用字符界定,通过\cregexpc 来 指定表达式,这里 c 就是一个备用的字符。 |
addr1,addr2 | 从 addr1 到 addr2 范围内的文本行,包含地址 addr2 在内。地址可能是上述任意 单独的地址形式。 |
first~step | 匹配由数字 first 代表的文本行,然后随后的每个在 step 间隔处的文本行。例如 1~2 是指每个位于奇数行号的文本行,5~5 则指第五行和之后每五行位置的文本行。 |
addr1,+n | 匹配地址 addr1 和随后的 n 个文本行。 |
addr! | 匹配所有的文本行,除了 addr 之外,addr 可能是上述任意的地址形式。 |
$ sed -n '1,5p' distros.txt
SUSE 10.2 12/07/2006
Fedora 10 11/25/2008
SUSE 11.0 06/19/2008
Ubuntu 8.04 04/24/2008
Fedora 8 11/08/2007
在这个例子中,我们打印出一系列的文本行,开始于第一行,直到第五行。为此,我们使用 p 命令, 其就是简单地把匹配的文本行打印出来。然而为了高效,我们必须包含选项 -n(不自动打印选项), 让 sed 不要默认地打印每一行。
$ sed -n '/SUSE/p' distros.txt
SUSE 10.2 12/07/2006
SUSE 11.0 06/19/2008
SUSE 10.3 10/04/2007
SUSE 10.1 05/11/2006
孤立出包含它的文本行,和 grep 程序的功能 是相同的。
最后,我们将试着否定上面的操作,通过给这个地址添加一个感叹号:
$ sed -n '/SUSE/!p' distros.txt
Fedora 10 11/25/2008
Ubuntu 8.04 04/24/2008
Fedora 8 11/08/2007
Ubuntu 6.10 10/26/2006
Fedora 7 05/31/2007
Ubuntu 7.10 10/18/2007
Ubuntu 7.04 04/19/2007
Fedora 6 10/24/2006
Fedora 9 05/13/2008
Ubuntu 6.06 06/01/2006
Ubuntu 8.10 10/30/2008
Fedora 5 03/20/2006
sed 基本编辑命令
命令 | 说明 |
---|---|
= | 输出当前的行号。 |
a | 在当前行之后追加文本。 |
d | 删除当前行。 |
i | 在当前行之前插入文本。 |
p | 打印当前行。默认情况下,sed 程序打印每一行,并且只是编辑文件中匹配 指定地址的文本行。通过指定-n 选项,这个默认的行为能够被忽略。 |
q | 退出 sed,不再处理更多的文本行。如果不指定-n 选项,输出当前行。 |
Q | 退出 sed,不再处理更多的文本行。 |
s/regexp/replacement/ | 只要找到一个 regexp 匹配项,就替换为 replacement 的内容。 replacement 可能包括特殊字符 &,其等价于由 regexp 匹配的文本。另外, replacement 可能包含序列 \1到 \9,其是 regexp 中相对应的子表达式的内容。更多信息,查看 下面 back references 部分的讨论。在 replacement 末尾的斜杠之后,可以指定一个 可选的标志,来修改 s 命令的行为。 |
y/set1/set2 | 执行字符转写操作,通过把 set1 中的字符转变为相对应的 set2 中的字符。 注意不同于 tr 程序,sed 要求两个字符集合具有相同的长度。 |
到目前为止,这个 s 命令是最常使用的编辑命令。我们将仅仅演示一些它的功能,
手动修改 日期格式不仅浪费时间而且易出错,但是有了 sed,只需一步就能完成修改:
$ sed 's/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/' distros.txt
SUSE 10.2 2006-12-07
Fedora 10 2008-11-25
SUSE 11.0 2008-06-19
Ubuntu 8.04 2008-04-24
Fedora 8 2007-11-08
SUSE 10.3 2007-10-04
Ubuntu 6.10 2006-10-26
Fedora 7 2007-05-31
Ubuntu 7.10 2007-10-18
Ubuntu 7.04 2007-04-19
SUSE 10.1 2006-05-11
Fedora 6 2006-10-24
Fedora 9 2008-05-13
Ubuntu 6.06 2006-06-01
Ubuntu 8.10 2008-10-30
Fedora 5 2006-03-20
仅用一步,我们就更改了文件中的日期格式。
此命令有这样一个基本的结构:
sed 's/regexp/replacement/' distros.txt
我们下一步是要弄明白一个正则表达式将要孤立出日期。因为日期是 MM/DD/YYYY 格式,并且 出现在文本行的末尾,我们可以使用这样的表达式:
[0-9]{2}/[0-9]{2}/[0-9]{4}$
正则表达式的新功能,逆参照 ,像这样工作:如果序列 \n
出现在 replacement 中 ,这里 n 是指从 1 到 9 的数字,则这个序列指的是在前面正则表达式中相对应的子表达式。为了 创建这个子表达式,我们简单地把它们用圆括号括起来,
现在我们有了三个子表达式。第一个表达式包含月份,第二个包含某月中的某天,以及第三个包含年份。 现在我们就可以构建 replacement ,如下所示:
\3-\1-\2
此表达式给出了年份,一个短划线,月份,一个短划线,和某天。
sed 's/([0-9]{2})/([0-9]{2})/([0-9]{4})$/\3-\1-\2/' distros.txt
两个问题。第一个是当 sed 试图解释这个 s 命令的时候在我们表达式中额外的斜杠将会使 sed 迷惑。 第二个是由于sed默认情况下只接受基本的正则表达式,在表达式中的几个字符会 被当作文字字面值,而不是元字符。我们能够通过反斜杠来转义字符
sed 's/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/' distros.txt
aspell(略)
一款交互式的拼写检查器。但它也可以作为一个独立的命令行工具使用
格式化输出样式的程序
不是改变文本自身。
nl - 添加行号
nl 程序是一个相当神秘的工具。它添加文件的行数。在它最简单的用途中,它相当于 cat -n
$ nl distros.txt | head
像 cat,nl 既能接受多个文件作为命令行参数,也能接受标准输入。
nl 在计算文件行数的时候支持一个叫“逻辑页面”的概念 。这允许nl在计算的时候去重设(再一次开始)可数的序列。用到那些选项 的时候,可以设置一个特殊的开始值,并且在某个可限定的程度上还能设置它的格式。一个逻辑页面被进一步分为 header,body 和 footer 这样的元素。在每一个部分中,数行数可以被重设,并且/或被设置成另外一个格式。如果nl同时处理多个文件,它会把他们当成一个单一的 文本流。文本流中的部分被一些相当古怪的标记的存在加进了文本:nl 标记
标记 | 含义 |
---|---|
::: | 逻辑页页眉开始处 |
:: | 逻辑页主体开始处 |
: | 逻辑页页脚开始处 |
每一个上述的标记元素肯定在自己的行中独自出现。在处理完一个标记元素之后,nl 把它从文本流中删除。
常用 nl 选项
选项 | 含义 |
---|---|
-b style | 把 body 按被要求方式数行,可以是以下方式: a = 数所有行 t = 数非空行。这是默认设置。 n = 无 pregexp = 只数那些匹配了正则表达式的行 |
-f style | 将 footer 按被要求设置数。默认是无 |
-h style | 将 header 按被要求设置数。默认是 |
-i number | 将页面增加量设置为数字。默认是一。 |
-n format | 设置数数的格式,格式可以是: ln = 左偏,没有前导零。 rn = 右偏,没有前导零。 rz = 右偏,有前导零。 |
-p | 不要在没一个逻辑页面的开始重设页面数。 |
-s string | 在没一个行的末尾加字符作分割符号。默认是单个的 tab。 |
-v number | 将每一个逻辑页面的第一行设置成数字。默认是一。 |
-w width | 将行数的宽度设置,默认是六。 |
# sed script to produce Linux distributions report
1 i\
\\:\\:\\:\
\
Linux Distributions Report\
\
Name
Ver. Released\
----
---- --------\
\\:\\:
s/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/
$ i\
\\:\
\
End Of Report
这个脚本现在加入了 nl 的逻辑页面标记并且在报告的最后加了一个 footer。记得我们在我们的标记中必须两次使用反斜杠, 因为他们通常被 sed 解释成一个转义字符。
下一步,我们将结合 sort, sed, nl 来生成我们改进的报告:
$ sort -k 1,1 -k 2n distros.txt | sed -f distros-nl.sed | nlLinux Distributions ReportName Ver. Released---- ---- --------1 Fedora 5 2006-03-202 Fedora 6 2006-10-243 Fedora 7 2007-05-314 Fedora 8 2007-11-085 Fedora 9 2008-05-136 Fedora 10 2008-11-257 SUSE 10.1 2006-05-118 SUSE 10.2 2006-12-079 SUSE 10.3 2007-10-0410 SUSE 11.0 2008-06-1911 Ubuntu 6.06 2006-06-0112 Ubuntu 6.10 2006-10-2613 Ubuntu 7.04 2007-04-1914 Ubuntu 7.10 2007-10-1815 Ubuntu 8.04 2008-04-24End Of Report
我们的报告是一串命令的结果,首先,我们给名单按发行版本和版本号(表格1和2处)进行排序,然后我们用 sed 生产结果, 增加了 header(包括了为 nl 增加的逻辑页面标记)和 footer。最后,我们按默认用 nl 生成了结果,只数了属于逻辑页面的 body 部分的 文本流的行数。
fold - 限制文件行宽
文本的行限制到特定的宽的过程。
$ echo "The quick brown fox jumped over the lazy dog." | fold -w 12
The quick br
own fox jump
ed over the
lazy dog.
这里我们看到了 fold 的行为。这个用 echo 命令发送的文本用 -w 选项分解成块。在这个例子中,我们设定了行宽为12个字符。 如果没有字符设置,默认是80。注意到文本行不会因为单词边界而不会被分解。增加的 -s 选项将让 fold 分解到最后可用的空白字符,即会考虑单词边界。
$ echo "The quick brown fox jumped over the lazy dog." | fold -w 12 -s
The quick
brown fox
jumped over
the lazy
dog.
fmt - 一个简单的文本格式器
同样折叠文本,外加很多功能。
让我们重新格式这个文本并且让它成为一个50 个字符宽的项目。我们能用 -w 选项对文件进行处理:
$ fmt -w 50 fmt-info.txt | head
默认情况下,输出会保留空行,单词之间的空格,和缩进;持续输入的具有不同缩进的文本行不会连接在一起;tab 字符在输入时会展开,输出时复原 。
所以,fmt 会保留第一行的缩进。幸运的是,fmt 提供了一个选项来更正这种行为:通过添加 -c 选项,现在我们得到了所期望的结果。
-p 选项可以格式文件选中的部分,通过在开头使用一样的符号。
$ cat > fmt-code.txt
# This file contains code with comments.
# This line is a comment.
# Followed by another comment line.
# And another.
This, on the other hand, is a line of code.
And another line of code.
And another.
使用 fmt,我们能格式注释并且 不让代码被触及。
$ fmt -w 50 -p '# ' fmt-code.txt
# This file contains code with comments.
# This line is a comment. Followed by another
# comment line. And another.
This, on the other hand, is a line of code.
And another line of code.
And another.
注意相邻的注释行被合并了
pr – 格式化打印文本
pr 程序用来把文本分页。当打印文本的时候,经常希望用几个空行在输出的页面的顶部或底部添加空白。此外,这些空行能够用来插入到每个页面的页眉或页脚。
$ pr -l 15 -w 65 distros.txt
2008-12-11 18:27 distros.txt Page 1
SUSE 10.2 12/07/2006
Fedora 10 11/25/2008
SUSE 11.0 06/19/2008
Ubuntu 8.04 04/24/2008
Fedora 8 11/08/2007
2008-12-11 18:27 distros.txt Page 2
SUSE 10.3 10/04/2007
Ubuntu 6.10 10/26/2006
Fedora 7 05/31/2007
Ubuntu 7.10 10/18/2007
Ubuntu 7.04 04/19/2007
在上面的例子中,我们用 -l 选项(页长)和 -w 选项(页宽)定义了宽65列,长15行的一个“页面”。 pr 为 distros.txt 中的内容编订页码,用空行分开各页面,生成了包含文件修改时间、文件名、页码的默认页眉。 pr 指令拥有很多调整页面布局的选项,我们将在下一章中进一步探讨。
printf – Format And Print Data
在 bash 中, printf 是内置的。
printf “format” arguments
首先,发送包含有格式化描述的字符串的指令,接着,这些描述被应用于参数列表上。格式化的结果在标准输出中显示。下面是一个小例子:
$ printf "I formatted the string: %s\n" foo
I formatted the string: foo
格式字符串可能包含文字文本(如“我格式化了这个字符串:” “I formatted the string:”),转义序列(例如\n,换行符)和以%字符开头的序列,这被称为转换规范。在上面的例子中,转换规范 %s 用于格式化字符串 “foo” 并将其输出在命令行中。我们再来看一遍:
$ printf "I formatted '%s' as a string.\n" foo
I formatted 'foo' as a string.
printf 转换规范组件
组件 | 描述 |
---|---|
d | 将数字格式化为带符号的十进制整数 |
f | 格式化并输出浮点数 |
o | 将整数格式化为八进制数 |
s | 将字符串格式化 |
x | 将整数格式化为十六进制数,必要时使用小写a-f |
X | 与 x 相同,但变为大写 |
% | 打印 % 符号 (比如,指定 “%%”) |
$ printf "%d, %f, %o, %s, %x, %X\n" 380 380 380 380 380 380
380, 380.000000, 574, 380, 17c, 17C
由于我们指定了六个转换符,我们还必须为 printf 提供六个参数进行处理。下面六个结果展示了每个转换符的效果。 可将可选组件添加到转换符以调整输出。
%[flags][width][.precision]conversion_specification
使用多个可选组件时,必须按照上面指定的顺序,以便准确编译。以下是每个可选组件的描述:
printf 转换规范组件
组件 | 描述 |
---|---|
flags | 有5种不同的标志: # – 使用“备用格式”输出。这取决于数据类型。对于o(八进制数)转换,输出以0为前缀.对于x和X(十六进制数)转换,输出分别以0x或0X为前缀。 0–(零) 用零填充输出。这意味着该字段将填充前导零,比如“000380”。 - – (破折号) 左对齐输出。默认情况下,printf右对齐输出。 ‘ ’ – (空格) 在正数前空一格。 + – (加号) 在正数前添加加号。默认情况下,printf 只在负数前添加符号。 |
width | 指定最小字段宽度的数。 |
.precision | 对于浮点数,指定小数点后的精度位数。对于字符串转换,指定要输出的字符数。 |
以下是不同格式的一些示例:
print 转换规范示例
自变量 | 格式 | 结果 | 备注 |
---|---|---|---|
380 | "%d" | 380 | 简单格式化整数。 |
380 | "%#x" | 0x17c | 使用“替代格式”标志将整数格式化为十六进制数。 |
380 | "%05d" | 00380 | 用前导零(padding)格式化整数,且最小字段宽度为五个字符。 |
380 | "%05.5f" | 380.00000 | 使用前导零和五位小数位精度格式化数字为浮点数。由于指定的最小字段宽度(5)小于格式化后数字的实际宽度,因此前导零这一命令实际上没有起到作用。 |
380 | "%010.5f" | 0380.00000 | 将最小字段宽度增加到10,前导零现在变得可见。 |
380 | "%+d" | +380 | 使用+标志标记正数。 |
380 | "%-d" | 380 | 使用-标志左对齐 |
abcdefghijk | "%5s" | abcedfghijk | 用最小字段宽度格式化字符串。 |
abcdefghijk | "%d" | abcde | 对字符串应用精度,它被从中截断。 |
再次强调,printf 主要用在脚本中,用于格式化表格数据,而不是直接用于命令行。
$ printf "%s\t%s\t%s\n" str1 str2 str3
str1 str2 str3
$ printf "Line: %05d %15.3f Result: %+15d\n" 1071 3.14156295 32589
Line: 01071 3.142 Result: +32589
$ printf "<html>\n\t<head>\n\t\t<title>%s</title>\n
\t</head>\n\t<body>\n\t\t<p>%s</p>\n\t</body>\n</html>\n" "Page Tit
le" "Page Content"
<html><head><title>Page Tit
le</title></head><body><p>Page Content</p></body>
</html>
文件格式化系统
Document Formatting Systems
groff
groff 是一套用GNU实现 troff 的程序。它还包括一个脚本,用来模仿 nroff 和其他 roff 家族。
roff 及其后继制作格式化文档的方式对现代用户来说是相当陌生的。今天的大部分文件都是由能够一次性完成排字和布局的文字处理器生成的。 在图形文字处理器出现之前,需要两步来生成文档。首先用文本编辑器排字,接着用诸如 troff 之类的处理器来格式化。 格式化程序的说明通过标记语言的形式插入到已排好字的文本当中。 类似这种过程的现代例子是网页。它首先由某种文本编辑器排好字,然后由使用 HTML 作为标记语言的 Web 浏览器渲染出最终的页面布局。