UNIX 系统上的文本操作简介



UNIX 系统上的文本操作简介

使用标准实用工具

本文将介绍 UNIX 平台上的文本操作,概述一些在大多数基于 UNIX 的发行版上广为使用并且已安装的标准实用工具。很多时候,因为有更现代的文本处理器(例如 Perl、Python 和 Ruby),所以这些标准实用工具常常被忽略,而系统上并非总是已经安装这些现代的文本处理器。本文将介绍性地评论这些工具,帮助想了解 UNIX 或 Linux 的新人或者想重新捡起自己遗忘的知识的老手了解这些工具。

简介

UNIX 的基本哲学之一就是创建只做一件事并将这一件事做好的程序(或进程)。这一哲学要求认真考虑接口以及结合这些更小(有望更简单)流程的方法,以产生有用的结果。文本数据通常是在这些接口之间流动。多年以来,越来越高级的文本处理工具和语言已经开发出来。从语言上来讲,早期专门处理文本的语言有 perl,后来又出现了 python 和 ruby。虽然这些语言以及其他语言都是非常强大的文本处理器,但这些工具并不是一直可行的,在生产环境下尤其如此。本文将介绍一些基本的 UNIX 文本处理命令,这些命令既可单独使用也可结合使用,可用来解决需要更新的语言才能解决的问题。对许多人来说,与长篇大论的解释相比,实例能够提供更多的信息。请注意,由于有多种可用的 UNIX 和 UNIX 类系统,因此不同实现之间的命令标志、程序行为和输出结果会略有不同。

使用 cat

cat 命令是最基本的命令之一。这个命令用来创建、追加、显示以及合并文本文件。

我们可以使用 cat 命令创建文件,方法是:使用 ‘>’ 将标准输入 (stdin) 重定向到文件。使用 ‘>’ 操作符会缩短指定输出文件的内容。在此之后输入的文本会重定向到 ‘>’ 操作符右侧指定的文件。control-d 表示文件结束,将控制权返回给 shell。

使用 cat 创建文件的示例
$ cat > grocery.list
apples
bananas
plums

$

使用 ‘>>’ 操作符将标准输入追加到现有文件。

使用 cat 追加文件的示例
$ cat >> grocery.list
carrots

使用 cat 命令不加标志,可查看 grocery.list 文件的内容。请注意文件的内容如何包含来自重定向的输入以及追加操作符的示例。

使用无标志 cat 的示例
$ cat grocery.list
apples
bananas
plums
carrots

可以使用 cat 命令对文件行进行编号。

使用 cat 计算行的示例:
$ cat -n grocery.list
     1  apples
     2  bananas
     3  plums
     4  carrots

使用 nl

nl 过滤器会从 stdin 或指定文件读取行。输出则会写入 stdout 并重定向到文件,或传到另一个进程中。nl 的行为是由不同命令行选项控制的。

在默认情况下,nl 会计算行数,与 cat -n 的功能类似。

nl 默认用法示例:
$nl grocery.list
     1  apples
     2  bananas
     3  plums
     4  carrots

使用 -b 标志指定要进行编号的行。此标志将参数作为 “类型”。该类型告诉 nl 需要给哪些行编号,使用 ‘a’ 给所有行编号,‘t’ 告诉 nl 不对空行和只有空格的行进行编号,‘n’ 指定不编号行。在示例中显示针对模式的类型 ‘p’。nl 给正则表达式模式指定的行编号,在本用例中,是以字母 ‘a’ 或 ‘b’ 开始的行。

使用 nl 对符合正则表达式的行进行编号的示例
$ nl -b p^[ba]grocery.list
     1  apples
     2  bananas
       plums
       carrots

在默认情况下,nl 行号和文本之间使用制表符进行分隔。使用 -s 指定其他分隔符,例如 ‘=’ 号。

使用 nl 指定其他分隔符的示例
$nl -s= grocery.list
     1=apples
     2=bananas
     3=plums
     4=carrots

使用 wc

wc (wordcount) 命令计算指定文件或来自 stdin 的行数、单词数(由空格分隔)和字符数。

wc 用法示例
$wc grocery.list
       4       4      29 grocery.list
$wc -l grocery.list
       4 grocery.list
$wc -w grocery.list
       4 grocery.list
$wc -c grocery.list
      29 grocery.list

使用 grep

grep 命令在指定文件或 stdin 中搜索与给定表达式相匹配的模式。grep 的输出由多个选项标志控制。

为了演示,这里新创建了一个文件,与 grocery.list 配合使用。

$cat grocery.list2
Apple Sauce
wild rice
black beans
kidney beans
dry apples
grep 基本用法示例
$ grep apple grocery.list grocery.list2
grocery.list:apples
grocery.list2:dry apples

grep 拥有相当可观的选项标志。下面的示例将演示其中几个选项的用法。

要显示文件名(处理多个文件的情况下)以及发现模式匹配的行数,在本用例中使用的模式是计算每个文件中出现单词 ‘apple’ 的行数。

grep 示例:计算文件中匹配数
$ grep -c apple grocery.list grocery.list2
grocery.list:1
grocery.list2:1

在搜索多个文件时,可以使用 -h 选项取消在输出中显示文件名。

grep 示例:取消在输出中显示文件名
$ grep -h apple grocery.list grocery.list2
apples
dry apples

在很多情况下,需要进行不区分大小写的搜索。grep 命令的 -i 选项可以在搜索时忽略大小写。

grep 示例:不区分大小写
$ grep -i apple grocery.list grocery.list2

grocery.list:apples
grocery.list2:Apple Sauce
grocery.list2:dry apples

有些时候,只需要输出文件名,不需要输出模式匹配的行。grep 提供 -l 选项,用于只输出包含匹配模式行的文件名。

grep 示例:只输出文件名
$ grep -l carrot grocery.list grocery.list2
grocery.list

