文本处理

前言

  1. 由于所有类 UNIX 操作系统都严重依赖于文本文件来进行某些数据类型的存储,所以需要有很多可以进行文本操作的工具。
  2. 本章主要介绍一些与切割文本有关的命令,第 21 章会进一步探讨文本处理工具,并重点讲解那些用于格式化输出以及其他满足人类需求的程序。
  3. 本章首先会回顾之前讲过的一些命令,然后讲解一些新的命令。
  • cat: 连接文件并打印到标准输出
  • sort: 对文本排序
  • uniq: 报告并省略重复行
  • cut: 从每一行中移除文本区域
  • paste: 合并文件文本行
  • join: 基于某个共享字段来联合两个文件的文本行
  • comm: 逐行比较两个已经排好序的文件
  • diff: 逐行比较文件
  • patch: 对原文件打补丁
  • tr: 转换或删除字符
  • sed: 用于过滤和转换文本的流编辑器
  • aspell: 交互式拼写检查器

文本应用程序

  1. 基本介绍
  • 到目前为止,我们总共介绍了两种文本编辑 (nano 和 vim) ,看过一堆配置文件,并且目睹了许多命令的输出都是文件格式。
  1. 文件
  • 许多人都采用纯文本格式编辑文件。虽然大家都知道用一些较小的文本文件进行一些简单的笔记很方便、很实用,但同样,我们也可以用文本格式编辑较大的文档。
  • 有一种常用的方法,即首先在文本编辑器中编辑大型文档的内容,然后使用标记语言描述文件格式。
  1. 网页
  • 网页可以说是世界上最常见的电子文档。网页属于文本文档,一般使用 HTML (Hypertext Markup Language) 或者 XML (eXtensible Markup Language) 等标记语言描述内容的可视化形式。
  1. 电子邮件
  • 电子邮件本质上是一种基本文本的媒介,即便是非文本附近,在传输的时候也会被转成文本格式。
  1. 打印机输出
  • 在类 UNIX 系统中,准备向打印机传送的信息是以纯文本格式传送的。如果该页包含图像,则将其转换成 PostScript 文本格式页面描述语言后再送至指定程序以打印图像像素。
  1. 程序源代码
  • 类 UNIX 系统中的许多命令行程序都是为了支持系统管理和软件开发而编写的,文本处理程序也不例外。
  • 文本处理对软件开发者如此重要,是因为所有的软件都是从文本开始的,程序员所编写的源代码,也总是以文本的形式编辑。

温故而知新

基本介绍

  1. 在第 6 章,我们学习了一些既支持命令行参数输入也支持标准输入的命令。不过当时只是泛泛而谈,现在我们详细讨论这些命令如何用于文本处理。

cat - 进行文件之间的拼接并且输出到标准输出

  1. cat 命令有许多有趣的参数选项,而其中多数则是用于提高文本内容的可视化效果。
  2. -A 选项就是一个例子,它用于显示文本中的非打印字符。
  3. 例如,用户有时候会想知道可见文本中是否嵌入了控制字符,其中最为常见的就是制表符 (而不是空格) 以及回车符,在 MS-DOS 风格的文本文件中,回车符经常作为结束符出现。另一种常见情况是文件中包含末尾带有空格的文件行。
  4. 我们创建一个测试文件,用 cat 程序作为一个简单的文字处理器。
  5. 为此,只需要输入 cat 命令 (随后指定了用于重定向输出的文件) 再输入文本内容,按 Enter 键结束行输入,最后按 Ctrl-D 告诉 cat 到达文件末尾。
  6. 下例中,我们输入了一个以 Tab 制表符开头、空格符结尾的文本行。
    1
    2
    3
    [me@linux ~]$ cat > foo.txt
    The quick brown jumped over the lazy dog.
    [me@linux ~]$
  7. 下面,我们利用带有 -A 选项的 cat 命令显示文本内容:
    1
    2
    3
    [me@linux ~]$ cat -A foo.txt
    ^IThe quick brown jumped over the lazy dog. $
    [me@linux ~]$
  8. 输出结果表明,文本中 Tab 制表符由符号 ^I 表示。这是一种常见的表示方法,意思是 Ctrl-I ,结果证明,它等同于 Tab 制表符。同时,在文件末尾出现的 $ 符说明行末尾存在空格。
  9. cat 也有很多用于修改文本的参数选。最著名的两个选项:-n ,对行编号;-s ,禁止输出多个空白行。示例如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [me@linux ~]$ cat > foo.txt
    The quick brown fox


    jumped over the lazy dog.
    [me@linux ~]$ cat -ns foo.txt
    1 The quick brown fox
    2
    3 jumped over the lazy dog.
    [me@linux ~]$
  10. 本例中,我们创建了一个 foo.txt 测试文件的新版本,该文件内容为两个文本行,并以空白行隔开。
  11. 用 cat 加 -ns 选项对其操作后,多余的空白行便被移除,并对剩余的行进行了编号。
  12. 然而这并不是多个进程在操作这个文本,只有一个进程。