行号可以显示在输出中。使用 -n 选项来包含行号。

grep 示例:包含行号
$ grep -n carrot grocery.list grocery.list2
grocery.list:4:carrots

有时期望输出与模式不匹配的行。这时就要使用 -v 选项。

grep 示例:输出不匹配行
$ grep -v beans grocery.list2
Apple Sauce
wild rice
dry apples

有时,需要的模式是一个单词,两边被空格或其他字符(例如连字符或括号)包围。grep 的多数版本都提供了 -w 选项,能方便地编写此类模式的搜索。

grep 示例:单词匹配
$ grep -w apples grocery.list grocery.list2
grocery.list:apples
grocery.list2:dry apples

流、管道、重定向、tee 和 here docs

在 UNIX 中,一个终端默认包含三个流,一个输入流,两个基于输出的流。输入流称为 stdin,通常映射到键盘(也可使用其他输入设备,或从其他进程中传入)。标准输出流称为 stdout,并通常输出到终端上,输出也可供其他进程使用(就像 stdin 一样)。另一个输出流 stderr 主要用作状态报告,通常输出到终端,如 stdout。这三个流都有各自的文件描述符,每一个流都可以从其他流中传入或重定向,即使是它们全都连接到终端。每个流的文件描述符分别是:

  • stdin = 0
  • stdout = 1
  • stderr = 2

这三个流可以传送或重定向到文件或其他进程。这个构造通常称为 “构建一个管道”。例如,程序员可能想将 stdout 流和 stderr 流合并,然后在终端上显示它们,再将结果保存到文件中以便检查版本问题。使用 2>&1stderr 流以及文件描述符 2 会被重定向到 &1(指向 stdout 流)。这样就能有效地将 stderr 合并到 stdout。使用 ‘|’ 符号表示管道。管道连接左侧进程 (make) 的 stdout 和右侧进程 (tee) 的 stdintee 命令会复制(合并)将数据发送到终端以及文件的 stdout 流,在本示例中,称为 build.log。

合并和拆分标准流的示例
$ make –f build_example.mk 2>&1 | tee build.log

另一个重定向示例,使用 cat 命令和一些流重定向制作文本文件副本。

使用重定向制作备份文件的示例
$ cat < grocery.list > grocery.list.bak

前面使用 nl 命令为在 stdout 中显示的文件加行号。管道可用于将 stdout 流(来自 cat grocery.list)发送到另外一个进程,在本用例中,是 nl 命令。

简单管道传送到 nl 的示例
$ cat grocery.list | nl
     1  apples
     2  bananas
     3  plums
     4  carrots

先前显示的另一个示例是针对模式执行不区分大小写的搜索。这也可以使用重定向来实现(在这本用例中为来自 stdin 或使用管道,与上述简单管道示例相似)。

grep 示例:通过 stdin 重定向和管道
$ grep -i apple < grocery.list2
Apple Sauce
dry apples
$cat grocery.list2 | grep -i apple
Apple Sauce
dry apples

在某些情况下,要将文本块重定向到某个命令或文件中作为脚本的一部分。实现此操作的机制是使用 ‘here document’(或 ‘here-doc’)。要将 here-doc 嵌入到脚本,需要使用 ‘<<’ 操作符重定向下列文本,直到到达文件结束分隔符为止。在 << 操作符后指定分隔符。

示例:命令行上的基本 here-doc
$ cat << EOF
> oranges
> mangos
> pinapples
> EOF
oranges
mangos
pinapples

可将此输出重定向到文件,在本示例中,分隔符 ‘EOF’ 改为 ‘!’。然后使用 tr 命令(稍后说明)将 here-doc 中的字母全部变成大写。

示例:将基本 here-doc 重定向到文件
cat << ! > grocery.list3
oranges
mangos
pinapples
!
$ cat grocery.list3
oranges
mangos
pinapples
$tr [:lower:] [:upper:] << !
> onions
> !
ONIONS

使用 head 和 tail

headtail 命令用来查看文件的顶部 (head) 或底部 (tail)。要显示文件顶部两行和底部两行,请分别使用这两个命令加 -n 选项标志。相同地,-c 选项显示文件中的前几个或最后几个字符。

示例:head 和 tail 命令的基本用法
$ head -n2 grocery.list
apples
bananas
$ tail -n2 grocery.list
plums
carrots
$ head -c12 grocery.list
apples
banan
$ tail -c12 grocery.list
ums
carrots

tail 命令的常见用途就是观察日志文件或者正在运行的进程输出,查看其中是否有问题,或者关注进程何时结束。-f (tail –f) 选项使 tail 持续观察流,即使是到达文件结束标记也继续观察,并在流包含更多数据时,持续显示输出。


使用 tr

tr 命令用来转换来自 stdin 的字符,在 stdout 中显示。tr 一般接受两个字符集合,用第二个集合中的字符替换第一个集合中的字符。有许多预定义的字符类(集合)可供 tr 使用,还有其他命令可用。

这些预定义的字符类是:

  • alnum:字母数字字符
  • alpha:字母字符
  • blank:空白字符
  • cntrl:控制字符
  • digit:数字字符
  • graph:图形字符
  • lower:小写字母字符
  • print:可打印字符
  • punct:标点字符
  • space:空间字符
  • upper:大写字符
  • xdigit:16 进制字符

tr 命令够将字符串中的小写字符转换成大写。

tr 示例:将字符串转换成大写
$ echo "Who is the standard text editor?" |tr [:lower:] [:upper:]
WHO IS THE STANDARD TEXT EDITOR?

tr 可以用来从字符串中删除指定字符。

tr 示例:从字符串中删除指定字符
$ echo 'ed, of course!' |tr -d aeiou
d, f crs!

使用 tr 将字符串中任何指定字符转换成空格。在序列中遇到多个指定字符时,它们会转换成一个空格。

-s 选项标志的行为在不同系统中表现不同。

tr 示例:将字符转变成空格
$ echo 'The ed utility is the standard text editor.' |tr -s astu ' '
The ed ili y i he nd rd ex edi or.

-s 选项标志可以用来取消字符串中多余的空格。

$ echo 'extra     spaces – 5’ | tr -s [:blank:]
extra spaces - 5
$ echo ‘extra		tabs – 2’ | tr -s [:blank:]
extra   tabs – 2

在基于 UNIX 和 Windows 系统之间转换文件时发生的常见问题就是行分隔符 (line delimiters)。在 UNIX 系统中,行分隔符为一个换行符,而在 Windows 系统中,则是用两个字符(即一个回车符和一个换行符)。使用 tr 配合某种重定向,可以解决这个格式问题。

tr 示例:消除回车符
$ tr -d '\r' < dosfile.txt > unixfile.txt

使用 colrm

使用 colrm,可以从流中剪切出文本列。在第一个示例中,使用 colrm 命令从管道的每行文本中剪切出第 4 列到行尾。然后,将同一个文件发送至 colrm,以删除第 4 列到第 5 列。

使用 colrm 删除列的示例
$ cat grocery.list |colrm 4
app
ban
plu
car
$ cat grocery.list |colrm 4 5
apps
banas
plu
carts

使用 expand 和 unexpand

expand 命令将制表符变成空格,而 unexpand 将空格变成制表符。这两个命令都接受 stdin 输入以及命令行指定文件的输入。使用 -t 选项可以设置一个或多个制表符停止位。

expand 和 unexpand 示例:
$ cat grocery.list|head -2|nl|nl
     1       1  apples
     2       2  bananas
$ cat grocery.list|head -2|nl|nl|expand -t 5
     1         1    apples
     2         2    bananas
$ cat grocery.list|head -2|nl|nl|expand -t 5,20
     1                   1 apples
     2                   2 bananas
$ cat grocery.list|head -2|nl|nl|expand -t 5,20|unexpand -t 1,5
                1                   1 apples
                2                   2 bananas

使用 comm、cmp 和 diff

为了演示这些命令,要新建两个文件。

新建演示文件:
cat << EOF > dummy_file1.dat
011 IBM 174.99
012 INTC 22.69
013 SAP 59.37
014 VMW 102.92
EOF
cat << EOF > dummy_file2.dat
011  IBM 174.99
012 INTC 22.78
013 SAP 59.37
014 vmw 102.92
EOF

diff 命令会对两个文件进行比较,报告两者之间的不同之处。diff 可接受多种选项标志。在下面示例中,首先显示默认的 diff,然后是使用 -w 选项的 diff 忽略空格,并以使用 -i 选项标志在比较中忽略大小写区别而结束。

diff 命令的示例
$ diff dummy_file1.dat dummy_file2.dat
1,2c1,2
< 011 IBM 174.99
< 012 INTC 22.69
---
> 011  IBM 174.99
> 012 INTC 22.78
4c4
< 014 VMW 102.92
---
> 014 vmw 102.92

$ diff -w dummy_file1.dat dummy_file2.dat
2c2
< 012 INTC 22.69
---
> 012 INTC 22.78
4c4
< 014 VMW 102.92
---
> 014 vmw 102.92

$ diff -i dummy_file1.dat dummy_file2.dat
1,2c1,2
< 011 IBM 174.99
< 012 INTC 22.69
---
> 011  IBM 174.99
> 012 INTC 22.78

comm 命令会对两个文件进行比较,但比较的方式与 diff 差别很大。comm 产生三列输出,仅出现在第 1 个文件(第 1 列)的行,仅出现在第 2 个文件(第 2 列)的行,两个文件中都有的常见行(第 3 列)。可使用选项标志来取消输出列。此命令可能在取消第 1 列和第 2 列时最有用,只显示两个文件中常见的行,如下所示。

comm 命令示例
$ comm dummy_file1.dat dummy_file2.dat
        011  IBM 174.99
011 IBM 174.99
012 INTC 22.69
        012 INTC 22.78
                013 SAP 59.37
014 VMW 102.92
        014 vmw 102.92

$ comm -12 dummy_file1.dat dummy_file2.dat
013 SAP 59.37

cmp 命令也会对这两个文件进行比较。但是,与 commdiff 不同,cmp 命令(默认)报告这两个文件刚开始不同的字节和行号。

cmp 命令示例
$ cmp dummy_file1.dat dummy_file2.dat
dummy_file1.dat dummy_file2.dat differ: char 5, line 1

使用 fold

使用 fold 命令可以将行拆分为指定的宽度。这个命令最初是用来对无法支持换行的定宽输出设备进行文本格式化。-w 选项标志允许使用指定行宽,而不是只使用默认的 80 列。

使用 fold 示例
$ fold -w8 dummy_file1.dat
011 IBM
174.99
012 INTC
 22.69
013 SAP
59.37
014 VMW
102.92

使用 paste

paste 命令用来合并文件,将每个文件的记录逐一合并。利用重定向,可以通过将一个文件中的每个记录与另一个文件的记录合并,来新建文件。

新建演示文件:
cat << EOF > dummy1.txt
IBM
INTC
SAP
VMW
EOF
cat << EOF > dummy2.txt
174.99
22.69
59.37
102.92
EOF
paste 示例:来自多文件的行
$ paste dummy1.txt dummy2.txt grocery.list
IBM     174.99  apples
INTC    22.69   bananas
SAP     59.37   plums
VMW     102.92  carrots

-s 选项标志用来一次处理多个文件(连续地),而不是并行处理。请注意,下面的列与上面示例中的行合并。

paste 示例 2:来自多文件的行
$ paste -s dummy1.txt dummy2.txt grocery.list
IBM     INTC    SAP     VMW
174.99  22.69   59.37   102.92
apples  bananas plums   carrots