sort - 对文本行进行排序

  1. sort 基本介绍
  • sort 是一个排序程序,它的操作对象为标准输入或是命令行中指定的一个或多个文件后将结果送至标准输出。与 cat 用法类似,如下所示,我们将直接使用键盘演示标准输入内容的处理过程。
    1
    2
    3
    4
    5
    6
    7
    8
    [me@linuxbox ~]$ sort > foo.txt
    c
    b
    a
    [me@linuxbox ~]$ cat foo.txt
    a
    b
    c
  • 输入 sort 命令后,输入字母 c、b、a ,最后按下 Ctrl-D 结束输入。然后查看处理结果,会发现这些行都以排好的顺序出现。
  • 由于 sort 命令允许多个文件作为其输入参数,所以可以将多个文件融合为一个已排序的整体文件。
  • 例如,我们有三个文本文件,并期望将它们拼接为一个已排序的整体文件。我们可以用下面的命令行去执行。
  1. 常见的 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 定义字段分隔符。默认情况下,字段是由空格或制表符分开的
  1. 案例一
  • 作为演示,我们可将 du 命令的输出结果进行排序,以确定最大的硬盘空间用户。
  • 正常情况下,du 命令会列出一个以路径名顺序排序的列表。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [me@linuxbox ~]$ du -s /usr/share/* | head
    8 /usr/share/CSI
    48 /usr/share/CoreDuetDaemonConfig.bundle
    0 /usr/share/ans2_dummy_dir
    1016 /usr/share/applecare
    512 /usr/share/calendar
    40 /usr/share/com.apple.languageassetd
    1552 /usr/share/cracklib
    8904 /usr/share/cups
    2928 /usr/share/dict
    9944 /usr/share/doc
  • 本例中,我们把结果管道到 head 命令,把输出结果限制为只显示前 10 行。
  • 我们能够产生一个按数值排序的列表,来显示 10 个最大空间消费者。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [me@linuxbox ~]$ du -s /usr/share/* | sort -nr | head
    132104 /usr/share/tokenizer
    70152 /usr/share/man
    46400 /usr/share/firmware
    28864 /usr/share/vim
    27896 /usr/share/icu
    26048 /usr/share/langid
    20352 /usr/share/terminfo
    13896 /usr/share/morphun
    10592 /usr/share/zsh
    9944 /usr/share/doc
  • 通过使用 -nr 参数选项,我们便可以产生一个逆向的数值排序,它使得最大数值排列在第一位。
  • 这种排序起作用是因为数值出现在每一行的开头。
  1. 案例二
  • 但是如果我们想要基于文本行中的某个数值排序,又会怎么样呢?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [me@linuxbox ~]$ ls -l /usr/bin | head
    total 184736
    lrwxr-xr-x 1 root wheel 74 1 1 2020 2to3- -> ../../System/Library/Frameworks/Python.framework/Versions/2.7/bin/2to3-2.7
    lrwxr-xr-x 1 root wheel 74 1 1 2020 2to3-2.7 -> ../../System/Library/Frameworks/Python.framework/Versions/2.7/bin/2to3-2.7
    -rwxr-xr-x 1 root wheel 206512 1 1 2020 AssetCacheLocatorUtil
    -rwxr-xr-x 1 root wheel 264704 1 1 2020 AssetCacheManagerUtil
    -rwxr-xr-x 1 root wheel 204096 1 1 2020 AssetCacheTetheratorUtil
    -rwxr-xr-x 1 root wheel 137536 1 1 2020 BuildStrings
    -rwxr-xr-x 1 root wheel 137536 1 1 2020 CpMac
    -rwxr-xr-x 1 root wheel 137536 1 1 2020 DeRez
    -rwxr-xr-x 1 root wheel 137536 1 1 2020 GetFileInfo
  • 此刻,忽略 ls 命令自有的根据文件大小排序的功能,而用 sort 程序依据文件大小进行排序。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [me@linuxbox ~]$ ls -l /usr/bin | sort -nr -k 5 | head
    -rwxr-xr-x 1 root wheel 28133808 1 1 2020 php
    -rwxr-xr-x 1 root wheel 11266800 1 1 2020 fileproviderctl
    -r-xr-xr-x 1 root wheel 6455408 1 1 2020 parl5.30
    -r-xr-xr-x 1 root wheel 6077168 1 1 2020 parl5.28
    -rwxr-xr-x 1 root wheel 4672160 1 1 2020 kmutil
    -rwxr-xr-x 1 root wheel 4548272 1 1 2020 vim
    -rwxr-xr-x 1 root wheel 4303584 1 1 2020 ssh
    -rwxr-xr-x 1 root wheel 3781856 1 1 2020 ssh-keyscan
    -rwxr-xr-x 1 root wheel 3779728 1 1 2020 ssh-keygen
    -rwxr-xr-x 1 root wheel 3766736 1 1 2020 dig
  • sort 的许多用法都与表格数据处理有关,比如上面 ls 命令的输出结果。
  • 如果我们把数据库这个术语应用到上面的表格中,我们会说每一行就是一项记录,而每一个记录又包含多个字段,诸如文件属性、链接数、文件名、文件大小等。
  • sort 能够处理独立的字段,在数据库术语中,我们可以指定一个或多个关键字段作为排序的关键值。在上面的例子中,指定了 n 和 r 选项进行数值的逆序排序,并指定 -k 5 让 sort 程序使用第 5 个字段作为排序的关键值。
  • k 这个参数选项非常有趣,并且有很多特性,但是首先我们需要了解 sort 是如何定义字段的。让我们考虑一个非常简单的文本文件,它只有一行,并且该行只包含了该作者的名字。
    1
    William Shotts
  • 默认情况下,sort 程序会把该行看作有两个字段。第一个字段包含 William 字符串,第二个字段则是 Shotts 。这意味着空白字符 (空格和制表符) 用作字段之间的界定符,并且在排序时,这些界定符是包括在字段中的。
  • 重新回到前面的 ls 例子,我们可以看到 ls 的输出行包含 8 个字段,并且第五个字段指的是文件大小。
  1. 案例三
  • 让我们考虑用下面的文件。该文件包含从 2006 年 ~ 2008 年三款流行的 Linux 发行版的发行历史。文件每行都有 3 个字段:发行版本、版本号和 MM/DD/YYYY 格式的发行日期。使用文本编辑器 Vim , 输入此数据并将其保存为 distros.txt 。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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
    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
    SUSE 10.1 05/11/2006
    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
  • 接下来,我们试着对该文件进行排序并观察其输出结果。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    [me@linuxbox ~]$ 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 9 05/13/2008
    SUSE 10.1 05/11/2006
    SUSE 10.2 12/07/2006
    SUSE 10.3 10/04/2007
    SUSE 11.0 06/19/2008
    Ubuntu 6.06 06/01/2006
    Ubuntu 6.10 10/26/2006
    Ubuntu 7.04 04/19/2007
    Ubuntu 7.10 10/18/2007
    Ubuntu 8.04 04/24/2008
    Ubuntu 8.10 10/30/2008
  • Fedora 的版本号排序时却出现了问题。因为字符集中,字符 1 是在字符 5 前面的,所以导致版本 10 位于第一行而版本 9 却在最后。
  • 为了解决这个问题,我们必须依据多个键值进行排序。首先我们对第一个字段进行字母排序,然后再对第二个点进行数值排序。
  • sort 支持 -k 选项的多个实例,所以可以指定多个排序键值。事实上,一个键值可能是一个字段范围,如果没有指定任何范围,sort 会使用一个键值,该键值始于指定的字段,一直扩展到行尾。
  • 如下便是采用多键值进行排序的语法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    [me@linuxbox ~]$ sort --key=1,1 --key=2n distros.txt
    Fedora 5 03/20/2006
    Fedora 6 10/24/2006
    Fedora 7 05/31/2007
    Fedora 8 11/08/2007
    Fedora 9 05/13/2008
    Fedora 10 11/25/2008
    SUSE 10.1 05/11/2006
    SUSE 10.2 12/07/2006
    SUSE 10.3 10/04/2007
    SUSE 11.0 06/19/2008
    Ubuntu 6.06 06/01/2006
    Ubuntu 6.10 10/26/2006
    Ubuntu 7.04 04/19/2007
    Ubuntu 7.10 10/18/2007
    Ubuntu 8.04 04/24/2008
    Ubuntu 8.10 10/30/2008
  • 虽然为了清晰,我们使用了选项的长格式,但是 -k1、-k 2n 格式是等价的。
  • 在第一个 key 选项的实例中,指定了一个字段范围。因为我们只想对第一个字段排序,所以指定了 1,1 ,它意味着始于并且结束与第一个字段。在第二个实例中,我们指定了 2n ,表示第二个字段是排序的键值,并且按照数值排序。
  • 一个选项的字母可能包含在一个键值说明符的末尾,用来指定排序的种类。这些选项字母与 sort 命令的全局选项一样:b (忽略开头空白字符) 、n (数值排序) 、r (逆序排序) 等。
  • 以上列表的第三个字段包含的日期形式并不利于排序。在计算机中,日期通常以 YYYY-MM-DD 的形式存储,以方便按时间顺序排序,但该文本中的时间则是以美国形式 MM/DD/YYYY 存储。
  1. 案例四
  • 幸好 sort 提供了一种解决方法。sort 的 key 选项允许在字段中指定偏移,所以我们可以在字段内定义键值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    [me@linuxbox ~]$ 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
    Fedora 9 05/13/2008
    Ubuntu 8.04 04/24/2008
    Fedora 8 11/08/2007
    Ubuntu 7.10 10/18/2007
    SUSE 10.3 10/04/2007
    Fedora 7 05/31/2007
    Ubuntu 7.04 04/19/2007
    SUSE 10.2 12/07/2006
    Ubuntu 6.10 10/26/2006
    Fedora 6 10/24/2006
    Ubuntu 6.06 06/01/2006
    SUSE 10.1 05/11/2006
    Fedora 5 03/20/2006
  • 通过指定 -k 3.7 ,我们告诉 sort 从第三个字段的第 7 个字符开始排序,也就是从年份开始排序。同样,指定 -k 3.1 和 -k 3.4 选项以区分日其中的月和日,另外我们利用 n、r 选项进行逆序数值排序。同时添加的 b 选项用来删除日期字段中开头的空格 (行与行之间的空格字符数量不同,因此会影响排序结果) 。
  1. 案例五
  • 有些文件并不是使用制表符或空格符作为字段定界符,例如这个 /etc/passwd 文件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [me@linuxbox ~]$ 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 选项定义字段分隔符,根据 passwd 文件的第 7 个字段内容进行排序 (用户默认的 shell 环境) ,如下面命令行:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [me@linuxbox ~]$ sort -t ':' -k 7 /etc/passwd | head
    me:x:1001:1001:Myself,,,:/home/me:/bin/bash
    root:x:0:0:root:/root:/root:bin/bash
    dhcp:x:101:102::/nonexistent:/bin/bash
    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
  • 指定冒号字符作为字段分隔符,便实现了依据第 7 个字段进行排序的目的。

uniq - 停止或省略重复的行

  1. 基本介绍
  • 与 sort 相比,uniq 算是一个轻量级的命令。uniq 执行的是一个看似简单的任务,给定一个已排好序的文件 (包括标准输入) 后,uniq 会删除任何重复的行并将结果输出到标准输出中。
  • uniq 通常与 sort 结合使用以删除 sort 输出内容中重复的行。
  • 创建一个文本文件以验证此特性。
    1
    2
    3
    4
    5
    6
    7
    [me@linuxbox ~]$ cat > foo,txt
    a
    b
    c
    a
    b
    c
  • 此刻,如果运行 uniq,文件内容并没有很大改动,重复的行也并没有被删除。
    1
    2
    3
    4
    5
    6
    7
    [me@linuxbox ~]$ uniq foo.txt
    a
    b
    c
    a
    b
    c
  • uniq 只有对已经排好序的文本才有用。
    1
    2
    3
    4
    [me@linuxbox ~]$ sort foo.txt | uniq
    a
    b
    c
  • 这是因为 uniq 只能移除相邻的重复行。
  1. 常见的 uniq 选项
选项 功能描述
-c 输出重复行列表,并且在重复行前面加上其出现的次数
-d 只输出重复行,而不包括单独行
-fn 忽略每行前 n 个字段。字段之间以空格分开,这与 sort 类似,但与 sort 不同的是,uniq 没有提供参数设置可选择的字段分隔符
-i 行与行之间比较是忽略大小写
-sn 跳过 (忽略) 每行的前 n 个字符
-u 仅输出不重复的行,该选项是默认的
  1. uniq 补充说明
  • 使用 uniq 的 -c 选项,可输出文本中重复行的数量,示例如下:
    1
    2
    3
    4
    [me@linuxbox ~]$ sort foo.txt | uniq -c
    2 a
    2 b
    2 c

切片和切块

基本介绍

  1. 下面讨论的 3 个命令,他们的作用是剥离文本文件的列,并将它们以期待的方式重组。

cut - 删除文本行中的部分内容

  1. cut 基本介绍
  • cut 命令用于从文本行中提取一段文字并将其输出至标准输出。它可以接受多个文件和标准输入作为输入参数。
  1. cut 选择选项
选项 功能描述
-c char_list 从文本行中提取 char_list 定义的部分内容。此列表可能会包含一个或更多冒号分开的数值范围
-f field_list 从文本行中提取 field_list 定义的一个或多个字段。该列表可能会包含由冒号分隔的一个、多个字段或字段范围
-d delim_char 指定 -f 选项后,使用 delim_char 作为字段的分界符。默认时,字段必须以单个 Tab 制表符隔开
--complement 从文本中提取整行,除了那些由 -c 和/或 -f 指定的部分
  1. 案例一
  • 正如大家所看到的,cut 提取文本的方式非常不灵活。cut 适合从其他命令的输出结果中提取文本内容,而不是直接从输入文本中提取。
  • 我们可以判断下面的 distros.txt 文件是否达到了 cut 的提取要求,利用 cat 的 -A 选项,可以检查该文件是否用了 Tab 作为字段的分隔符的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    [me@linuxbox ~]$ 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^I^I10/24/2006$
    Fedora^I9^I^I05/13/2008$
    Ubuntu^I6.06^I06/01/2006$
    Ubuntu^I8.10^I10/30/2008$
    Fedora^I5^I03/20/2006$
  • 鉴于文件使用的是制表符而不是空格,所以可以使用 -f 选项提取字段内容。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    [me@linuxbox ~]$ 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
  • 由于 distros 文件是以制表符作为分界符的,所以用 cut 提取字段而不是字符在合适不过了。这是因为用 Tab 作为分界符的文件,一般每行不会包含相同的字符数,所以计算字符在行内的位置很苦难或是根本不可能。
  • 然而,在上例中,我们已经提取好了包含相同长度数据的字段,从而可以拿此字段作为字符提取实例。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    [me@linuxbox ~]$ 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 操作,便可以将字段中与年份相对应的第 7 至第 10 个字符提取出来,命令行中的符号 7-10 指范围。
  • cut 的 man 手册页包含了关于范围指定的完整描述。
  1. 案例二
  • 当处理字段时,我们可以指定非 Tab 字符作为分界符,如下所示例子演示的是从 /etc/passwd 文件中提取了每一行的第一个字段。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [me@linuxbox ~]$ cut -d ":" -f 1 /etc/passwd | head
    root
    daemon
    bin
    sys
    sync
    games
    man
    lp
    mail
    news
  • 此处我们还使用了 -d 选项指定冒号作为字段的分界符。

paste - 合并文本行

  1. paste 基本介绍
  • paste 命令是 cut 的逆操作,它不是文本文件中提取列信息,而是向文件中增加一个或更多文本列。
  • paste 命令读取多个文件并将每个文件中提取的字段结合为一个整体的标准输出流。
  • 与 cut 类似,paste 也可以接受多个文件输入参数和标准输入。
  1. paste 案例
  • 至于 paste 是如何运行的,我们可以通过下面的例子进行了解。如下所示,我们描述了一个使用 paste 对 distros.txt 文件按照发行版本的时间顺序而排序。
  • 首先,我们用前面所学的 sort 命令,得到一个依据日期进行排序的 distros 列表,并将输出结果存储于文件 distros-by-date.txt 中。
    1
    [me@linuxbox ~]$ sort -k 3.7nbr -k 3.1nbr -k 3.4nbr distros.txt > distros-by-date.txt
  • 接下来,我们使用 cut 提取文件中前两个字段 (distros 的名字和发行版本) ,并将结果存于文件 distros-versions.txt 中。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [me@linuxbox ~]$ linux cut -f 1,2 distros-by-date.txt > distros-versions.txt
    [me@linuxbox ~]$ 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
  • 最后一步准备工作就是提取发行版本日期,并将结果存于 distros-date.txt 文件中。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [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 将提取的日期这一列内容置于 distro 中操作系统名称和发行版本号这两列之前,于是便生成了一个按时间排列的发行版本列表。
  • 这一过程只是简单地使用 paste 命令来将各参数按指定顺序进行排列。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [me@linuxbox ~]$ 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 - 连接两文件中具有相同字段的行

  1. join 基本介绍
  • 从某种程度上来说, join 与 paste 类似,因为它也是向文件增加列信息,只是实现方式有些不同。
  • join 操作符通常与关联数据库联系在一起,它在关联数据库中把共享关键字段多个表格的数据组合成一个期望结果。
  • join 是一个基于共享关键字段将多个文件的数据拼接在一起的操作。
  1. join 案例
  • 作为演示,我们需要创建两个具有共有字段的文件,于是便可以使用 distros-by-date.txt 文件。
  • 利用 distros-by-date.txt 文件,可以生成两个附属文件,其中一个文件包含的内容是发行时间和发行版本名。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    [me@linuxbox ~]$ cut -f 1,1 distros-by-date.txt > distros-names.txt
    [me@linuxbox ~]$ paste distros-dates.txt distros-names.txt > distros-key-names.txt
    [me@linuxbox ~]$ 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
  • 第二个文件的内容则包含发行时间和发行版本号。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    [me@linuxbox ~]$ cut -f 2,2 distros-by-date.txt > distros-vernums.txt
    [me@linuxbox ~]$ paste distros-dates.txt distros-vernums.txt > distros-key-vernums.txt
    [me@linuxbox ~]$ 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 才能正常工作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [me@linuxbox ~]$ 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 会把空格当作输入字段的分界符,而以单个空格作为输出字段的分界符,当然我们也可以通过指定参数选项改变这一默认属性。我们可以查看 join 的 man 手册页获取更详细信息。

文本比较

基本介绍

  1. 比较文本文件的版本号通常很有用,尤其对系统管理者以及软件开发者而言。
  2. 例如,一个系统的管理者可能需要将已存在的配置文件与原先的配置文件相比较,以检查出系统的漏洞,与此类似,程序员也会经常需要查看程序代码所经历的变化。

comm - 逐行比较两个已排序文件

  1. comm 基本介绍
  • comm 命令一般用于文本文件之间的比较,显示两文件中相异的行以及相同的行。
  1. comm 案例
  • 作为演示,我们首先利用 cat 生成两个近乎一样的文本文件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [me@linuxbox ~]$ cat > file1.txt
    q
    b
    c
    d
    [me@linuxbox ~]$ cat > file2.txt
    b
    c
    d
    e
  • 接下来,便利用 comm 比较两个文件的差异。
    1
    2
    3
    4
    5
    6
    [me@linuxbox ~]$ comm file1.txt file2.txt
    a
    b
    c
    d
    e
  • 从以上结果可以看出,comm 输出三列的内容。第一列显示的是第一个文件中独有的行,第二列显示的是第二个参数文件中独有的行,第三列显示的则是两个文件所共有的行。
  • comm 还支持 -n 形式的参数选项,此处的 n 可以是 1,2 或者3 ,使用时,它表示省略第 n 列的内容。
  • 例如,如果只想显示两个文件的共同行,便可以省略第 1 列和第 2 列的内容,示例如下:
    1
    2
    3
    4
    [me@linuxbox ~]$ comm -12 file1.txt file2.txt
    b
    c
    d

diff - 逐行比较文件

  1. diff 基本介绍
  • 与 comm 命令类似, diff 用于检测文件之间的不同。然而,diff 比 comm 更复杂,它支持多种输出形式,并且具备一次性处理大文件集的能力。
  • diff 通常被软件开发者用于检查不同版本的源代码之间的差异,因为它能够递归检查源代码目录 (通常成为源树) 。
  • diff 的常见用法就是创建 diff 文件和补丁,它们可以为诸如 patch 这样的命令所用,从而实现一个版本的文件更新为另一个版本。
    1
    2
    3
    4
    5
    [me@linuxbox ~]$ diff file1.txt file2.txt
    1d0
    < a
    4a4
    > e
  • 默认形式中,每一组改动的前面都有一个以 “范围 执行操作 范围” 形式 (range operation range) 表示的改变操作命令,该命令会告诉程序对第一个文件的某个位置进行某种改变,便可实现与第二个文件内容一致。
  1. diff 改变命令
改变操作 功能描述
rlar2 将第二个文件中 r2 位置的行添加到第一个文件夹中的位置 r1 处。
rlcr2 用第二个文件 r2 处的行替代第一个文件 r1 处的行。
rldr 删除第一个文件 r1 处的行,并且删除的内容作为第二个文件 r2 行范围内容。
  1. diff 案例一
  • 此格式中,范围 range 一般是由冒号隔开的起始行和末尾行组成。虽然,此格式是默认的 (大多数情况下是为了兼容POSIX的同时向后兼容传统 UNIX 版本的 diff) ,但它并没有其他格式的应用广泛,上下文格式和统一格式才是比较普遍使用的格式。
  • 上下文格式的输出结果如下。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    [me@linuxbox ~]$ diff -c file1.txt file2.txt
    *** file1.txt 2021-01-14 09:53:35.000000000 +0800
    --- file2.txt 2021-01-14 09:53:55.000000000 +0800
    ***************
    *** 1,4 ****
    - a
    b
    c
    d
    --- 1,4 ----
    b
    c
    d
    + e
  • 该结果以两个文件的名字和时间信息开头,第一个文件用星号表示,第二个文件用破折号表示。
  • 输出结果的其余部分出现的星号和破折号则分别表示各自所代表的的文件。其他的内容便是两个文件之间的差异组,包括文本的默认行号。
  • 第一组差异,以 *** 1,4 **** 开头,表示第一个文件中的第1行至第 4 行;第二组便以 — 1,4 — 开头,表示第二个文件的第 1 行至第 4 行。
  1. diff 上下文格式差异标识符
标识符 含义
(无) 该行表示上下文文本。表示两个文件共有的行
- 缺少的行。指此行内容只在第一个文件中出现,第二个文件中则没有
+ 多余的行。此行内容只有第二个文件才有,第一个文件则没有
! 改变的行。两个版本的行内容都会显示出来,每一个都各自出现在差异组中相应的部分
  1. diff 案例二
  • 统一格式与上下文格式相似但是更简明,此格式用 -u 选项指定。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    [me@linuxbox ~]$ diff -u file1.txt file2.txt
    --- file1.txt 2021-01-14 09:53:35.000000000 +0800
    +++ file2.txt 2021-01-14 09:53:55.000000000 +0800
    @@ -1,4 +1,4 @@
    -a
    b
    c
    d
    +e
  • 上下文格式和统一格式之间最显著的区别就是,统一格式下没有重复的文本行,这使得统一格式的输出结果比上下文格式更精简。
  • 上例中,也输出了上下文格式中出现的文件时间信息,并且后面紧跟着 @@ -1,4 +1,4 @@ 字符串,它表示差异组描述的两个文件各自的行范围。
  • 此字符串之后便是行本身,其中包含默认的三行文本内容。
  1. diff 统一格式的差异标识符
字符 含义
(无) 两个文件共有的行
- 相对于第二个文件而言,第一个文件中没有的行
+ 第一个文件多余的行

patch - 对原文件进行 diff 操作

  1. patch 基本介绍
  • patch 命令用于更新文本文件。它利用 diff 命令的输出结果将较旧版本的文本升级成较新版本。
  • 下面看一个众所周知的例子:Linux 内核是由一个很大的、阻止松散的志愿者团队开发的,其源代码处于持续不断更新中。Linux 内核包含几百万行代码,所以相比而言,某位开发成员每次所做的修改是如此微不足道。
  • 因此,对于每位开发者来说,每对代码改动一次就得向其他开发者发送整个内核源代码树多么不切实际。
  • 事实上,一般只要发送 diff 补丁文件即可。 diff 补丁的内容是内核从比较旧版本转变为较新版本所经历的改变,接收者然后使用 patch 命令将这些改变应用于自身的源代码树。
  • diff/patch 有两个重要的优点。
    • 与源代码树的大小相比,diff 文件很小。
    • diff 文件非常简洁地描述了文件所做的改变,便于补丁的接收者快速对其进行评价。
  • 当然,diff/patch 不仅仅局限于源代码,它适用于任何文本文件。因此,它同样适用于配置文件以及其他文本文件。
  • 生成供 patch 使用的 diff 文件,GNU 文件系统建议采用如下方式使用 diff 。
    1
    diff -Naur old_file new_file > diff_file
  • 此处 old_file 和 new_file 既可以是单独文件也可以是包含文件的目录,使用 -r 参数选项则是为了进行递归目录数搜索。
  • 一旦创建了 diff 文件,便可以将其用于修补原文件 old_file ,从而升级为新文件 new_file 。
    1
    patch < diff_file
  1. patch 案例
  • 以前面的测试文件为例:
    1
    2
    3
    4
    5
    6
    7
    8
    [me@linuxbox ~]$ diff -Naur file1.txt file2.txt > patchfile.txt
    [me@linuxbox ~]$ patch < patchfile.txt
    patching file file1.txt
    [me@linuxbox ~]$ cat file1.txt
    b
    c
    d
    e
  • 本例中,我们创建了一个叫做 patch.file 的 diff 文件,然后使用 patch 命令进行修补。
  • 请注意该命令中,并没有给 patch 命令指定目标文件,因为 diff 文件 (统一形式) 已经在页面中包含了文件名,可以发现修补后,file1.txt 便与 file2.txt 一致了。
  • patch 有很多参数选项,而且有很多额外的工具命令可以分析并编辑这些补丁文件。

非交互式文本编辑

基本介绍

  1. 之前所描述的文本编辑大多数是交互式,也就是说只要手动移动鼠标然后输入需要进行的改变,然而,也可以用非交互的方式进行文本编辑。例如,可以用一个简单的命令一次性更改多个文件。

tr - 替换或删除字符

  1. tr 基本介绍
  • tr 是替换字符命令,可以将其看作一种基于字符的查找和替换操作。所谓替换,实际是指将字符从一个字母更换为其他字母。
  • 例如,将小写字母转变为大写字母。如下便是一个利用 tr 进行大小写字母替换的例子。
    1
    2
    [me@linux ~]$ echo "lowercase letters" | tr a-z A-Z
    LOWERCASE LETTERS
  • 该输出结果表明,tr 可对标准输入进行操作并且将结果以标准形式输出。
  • tr 有两个参数:等待转换的字符集和与之相对应的替换字符集。字符集的表示方法可以是下面三种方法中任选一种。
    • 枚举列表:例如,ABCDEFGHIJKLMNOPQRSTUVXYZ 。
    • 字符范围:例如,A-Z 。请注意,这种方法有时会与其他命令一样受限于同一个问题 (由于不同系统的排序顺序) ,因此使用时要小心。
    • POSIX字符类:例如,[:upper:] 。
  • 多数情况下,这两个字符集应该是同等长度:然而,第一个字符集比第二个字符长也是有可能的,如下便是一个将多个字符替换为单个字符的例子。
    1
    2
    [me@linux ~]$ echo "lowercase letters" | tr [:lower:] A
    AAAAAAAAA AAAAAAA
  • 除了替换,tr 还可以直接从输入流中删除字符。
  • 本章前篇,讨论了将 MS-DOS 类型的文本文件向 UNIX 类型转换的问题。要进行这样的转换,需要移除每行末尾的回车符,如此便可以用 tr 命令解决,如下所示:
    1
    tr -d '\r' < dos_file > unix_file
  • 这里的 dos_file 是待转换的文件,而 unix_file 则是转换的结果文件。此命令形式使用了转义字符 \r 代替回车符。
  • 如若想了解 tr 支持的所有转义符号和字符类,请查看 tr 的帮助手册。
    1
    [me@linux ~]$ tr --help
  • tr 还有另外一个奇妙的用法。使用 -s 选项,tr 可以挤兑 (删除) 重复出现的字符,示例如下:
    1
    2
    [me@linux ~]$ echo "aaabbbccc" | tr -s ab
    abccc
  • 本例字符串中含有重复字符,通过给 tr 指定 ab 字符集,便消除了该字符集重复的 a 和 b 字母,而字母 c 并没有改变,因为 tr 设定的字符集中并没有包含 c 。请注意重复的字符必须是毗邻的,否则该挤兑操作将不起作用。
    1
    2
    [me@linux ~]$ echo "abcabcabc" | tr -s ab
    abcabcabc

sed - 用于文本过滤和转换的流编辑器

  1. sed 基本介绍
  • sed 是 stream editor (流式编辑器) 的缩写,他可以对文本流、指定文件集或标准输出集进行文本编辑。
  • sed 功能非常强大,并且从某种程度上来说,他还是一个比较复杂的命令。
  • sed 的用法,总的来说,首先给定 sed 某个简单的编辑命令 (在文本行中) 或是包含多个命令的脚本文件名,然后 sed 便对文本流的内容执行给定的编辑命令。
  • 下面是一个非常简单的 sed 应用实例。
    1
    2
    [me@linuxbox ~]$ echo 'front' | sed 's/front/back/'
    back
  • 该例先利用 echo 生成了只包含一个单词的文本流,然后将该文本流交给 sed 处理,而 sed 则对文本流执行 s/front/back/ 指令,最后输出 back 作为运行结果。
  • 所以可以认为,本例中的 sed 与 vi 中的替代 (查看与替换) 命令相似。
  • sed 中的命令总是以单个字母开头。上例中,替换命令便是由字母 s 代替,其后紧跟替换字符,替换字符由作为分界符的斜线字符分开。
  • 分界符的选择是随意的,习惯上一般使用斜线,但是 sed 支持任意字符作为分界符。
  • sed 会默认紧跟在 sed 的命令之后的字符作为分界符,下面的命令行能得到相同的效果。
    1
    2
    [me@linuxbox ~]$ echo 'front' | sed 's_front_back_'
    back
  • 由于下划线是紧跟在命令字符 s 之后的,所以它便是分界符。由此可见,这种自动设定分界符的能力增强了命令行的可读性。
  • sed 中的多数命令允许在其添加一个地址,该地址用来指定输入流的哪一行被编辑。
  • 如果该地址省略了,便会默认对输入流的每一行执行该编辑命令。
  • 最简单的地址形式就是一个行号。例如,在上例中增加一个 1 ,如下所示:
    1
    2
    [me@linuxbox ~]$ echo 'front' | sed '1s/front/back/'
    back
  • 增加的 1 表示此替换操作只对输入流的第一行起作用。当然也可以指定其他行号,如下所示:
    1
    2
    [me@linuxbox ~]$ echo 'front' | sed '2s/front/back/'
    front
  • 结果显示此替换明并未执行,这是因为该输入流中并没有第二行。地址可以有多种方式表达。
  1. sed 的地址表达法
地址 功能说明
n n 是正整数表示行号
$ 最后一行
/regexp/ 用 POSIX 基本正则表达式描述的行。请注意,这里的正则表达式用的是斜线作为分界符,当然,也可以自己选择分界符,只要用 \cregexpc 选项指定即可,这里用 c 就是用于取代斜杠的分界符
addr1, addr2 行范围,表示从 addr1 至 addr2 的所有行。地址可以是上面所述形式的任何一种
first-step 代表行号从 first 行开始,以 step 为间隔的所有行。例如,1-2 是指所有奇数行,而 5-5 是指第 5 行和以及随后的所有是 5 的倍数的行
addr1,+n addrc1 行及其之后的 n 行
addr! 出了 addrc 行之外的所有行,addr 可以用上面的任何一种形式表达
  1. sed 案例一
  • 接下来,我们会用本章前面所使用的 distros.txt 文件演示多种不同形式的地址表达。首先,用行号范围的表达式如下所示:
    1
    2
    3
    4
    5
    6
    [me@linuxbox ~]$ 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
  • 此例显示了 distros.txt 文件中第1行到第5行的内容,利用 p 命令输出指定匹配行的内容,从而完成上述操作。然而,想要得到正确结果,就必须添加选项 -n (不会自动打印选项) 以防 sed 会默认输出每一行的内容。
  • 下面尝试使用正则表达式。
    1
    2
    3
    4
    5
    [me@linuxbox ~]$ 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
  • 此处用的是以斜杠隔开的正则表达式 /SUSE/ ,查找包含 SUSE 字符串的文本行,该用法与 grep 的用法类似。
  • 最后尝试在地址前添加表示否定意义的感叹号 ! ,用法如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    [me@linuxbox ~]$ 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 的两个编辑命令 - s 和 p 。
  1. sed 基本编辑指令
命令 功能描述
= 输出当前行号
a 在当前行后附加文本
d 删除当前行
i 在当前行前输入文本
p 打印当前行。默认情况下, sed 会输出每一行并且只编辑文件内那些匹配指定地址的行。当指定 -n 选项是,默认操作会被覆盖。
q 退出 sed 不再处理其他行,如果没有指定 -n 选项,就会输出当前行
Q 直接退出 sed 不再处理行
s/regex/replacement 将 regexp 的内容替换为 replacement 代表的内容。 replacement 可能会包含特殊字符 & ,它代表的其实就是 regexp 所表示的内容。除此之外,replacement 也可能包含 \1 到 \9 的序列,他们代表的是 regexp 中相应位置的描述内容。跟在 replacement 后面的反斜杠,可以指定一个可选择的标志以修改 s 命令的行为。
y/set1/set2 将字符集 set1 转换为字符集 set2 。请注意,与 tr 不同, sed 要求这两个字符集等长。
  1. sed 案例二
  • s 命令是目前为止使用最普遍的编辑命令。接下来,我们通过编辑 distros.txt 文件来演示其强大的功能的一小部分。
  • 之前,讨论过 distros.txt 文件中的时间字段并不是以计算机友好的形式存储,因为此时间形式是 MM/DDD/YYYY ,YYYY-MM-DD 这样的形式会方便很多 (更容易排序) 。
  • 但如果手动更改文件,不仅浪费时间而且容易出错,而 sed 可以一步完成这样的操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    [me@linuxbox ~]$ 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 命令有其基本结构,如下所示:
    1
    sed 's/regexp/replacement' distros.txt
  • 接下来我们需要理解将日期分隔开来的正则表达式。由于它是以 MM/DD/YYYY 的形式存在,并且出现在行末尾,是以使用如下的表达式。
    1
    [0-9]{2}/[0-9]{2}/[0-9]{4}$
  • 该表达式的匹配格式:两位数字、斜杠、两位数字、斜杠、4 位数字以及行尾标志。所以这代表了 regexp 表达式的形式,但是怎么处理 replacement 的表达式 ?
  • 解决这一问题,我们必须引进正则表达式的一个新特性,该特性一般存在那些使用 BRE 的应用中。
  • 此特性称为回参考,并且工作方式类似于此,即如果 replacement 中出现转义 \n 转义字符,并且这里的 n 是 1-9 之间的任意数字,那么此转义字符就是指前面正则表达式中与之相对应的子表达式。
    1
    ([0-9]{2})/([0-9]{2})/([0-9]{4})$
  • 现在,我们便有了三个子表达式。第一个的内容是月份,第二个内容是具体的日,第三个则是之年份。于是便可用如下命令行构建替换字符。
    1
    \3-\1-\2
  • 该表达式表示的书信如下:年份、斜杠、月份、斜杠和具体的日。
  • 于是,整个命令行如下:
    1
    sed 's/([0-9]{2})/([0-9]{2})/([0-9]{4})$/\3-\1-\2' distros.txt
  • 但是仍有两个遗留问题
    • 当 sed 试图编译 s 命令时,正则表达式中多余的斜杠会令 sed 混淆。
    • sed 默认情况下只接受基本正则表达式,所有正则表达式中的部分元字符会被当成文字字符。我们可以易用反斜杠来避免这些冒犯字符,从而一次性解决这两个问题。
  • 这样便大功告成了。
    1
    sed 's/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/' distros.txt
  1. sed 案例三
  • s 命令的另外一个特点,就是替换字符串后面可以紧跟可选择标志符。其中最重要的标志符就是 g ,该标志告诉 sed 对每行的所有匹配项进行替换操作,而不是默认的只替换第一个匹配项。
    1
    2
    [me@linuxbox ~]$ echo 'aaabbbccc' | sed 's/b/B/'
    aaaBbbccc
  • 我们可以看到执行了替换操作,但只是对第一个字母 b 有效,剩下的字母并没有改变。通过增加 g 标志符,便可以对所有的 b 进行替换操作。
    1
    2
    [me@linuxbox ~]$ echo 'aaabbbccc' | sed 's/b/B/g'
    aaaBBBccc
  • 到目前为止,我们只使用了命令行方式向 sed 传送操作命令,其实也可以用 -f 选项建立更复杂的命令行脚本。
  • 作为演示实例,我们运用 distros.txt 文件结合 sed 创建一个报告。该报告的构成有顶端标题、修改时间和大写字母组成的所有发行版本名。
  • 进行这些操作之前,我们需要编写一个脚本文件所以启动文本编辑器并输入如下内容。
    1
    2
    3
    4
    5
    6
    7
    8
    # sed script to produce Linux distributions report

    1 i\
    \
    Linux Distributions Report\

    s/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/
    y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/
  • 将此 sed 脚本保存为 distros.sed distros.txt,并且照如下方式运行。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    [me@linuxbox ~]$ sed -f distros.sed distros.txt

    Linux Distributions Report

    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
  • 让我们再次查看脚本,并且使用 cat 将行号都标注出来。
    1
    2
    3
    4
    5
    6
    7
    8
    1 # sed script to produce Linux distributions report
    2
    3 1 i\
    4 \
    5 Linux Distributions Report\
    6
    7 s/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/
    8 y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/
  • 第一行只是一个声明,与 Linux 系统中的许多配置文件和编程语言一样,生命一般都是以 # 符号开头。剩下的则是一些人类可理解的文本。
  • 声明可以放于脚本文件的任何位置 (只要不在命令行中) ,并且对于任何一个需要验或是维护该脚本的人都很有用。
  • 第二行是空白行。与声明一样,空白行也是为了增加可读性。
  • 多数 sed 命令都支持行地址,这些行地址用于指定哪些输入行执行指定操作。行地址可以用简单的符号来描述,也可以用行号范围以及 $ 表示,$ 是一个表示文本最后一行的特殊符号。
  • 第 3 ~ 6 行包含的则是要插入文本中第一行的内容。i 命令后面紧跟转义回车符,转义回车符由反斜杠和回车组成,亦称为行继续符。
  • 此种先反斜杠后回车符的顺序,可以用于包括 shell 脚本在内的很多场合,可确保文本流中嵌入回车符但不会告诉编辑器 (在这个例子中) 已经到行末尾。
  • i 命令、a 命令 (追加文本) 、以及 c 命令 (替换文本) 能作用于多个文件行,只要除了最后一行的每行都以行继续符结尾。
  • 该脚本文件的第 6 行确实是所输入文本的末尾行,并且标注 i 命令结尾的符号是一个简单的回车符而不再是行继续符。
  • 行继续符是由反斜杠后紧跟回车符组成,两者之间不容许有任何空格。
  • 第 7 行是查找和替换命令。由于该命令前并未指定地址,所以该命令将对输入流的每一行进行操作。
  • 第 8 行则实现小写字母向大写字母的转变。注意,与 tr 不同,sed 总的 y 命令并不支持字符范围 (例如 [a-z] ) ,也不支持 POSIX 字符类。同样,由于 y 命令前并没有指定地址,所以将对输入流的每一行执行操作。

aspell - 交互式拼写检查工具

  1. aspell 基本介绍
  • aspell 是交互式的拼写检查工具,aspell 命令继承的是早期的ispell 命令,并且多数情况下,可以直接去带 ispell 。
  • 虽然 aspell 命令通常为那些需要进行拼写检查的程序所有,但它同样可以作为一个独立于命令行的工具发挥其效用。
  • aspell 可以智能地检查不同类型文本文件的错误,包括 HTML 文件、C/C++程序、email 消息以及其他专业的文本文件。
  1. aspell 案例
  • 检查一篇简单散文的拼写错误,可以用如下方式使用 aspell 。
    1
    aspell check textfile
  • 此处的 test.file 是要进行检查的文件名。作为实例进行讲解,下面创建了一个简单的 foo.txt 文本文件,它包含一些故意的拼写错误。
    1
    2
    [me@linuxbox ~]$ cat > foo.txt
    The quick brown fox jimped over the laxy dog.
  • 接下来使用 aspell 检查文件中的拼写错误。
    1
    [me@linuxbox ~]$ aspell check foo.txt
  • 由于 aspell 在检查模式下是与用户交互的,显示内容的顶部,被怀疑错误的字符是以高亮的形式显示的。中间部分,有 10 个标号从 0 ~ 9 的替换拼写建议以及其他可能的动作选项。最后,末端有一个提示框用户进行操作选择。
  • 假定我们输入 1 ,则 aspell 会将错误的单词用 jumped 取代并且继续处理下一个错误单词 laxy 。如果选择替代单词 lazy ,aspell 便执行此替换操作然后终止程序。
  • aspell 命令检查结束后,可以再次查看文件,会发现那些拼写错误的单词已经改正过来,如下所示。
    1
    2
    [me@linuxbox ~]$ cat foo.txt
    The quick brown fox jumped over the lazy dog.
  • 除非额外指定了命令行选项 --done-backup ,不然 aspell 将会创建一个包含原文本内容的备份文件,此备份文件文件名则由原文件名加上后缀 .bak 组成。
  • sed 其实有更强大的编辑功能,恢复 foo.txt 文件中原有的拼写错误以便再次利用:
    1
    [me@linuxbox ~]$ sed -i 's/lazy/laxy/;s/jumped/jimped/' foo.txt
  • sed 选项 -i 告诉 sed 原地编辑文件,这表示 sed 不会将编辑结果送至标准输出,而是将改变后的文本重新写入文件中。同样可以看出,一个命令行中可以输入多个编辑命令,只要用分号将它们隔开即可。

参考文章

  • 转载:Linux 命令行大全