如果只指定一个文件,或者 paste 正处理 stdin,输入会默认显示在一个列中。使用 -s 选项标志,输出会显示在一个行中。由于输出缩减到一行,所以使用分隔符来分隔返回的域(默认的分隔符是制表符)。在本示例中,使用 find 命令寻找 64 位库所在的目录,然后构建一个合适的路径,附加到变量 $LD_LIBRARY_PATH 中。

paste 示例:使用分隔符
$ find /usr -name lib64 -type d|paste -s -d:
/usr/lib/qt3/lib64:/usr/lib/debug/usr/lib64:/usr/X11R6/lib/X11/locale/lib64:/usr/X11R6/
lib64:/usr/lib64:/usr/local/ibm/gsk7_64/lib64:/usr/local/lib64

$ paste -d, dummy1.txt dummy2.txt
IBM,174.99
INTC,22.69
SAP,59.37
VMW,102.92

使用 bc

在 Shell 上进行算术计算的简易方法是使用 bc(“basic calculator” 或 “bench calculator”)。有些 shell 自带了算术计算功能,有些则依靠 expr 对表达式进行运算。使用 bc,计算可以在不同的 Shell 和 UNIX 系统间移植,只要注意不同厂商的扩展即可。

bc 示例:简单计算
$ echo 2+3|bc
5

$ echo 3*3+2|bc
11

$ VAR1=$(echo 2^8|bc)
$ echo $VAR1
256

$ echo "(1+1)^8"|bc
256

bc 不仅可以执行这些简单计算。它是一个解释器,有自己内部的和用户自定义的函数、语法和流程控制,就像编程语言一样。在默认情况下,bc 在小数点右侧不包含任何数字。要提高输出的精度,需要使用特殊的 scale 变量。如示例所示,bc 支持大数字,可实现更长的精度。使用 obaseibase 可以控制输入和输出数字的转换基础。在下面的示例中:

  • obase 改变默认的输入基(10 进制),将结果转变成 16 进制
  • 对于 2 的平方根,scale 指定了小数点右侧的数字个数
  • 求 2 的 128 次方演示了对大数字的支持
  • 调用内部函数 sqrt() 计算 2 的平方根
  • ksh 中,计算和输出百分比
bc 示例:更多计算
$ echo "obase=16; 2^8-1"|bc
FF

$ echo "99/70"|bc
1

$ echo "scale=20; 99/70"|bc
1.41428571428571428571

$ echo "scale=20;sqrt(2)"|bc
1.41421356237309504880

$ echo 2^128|bc
340282366920938463463374607431768211456

$ printf "Percentage: %2.2f%%\n" $(echo .9963*100|bc)
Percentage: 99.63%

bc 的手册页面中有详细说明,并有相关示例。


使用 split

split 命令的一大作途就是将大型数据文件分解成小的文件以方便处理。在本示例中,BigFile.dat 经 wc 命令统计有 165782 行。-l 选项标志规定了 split 为每个输出文件生成的最大行数。split 支持为输出文件名指定前缀,例如下面的示例指定 BigFile_ 为前缀。其他选项支持后缀控制,在 BSD 系统上的 -p 选项标志支持按正则表达式进行拆分,就像 csplit(上下文拆分)命令一样。更多信息请参阅手册页面。

split 示例:
$ wc BigFile.dat
165782  973580 42557440 BigFile.dat

$ split -l 15000 BigFile.dat BigFile_

$ wc BigFile*
  165782  973580 42557440 BigFile.dat
   15000   87835 3816746 BigFile_aa
   15000   88483 3837494 BigFile_ab
   15000   89071 3871589 BigFile_ac
   15000   88563 3877480 BigFile_ad
   15000   88229 3855486 BigFile_ae
    7514   43817 1908914 BigFile_af
  248296 1459578 63725149 total

使用 cut

cut 命令用来 "裁剪" 文件中以列为基础小节或从 stdin 传送而来的数据。它可按字节 (-b)、字符 (-c) 和列表指定的域 (-f) 进行修剪。使用逗号分隔列表和连字符指定域列表或字节/字符位置。如果只需要输出一个位置或域,则直接指定位置或域即可。可以使用连字符指定一系列域,例如 1-3 输出 1-3 域(或位置),-2 从行开始前两个域(或字节/字符)开始输出,3- 则让 cut 从域(或位置)3 开始输出到行尾。多个域之间以逗号分隔。其他有用的标志有:-d 指定域分隔符,-s 取消没有分隔符的行。

cut 示例
$ cat << EOF > dummy_cut.dat
# this is a data file
ID,Name,Score
13BA,John Smith,100
24BC,Mary Jones,95
34BR,Larry Jones,94
36FT,Joe Ruiz,93
40RM,Kay Smith,91
EOF

$ cat dummy_cut.dat |cut -d, -f1,3
# this is a data file
ID,Score
13BA,100
24BC,95
34BR,94
36FT,93
40RM,91

$ cat dummy_cut.dat |cut -b6-
s is a data file
me,Score
John Smith,100
Mary Jones,95
Larry Jones,94
Joe Ruiz,93
Kay Smith,91

$ cat dummy_cut.dat |cut -f1- -d, -s
ID,Name,Score
13BA,John Smith,100
24BC,Mary Jones,95
34BR,Larry Jones,94
36FT,Joe Ruiz,93
40RM,Kay Smith,91

使用 uniq

uniq 命令通常用来惟一地列出输入源(通常是文件或 stdin)中的行。要正确操作,重复的行必须连续放置于输入中。uniq 命令的输入通常会进行排序,因此重复的行会进行合并。与 uniq 命令搭配使用的两个常用标志是:-c 输出每行出现的次数,-d 用来显示重复行的一个实例。

uniq 示例
$ cat << EOF > dummy_uniq.dat

13BAR   Smith   John    100
13BAR   Smith   John    100
24BC    Jone    Mary    95
34BRR   Jones   Larry   94
36FT    Ruiz    Joe     93
40REM   Smith   Kay     91
13BAR   Smith   John    100
99BAR   Smith   John    100
13XIV   Smith   Cindy   91


EOF

$ cat dummy_uniq.dat | uniq

13BAR   Smith   John    100
24BC    Jone    Mary    95
34BRR   Jones   Larry   94
36FT    Ruiz    Joe     93
40REM   Smith   Kay     91
13BAR   Smith   John    100
99BAR   Smith   John    100
13XIV   Smith   Cindy   91

$ cat dummy_uniq.dat | sort |uniq

13BAR   Smith   John    100
13XIV   Smith   Cindy   91
24BC    Jone    Mary    95
34BRR   Jones   Larry   94
36FT    Ruiz    Joe     93
40REM   Smith   Kay     91
99BAR   Smith   John    100

$ cat dummy_uniq.dat | sort |uniq -d

13BAR   Smith   John    100

$ cat dummy_uniq.dat | sort |uniq -c
   3
   3 13BAR   Smith   John    100
   1 13XIV   Smith   Cindy   91
   1 24BC    Jone    Mary    95
   1 34BRR   Jones   Larry   94
   1 36FT    Ruiz    Joe     93
   1 40REM   Smith   Kay     91
   1 99BAR   Smith   John    100

使用 sort

要按指定顺序对 stdin 或文件的内容排序,例如按字母顺序或数字顺序,则可以使用 sort 命令。在默认情况下,sort 的输出写在 stdout 中。LC_ALL、LC_COLLATE 和 LANG 等环境变量可以影响 sort 及其他命令的输出。请注意,示例文件显示了 2 个分离的重复记录,一个重复是 IBM,另一个重复是空行。

sort 示例:默认行为
$ cat << EOF > dummy_sort1.dat

014 VMW, 102.92
013 INTC, 22.69
012 sap,  59.37
011 IBM, 174.99
011 IBM, 174.99

EOF

$ sort dummy_sort1.dat


011 IBM, 174.99
011 IBM, 174.99
012 sap,  59.37
013 INTC, 22.69
014 VMW, 102.92

sort 有一个非常强大的标志,在多数情况下可以代替 uniq 命令。-u 选项标志对文件进行排序,删除重复行,以生成一个只包含惟一行的输出清单。

sort 示例:惟一排序
$ sort -u dummy_sort1.dat

011 IBM, 174.99
012 sap,  59.37
013 INTC, 22.69
014 VMW, 102.92

有时,希望将输入倒序输出。在默认情况下,sort 按从小到大(数字)和字符数据的字母顺序排序。使用 -r 选项标志可以将默认排序倒过来。

sort 示例:倒序排序
$ sort -ru dummy_sort1.dat
014 VMW, 102.92
013 INTC, 22.69
012 sap,  59.37
011 IBM, 174.99

不同情况下,可能要求根据某种域或 “键” 对文件进行排序。幸运的是,sort 的 -k 选项标志支持按位置指定排序键。域之间默认以空格分隔。

sort 示例:按键排序
$ sort -k2 -u dummy_sort1.dat

011 IBM, 174.99
013 INTC, 22.69
014 VMW, 102.92
012 sap,  59.37

如果需要区分大小写,sort 的 -f 选项标志可以在进行比较时忽略大小写。在结合如下所示的多个标志时,有些版本的 UNIX 需要用不同的顺序指定这些标志。

sort 示例:不区分大小写排序
$ sort -k2 -f -u dummy_sort1.dat

011 IBM, 174.99
013 INTC, 22.69
012 sap,  59.37
014 VMW, 102.92

以上的排序均是对字母的排序。如果需要按数字顺序对数据排序,则要使用 -n 选项标志。

sort 示例:按数字排序
$ sort -n -k3 -u dummy_sort1.dat

013 INTC, 22.69
012 sap,  59.37
014 VMW, 102.92
011 IBM, 174.99

有些输入可能不使用空格而是使用字符在行中区分域。使用 -t 选项标志指定非默认分隔符,例如逗号。

sort 示例:使用非默认分隔符对域排序
$ sort -k2 -t"," -un dummy_sort1.dat

013 INTC, 22.69
012 sap,  59.37
014 VMW, 102.92
011 IBM, 174.99

使用 join

凡是熟悉数据库查询编写的人,都认得 join 命令这个实用工具。与多数 UNIX 命令一样,这个命令的输出也显示在 stdout 中。要将文件连接 (“join”) 在一起,请逐行比较来自两个文件中指定的域。如果没有指定域,join 则会从每一行的开始进行域区匹配。默认域分隔符是空格(有些系统使用一个空格,有的则使用相邻多个空格)。找到域匹配后,会根据域匹配的两行输出一行结果。要得到合理的结果,每个文件都应该按照匹配的域进行排序。各个系统实现 join 的方式略有不同。

本示例使用 -t 指定域分隔符,并演示了在逗号分隔的第一个域(默认)上对两个文件进行连接。数据库操作人员将其看作是内部连接,只显示匹配的行。

join 示例:使用非默认域分隔符
cat << EOF > dummy_join1.dat
011,IBM,Palmisano
012,INTC,Otellini
013,SAP,Snabe
014,VMW,Maritz
015,ORCL,Ellison
017,RHT,Whitehurst
EOF

cat << EOF > dummy_join2.dat
011,174.99,14.6
012,22.69,10.4
013,59.37,26.4
014,102.92,106.1
016,27.77,31.2
EOF

cat << EOF > dummy_join3.dat
IBM,Armonk
INTC,Santa Clara
SAP,Walldorf
VMW,Palo Alto
ORCL,Redwood City
EMC,Hopkinton
EOF

$ join -t, dummy_join1.dat dummy_join2.dat
011,IBM,Palmisano,174.99,14.6
012,INTC,Otellini,22.69,10.4
013,SAP,Snabe,59.37,26.4
014,VMW,Maritz,102.92,106.1

要指定在每个文件中 “连接” 哪个域,可以使用 -j[1,2] x 选项标志(或者只使用 -1 x 或 -2 x)。选项标志 -j1 2 或 -1 2 指定第 1 个文件的第 2 个域,第 1 个文件是命令中列出的第一个文件。本示例将演示如何根据第 1 个文件的第 1 个域和第 2 个文件的第 2 个域连接文件,这个连接也是内部连接,只连接匹配的行。

join 示例:指定域
$ join -t, -j1 1 -j2 2 dummy_join3.dat dummy_join1.dat
IBM,Armonk,011,Palmisano
INTC,Santa Clara,012,Otellini
SAP,Walldorf,013,Snabe
VMW,Palo Alto,014,Maritz
ORCL,Redwood City,015,Ellison

与数据库相关示例概念一致,可以用标志实现一个左外连接 (left outer join)。左外连接包含左侧第一个文件或表中的所有行以及第二个文件或表中的匹配行。使用 -a 可以包含指定文件中的所有行。

join 示例:左外连接
$ join -t, -a1 dummy_join1.dat dummy_join2.dat
011,IBM,Palmisano,174.99,14.6
012,INTC,Otellini,22.69,10.4
013,SAP,Snabe,59.37,26.4
014,VMW,Maritz,102.92,106.1
015,ORCL,Ellison
017,RHT,Whitehurst

全外连接包含两个文件或表中的所有行,并不关心域是否匹配。可以使用 -a 选项标志指定两个文件来实现全外连接。

join 示例:全外连接
$ join -t, -a1 -a2 -j1 2 -j2 1 dummy_join1.dat dummy_join3.dat
IBM,011,Palmisano,Armonk
INTC,012,Otellini,Santa Clara
SAP,013,Snabe,Walldorf
VMW,014,Maritz,Palo Alto
ORCL,015,Ellison,Redwood City
EMC,Hopkinton
017,RHT,Whitehurst

使用 sed

sed 流编辑器 (stream editor) 是个有用的文本解析和操作实用工具,可以方便地进行文件或数据流的转换。它一次一行地读取文本,在文本行中应用指定的命令。默认输出到 stdoutsed 使用的命令可以执行多种操作,如删除缓冲区的文本、将文本附加或插入到缓冲区、写入到一个文件中,以及根据正则表达式转换文本等。

sed 替换的基本示例显示使用 -e 选项标志来指定表达式或编辑文本。在一个 sed 执行中可以指定多个表达式或编辑文本。请注意 sed 文本编辑的组件。文本编辑开始的 “s” 代表这是个替换命令。使用 “/” 作为分隔符,先指明要替换的 “IBM”。接下来,替换模式出现在两个 “/” 分隔符之间。最后,“g” 指明在当前文本缓冲区中进行全局修改。本示例的第三个演示解释了三个文本编辑的组合:用斜杠代替反斜杠,用下划线代替空格,以及删除冒号(请注意其中反斜杠 “\” 字符的转义表示方式)。

sed 示例:基本替换/多个编辑文本
$ echo "IBM 174.99" |sed –e 's/IBM/International Business Machines/g'
International Business Machines 174.99

$ echo "Oracle DB"|sed -e 's/Oracle/IBM/g' -e 's/DB/DB2/g'
IBM DB2

$ echo "C:\Program Files\PuTTY\putty.exe"| sed -e 's/\\/\//g' -e 's/ /_/g' -e 's/://g'
C/Program_Files/PuTTY/putty.exe

在下面的示例中,将创建一个文件来演示 sed 的另外一个特性。除了替换之外,筛选也是 sed 常用的功能。UNIX 的 grep 命令是常用的筛选器,在命令行上发现多种文本操作方式很常见。本示例将演示如何使用 sed 删除命令删除以 “#” 或以空格加 “#” 开始的行。同时还列出了采用相同模式的 grep 示例以作参考。

sed 示例:筛选
cat << EOF > dummy_sed.txt
# top of file
  # the next line here
# Last Name, Phone
Smith, 555-1212
Jones, 555-5555 # last number
EOF

$ sed '/^[[:space:]]*#/d' dummy_sed.txt
Smith, 555-1212
Jones, 555-5555 # last number

$ grep -v  ^[[:space:]]*# dummy_sed.txt
Smith, 555-1212
Jones, 555-5555 # last number

为了更好地理解 sed 行为,这里要多演示几个模式。为了让这些模式有文本可处理,还新建了一个文件。第一个 sed 模式显示了如何从文件列出的字符串(文件名)中删除最后 4 个字符。接着,该模式删除圆点 (“.”) 右侧的全部字符,即文件扩展名。还列出一个删除空行的模式。特殊字符 “&” 允许在输出中使用搜索模式。在本示例中,IBM 是输入模式的一部分,并使用 “&” 将其指定为输出的一部分。本系列最后展示的一个模式显示了如何使用 sed 删除从基于 Windows 系统上传输过来的文本文件的回车符。 要在命令行向脚本中输入 “^M”,请先按 control-v,再按 control-m。请注意,终端的特征可能影响 control-v、control-m 组合的输入。

sed 示例:更多模式
cat << EOF > filelist.txt
PuTTY.exe

sftp.exe
netstat.exe
servernames.list
EOF

$ sed 's/....$//' filelist.txt
PuTTY

sftp
netstat
servernames.

$ sed 's/\..*$//g' filelist.txt
PuTTY

sftp
netstat
servernames

$ sed '/^$/d' filelist.txt
PuTTY.exe
sftp.exe
netstat.exe
servernames.list

$ echo "IBM 174.99" |sed 's/IBM/&-International Business Machines/g'
IBM-International Business Machines 174.99

$ cat dosfile.txt | sed 's/^M//' > unixfile.txt

sed 命令可以在指定的地址范围内操作。下面的示例显示了一些 sed 可以控制的寻址方式。“-n” 选项标志取消 sed 将每行输入显示在输出的默认行为。在第一个示例中,sed 在文件第 4 行到第 7 行上操作。请注意其中如何只显示文件最先列出的表行(行 4 到 7)。接下来,sed 只显示文件中的第一行和最后一行。有些版本的 sed 支持使用模式来指定在命令上应用地址范围。请注意在输出中,只是从表中删除了逗号,并未在注释中删除。

sed 示例:地址范围
cat << EOF > dummy_table.frag

This, is a paragraph.

row 1, 1st cell row 1, 2nd cell
row 2, 1st cell row 2, 2nd cell
EOF $ sed -n 4,7p dummy_table.frag row 1, 1st cell row 1, 2nd cell $ sed -n -e 1p -e \$p dummy_table.frag $ sed '/^

This, is a paragraph.

row 1 1st cell row 1 2nd cell
row 2 1st cell row 2 2nd cell

表达式内的模式可以进行分组,并引用在输出中。在许多环境中,这种做法很有用,例如在进行值交换或使用位置变量时。括号用来在表达式中标记出模式,必须用反斜杠 \ 转义(模式 \)。在表达式其他地方使用 \n 引用模式,其中 n 是模式在标记模式中的顺序号。将表达式分解后,可以更容易理解它的工作方式:

模式 注解
/^#.*$/d 从输出中删除以 # 开始的行
/^$/d 从输出中删除空行
s/\([:a-z:]*\):\(.*\) /\2:\1 / 这个语句标记着以冒号结尾的第一串小写字母,然后标记紧接冒号后的字符串。在输出时,这些标记的字符串会交换位置。
sed 示例:分组模式
cat << EOF > sed_chown_example.txt
# use sed to swap the group:owner to owner:group

sudo chown dba:jdoe oraenv.ksh
sudo chown staff:jdoe sysenv.ksh
...
EOF

$ sed '/^#.*$/d;/^$/d;s/\([:a-z:]*\):\(.*\) /\2:\1 /' sed_chown_example.txt
sudo chown jdoe:dba oraenv.ksh
sudo chown jdoe:staff sysenv.ksh
...

使用 awk

awk 程序是个方便的文本操作工具,执行的工作包括文本的解析、筛选和简易格式化。它的输入来自 stdin 或文件,默认在 stdout 上显示输出。awk 有不同的发行版,并使用不同的名称,例如 nawkgawk。不同版本和供应商提供的 awk 其行为也各不相同。awk 与本文介绍的其他命令不同,因为它是一个编程语言。该语言内置算术、字符串操作、流程控制以及文本格式化的函数。程序员也可以自己定义函数,创建用户自定义函数库或独立脚本。因为 awk 包含的特性如此之多,所以这里只演示少量的示例。欲了解更多信息,请参阅 参考资料 小节或手册页面。

在本示例中,首先将 awk 作为筛选器,只输出 Linux 系统上的完整文件系统。在默认情况下,awk 使用空白来识别各个列。该示例检查了第 5 列,因为此列显示的是磁盘已用空间百分比。如果磁盘利用率是 100%,则第一个示例会在 stdout 中输出记录。接下来的语句对第一个示例做了扩展,对消息进行格式化,可能要在电子邮件中发送,或者放在消息中写入日志文件。接下来的示例显示如何创建一个使用数值比较的匹配。

awk 示例:筛选器
$ df –k
Filesystem           1K-blocks      Used Available Use% Mounted on
/dev/sda1             61438632  61381272     57360 100% /
udev                    255788       148    255640   1% /dev
/dev/mapper/datavg     6713132   3584984   3128148  54% /data
rmthost1:/archives/backup    -         -         -   -  /backups
rmthost1:/archives/          -         -         -   -  /amc
rmthost1:/archives/data2     -         -         -   -  /data2

$ df -k |awk '$5 ~ /100%/ {print $0}'
/dev/sda1             61438632  61381272     57360 100% /

$ df -k |awk '$5 ~ /100%/ {printf("full filesystem: %s, mountpoint: %s\n",$6,$1)}'
full filesystem: /, mountpoint: /dev/sda1

$ df -k |awk '$4 > 3000000  {print $0}'
Filesystem           1K-blocks      Used Available Use% Mounted on
/dev/mapper/datavg     6713132   3584984   3128148  54% /data

有时,数据不是用空白分隔的。例如 /etc/passwd 文件就是用冒号 “:” 分隔的。本示例显示了 awk 如何使用 -F 标志输出 /etc/passwd 中前 5 个条目的用户名和 UID。接下来,将通过输出 /etc/passwd 文件中第 1 列的前 3 个字符来展示 awk 的 substr() 函数。

awk 示例:域分隔符/字符串函数
$ cat /etc/passwd |awk -F: '{printf("%s %s\n", $1,$3)}' |head -5
root 0
daemon 1
bin 2
sys 3
adm 4

cat /etc/passwd |awk -F: '{printf("%s \n", substr($1,1,3))}'|head -5
roo
dae
bin
sys
adm

很多时候,系统管理员或程序员会编写自己的 awk 脚本来执行某种作业。下面示例为 awk 程序求文件中第 3 列中发现的数字的平均值。这个计算是通过手动将第 3 列数据添加到汇总变量中。NR 是一个特殊的内部变量,awk 用它来跟踪已经处理的记录数量。将总汇变量除以 NR,就得到了第 3 列的平均值。该程序会显示中间结果和数据,所以很容易理解其中的逻辑。

awk 示例:程序/算术
cat << EOF > dummy_file2.dat
011  IBM 174.99
012 INTC 22.78
013 SAP 59.37
014 vmw 102.92
EOF

$ cat avg.awk
awk 'BEGIN {total=0;}
           {printf("tot: %.2f arg3: %.2f NR: %d\n",total, $3, NR); total+=$3;}
     END {printf "Total:%.3f Average:%.3f \n",total,total/NR}'

$ cat dummy_file2.dat | avg.awk
tot: 0.00 arg3: 174.99 NR: 1
tot: 174.99 arg3: 22.78 NR: 2
tot: 197.77 arg3: 59.37 NR: 3
tot: 257.14 arg3: 102.92 NR: 4
Total:360.060 Average:90.015

基于 Shell 的字符串操作

Shell 可以是强大的编程语言。与 awk 一样,Shell 提供了丰富的选项来执行字符串操作、算术功能、数组、流程控制,以及文件操作。下面的几个示例将显示如何从某一方提取字符串的各个部分。此操作并不改变字符串的值,只是提取出需要的结果,通常用来对变量赋值。使用百分号 “%” 截断模式的右侧,并用 “#” 号截断模式的左侧。

Shell 脚本的示例:字符串提取
$ cat string_example1.sh
#!/bin/sh
FILEPATH=/home/w/wyoes/samples/ksh_samples-v1.0.ksh
echo '${FILEPATH}      =' ${FILEPATH}      "  # the full filepath"
echo '${#FILEPATH}     =' ${#FILEPATH}      "  # length of the string"
echo '${FILEPATH%.*}   =' ${FILEPATH%.*}   "  # truncate right of the last dot"
echo '${FILEPATH%%.*}  =' ${FILEPATH%%.*}  "  # truncate right of the first dot"
echo '${FILEPATH%%/w*} =' ${FILEPATH%%/w*} "  # truncate right of the first /w"
echo '${FILEPATH#/*/*/}  =' ${FILEPATH#/*/*/}  "  # truncate left of the third slash"
echo '${FILEPATH##/*/} =' ${FILEPATH##/*/} "  # truncate left of the last slash"

$ ./string_example1.sh
${FILEPATH}=/home/w/wyoes/samples/ksh_samples-v1.0.ksh # the full filepath
${#FILEPATH} = 42                                      # length of the string
${FILEPATH%.*}=/home/w/wyoes/samples/ksh_samples-v1.0  # truncate right of the last dot
${FILEPATH%%.*}=/home/w/wyoes/samples/ksh_samples-v1   # truncate right of the first dot
${FILEPATH%%/w*}=/home                                 # truncate right of the first /w
${FILEPATH#/*/*/}=wyoes/samples/ksh_samples-v1.0.ksh   # truncate left of the third slash
${FILEPATH##/*/}=ksh_samples-v1.0.ksh                  # truncate left of the last slash

例如,系统管理员可能需要将一批 .jpg 文件的扩展名全部修改成小写字母。因为 UNIX 服务器区分大小写,而有些应用程序可能要求小写扩展名,也有可能管理员只是想将文件扩展名标准化。手动或通过 GUI 界面修改大量文件可能需要几小时才能完成。下面的 shell 样例脚本显示了解决这一问题办法。该示例由两个文件组成。第一个是 setup_files.ksh,用来创建样例目录树并使用一些文件填充树。它还创建了需要修改扩展名的文件列表。第二个脚本 fix_extension.ksh 读取该文件列表,修改文件的扩展名。作为 mv 命令的一部分,% 字符串操作符用来截断文件名最后一个圆点 “.” 右侧的字符(截断扩展名)。在运行之后,两个脚本还使用 find 命令显示成果。

Shell 脚本示例:修改文件扩展名
$ cat setup_files.ksh
mkdir /tmp/mv_demo
[ ! -d /tmp/mv_demo ] && exit

cd /tmp/mv_demo
mkdir tmp JPG 'pictures 1'
touch a.JPG b.jpg c.Jpg d.jPg M.jpG P.jpg JPG_file.JPG JPG.file2.jPg file1.JPG.Jpg 'tmp/
pic 2.Jpg' 10.JPG.bak 'pictures 1/photo.JPG' JPG/readme.txt JPG/sos.JPG

find . -type f|grep -i "\.jpg$" |sort| tee file_list.txt

$ ./setup_files.ksh
./JPG.file2.jPg
./JPG/sos.JPG
./JPG_file.JPG
./M.jpG
./P.jpg
./a.JPG
./b.jpg
./c.Jpg
./d.jPg
./file1.JPG.Jpg
./pictures 1/photo.JPG
./tmp/pic 2.Jpg

$ cd /tmp/mv_demo
$ cat /tmp/fix_extension.ksh
while read f ; do
    mv "${f}" "${f%.*}.jpg"
done < file_list.txt

find . -type f|grep -i "\.jpg$" |sort

$ /tmp/fix_extension.ksh
./JPG.file2.jpg
./JPG/sos.jpg
./JPG_file.jpg
./M.jpg
./P.jpg
./a.jpg
./b.jpg
./c.jpg
./d.jpg
./file1.JPG.jpg
./pictures 1/photo.jpg
./tmp/pic 2.jpg

本着创建有用可重用工具的精神,应该将修改文件扩展名的示例做得更通用。想到的一些改进有:传递进要修改的文件名称流,例如通过管道传递。可添加选项标志来指定要修改的文件扩展名(例如 .mp3 或 .mov),并指定如何格式化文件扩展名(例如大写、小写,或大小写混合)。可能性只受程序员的想象力和时间的限制。


结束语

UNIX 提供了各种以本机方式进行文本解析的工具,在很多情况下,不需要依赖那些系统上可能没有安装的特殊解释器。本文只是宽泛地介绍了各种命令,并未对其用途进行深入探讨。本文中只对命令进行了部分演示,各个系统上实现的标志或行为可能会各有所异。UNIX 提供了更多命令和方式来实现这些相同的任务,“有不止一种的方式来完成任务”。


你可能感兴趣的:(Unix,unix)