技巧: 用 cat 合并文件
您常常需要将几个文件处理成一个文件并将这种处理的结果保存到一个单独的输出文件。 cat (“concatenate”的缩写)命令在其输入上接受一个或多个文件并将它们作为一个单独的文件打印到它的输出。例如, cat chapter01 chapter02 chapter03 > book 将三个chapterXX 文件保存在一个单独的book 文件中。
输入文件按照它们在 cat 命令后的排列顺序被打印,因此,要调换信息的顺序,就必须先调换输入文件的顺序。此外,当需要处理的文件数目对于您来说过大而无法手工输入这些文件的名称 时,您可以使用通配符,如在 cat chapter* > book中使用的那样,记住,文件名将会按升序排列。当您突然发现chapter13 被发送到输出中时会在chapter2之前,而会在 chapter02之后时,这会引起很有意思的问题。
当 cat 的输出没有被重定向到一个文件或另一个命令的标准输出时, cat 表现出来的行为与多数命令行工具一样,即将其输出发送到控制台。 这意味着您可以使用 cat 来显示文件;例如,您可以使用 cat /etc/passwd来显示系统密码文件的内容。为方便起见,您应该用less查看大文件,如在 less /etc/passwd中那样(您可以通过输入 man less 学习更多关于less 的知识)。
尽管 cat 主要用于合并文件,您还可以将它用于输入的简单自动处理。例如,您可以使用 一个单独的空白行来除去多行空白行(使用 -s 选项),这是一个在您将源代码公诸于世前进行清除工作的好办法。遗憾的是, cat 并没有用于一次清除所有空白行的选项。但这并不是什么大问题,因为您 可以使用方便的 sed 命令将这些空白行除去:
清单1. 使用sed 与cat 除去空白行
$ cat -s /etc/X11/XF86Config | sed '/^[[:space:]]*$/d'
...
# Multiple FontPath entries are allowed (they are concatenated together)
# By default, Red Hat 6.0 and later now use a font server independent of
# the X server to render fonts.
FontPath "/usr/X11R6/lib/X11/fonts/TrueType"
FontPath "unix/:7100"
EndSection
...
对于读取配置文件和HTML 页面的源文件,特别是那些由脚本生成的插入了不必要新行的源文件,以及那些包含大型条件结构(其各个项之间已经用空行分开)的源文件来说,空白行紧缩是一个方便的技巧。
cat 的另外一个重要的功能是它可以对行进行编号。这种功能对于程序文档的 编制以及法律和科学文档的编制很方便。打印在左边的行号使得参考文档的某一部分变得容易。这在编程、科学研究、业务报告或甚至是立法工作中都是非常重要的。 对行进行编号功能有两个选项: -b 选项(只能对非空白行进行编号)和 -n 选项(可以对所有行进行编号):
清单2. 对行进行编号
$ cat -b /etc/X11/XF86Config
...
14 # Multiple FontPath entries are allowed (they are concatenated together)
15 # By default, Red Hat 6.0 and later now use a font server independent of
16 # the X server to render fonts.
17 FontPath "/usr/X11R6/lib/X11/fonts/TrueType"
18 FontPath "unix/:7100"
19 EndSection
...
$ cat -n /etc/X11/XF86Config
...
20 # Multiple FontPath entries are allowed (they are concatenated together)
21 # By default, Red Hat 6.0 and later now use a font server independent of
22 # the X server to render fonts.
23
24 FontPath "/usr/X11R6/lib/X11/fonts/TrueType"
25 FontPath "unix/:7100"
26
27 EndSection
...
cat 还可以在您查看包含如制表符这样的非打印字符的文件时起帮助作用。 您可以用以下选项来显示制表符:
-T 将制表符显示为 ^I -v 显示非打印字符,除了换行符和制表符,它们使用各自效果相当的 “ 控制序列 ” 。 例如,当您处理一个在 Windows 系统中生成的文件时,这个文件将使用 Control-M ( ^M )来标记行的结束。对于代码大于 127 的字符,它们的前面将会被加上 M- (表示 “meta” ),这与其它系统中在字符前面加上 Alt- 相当。 -E 在每一行的结束处添加美元符( $ )。清单3. 显示非打印字符
$ cat -t /etc/X11/XF86Config
...
# Multiple FontPath entries are allowed (they are concatenated together)
# By default, Red Hat 6.0 and later now use a font server independent of
# the X server to render fonts.
^IFontPath^I"/usr/X11R6/lib/X11/fonts/TrueType"
^IFontPath^I"unix/:7100"
EndSection
...
$ cat -E /etc/X11/XF86Config
...
# Multiple FontPath entries are allowed (they are concatenated together)$
# By default, Red Hat 6.0 and later now use a font server independent of$
# the X server to render fonts.$
$
FontPath "/usr/X11R6/lib/X11/fonts/TrueType"$
FontPath "unix/:7100"$
$
EndSection$
...
$ cat -v /etc/X11/XF86Config
...
^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@M-|M-8^X^@^@^@
P^@^O"M-X^O M-@^M^@^@^@M-^@^O"M-@M-k^@M-8*^@
@M-^H$M-@M-9|A(M-@)M-yM-|M-sM-*M-hW^A^@^@j^@
M-|M-sM-%1M-@M-9^@^B^@^@M-sM-+fM-^A= ^@ ^@
F^@^@ ^@M-9^@^H^@^@M-sM-$M-G^E(l!M-@M-^?
^IM-A5^@^@^D^@PM-^]M-^\X1M-H%^@^@^D^@tyM-G
...
下回我们将看一下 head 和tail 。到时候见!
技巧: 使用 head 和 tail 以块方式读取文本流
假定您想只处理文件的一部分,譬如头几行或后几行,那您该怎么做呢?请使用 head
(它将头 10 行发送至标准输出)或 tail
(它将后 10 行发送至标准输出)。
您可以通过使用 -n
选项改变这些命令发送至其标准输出的行数(当然,输出结果将随XF86Config 文件的内容而不同):
清单 1. 将 XF86Config 中选定行数的内容发送至标准输出
$ head -n 4 /etc/X11/XF86Config
# File generated by anaconda.
# **********************************************************************
# Refer to the XF86Config(4/5) man page for details about the format of
# this file.
$ tail -n 4 /etc/X11/XF86Config
Modes "1600x1200"
ViewPort 0 0
EndSubsection
EndSection
如果您想让 head
或 tail
以字节而不是以行为单位,那该怎么办呢?您可以用 -c
选项代替 -n
选项。因此,要显示前 200个字符,请使用 head -c 200
file ,或者使用 tail -c 200
file 来显示后 200 个字符。如果数字后面跟有 b
(表示块(block)),那么这个数字将被乘以512。类似地,跟有 k
(表示千字节(kilobyte))表示用 1024 去乘给定的数字,而跟有 m
(表示兆字节(megabyte))表示用 1048576 字节去乘给定的数字。
请记住, headfile1 file2 file3
和 cat file1 file2 file3 | head
之间有重大差别。前者将打印每个文件指定行数的内容,不同文件的内容之间用头信息隔开,头信息以 ==>
后跟文件名开头。后者将打印由cat 命令后所列文件组成的输入流中指定行数的内容,但将把输入流作为单个文件对待。可以使用 -q
(表示静默(quiet))选项关闭文件名头信息。与 -q
相反的是 -v
(表示详列(verbose))。
假如您要处理的文件在处理期间一直在发生变化(比如,当您让 head
或 tail
读取来自正在被另一个命令写入的文件的数据时,就是这种情况),请使用 -f
选项让 tail 持续读取来自指定文件的数据并将这些数据发送至tail 自己的标准输出中。通过管道发送数据时该选项会被忽略。因此, cat file | tail -f
将不会得到所期望的结果,但 tail -f file
则可以。
(如果 tail
正在读取的文件不止一个,那么各行内容之间将用标准头信息隔开,以指明它们来自哪个文件,标准头信息以 ==>
开头。)
这个选项用于监视系统日志再合适不过,譬如,在单独的终端窗口(或单独的控制台)中执行的 tail -f /var/log/access.log
将持续打印每次点击后新添加的Apache 访问日志条目,一直到您用Ctrl-C 停止它为止。
通过组合使用 head
和 tail
,可以从文件的中间部分读取给定长度的一块数据!下面说明如何做到:假定您想从文件开头算起第1000 字节处开始读取一块789 字节的数据。可以使用 cat file | head -c 1788| tail -c 789
来解决这一问题。
tac 命令逆序排序文件
如果您想对文件中的各行进行逆序排序,该怎么做呢?这就要用到 tac
命令。(请注意, tac
由 cat
逆序拼写而成。)该命令对所列出的文件中的各行或各段进行逆序排序。
该命令不能逆序排序各文件的顺序 — 这个任务您得自己做,以逆向顺序在 tac
命令后列出各文件即可。作为说明 tac
的工作原理的示例,请在您的主目录中对一些文件使用 ls -l | tail
和 ls -l | tail | tac
,比较其结果。
下回我们将看看 sort
和 tsort
命令。到时候见!
技巧: 用 sort 和 tsort 对文件进行排序
尽管可以用 Perl 或 Awk 编写高级排序应用程序,但并非总是有此必要,而且这样的工作也常常令人感到头疼。使用 sort
命令,您同样可以实现您所需的大多数功能,而且更容易,它可以对多个文件中的行进行排序、合并文件甚至可以查看是否有必要对它们进行排序。您可以指定排序键(用于比较的行中的一部分),也可不指定,后一种情况下 sort
就比较所有行。
因此,如果您想对密码文件进行排序,就可以使用下列命令(请注意,您不能将输出直接发送到输入文件,因为这会破坏输入文件。这就是为何您需要将它发送到临时文件中,然后将该文件重命名为/etc/passwd 的原因,如下所示)。
清单 1. 简单排序
$ su -
# sort /etc/passwd > /etc/passwd-new
# mv /etc/passwd-new /etc/passwd
有关 sort 和 tsort 的更多信息
通过打开 有关排序操作的GNU 手册页来学习手册页中的内容,或者通过在命令行中输入 man sort
或 man tsort
在新的终端窗口的手册页或信息页中查看这些选项。
如果您想倒转排序的次序,则应当使用 -r
选项。您还可以用 -u
选项来禁止打印相同的行。
sort
的一个非常实用的特性是它用字段键进行排序的能力。字段是一个文本字符串,通过某个字符与其它字段分隔开。例如,/etc/passwd中的字段是用冒号( :
)分隔的。因此,如果愿意的话,您可以按照用户标识、组标识、注释字段、主目录或shell 对/etc/passwd 进行排序。要做到这一点,请使用 -t
选项,其后跟着用作分隔符的字符,接着是用作排序键的字段编号,再跟作为键的最后一个字段的编号;例如, sort -t : -k 5,5/etc/passwd
按照注释字段对密码文件进行排序,该字段中存储了完整的用户名(如“John Smith”)。而 sort -t : -k 3,4 /etc/passwd
同时使用用户标识和组标识对同一个文件进行排序。如果您省略了第二个数字,那么sort
会假定键是从给定的字段开始,一直到每一行的末尾。动手试一试,并观察其中的区别(当数字排序看上去有错时,请添加 -g
选项)。
还要注意的是,空白过渡是缺省的分隔符,因此,如果字段已经用空白字符分隔了,那么您可以省略分隔符,只使用-t
(另注:字段的编号是从 1 开始的)。
为了更好地进行控制,您可以使用键和偏移量。偏移量是用点与键相分隔的,比如在 -k 1.3,5.7
中,表示排序键应当从第 1 个字段的第 3 个字符开始,到第 5 个字段的第 7 个字符结束(偏移量也是从 1开始编号的)。何时会用得着偏移量呢?嗯,我时常用它来对Apache 日志进行排序;键和偏移量表示法让我跳过了日期字段。
另一个要关注的选项是 -b
,它告知 sort
忽略空白字符(空格、跳格等等)并将行中的第一个非空白字符当做是排序键的开始。还有,如果您使用该选项,那么将从第一个非空白字符开始计算偏移量(当字段分隔符不是空白字符,且字段可能包含以空白字符开头的字符串时,这非常有用)。
可以用下面这些选项来进一步修改排序算法: -d
(只将字母、数字和空白用作排序键)、 -f
(关闭大小写区分,认为小写和大写字符是一样的)、 -i
(忽略非打印的 ASCII 字符)、 -M
(使用三个字母的月份名称缩写:JAN、FEB、MAR … 来对行进行排序)和 -n
(只用数字、- 和逗号或另外一个千位分隔符对行进行排序)。这些选项以及 -b
和 -r
选项可以用作键编号的一部分,在这种情况下,它们只适用于该键而非全局,其作用就跟在键定义外使用它时一样。
以键编号的用法为例,请考虑:
sort -t: -k4g,4 -k 3gr,3 /etc/passwd
这条命令将按照组标识对 passwd 文件进行排序,而在组内按照用户标识进行逆向排序。
但是这并非 sort
的全部能力。如果您所使用的键不能用来确定哪一行是在先,那么它也可以解决这类平局问题。增加一个解决平局问题的提示,请添加另一个 -k
选项,让它跟在字段和(可选的)偏移量后面,使用与前面用于定义键相同的表示法;例如, sort -k 3.4,4.5 -k7.3,9.4 /etc/passwd
对行进行排序时,使用从第 3 个键的第 4 个字符开始到第 4 个键的第 5 个字符结束的键,然后再采用从第7 个字段的第3 个字符到第9 个字段的第4 个字符结束的键来解决上述难题。
最后一组选项处理输入、输出和临时文件。例如, -c
选项,当它用于 sort -c <
file 中时,它检查输入文件是否已进行了排序(您也可以使用其它选项),如果已进行了排序,则报告一个错误。这样,在处理可能需要花很长时间进行排序的大型文件之前,可以很方便地对其进行检查。当您将 -u
选项和 -c
选项一起使用时,会被解释为一个请求:检查输入文件中不存在两个相同的行。
当您处理大型文件时还有一个很重要的 -T
选项,它用于为临时文件(这些临时文件在 sort
完成工作之后会被除去)指定其它目录,而不是缺省的 /tmp
目录。
您可以使用 sort
来同时处理多个文件,这样做的方式基本上有两种:首先可以使用 cat
来并置它们,如下所示:
cat file1file2 file3 | sort > outfile
或者,可以使用下面这个命令:
sort -m file1file2 file3 > outfile
第二种情况有个条件:在将所有输入文件一起进行 sort -m
之前,每个文件都必须经过排序。这看起来似乎是个不必要的负担,但事实上这加快了工作速度并节约了宝贵的系统资源。对了,别忘了 -m
选项。在这里您可以使用 -u
选项来禁止打印相同的行。
如果需要某种更深奥的排序方法,您可能要查看 tsort
命令,该命令对文件执行拓扑排序。拓扑排序和标准 sort
之间的差别如清单 2 所示(您可以从 参考资料下载happybirthday.txt)。
清单 2. 拓扑排序和标准排序之间的差别
$ cat happybirthday.txt
Happy Birthday to You!
Happy Birthday to You!
Happy Birthday Dear Tux!
Happy Birthday to You!
$ sort happybirthday.txt
Happy Birthday Dear Tux!
Happy Birthday to You!
Happy Birthday to You!
Happy Birthday to You!
>$ tsort happybirthday.txt
Dear
Happy
to
Tux!
Birthday
You!
当然,对于 tsort
的使用来说,这并非一个非常有用的演示,只是举例说明了这两个命令输出的不同。
tsort
通常用于解决一种逻辑问题,即必须通过观察到的部分次序预测出整个次序;例如(来自tsort 信息页中):
tsort <<EOF
a b c
d
e f
b c d e
EOF
会产生这样的输出
a
b
c
d
e
f
下一次,我们将深入研究 tr
。
技巧: 用 tr 过滤文件
您可以将 tr 看作为 sed 的(极其)简化的变体:它可以用一个字符来替换另一个字符,或者可以完全除去一些字符。您也可以用它来除去重复字符。这就是所有 tr 所能够做的。
那么,为什么要使用 tr,而不使用 sed 呢?当然是为了使事情简单。例如,如果我们希望用字母“z”来替换出现的所有字母“a”,则可以用 tr a z
,这条命令毫无疑问比 sed -e s/a/z/g
简单,尤其在把它用到脚本中时,其中的引号转义很让人头痛。另外,在使用tr 时,可以避免写那些让人讨厌的正则表达式。
使用 tr 很简单:使用前面一段中所给出的符号表示法,用一个字符去替换出现的所有另一个字符。当需要替换多个字符时,使用类似于这样的表示法: tr
abc xyz ,它表示用字母“x”去替换出现的所有字母“a”,用字母“y”去替换所有字母“b”,用字母“z”去替换所有字母“c”。这两组中所列出的字符的数目不必相等。
您也可以指定字符的范围。例如, tr a-z A-Z
将用对应的大写字母来替换所有的小写字母(例如,它将“nosmoking”转换成“NOSMOKING”)。当您在vi 编辑器中想强调正在编辑的文本的某一部分时,使用这一特殊技巧非常方便。只要按一下Escape键,然后按 :
键,再输入 2,4!tr 'a-z' 'A-Z'
,最后按一下Return键。现在,从第2 行到第 4 行的字母就都转换成了大写字母。
另外,当有人给您发送了一个在 Mac OS 或 DOS/Windows 机器上创建的文本文件时,您会发现 tr非常有用。如果没有将文件保存为使用UNIX 换行符来表示行结束这种格式,则需要将这样的文件转换成本机UNIX格式,否则一些命令实用程序不会正确地处理这些文件。MacOS的行尾以回车字符结束,许多文本处理工具将这样的文件作为一行来处理。为了纠正这个问题,可以用下列技巧:
Mac -> UNIX :tr '\r' '\n' < macfile > unixfile
UNIX -> Mac
:
tr '\n' '\r' < unixfile > macfile
Microsoft DOS/Windows 约定,文本的每行以回车字符并后跟换行符结束。为了纠正这个问题,可以使用下列命令:
DOS -> UNIX :tr -d '\r' < dosfile > unixfile
UNIX -> DOS
:在这种情况下,需要用
awk
,因为 tr
不能插入两个字符来替换一个字符。要使用的 awk
命令为
awk '{ print $0"\r" }' < unixfile > dosfile
另外,当您需要对文本文件做一些简单的整理工作(如用 tr -d '\t'
除去制表符,用 tr -s ' '
除去多余的空格,或者用 tr -d '\n'
将分开的几行合成一行)时,会需要用tr。同样,可以在vi 内使用所有这些命令;只要记住:在tr命令前要加上您希望处理的行范围和感叹号(!),如 1,$!tr -d '\t'
(美元符号表示最后一行)中所示。
下次,我们将讨论 uniq
。到那时见!
技巧: 用 uniq 除去重复行
进行排序之后,您会发现有些行是重复的。有时候该重复信息是不需要的,可以将它除去以节省磁盘空间。不必对文本行进行排序,但是您应当记住 uniq 在读取行时会对它们进行比较并将只除去两个或更多的连续行。下面的示例说明了它实际上是如何工作的:
清单 1. 用 uniq 除去重复行
$ cat happybirthday.txt
Happy Birthday to You!
Happy Birthday to You!
Happy Birthday Dear Tux!
Happy Birthday to You!
$ sort happybirthday.txt
Happy Birthday Dear Tux!
Happy Birthday to You!
Happy Birthday to You!
Happy Birthday to You!
$ sort happybirthday.txt | uniq
Happy Birthday Dear Tux!
Happy Birthday to You!
警告:请不要使用 uniq 或任何其它工具从包含财务或其它重要数据的文件中除去重复行。在这种情况下,重复行几乎总是表示同一金额的另一个交易,将它除去会给会计部造成许多困难。千万别这么干!
如果您希望您的工作轻松点,比如只显示唯一的或重复的行,那么该怎么办呢?您可以用 -u (唯一)和 -d (重复)选项来做到这一点,例如:
清单 2. 使用 -u 和 -d 选项
$ sort happybirthday.txt | uniq -u
Happy Birthday Dear Tux!
$ sort happybirthday.txt | uniq -d
Happy Birthday to You!
您还可以用 -c 选项从uniq 中获取一些统计信息:
清单 3. 使用 -c 选项
$ sort happybirthday.txt | uniq -uc
1 Happy Birthday Dear Tux!
$ sort happybirthday.txt | uniq -dc
3 Happy Birthday to You!
就算 uniq 对完整的行进行比较,它仍然会很有用,但是那并非该命令的全部功能。特别方便的是:使用 -f 选项,后面跟着要跳过的字段数,它能够跳过给定数目的字段。当您查看系统日志时这非常有用。通常,某些项要被复制许多次,这使得查看日志很难。使用简单的 uniq 无法完成任务,因为每一项都以不同的时间戳记开头。但是如果您告诉它跳过所有的时间字段,您的日志一下子就会变得更加便于管理。试一试 uniq -f 3 /var/log/messages,亲眼看看。
还有另一个选项 -s ,它的功能就像-f 一样,但是跳过给定数目的字符。您可以一起使用 -f 和-s 。uniq 先跳过字段,再跳过字符。如果您只想使用一些预先设置的字符进行比较,那么该怎么办呢?试试看 -w 选项。
下一次,我们将研究 nl 。到时候见!
技巧:快速提高Vi/Vim 使用效率的原则与途径
Vi/Vim 版本的选择
“工欲善其事,必先利其器”。在 Vi/Vim 版本的选择上,原则是“能用 Vim 就不要使用 Vi;能用最新版就不要守着旧版本”。Vim 提供的功能和特性要比 Vi 多得多,如语法加亮着色功能等。就使用效果及效率来说,编辑同样的文件,使用Vim 更胜一筹;就版本来说,新版的往往会修复旧版的一些缺陷及不足。这就要求我们在可能的情况下一定要使用最新版的Vim。
小技巧:
在 Linux 下,如果以 root 用户登录系统的话,通过 vi 命令打开的 Vim 编辑器往往只加载最基本的功能,像语法加亮着色的功能基本上没有。在 root 用户下使用 Vim 所有功能的技巧是用 vim 命令打开 Vim 编辑器。
打开及关闭 Vi/Vim 的方法有很多,既可以只启动 Vi/Vim 编辑器本身,也可以在启动Vi/Vim 编辑器的同时打开一个或多个文件;既可以放弃存盘退出,也可以只保存文件的一部分。以下为相关命令列表:
表 1. 启动及关闭 Vi/Vim 的基本命令
功能
命令
说明
启动
vi 打开 Vi/Vim
vi <file> 打开 Vi/Vim 并加载文件 <file>
退出
ZQ 无条件退出
:q! 无条件退出
ZZ 存盘并退出
:wq 存盘并退出
保存部分文件
:m,nw <file> 将 m 行到 n 行部分的内容保存到文件 <file> 中
:m,nw >> <file> 将 m 行到 n 行的内容添加到文件 <file> 的末尾
掌握并熟练这些基本命令是使用 Vi/Vim 的基本要求。只有这样,才能在实际使用过程中做到按需选择,灵活使用,提高 Vi/Vim 的使用效率。需要说明的一点是:在使用Vi/Vim 的时候,有时想临时退出Vi/Vim,转到shell 环境里去做一些操作,等这些操作结束后,再继续回到刚才的Vi/Vim 状态。针对这一经常碰到的现实需求,很多人在大多数情况下会将保存退出Vi/Vim,进入shell 执行操作,然后再重新打开刚才编辑的文件。其实这是一个很低效的方法,因为再次打开需要重新定位刚才编辑的地方,麻烦不说,Vi/Vim编辑器的状态也完全不一样了。其实,有两种方法可以可以实现临时退出Vi/Vim、进入shell 环境后再回来的要求:
方法一:使用 Ctrl-z 以及 fg 这两个命令组合。
这一解决方法主要利用了 Linux/Unix 的作业机制。具体原理是:Ctrl-z 命令将当前的 Vi/Vim 进程放到后台执行,之后shell 环境即可为你所用;fg命令则将位于后台的Vi/Vim 进程放到前台执行,这样我们就再次进入Vi/Vim 操作界面并恢复到原先的编辑状态。
方法二:使用行命令 :sh。
在 Vi/Vim 的正常模式下输入 :sh即可进入Linux/Unix shell 环境。在要返回到 Vi/Vim 编辑环境时,输入 exit 命令即可。
这两种方法实现机制不一定,但效果一样,都非常快捷有效。
Vi/Vim 中关于光标移动的命令非常多,这也是很多人经常困惑并且命令用不好的地方之一。其实 Vi/Vim 中很多命令是针对不同的操作单位而设的,不同的命令对应不同的操作单位。因而,在使用命令进行操作的时候,首先要搞清楚的就是要采用哪种操作单位,也就是说,是要操作一个字符,一个句子,一个段落,还是要操作一行,一屏、一页。单位不同,命令也就不同。只要单位选用得当,命令自然就恰当,操作也自然迅速高效;否则,只能是费时费力。这也可以说是最能体现Vi/Vim 优越于其它编辑器的地方之一,也是 Vi/Vim 有人爱有人恨的地方之一。在操作单位确定之后,才是操作次数,即确定命令重复执行的次数。要正确高效的运用 Vi/Vim 的各种操作,一定要把握这一原则:先定单位再定量。操作对象的范围计算公式为:操作范围 = 操作次数 * 操作单位。比如:5h 命令左移 5 个字符,8w 命令右移 8 个单词。
注:有些操作单位(如文件)是不能加操作次数。具体说明请参考Vi/Vim 使用手册。
Vi/Vim 中操作单位有很多,按从小到大的顺序为(括号内为相应的操作命令):字符(h、l)→ 单词 (w、W、b、B、e) → 行 (j、k、0、^、$、:n) → 句子((、))→ 段落({、})→ 屏 (H、M、L) → 页(Ctrl-f、Ctrl-b、Ctrl-u、Ctrl-d) → 文件(G、gg、:0、:$)。
具体命令解释如下:
表 2. 移动光标的基本命令
操作单位
命令
说明
字符
h 左移一字符
l 右移一字符
单词
w/W 移动到下一单词的开头
b/B 移动到上一单词的开头
e/E 移动到光标所在单词的末尾
W、B、E 命令操作的单词是以空白字符(空格、Tab)分隔的字串,比如字符串“str1-str2 str3-str4”,对 W、B、E 命令来说是两个单词,而对 w、b、e 命令来说则是四个单词。
行
j 下移一行
k 上移一行
0 移到当前行开头
^ 移到当前行的第一个非空字符
$ 移到当前行末尾
:n 移动到第 n 行
0 为数字零(zero)
句子
) 移动到当前句子的末尾
( 移动到当前句子的开头
段落
} 移动当前段落的末尾
{ 移到当前段落的开头
屏
H 移动到屏幕的第一行
M 移动到屏幕的中间一行
L 移动到屏幕的最后一行
页
Ctrl-f 向前滚动一页
Ctrl-b 向后滚动一页
Ctrl-u 向前滚动半页
Ctrl-d 向后滚动半页
文件
G 移动到文件末尾
gg 移动到文件开头
:0 移动到文件第一行
:$ 移动到文件最后一行
0 为数字零(zero)
除了这些基本单位之外,还有 %(跳转到与之匹配的括号处),`.(跳转到最近修改过的位置并定位编辑点),'.(跳转到最近修改过的位置但不定位编辑点)这三个命令也非常重要,在Vi/Vim 中灵活使用会极大提高效率。%除用于光标移动之后,还可用于检测源码中各种括号的匹配情况。
与光标移动一样,Vi/Vim 中关于编辑操作的命令也比较多,但操作单位要比移动光标少得多。按从小到大的顺序为(括号内为相应的操作命令):字符 (x、c、s、r、i、a)→ 单词 (cw、cW、cb、cB、dw、dW、db、dB) → 行 (dd、d0、d$、I、A、o、O) → 句子((、))→ 段落({、})。这些操作单位有些可以加操作次数。操作对象的范围计算公式为:操作范围 = 操作次数* 操作单位。比如:d3w 命令删除三个单词,10dd命令删除十行。
具体命令解释如下:
表 3. 文本编辑的基本命令
操作单位
命令
说明
字符
x 删除光标位置的字符
c 更改当前字符并进入插入模式
s 替换光标位置的字符并进入插入模式
r 替换光标位置的字符但不进入插入模式
i 在当前位置的字符之前进入插入模式
a 在当前位置的字符之后进入插入模式
单词
cw/cW 删除当前单词从光标开始的部分并进入插入模式
cb/cB 删除当前单词从光标所在位置至单词开始的部分并进入插入模式
dw/dW 删除当前单词从光标开始的部分但不进入插入模式
db/dB 删除当前单词从光标所在位置至单词开始的部分但不进入插入模式
cW、cB、dW、dB 命令操作的单词是以空白字符(空格、Tab)分隔的字串,比如字符串“str1-str2 str3-str4”,对 cW,cB,dW,dB 命令来说是两个单词,而对 cw、cb、dw、db 命令来说则是四个单词。
行
dd 删除当前行
d0 删除从当前光标开始到行首的内容
d$ 删除从当前光标开始到行末的内容
I 在当前行的行首进入插入模式
A 在当前行的行尾进入插入模式
o 在当前行下方另起一行进入插入模式
O 在当前行上方另起一行进入插入模式
句子
d) 删除当前句子从光标位置开始到句末的内容
d( 删除当前句子从光标位置开始到句首的内容
段落
d} 删除当前段落从光标位置开始到段末的内容
d{ 删除当前段落从光标位置开始到段首的内容
除上述最基本的文本编辑命令这外,Vi/Vim 还提供了许多其它的编辑命令或相关组合。使用这些命令或相关组合往往在极大提高文本编辑的效率与速度。现将这些命令按功能列如表下:
表 4. 文本编辑的高效命令
功能
命令
说明
复制与粘贴
yw 复制当前单词从光标开始的部分
yy 复制光标所在行的所有字符
p 将最后一个删除或复制文本放在当前字符
P 将最后一个删除或复制文本放在当前字符之前
配合操作数使用可快速拷贝编辑文本
撤消与重做
u 撤消更改
Ctrl-R 重做更改
非常实用的一个命令
重复操作
. 重复上次操作
. 为小数点( dot ) 配合光标移动命令使用; 不用重复输入先前的复杂命令即可在不同的地方做同样的操作,有点象 MS Office 的格式刷交换相邻字符或行
xp 交换光标位置的字符和它右边的字符
ddp 交换光标位置的行和它的下一行
大小写转换
~ 将光标下的字母大小写反向转换
guw 将光标所在的单词变为小写
gUw 将光标所在的单词变为大写
guu 光标所在的行所有字符变为小写
gUU 光标所在的行所有字符变为大写
g~~ 光标所在的行所有字符大小写反向转换
取得外部输入
: r!<cmd> 将命令 <cmd> 的输出结果插入到当前光标所在位置
:r <file> 将文件 <file> 读入到当前光标所在位置
排序
:1,$!sort 将文件内的所有内容排序
加入行号
:%!nl 在所有非空行前加入行号
:%!nl -ba 在所有行前加入行号
利用 Linux 命令 nl 来实现的
缩进
>> 右缩进(可配合操作数使用)
<< 左缩进(可配合操作数使用)
配合操作数使用,在编辑源码的时候非常有用。
自动补全
Ctrl-p 自动补全
在编写代码的时候非常有用。比如,输入 prin 后按 Ctrl-p 将自动帮你输入 printf 函数名后面的部分,同时将相关备选函数在底下列出来。
显示当前编辑文件名
Ctrl-g 显示当前编辑文件名及行数
可以在不退出 Vi/Vim 情况下了解当前编辑文件的信息
显示字符内码
ga 显示光标所在字符的内码(包括十进制码,十六进制码以及八进制码)
显示的内码为当前 encoding 下的内码
Vi/Vim 用于文本搜索的主要有下面的三个基本命令:
表 5. 文本搜索的基本命令
功能
命令
说明
搜索
/ 在文件中向前搜索
? 在文件中向后搜索
搜索下一个
n 搜索下一个
N 反向搜索下一个
除这三个基本命令之外,还有以下三个非常有效快捷的与搜索查找有关的命令:
表 6. 文本搜索的高效命令
功能
命令
说明
快速搜索
* 在文件中向前搜索当前光标所在的单词
# 在文件中向后搜索当前光标所在的单词
非常快捷的搜索命令
显示搜索命令历史
q/ 显示搜索命令历史的窗口
q? 显示搜索命令历史的窗口
可以选择重用以前用过的搜索查找命令
查找帮助
Shift-k 查找光标所在命令或函数的 man 帮助
可以在不退出 Vi/Vim 情况下快速查询命令或函数的使用方法; 按 q 键退出 man 帮助
关于替换主是要结合搜索使用行命令来实现,命令格式为:
:m,ns/str1/str2/g将 m 行到 n 行中的字串 str1 全部替换为字串 str2。
在众多使用正则表达式进行替换的命令中,平时需要掌握的一个命令是如何快速去除 ^M 字符。在 Linux/Unix 系统中编辑 Windows 操作系统中生成的文件时,如果上传时回车换行符处理不正确的话,用Vi/Vim 打开经常会出现^M 字符。如果上传的文件是shell 脚本的话,即使赋予了正确的执行权限该脚本还是无法运行。这也是各种文件在Linux/Unix 和Windows 中传输经常会出现问题的地方之一。其实,在Vi/Vim 中使用替换命令:1,$s/^M//g即可以很容易地快速去掉 ^M 字符。这里需要注意的是 ^M 是使用 Ctrl-v、Ctrl-m 输入的。
在 Vi/Vim 中还可以在正常模式下执行各种外部命令,命令格式如下:
表 7. 执行外部命令格式
功能
命令
说明
执行外部命令
:!<cmd> 执行外部命令 <cmd>
在正常模式下输入该命令
显示命令行命令历史
q: 显示命令行命令历史的窗口
可以选择重用以前用过的命令行命令
其中命令 q:会显示使用过的行命令历史,可以从中选择重用以前用过的命令。这对于需要重复应用那些复杂的命令来说,非常方便快捷。
Vi/Vim 设置
Vi/Vim 有很多内部变量,可以根据需要进行相应的设置。变量类型不同往往设置方式也不一样,简单的只要设置特定的变量名即可,复杂的则需要指定和分配一个显式值来设置变量。在实际应用中,如果有需要,请参考Vi/Vim 的使用手册。这里主要列出大家经常使用并能提高编辑效率的一些设置命令:
表 8. Vi/Vim 设置命令
功能
命令
说明
查看设置的当前值
:set all 查看 vi 或 Vim 中设置的所有选项的当前值
:set <option>? 查看特定选项 <option> 的当前值
设置行号显示与否
:set number 显示行号
:set no number 取消行号显示
命令的简写形式:
:set nu
:set no nu
设置自动缩进
:set autoindent 设置自动缩进
:set no autoindent 取消自动缩进设置
命令的简写形式:
:set ai
:set no ai
设置缩进宽度
:set shiftwidth=4 设置缩进宽度为 4
命令的简写形式:
:set sw=4
设置大小写忽略与否
:set ignorecase 设置忽略大小
:set no ignorecase 取消忽略大小设置
命令的简写形式:
:set ic
:set no ic
设置不可见字符显示与否
:set list 显示不可见字符
:set nolist 取消显示不可见字符设置
在显示不可见字符的情况下,TAB 键显示为 ^I,而 $ 显示在每行的结尾。
在正常模式 (Normal mode) 时,执行这些设置命令只修改当前会话的设置,退出或重启 Vi/Vim 这些设置就丢失了。要想保持住这些设置,就必须将这些设置写入Vi/Vim 的vimrc 文件。对Linux/Unix 操作系统来说,每个用户的vimrc 文件位于该用户的主目录下,文件名为.vimrc。Vi/Vim在每次启动的时候都会读取用户主目录下的vimrc 文件并据此设置Vi/Vim 的使用环境。Vi/Vim在安装的同时也会安装vimrc 文件的一个示例vimrc_example.vim 到 /usr/share/vim/vim<version> 目录下。可以根据需要将这个示例文件拷贝到当前用户的主目录下并重命名为.vimrc,在此基础上进行修改会相对容易一些。其它关于Vi/Vim 的一些定制和键映射等相关设置基本上也是写入用户的vimrc 文件中,更详细的介绍请参考Vi/Vim 的使用手册。用户也可以维护一个自己的vimrc 文件,并将这个文件拷贝到自己的使用的环境中,保持不同环境中Vi/Vim 特性的一致,以符合自己的使用习惯。
文件和目录管理
Linux 和 UNIX® 系统中的所有文件都可以作为一个大型树型文件系统的一部分访问,这个树型文件系统的根为 /。通过挂载 分支可以将它们添加到树中,通过解除挂载 可以移除它们。挂载和解除挂载的内容将在挂载和解除挂载文件系统 一文介绍。 (参见 学习 Linux,101:LPIC-1 路线图)。
在本文中,我们将使用 “学习 Linux,101:文本流和过滤器” 一文中创建的文件来练习命令。如果您完成了上篇文章的练习,那么您应该在您的主目录中创建了一个目录lpi103-2。如果还没有的话,那么可以使用系统中的另一个目录来练习本文讨论的命令。
文件和目录名可以是绝对 的,这表示名称以 / 开头,也可以相对 于当前工作目录,这表示不是以/ 开头。文件或目录的绝对路径的组成为:在0 个或多个目录名后附加一个/,其中每个目录名的后面都有一个/,然后是一个最终文件名。
至于相对于当前工作目录的文件或目录名,只需要将工作目录的绝对名、/ 和相对名连接在一起。例如,我们在早期文章中在我的主目录 /home/ian 中创建的目录lpi103-2,它的完整(即绝对)路径为/home/ian/lpi103-2。
您可以使用 pwd
命令显示当前工作目录的名称。此命令通常也可以用于PWD 环境变量。清单1 展示了 pwd
命令的使用,以及通过三种不同的方法使用 ls
命令列出此目录中的文件。
清单 1. 列出目录条目
[ian@echidna lpi103-2]$ pwd
/home/ian/lpi103-2
[ian@echidna lpi103-2]$ echo "$PWD"
/home/ian/lpi103-2
[ian@echidna lpi103-2]$ ls
sedtab text1 text2 text3 text4 text5 text6 xaa xab yaa yab
[ian@echidna lpi103-2]$ ls "$PWD"
sedtab text1 text2 text3 text4 text5 text6 xaa xab yaa yab
[ian@echidna lpi103-2]$ ls /home/ian/lpi103-2
sedtab text1 text2 text3 text4 text5 text6 xaa xab yaa yab
可以看到,您可以将一个相对或绝对目录名作为 ls
目录的参数,它将列出该目录中的内容。
在一台存储设备中,文件或目录被包含到一个块(block)组合中。有关文件的信息被包含在一个索引节点(inode)中,其中记录如下信息:所有者、最后一次访问文件的时间、文件大小、是否为目录以及谁可以读取或写入数据。inode编号也被称为文件序列号(fileserial number),并且在一个特定文件系统中是唯一的。我们可以使用 -l
(或 --format=long
)选项来显示存储在 inode 中的某些信息。
默认情况下,ls
命令不会列出特殊文件,这些文件的文件名以点号(.) 开头。除根目录外的所有目录都至少包含两个特殊条目:目录本身(.) 和父目录(..)。根目录没有父目录。
清单 2 使用 -l
和 -a
选项显示所有文件的长格式的列表,包括. 和 .. 目录条目。
清单 2. 显示一个长目录列表
[ian@echidna lpi103-2]$ ls -al
total 52
drwxrwxr-x. 2 ian ian 4096 2009-08-11 21:21 .
drwx------. 35 ian ian 4096 2009-08-12 10:55 ..
-rw-rw-r--. 1 ian ian 8 2009-08-11 21:17 sedtab
-rw-rw-r--. 1 ian ian 24 2009-08-11 14:02 text1
-rw-rw-r--. 1 ian ian 25 2009-08-11 14:27 text2
-rw-rw-r--. 1 ian ian 63 2009-08-11 15:41 text3
-rw-rw-r--. 1 ian ian 26 2009-08-11 15:42 text4
-rw-rw-r--. 1 ian ian 24 2009-08-11 18:47 text5
-rw-rw-r--. 1 ian ian 98 2009-08-11 21:21 text6
-rw-rw-r--. 1 ian ian 15 2009-08-11 14:41 xaa
-rw-rw-r--. 1 ian ian 9 2009-08-11 14:41 xab
-rw-rw-r--. 1 ian ian 17 2009-08-11 14:41 yaa
-rw-rw-r--. 1 ian ian 8 2009-08-11 14:41 yab
在清单 2 中,第一行显示所列文件使用的磁盘块的总数(52)。其余行列出了目录的条目。
第一个字段(本例中为 drwxrwxr-x 或 -rw-rw-r-- )告诉我们,文件是一个目录 (d) 还是一个普通文件 (-) 。对于特殊文件,还会看到符号链接 (l) 或其他值(例如 /dev 文件系统中的文件)。您将在创建和修改硬链接和符号链接 一文(参见 学习 Linux,101:LPIC-1 路线图 )中了解到有关符号链接的更多内容。类型之后是针对所有者、所有者所在组的成员、每一个成员的三组特权。这三个值分别表示用户、组、组成员是否拥有读 (r) 、写 (w) 或 (x) 执行权限。诸如 setuid 之类的用户将在管理文件权限和所有权(参见 学习 Linux,101:LPIC-1 路线图 )一文中介绍 。 下一个字段是一个数字,告诉我们文件的硬链接 的数量。我们已经介绍过,inode 包含有关文件的信息。文件的目录条目包含到文件的 inode 的硬链接(或指针),因此列出的每个条目都应该至少拥有一个硬链接。目录条目对 . 条目和每个子目录条目使用另外的硬链接。因此我们可以从清单 2 中可以看到,使用 .. 表示的主目录有大量子目录,因此包含 35 个硬链接。 接下来两个字段分别为文件的所有者和所有者的主组。某些系统,例如 Red Hat 或 Fedora 系统,在默认情况下为每个用户提供单独的组。在其他系统中,所有用户可能位于一个或多个组中。 下一个字段包含文件的长度,以字节为单位 。 倒数第二个字段包含最后一次修改的时间戳。 最后一个字段包含文件或目录的名称 。ls
命令的 -i
选项将显示 inode 号。您将在本文后面以及 创建和修改硬链接和符号链接(参见 学习 Linux,101:LPIC-1 路线图)中再次见到有关 inode的介绍。
您还可以为 ls
命令指定多个参数,其中的每个名称都可能是文件或目录的名称。对于目录名,ls
命令将列出目录的内容,而不是关于目录本身的信息。在我们的示例中,假设当在父目录中列出目录时,我们希望获得有关lpi103-2 目录条目本身的信息。命令 ls -l ../lpi103-2
将提供类似前例的列表。清单3 将展示如何添加 -d
选项以列出有关目录条目的信息,而不是目录的内容,以及如何列出多个文件或目录的条目。
清单 3. 使用 ls -d
[ian@echidna lpi103-2]$ ls -ld ../lpi103-2 sedtab xaa
drwxrwxr-x. 2 ian ian 4096 2009-08-12 15:31 ../lpi103-2
-rw-rw-r--. 1 ian ian 8 2009-08-11 21:17 sedtab
-rw-rw-r--. 1 ian ian 15 2009-08-11 14:41 xaa
注意,lpi103-2 的修改时间不同于前一个列表中的修改时间。同样,和前一个列表相同,它与该目录中的任何文件的时间戳都不同。这是否就是您所期望的?并不是这样。然而,在撰写本文时,我创建了一些额外的的例子并删除了它们,因此目录时间戳反映了这一更改。稍后在 处理多个文件和目录 中,我们将更详细地讨论文件时间。
默认情况下,ls
将按字母顺序列出文件。可以使用多种选项对输出进行排序。例如,ls -t
将按照修改时间排序(从最新到最旧),而 ls -lS
将生成一个按大小排序的长列表(从最大到最小)。添加 -r
将反向排序。例如,使用 ls -lrt
生成一个按从最旧到最新排序的长列表。参考手册页面,了解有关排列文件和目录的其他方式。
我们现在已经了解了一些创建文件的方法,但是假设我们希望复制文件、重命名文件、在文件系统层级结构中移动文件,甚至删除它们。我们使用三个简短的命令来实现这些目的。
cp
用于复制一个或多个文件或目录。您必须提供一个(或多个)源 名和一个目标 名。源名或目标名可能包含一个路径说明。如果目标是一个现有目录,那么所有源将被复制到目标中。如果目录是一个不存在的目录,那么(单一)源也必须为一个目录,并且源目录的副本及其内容使用目标名作为新名称。如果目标是一个文件,那么(单一)源必须也为文件,而源文件的副本使用目标名作为新名,替换任何现有的具有相同名称的文件。注意,在DOS 和Windows 操作系统中,不会做出目标为当前目录的默认假设。
mv
用于移动 或重命名 一个或多个文件或目录。一般来说,您使用的名称将遵守与 cp
相同的规则;您可以重命名某个文件或将一组文件移动到一个新目录中。由于名称只是一个链接到某个inode 的目录条目,因此inode 号只有在文件被移动到另一个文件系统才会发生更改就不足为怪了,在这种情况下,移动文件看上去就类似于在复制文件之后删除它。
rm
用于删除 一个或多个文件。我们后面将介绍如何删除目录。
rename 命令在哪里?
如果您熟悉 DOS 或 Windows® 系统,您会发现使用 mv
重命名文件有点怪异。Linux确实提供了 rename
命令,但是该命令与 DOS 和 Windows 下相同名称的命令具有不同的语法。查看手册页,了解该命令的使用详情。
清单 4 演示了 cp
和 mv
的使用,它们对我们的文本文件执行了一些备份复制。我们使用 ls -i
展示其中一些文件的inode。
mkdir
命令创建一个备份子目
录
我们为文本 1
生成第二个备份副本,这一次是在备份目录中,并显示出所有三个文件都具有不同的 inode
。
随后将 text1.bkp
移动到备份目录中,然后将其重命名,使其与第二个备份更加一致。我们本来可以使用一个单个命令完成这些操作,但是为了演示的目的,我们在这里使用了两个命令。
我们再次检查 inode
,然后确定 inode
为 934193
的 text1.bkp
不再存在于 lpi103-2
目录,但是该 inode
仍然为备份目录中的 text1.bkp.1
保留下来
。
清单 4. 复制和移动文件
[ian@echidna lpi103-2]$ cp text1 text1.bkp
[ian@echidna lpi103-2]$ mkdir backup
[ian@echidna lpi103-2]$ cp text1 backup/text1.bkp.2
[ian@echidna lpi103-2]$ ls -i text1 text1.bkp backup
933892 text1 934193 text1.bkp
backup:
934195 text1.bkp.2
[ian@echidna lpi103-2]$ mv text1.bkp backup
[ian@echidna lpi103-2]$ mv backup/text1.bkp backup/text1.bkp.1
[ian@echidna lpi103-2]$ ls -i text1 text1.bkp backup
ls: cannot access text1.bkp: No such file or directory
933892 text1
backup:
934193 text1.bkp.1 934195 text1.bkp.2
一般来说,cp
将在现有副本上复制文件,如果现有文件可写的话。另一方面,如果目标存在,mv
不会移动或重命名文件。有一些有用的选项与 cp
和 mv
的这种行为有关。
-f
或 --force
将促使 cp
尝试阐释一个现有目标文件,即使它不是可写的
-i
或 --interactive
将要求在尝试替换某个现有文件之前进行确认
-b
或 --backup
将为即将被替换的任何文件生成备份
和前面一样,您需要参考手册页来获得这些和其他复制和移动选项的详细内容。
清单 5 演示了备份复制和文件删除。
清单 5. 生成备份副本并删除文件
[ian@echidna lpi103-2]$ cp text2 backup
[ian@echidna lpi103-2]$ cp --backup=t text2 backup
[ian@echidna lpi103-2]$ ls backup
text1.bkp.1 text1.bkp.2 text2 text2.~1~
[ian@echidna lpi103-2]$ rm backup/text2 backup/text2.~1~
[ian@echidna lpi103-2]$ ls backup
text1.bkp.1 text1.bkp.2
注意,rm
命令还接受 -i
(交互式)和 -f
(强制选项)。当您使用 rm
删除文件后,文件系统将不再访问它。某些系统在默认情况下为根用户设置一个别名 alias rm='rm -i'
,以防止出现意外的文件删除。如果您担心会不小心删除文件的话,这对于普通用户来说也是一个好主意。
在结束这些内容的讨论之前,应当注意 cp
命令在默认情况下会为新的文件创建一个新的时间戳。所有者和组均被设置为执行复制的用户的所有者和组。-p
选项可能被用于保存选择的属性。注意,根用户可能为可以保留所有权的唯一用户。参考手册页获得详情。
我们已经了解了如何使用 mkdir
创建目录。现在我们将进一步查看 mkdir
并介绍 rmdir
,后者用于删除目录。
假设我们希望在 lpi103-2 目录中创建子目录 dir1 和 dir2。和前面介绍过的其他命令一样,mkdir
可以一次处理多个目录创建请求,如清单6 所示。
清单 6. 创建多个目录
[ian@echidna lpi103-2]$ mkdir dir1 dir2
注意,在成功完成后不会产生输出,但是您可以使用 echo $?
来确认退出代码确实为 0。
相反,如果您希望创建一个嵌入式的子目录,比如 d1/d2/d3,那么命令将会失败,因为 d1 和 d2 目录并不存在。幸运的是,mkdir
具有一个 -p
选项,它允许创建任何所需的父目录,如清单 7 所示。
清单 7. 创建父目录
[ian@echidna lpi103-2]$ mkdir d1/d2/d3
mkdir: cannot create directory `d1/d2/d3': No such file or directory
[ian@echidna lpi103-2]$ echo $?
1
[ian@echidna lpi103-2]$ mkdir -p d1/d2/d3
[ian@echidna lpi103-2]$ echo $?
0
使用 rmdir
命令删除目录正好与创建过程相反。同样,可以用 -p
选项来删除父目录。只有在目录为空的情况下才可以使用 rmdir
删除目录,因为不存在可以强制删除的选项。我们将在讨论 递归操作 时查看另一种可以完成这一特殊任务的方法。了解了这种方法后,您将很少会在命令行中使用 rmdir
,但是了解该命令仍然是有用的。
为了解释目录删除,我们将 text1 文件复制到目录 d1/d2 中,这样它就不会成为空目录。我们随后使用 rmdir
来删除刚刚用 mkdir
创建的所有目录。可以看到,d1和 d2 没有被删除,因为 d2 不为空。另一个目录则被删除,当我们从d2 删除text1 的副本时,我们只需要调用 rmdir -p
即可删除 d1 和 d2。
清单 8. 删除目录
[ian@echidna lpi103-2]$ cp text1 d1/d2
[ian@echidna lpi103-2]$ rmdir -p d1/d2/d3 dir1 dir2
rmdir: failed to remove directory `d1/d2': Directory not empty
[ian@echidna lpi103-2]$ ls . d1/d2
.:
backup sedtab text2 text4 text6 xab yab
d1 text1 text3 text5 xaa yaa
d1/d2:
text1
[ian@echidna lpi103-2]$ rm d1/d2/text1
[ian@echidna lpi103-2]$ rmdir -p d1/d2
到目前为止,我们使用的命令已经处理了一个单个文件,或者一些个别命名的文件。在本文的其余部分中,我们将查看处理多个文件的各种操作,递归式处理文件树的某一部分,保存或恢复多个文件或目录。
ls
命令有一个 -R
(注意为大写 “R”)选项,可以列出一个目录及其所有子目录。递归式操作只能应用于目录名;它不会在目录树中查找名为‘text1’ 之类的文件。您可以将 -R
与已经介绍过的其他选项结合使用。lpi103-2目录的递归式列表包括inode 号,如清单9 所示。
清单 9. 递归式显示目录列表
[ian@echidna lpi103-2]$ ls -iR
.:
934194 backup 933892 text1 933898 text3 933900 text5 933894 xaa 933896 yaa
933901 sedtab 933893 text2 933899 text4 933902 text6 933895 xab 933897 yab
./backup:
934193 text1.bkp.1 934195 text1.bkp.2
可以使用 -r
(或 -R
或 --recursive
)选项来使 cp
命令进入到源目录并以递归的方式复制目录。为了防止出现无穷递归,源目录本身可能不会被复制。清单10 展示了如何将lpi103-2 目录中的所有内容复制到copy1 子目录。我们使用 ls -R
展示生成的目录树。
清单 10. 递归复制
[ian@echidna lpi103-2]$ cp -pR . copy1
cp: cannot copy a directory, `.', into itself, `copy1'
[ian@echidna lpi103-2]$ ls -R
.:
backup copy1 sedtab text1 text2 text3 text4 text5 text6 xaa xab yaa yab
./backup:
text1.bkp.1 text1.bkp.2
./copy1:
text2 text3 text5 xaa yaa yab
我们前面提到,rmdir
只能删除空目录。我们可以使用 -r
(或 -R
或 --recursive
)选项来使 rm
命令同时删除文件和目录,如清单11 所示,我们将删除刚刚创建的copy1 目录和它包含的内容,包括备份子目录及其内容。
清单 11. 递归式删除
[ian@echidna lpi103-2]$ rm -r copy1
[ian@echidna lpi103-2]$ ls -R
.:
backup sedtab text1 text2 text3 text4 text5 text6 xaa xab yaa yab
./backup:
text1.bkp.1 text1.bkp.2
如果您具有不可写的文件,那么可能需要添加 -f
选项来强制删除。这通常由根用户在清理系统时执行,但是会发出警告,因为您有可能会不小心删除重要的数据。
通常,您需要对多个文件系统对象执行单一操作,而不需要像前面的递归操作一样对整个树进行操作。例如,您可能想要找出在 lpi103-2 中创建的所有文本文件的修改时间,而不需要列出分散的文件。尽管这很容易在小目录中实现,但是对于大型文件系统则非常困难。
要解决这个问题,可以使用 bash shell 中内置的通配符支持。这种支持也称为 “globbing”(因为它最初被实现为一个名为 /etc/glob 的程序),让您能够使用通配符模式指定多个文件。
包含任何 '?'、'*' 或 '[' 字符的字符串就是一个通配符模式。Globbing是指 shell(或另一个程序)将这些模式扩展为一组匹配该模式的参数的过程。这种匹配按照如下方式完成:
?
匹配任何单个字读。
*
匹配任何字符串,包括空字符串。
[
引入了一个字符类(character class)。字符类是一个非空字符串,以']' 结尾。匹配意味着需要与方括号中包括的任何单个字符相一致。这里需要考虑一些特殊的事项。
-
'*' 和 '?' 字符与它们自身匹配。如果在文件名中使用这些字符,那么需要注意适当的引用或转义。
-
由于字符串必须是非空的并以 ']' 终止,如果您需要匹配字符串的话,您必须将 ']' 放到字符串的首位。
-
两个字符之间的 '-' 字符表示一个范围,包括这两个字符和排序序列中介于这两个字符之间的所有字符。例如,[0-9a-fA-F]表示任何大写或小写十六进制数位。您可以通过将'-' 放到一个范围的首位或末位来匹配它。
-
如果范围的首个字符为 '!' 字符,那么它将对范围求余,即它将匹配剩余字符以外的所有字符。例如,[!0-9]表示除0 到9 之间数字的任何字符。将'!' 放在首位以外的任意位置都可以匹配它本身。注意'!' 也可以用于shell history 函数,因此需要小心地对它进行适当的转义。
注意:通配符模式和常规表达式模式具有一些共同点,但是它们是不同的。需要仔细留意。
Globbing 被单独应用到路径名的每个组成中。您无法匹配 '/',也不能把它包含在一个范围中。您可以在指定多个文件或目录名时使用它,例如在 ls
、cp
、mv
或 rm
命令中。在清单 12 中,我们首先创建一对名字奇怪的文件,然后对通配符模式使用 ls
和 rm
命令。
清单 12. 通配符模式示例
[ian@echidna lpi103-2]$ echo odd1>'text[*?!1]'
[ian@echidna lpi103-2]$ echo odd2>'text[2*?!]'
[ian@echidna lpi103-2]$ ls
backup text1 text2 text3 text5 xaa yaa
sedtab text[*?!1] text[2*?!] text4 text6 xab yab
[ian@echidna lpi103-2]$ ls text[2-4]
text2 text3 text4
[ian@echidna lpi103-2]$ ls text[!2-4]
text1 text5 text6
[ian@echidna lpi103-2]$ ls text*[2-4]*
text2 text[2*?!] text3 text4
[ian@echidna lpi103-2]$ ls text*[!2-4]* # Surprise!
text1 text[*?!1] text[2*?!] text5 text6
[ian@echidna lpi103-2]$ ls text*[!2-4] # Another surprise!
text1 text[*?!1] text[2*?!] text5 text6
[ian@echidna lpi103-2]$ echo text*>text10
[ian@echidna lpi103-2]$ ls *\!*
text[*?!1] text[2*?!]
[ian@echidna lpi103-2]$ ls *[x\!]*
text1 text[*?!1] text10 text2 text[2*?!] text3 text4 text5 text6 xaa xab
[ian@echidna lpi103-2]$ ls *[y\!]*
text[*?!1] text[2*?!] yaa yab
[ian@echidna lpi103-2]$ ls tex?[[]*
text[*?!1] text[2*?!]
[ian@echidna lpi103-2]$ rm tex?[[]*
[ian@echidna lpi103-2]$ ls *b*
sedtab xab yab
backup:
text1.bkp.1 text1.bkp.2
[ian@echidna lpi103-2]$ ls backup/*2
backup/text1.bkp.2
[ian@echidna lpi103-2]$ ls -d .*
. ..
注意:
结合使用 '*' 会出现一些意外。模式 '*[!2-4]' 匹配名称中不包含 2 、3 或 4 的最长的一部分,这部分可以同时被 text[*?!1] 和 text[2*?!] 匹配。现在所有这些意外都应该清楚了。 和前面的ls
示例一样,如果模式扩展导致一个目录名,并且没有指定
-d
选项,那么该目录的内容将被列出(和前例中的模式 '*b*'
一样)。
如果一个文件名以句点 (.)
开头,那么该字符必须被明确匹配。注意,只有最后一个
ls
命令列出两个特殊的目录条目(.
和 ..
)。
请注意,命令中的任何通配符都可以被 shell 扩展,这将导致意外的结果。并且,如果您指定一个不匹配任何文件系统对象的模式,那么 POSIX 要求原始模式字符串被传递给命令。一些早期的实现将一个null 列表传递给命令,因此您可能会遇到一些表现出异常行为的老脚本。我们将在清单13 中解释这些内容。
清单 13. 通配符模式异常
[ian@echidna lpi103-2]$ echo text*
text1 text10 text2 text3 text4 text5 text6
[ian@echidna lpi103-2]$ echo "text*"
text*
[ian@echidna lpi103-2]$ echo text[[\!?]z??
text[[!?]z??
有关 globbing 的更多信息,请查阅 man7 glob
。您将需要章节号,因为第 3 节中也介绍了 glob 信息。理解所有不同 shell 交互的最佳方式是进行实践,因此您需要多多尝试这些通配符。注意,在使用 cp
、mv
或 rm
出现异常行为之前,使用 ls
检查您的通配符模式。
对文件执行 Touch 命令
我们现在来看看 touch
命令,它将更新文件访问和修改时间或创建空文件。在下一部分中,我们将探讨如何使用这些信息查找文件和目录。我们将继续在示例中使用lpi103-2 目录。我们还将查看各种指定时间戳的方法。
不包含任何选项的 touch
命令使用一个或多个文件名作为参数,并将更新文件的修改时间。这个时间戳通常会和一个长目录列表一同显示。在清单14 中,我们使用 echo
创建了一个小文件 f1,然后使用一个长目录列表来显示修改时间(即 mtime)。在本例中,修改时间正好是文件的创建时间。我们随后使用 sleep
命令来等待 60 秒,并在此运行 ls
。注意,该文件的时间戳已经修改了一段时间。
清单 14. 使用 touch 更新修改时间
[ian@echidna lpi103-2]$ echo xxx>f1; ls -l f1; sleep 60; touch f1; ls -l f1
-rw-rw-r--. 1 ian ian 4 2009-08-14 18:24 f1
-rw-rw-r--. 1 ian ian 4 2009-08-14 18:25 f1
如果为一个并不存在的文件指定文件名,那么 touch
通常会为您创建一个空文件,除非您指定了 -c
或 --no-create
选项。清单 15 展示了这两个命令。注意,只有f2 被创建。
清单 15. 使用 touch 创建空文件
[ian@echidna lpi103-2]$ touch f2; touch -c f3; ls -l f*
-rw-rw-r--. 1 ian ian 4 2009-08-14 18:25 f1
-rw-rw-r--. 1 ian ian 0 2009-08-14 18:27 f2
touch
命令还可以将文件的修改时间(也称为 mtime)设置为一个特定日期和时间,可以使用 -d
或 -t
选项。-d
可以非常灵活地处理它将接受的日期和时间格式,而 -t
选项需要至少一个MMDDhhmm 时间,年份和秒数是可选的。清单16 展示了一些例子。
清单 16. 使用 touch 设置 mtime
[ian@echidna lpi103-2]$ touch -t 200908121510.59 f3
[ian@echidna lpi103-2]$ touch -d 11am f4
[ian@echidna lpi103-2]$ touch -d "last fortnight" f5
[ian@echidna lpi103-2]$ touch -d "yesterday 6am" f6
[ian@echidna lpi103-2]$ touch -d "2 days ago 12:00" f7
[ian@echidna lpi103-2]$ touch -d "tomorrow 02:00" f8
[ian@echidna lpi103-2]$ touch -d "5 Nov" f9
[ian@echidna lpi103-2]$ ls -lrt f*
-rw-rw-r--. 1 ian ian 0 2009-07-31 18:31 f5
-rw-rw-r--. 1 ian ian 0 2009-08-12 12:00 f7
-rw-rw-r--. 1 ian ian 0 2009-08-12 15:10 f3
-rw-rw-r--. 1 ian ian 0 2009-08-13 06:00 f6
-rw-rw-r--. 1 ian ian 0 2009-08-14 11:00 f4
-rw-rw-r--. 1 ian ian 4 2009-08-14 18:25 f1
-rw-rw-r--. 1 ian ian 0 2009-08-14 18:27 f2
-rw-rw-r--. 1 ian ian 0 2009-08-15 02:00 f8
-rw-rw-r--. 1 ian ian 0 2009-11-05 00:00 f9
如果您不确定某个日期表达式所表示的日期,那么可以使用 date
命令确定日期。它还接受 -d
选项并解析与 touch
相同的日历格式。
可以使用 -r
(或 --reference
)选项以及一个引用文件名 来表示 touch
(或 date
)应当使用现有文件的时间戳。清单17 给出了一些示例。
清单 17. 引用文件的时间戳
[ian@echidna lpi103-2]$ date
Fri Aug 14 18:33:48 EDT 2009
[ian@echidna lpi103-2]$ date -r f1
Fri Aug 14 18:25:50 EDT 2009
[ian@echidna lpi103-2]$ touch -r f1 f1a
[ian@echidna lpi103-2]$ ls -l f1*
-rw-rw-r--. 1 ian ian 4 2009-08-14 18:25 f1
-rw-rw-r--. 1 ian ian 0 2009-08-14 18:25 f1a
Linux 系统同时记录文件修改 时间和文件访问 时间。这两个时间也分别被称为 mtime 和 atime。当文件被创建时,这两个时间戳均被设置为相同的值,在文件被修改时,两个值同时被重置。如果文件被访问过,那么访问时间将被更新,即使文件未被修改。对于我们的最后一个 touch
例子,我们将查看文件访问 时间。-a
(或 --time=atime
、--time=access
或 --time=use
)选项表示访问时间应该被更新。清单 18 使用 cat
命令访问 f1 文件并显示其内容。我们随后使用 ls -l
和 ls -lu
分别显示 f1 和 f1a 的修改和访问时间,f1a 是使用 f1 作为引用文件创建的。我们随后使用 touch -a
将 f1 的访问时间重置为 f1a 的访问时间,然后检验它是否被重置。
清单 18. 访问时间和修改时间
[ian@echidna lpi103-2]$ ls -lu f1*
-rw-rw-r--. 1 ian ian 4 2009-08-14 18:39 f1
-rw-rw-r--. 1 ian ian 0 2009-08-14 18:25 f1a
[ian@echidna lpi103-2]$ ls -l f1*
-rw-rw-r--. 1 ian ian 4 2009-08-14 18:25 f1
-rw-rw-r--. 1 ian ian 0 2009-08-14 18:25 f1a
[ian@echidna lpi103-2]$ touch -a -r f1a f1
[ian@echidna lpi103-2]$ ls -lu f1*
-rw-rw-r--. 1 ian ian 4 2009-08-14 18:25 f1
-rw-rw-r--. 1 ian ian 0 2009-08-14 18:25 f1a
有关大量可用日期和时间规范的更多完整信息,请参考手册或信息页中有关 touch
和 date
命令的内容。
现在我们已经介绍了文件和目录主题中的递归和 globbing 内容,递归涉及文件的所有方面,而 globbing 更具有针对性,让我们看看 find
命令,该命令更像是外科医生的手术刀。find
命令用于根据名称、时间戳或大小等条件查找一个或多个目录树中的文件。我们再一次使用lpi103-2 目录。
find
命令将使用全名或部分名称搜索文件或目录,或者按照其他搜索条件搜索,例如大小、类型、文件所有者、创建日期或最近一次的访问时间。最基本的查找是按全名或部分名称查找。清单19 展示了lpi103-2 目录中的一个例子,我们将查找文件名中包含'1' 或'k' 的所有文件,然后执行一些路径搜索,下面的说明部分将加以解释。
清单 19. 按名称查找文件
[ian@echidna lpi103-2]$ find . -name "*[1k]*"
./f1a
./f1
./text10
./backup
./backup/text1.bkp.1
./backup/text1.bkp.2
./text1
[ian@echidna lpi103-2]$ find . -ipath "*ACK*1"
./backup/text1.bkp.1
[ian@echidna lpi103-2]$ find . -ipath "*ACK*/*1"
[
说明:
您可以使用 shell 通配符模式,比如前面 通配符和 globbing 小节中的模式 。 可以使用-path
代替
-name
来匹配完全路径而不是基本文件名。在本例中,模式可能跨越路径组件,这与普通通配符匹配不同,后者只匹配路径的某个部分。
如果希望进行不区分大小写的搜索,如前面的
ipath
所示,那么在搜索字符串或模式的
find
选项前面加一个 'i'
。
如果希望搜索名称以点开头的文件或目录名,比如 .bashrc
或当前目录 (.)
,那么您必须将一个前导圆点指定为模式的一部分。否则,名称搜索将忽略这些文件或目录。
在上面的第一个例子中,我们查找所有文件和一个目录(./backup)。使用 -type
参数以及一个单字母类型来限制搜索。使用 'f' 表示普通文件,使用 'd' 查找目录,使用 'l' 查找符号链接。参考手册页,获得 find
的其他可能类型。清单 20 展示了只搜索目录的结果(-type d
)和文件名(*,在本例中可为任何内容)搜索结果。
清单 20. 按类型搜索文件
[ian@echidna lpi103-2]$ find . -type d
.
./backup
[ian@echidna lpi103-2]$ find . -type d -name "*"
.
./backup
注意,-type d
不包含任何形式的名称指定,将显示在其名称中具有一个前导圆点的目录(在本例中只包括当前目录),和通配符“*” 的作用相同。
我们还可以按照文件大小进行搜索,可以搜索具有指定大小的文件 (n),或者搜索文件大小大于 (+n) 或小于 (-n) 某个给定值的文件。通过使用上限和下限大小,我们可以查找大小在某个给定范围内的文件。默认情况下,find
的 -size
选项假设包含 512 字节的块为一个单位,用 'b' 表示。此外,指定 'c' 表示字节,或指定 'k' 表示千字节。在清单 21 中,我们首先查找大小为 0 的所有文件,然后查找大小为24 或 25 字节的所有文件。注意,指定 -empty
而不是 -size 0
也将查找空文件。
清单 21. 按大小查找文件
[ian@echidna lpi103-2]$ find . -size 0
./f1a
./f6
./f8
./f2
./f3
./f7
./f4
./f9
./f5
[ian@echidna lpi103-2]$ find . -size -26c -size +23c -print
./text2
./text5
./backup/text1.bkp.1
./backup/text1.bkp.2
./text1
清单 21 中的第二个例子引入了 -print
选项,它演示了一个可能会对搜索返回的结果执行的操作。在 bash shell 中,如果没有指定任何操作的话,那么这将是默认操作。在某些系统和 shell 中,需要指定一个操作;否则,不会生成输出。
其他操作包括 -ls
(输出文件信息,与 ls -lids
命令的输出差不多)和 -exec
(为每个文件执行一个命令)。-exec
必须以分号终止,必须对分号进行转义,以避免 shell 首先解释分号。如果希望在命令中使用返回的文件,还必须指定{}。记住,花括号对于shell 也是有意义的,因此必须进行转义(或引用)。清单22 展示了如何使用 -ls
和 -exec
选项列出文件信息。注意第二个表单没有列出inode 信息。
清单 22. 查找和处理文件
[ian@echidna lpi103-2]$ find . -size -26c -size +23c -ls
933893 4 -rw-rw-r-- 1 ian ian 25 Aug 11 14:27 ./text2
933900 4 -rw-rw-r-- 1 ian ian 24 Aug 11 18:47 ./text5
934193 4 -rw-rw-r-- 1 ian ian 24 Aug 12 15:36 ./backup/text1.bkp.1
934195 4 -rw-rw-r-- 1 ian ian 24 Aug 12 15:36 ./backup/text1.bkp.2
933892 4 -rw-rw-r-- 1 ian ian 24 Aug 11 14:02 ./text1
[ian@echidna lpi103-2]$ find . -size -26c -size +23c -exec ls -l '{}' \;
-rw-rw-r--. 1 ian ian 25 2009-08-11 14:27 ./text2
-rw-rw-r--. 1 ian ian 24 2009-08-11 18:47 ./text5
-rw-rw-r--. 1 ian ian 24 2009-08-12 15:36 ./backup/text1.bkp.1
-rw-rw-r--. 1 ian ian 24 2009-08-12 15:36 ./backup/text1.bkp.2
-rw-rw-r--. 1 ian ian 24 2009-08-11 14:02 ./text1
-exec
选项可用于您能够想象到的所有用途。例如:find . -empty -exec rm '{}' \;
删除目录树中的所有空文件find . -name "*.htm" -exec mv'{}' '{}l' \;
将所有 .htm文件重命名为.html 文件。
在最后一个 find
例子中,我们使用由 touch
命令描述的时间戳来查找具有特定时间戳的文件。清单23 给出了三个例子:
-mtime -2
时,
find
命令将查找在最近两天以内修改的所有文件。在本例中,一天即指与当前日期和时间相关的 24
个小时。注意,如果您希望根据访问时间而不是修改时间查找文件,那么将使用
-atime
。
添加
-daystart
选项意味着我们希望使用日历计算天数,从午夜开始。现在,f3
已被排除在列表以外。
最后,我们将展示如何使用以分钟计算的时间范围而不是天数来查找在过去的 1
个小时(60
分钟)至 10
个小时(600
分钟)之间发生修改的文件。
清单 23. 按时间戳查找文件
[ian@echidna lpi103-2]$ date
Sat Aug 15 00:27:36 EDT 2009
[ian@echidna lpi103-2]$ find . -mtime -2 -type f -exec ls -l '{}' \;
-rw-rw-r--. 1 ian ian 0 2009-08-14 18:25 ./f1a
-rw-rw-r--. 1 ian ian 4 2009-08-14 18:25 ./f1
-rw-rw-r--. 1 ian ian 0 2009-08-13 06:00 ./f6
-rw-rw-r--. 1 ian ian 0 2009-08-15 02:00 ./f8
-rw-rw-r--. 1 ian ian 0 2009-08-14 18:27 ./f2
-rw-rw-r--. 1 ian ian 58 2009-08-14 17:30 ./text10
-rw-rw-r--. 1 ian ian 0 2009-08-14 11:00 ./f4
-rw-rw-r--. 1 ian ian 0 2009-11-05 00:00 ./f9
[ian@echidna lpi103-2]$ find . -daystart -mtime -2 -type f -exec ls -l '{}' \;
-rw-rw-r--. 1 ian ian 0 2009-08-14 18:25 ./f1a
-rw-rw-r--. 1 ian ian 4 2009-08-14 18:25 ./f1
-rw-rw-r--. 1 ian ian 0 2009-08-15 02:00 ./f8
-rw-rw-r--. 1 ian ian 0 2009-08-14 18:27 ./f2
-rw-rw-r--. 1 ian ian 58 2009-08-14 17:30 ./text10
-rw-rw-r--. 1 ian ian 0 2009-08-14 11:00 ./f4
-rw-rw-r--. 1 ian ian 0 2009-11-05 00:00 ./f9
[ian@echidna lpi103-2]$ find . -mmin -600 -mmin +60 -type f -exec ls -l '{}' \;
-rw-rw-r--. 1 ian ian 0 2009-08-14 18:25 ./f1a
-rw-rw-r--. 1 ian ian 4 2009-08-14 18:25 ./f1
-rw-rw-r--. 1 ian ian 0 2009-08-14 18:27 ./f2
-rw-rw-r--. 1 ian ian 58 2009-08-14 17:30 ./text10
find
命令的手册页可以帮助您了解各种选项,我们无法在这篇简短的介绍中一一介绍它们。
文件名通常具有一个后缀,比如 gif、jpeg 或 html,它将告诉您文件中可能包含的内容。Linux 不需要这种后缀,因此通常不会使用它们来识别文件类型。了解正在处理的文件的类型将使您知道程序将使用什么工具显示或操作文件。file
命令将使您了解到一个或多个文件中的数据的类型。清单 24 展示了使用 file
命令的一些例子。
清单 24. 识别文件内容
[ian@echidna lpi103-2]$ file backup text1 f2 ../p-ishields.jpg /bin/echo
backup: directory
text1: ASCII text
f2: empty
../p-ishields.jpg: JPEG image data, JFIF standard 1.02
/bin/echo: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically
linked (uses shared libs), for GNU/Linux 2.6.18, stripped
file
命令尝试使用三种测试来对每个文件进行分类。比如,文件系统测试使用 stat
命令的结果来判断某个文件或目录是否为空。名为 magic 的测试将检查文件中的特定内容,这些内容可以识别文件。这些签名也被称为魔术数字(magicnumber)。最后,语言测试将查看文本文件中的内容,尝试确定某个文件是 XML 文件、C 或 C++ 语言源文件、troff 文件,或是其他被认为是某些语言处理器的源文件。找到的第一种类型将被生成报告,除非 -k
或 --keep-going
选项被指定。
file
命令具有许多选项,您可以通过手册页获得详细介绍。清单25 展示了如何使用 -i
(或 --mime
)选项来将文件类型显示为MIME 字符串,而不是普通的人类可读输出。
清单 25. 将文件内容识别为 MIME
[ian@echidna lpi103-2]$ file -i backup text1 f2 ../p-ishields.jpg /bin/echo
backup: application/x-directory; charset=binary
text1: text/plain; charset=us-ascii
f2: application/x-empty; charset=binary
../p-ishields.jpg: image/jpeg; charset=binary
/bin/echo: application/x-executable; charset=binary
魔术数字文件也由 file
命令管理。请在此查看手册页获得更多信息。
注意:作为 ImageMagick 包的一部分,identify
命令可以用作额外一种工具,为识别图像文件类型提供更多细节。
当您备份、归档或传输文件时,通常会对文件进行压缩。在 Linux 环境中,两个流行的压缩程序为 gzip
和 bzip2
。gzip
命令使用Lempel-Ziv 算法,而 bzip2
使用Burrows-Wheeler 块分类算法。
使用 gzip 和 gunzip
压缩通常对文本文件比较有效。许多图像格式的文件已经进行了数据压缩,因此对这些或其他二进制文件进行压缩效果不大。为了演示对较大文本文件执行的压缩,让我们将/etc/services 复制到我们一直使用的目录中,并使用gzip 对它执行压缩,如清单26 所示。我们使用 cp
的 -p
选项保留/etc/services 的时间戳。注意,压缩后的文件具有相同的时间戳,并且具有一个.gz 后缀。
清单 26. 使用 gzip 进行压缩
[ian@echidna lpi103-2]$ cp -p /etc/services
[ian@echidna lpi103-2]$ ls -l serv*
-rw-r--r--. 1 ian ian 630983 2009-04-10 04:42 services
[ian@echidna lpi103-2]$ gzip services
[ian@echidna lpi103-2]$ ls -l serv*
-rw-r--r--. 1 ian ian 124460 2009-04-10 04:42 services.gz
您使用 gzip
的 -d
选项解压缩使用 gzip 压缩过的文件,更常见的是使用 gunzip
命令。清单 27 展示了第一种方法。注意,未压缩的文件现在具有原始名和时间戳。
清单 27. 使用 gzip 解压缩
[ian@echidna lpi103-2]$ gzip -d services.gz
[ian@echidna lpi103-2]$ ls -l serv*
-rw-r--r--. 1 ian ian 630983 2009-04-10 04:42 services
使用 bzip2 和 bunzip2
bzip2
命令以与 gzip
类似的方式运行,如清单 28所示。
清单 28. 使用 bzip2 进行压缩
[ian@echidna lpi103-2]$ ls -l serv*
-rw-r--r--. 1 ian ian 630983 2009-04-10 04:42 services
[ian@echidna lpi103-2]$ bzip2 services
[ian@echidna lpi103-2]$ ls -l serv*
-rw-r--r--. 1 ian ian 113444 2009-04-10 04:42 services.bz2
[ian@echidna lpi103-2]$ bunzip2 services.bz2
[ian@echidna lpi103-2]$ ls -l serv*
-rw-r--r--. 1 ian ian 630983 2009-04-10 04:42 services
gzip 和 bzip2 之间的区别
根据设计,bzip2
的许多选项与 gzip
的选项是相似的,但是这两个命令不包含完全相同的选项。您可能已经注意到,在我们的两个例子中,未压缩的文件具有与原始文件相同的文件名和时间戳。然而,重命名或touch 已压缩文件会改变这一行为。gzip
命令的 -N
或 --name
选项会强制保留名称和时间戳,但是 bzip2
不会。gzip
命令还有一个 -l
选项,用于显示与已压缩文件有关的信息,包括将在解压缩时使用的名称。清单 29 解释了这两个命令之间的一些区别。
清单 29. gzip 和 bzip2 之间的区别
[ian@echidna ~]$ ls -l serv*
-rw-r--r--. 1 ian ian 630983 2009-04-10 04:42 services
[ian@echidna ~]$ gzip -N services
[ian@echidna ~]$ touch services.gz
[ian@echidna ~]$ mv services.gz services-x.gz
[ian@echidna ~]$ ls -l serv*
-rw-r--r--. 1 ian ian 124460 2009-09-23 14:08 services-x.gz
[ian@echidna ~]$ gzip -l services-x.gz
compressed uncompressed ratio uncompressed_name
124460 630983 80.3% services-x
[ian@echidna ~]$ gzip -lN services-x.gz
compressed uncompressed ratio uncompressed_name
124460 630983 80.3% services
[ian@echidna ~]$ gunzip -N services-x.gz
[ian@echidna ~]$ ls -l serv*
-rw-r--r--. 1 ian ian 630983 2009-04-10 04:42 services
[ian@echidna ~]$
[ian@echidna ~]$ bzip2 services
[ian@echidna ~]$ mv services.bz2 services-x.bz2
[ian@echidna ~]$ touch services-x.bz2
[ian@echidna ~]$ ls -l serv*
-rw-r--r--. 1 ian ian 113444 2009-09-23 14:10 services-x.bz2
[ian@echidna ~]$ bunzip2 services-x.bz2
[ian@echidna ~]$ ls -l serv*
-rw-rw-r--. 1 ian ian 630983 2009-09-23 14:10 services-x
[ian@echidna ~]$ rm services-x # Don't need this any more
gzip
和 bzip2
都接受来自 stdin 的输入。两者都支持用 -c
选项将输出导出到stdout。
bzip2
还有另外两个相关的命令。
bzcat
命令将文件解压缩到 stdout
,其作用等同于
bzip2 -dc
。
bzip2recover
命令尝试从受损害的 bzip2
文件中恢复数据。
手册页将帮助您了解 gzip
和 bzip2
的其他选项。
另外两个更旧一点的程序是 compress
和 uncompress
,它们仍然频繁出现在 Linux 和 UNIX 系统中。
此外,来自 Info-ZIP 项目的 zip
和 unzip
命令也在 Linux 中得到了实现。它们提供了跨平台的压缩功能,可以广泛应用于各种硬件和操作系统。注意,并不是所有操作系统都支持相同的文件属性或文件系统功能。如果您下载用zip 压缩过的产品文件并在Windows 系统上解压缩它,然后将结果文件传输到CD 或 DVD中以用于Linux 安装,您可能会遇到安装问题,原因在于,例如,Windows系统不支持原始的未压缩文件集所含的符号链接。
有关这些或其他压缩程序的更多信息,请参考相关的手册页。
tar
、cpio
和 dd
命令被常用于备份文件组甚至是整个分区,目的是归档文件或传输到另外一个用户或站点。LPIC-2认证中的考试201 关注更加详细的备份事项。
有三种常用的备份方法:
差异 或累积 备份是指对自上一次完全备份之后发生更改的所有内容进行备份。要执行恢复,需要最近一次的完全备份和最新的差异备份。 增量 备份是指只对自上一次增量备份后发生变化的内容执行备份。要执行恢复,需要最近一次的完全备份以及这次完全备份之后的所有增量备份(按顺序)。 完全 备份是指一次完整的备份,通常为整个文件系统、目录或相关文件组。这种备份所需的时间是三种备份中最长的,因此通常与其他两种方法的其中一种结合使用。这些命令以及本文中学习的其他命令为您提供了执行任何这些备份任务的工具。
tar
(最初来自 Tape ARchive)从一组输入文件或目录中创建一个归档文件,即 tarfile或 tarball;它还从此类归档中恢复文件。如果某个目录被作为输入提供给 tar
,所有文件和子目录都被自动包括,这使得 tar
可以非常方便地归档您的目录结构的子树。
输出可以被导入到文件、磁带或软盘之类的设备或是 stdout 中。输出位置使用 -f
选项指定。其他常见选项为:-c
用于创建归档,-x
用于提取归档,-v
用于详细输出,它将列出被处理的文件,-z
用于使用 gzip 压缩,而 -j
用于使用 bzip2 压缩。大部分 tar
选项包含使用一个连字符的简单格式和使用一对连字符的详细格式。这里解释了简单格式。参考手册页,获得有关详细格式和额外选项的内容。
清单 30 展示了如何使用 tar
创建lpi103-2 目录的备份。
清单 30. 使用 tar 备份 lpi103-2 目录
[ian@echidna lpi103-2]$ tar -cvf ../lpitar1.tar .
./
./text3
./yab
...
./f5
您通常希望压缩归档文件来节省空间或减少传输时间。tar
命令的 GNU 版本允许您使用一个单个选项实现此目的— -z
表示使用 gzip
压缩,而 -b
表示使用 bzip2
压缩。清单 31 演示了 -z
选项的使用,以及两种归档文件在大小方面的差异。
清单 31. 使用 gzip 压缩 tar 归档
[ian@echidna lpi103-2]$ tar -zcvf ../lpitar2.tar ~/lpi103-2/
tar: Removing leading `/' from member names
/home/ian/lpi103-2/
/home/ian/lpi103-2/text3
/home/ian/lpi103-2/yab
...
/home/ian/lpi103-2/f5
[ian@echidna lpi103-2]$ ls -l ../lpitar*
-rw-rw-r--. 1 ian ian 30720 2009-09-24 15:38 ../lpitar1.tar
-rw-rw-r--. 1 ian ian 881 2009-09-24 15:39 ../lpitar2.tar
清单 31 还显示了 tar
的另外一个重要特性。我们使用了一个绝对目录路径,而输出的第一行告诉我们 tar
正在从成员名称中删除前导斜线(/)。这允许将文件恢复到其他位置以进行检验,如果您尝试恢复系统文件的话,这一点尤其重要。如果您确实需要存储绝对名称,那么使用 -p
选项。在创建归档时,避免将绝对路径名和相对路径名混在一起始终是一种好的想法,因为在从归档恢复时,所有名称都将为相对名称。
tar
命令可以使用 -r
或 --append
选项将额外的文件附加到归档中。这会造成归档中出现文件的多个副本。在这种情况下,最后 一个文件将在恢复操作期间被恢复。您可以使用 --occurrence
选项从多个文件中选择一个特定文件。如果归档位于常规文件系统而非磁带中,那么可以使用 -u
或 --update
选项来更新一个归档。这种工作方式类似于附加到一个归档,区别在于,归档中的文件的时间戳将与文件系统中的时间戳进行比较,并且只有在归档版本之后发生修改的文件被附加。如前所述,这不适用于磁带归档。
tar
命令也可以将归档与当前文件系统加以比较,并恢复来自归档的文件。使用 -d
、--compare
或 --diff
选项执行比较。输出将显示内容有差异的文件,以及时间戳不同的文件。一般情况下,将只列出不同的文件(如果有的话)。使用前面讨论的 -v
选项获得详细输出。-C
或 -- directory
选项将告诉 tar
从指定的目录开始执行操作,而不是从当前目录开始。
清单 32 展示了一些例子。我们使用 touch
来修改 f1 文件的时间戳,然后演示了 tar
的比较操作,之后从其中一个归档中恢复 f1。我们将使用各种操作来进行说明。
清单 32. 使用 tar 执行比较和恢复
[ian@echidna lpi103-2]$ touch f1
[ian@echidna lpi103-2]$ tar --diff --file ../lpitar1.tar .
./f1: Mod time differs
[ian@echidna lpi103-2]$ tar -df ../lpitar2.tar -C /
home/ian/lpi103-2/f1: Mod time differs
[ian@echidna lpi103-2]$ tar -xvf ../lpitar1.tar ./f1 # See below
./f1
[ian@echidna lpi103-2]$ tar --compare -f ../lpitar2.tar --directory /
您为恢复指定的文件或目录必须匹配归档中的名称。在本例中,如果尝试只恢复 f1 而不是 ./f1,那么将是无效的。您可以使用 globbing,但是需要注意不要恢复过多或过少的内容。如果您不确定归档内容的话,您可以使用 --list
或 -t
选项来列出归档内容。清单33 展示了一个通配符说明示例,它将恢复更多的文件,而不仅仅是./f1。
清单 33. 使用 tar 列出归档内容
[ian@echidna lpi103-2]$ tar -tf ../lpitar1.tar "*f1*"
./f1a
./f1
可以使用 find
命令选择要归档的文件,然后将结果传递给tar。我们将在讨论 cpio
时探讨这种方法,但是同样的方法也可以用于 tar
。
和本文介绍的其他命令一样,这篇简要的介绍无法涵盖所有的选项。请参考手册页或信息页了解更多内容。
cpio
命令在 copy-out 模式下创建归档,在 copy-in模式下恢复归档,或在 copy-pass 模式下将一组文件从一个位置复制到另一个位置。您将对 copy-out 模式使用 -o
或 --create
选项,对 copy-in 模式使用 -i
或 --extract
选项,而对copy-pass 模式使用 -p
或 --pass-through
选项。输入是在stdin 中提供的一组文件。输出被指向stdout,或者是由 -f
或 --file
选项指定的设备或文件。
清单 34 展示了如何使用 find
命令生成一组文件,并将列表传递给 cpio
。注意,对 find
使用了 -print0
选项来为文件名生成null-terminate 字符串,而 cpio
上的对应的 --null
选项将读取这个格式。这将正确地处理包含内嵌空格或换行符的文件名。-depth
选项告诉 find
在目录名的前面列出目录条目。在本例中,我们仅仅创建了 lpi103-2 目录的两个归档,一个使用相对名称,一个使用绝对名称。我们没有使用 find
的众多功能对所选文件进行限制,比如只查找在本周修改过的文件。
清单 34. 使用 cpio 备份目录
[ian@echidna lpi103-2]$ find . -depth -print0 | cpio --null -o > ../lpicpio.1
3 blocks
[ian@echidna lpi103-2]$ find ~/lpi103-2/ -depth -print0 | cpio --null -o > ../lpicpio.2
4 blocks
如果您希望按照归档顺序列出文件,那么对 cpio
添加 -v
选项。
copy-in 模式下的 cpio
命令(选项 -i
或 --extract
)可以列出归档的内容或恢复所选文件。当您列出文件时,指定 --absolute-filenames
选项将减少无关消息的数量,较旧版本的 cpio
在从包含前导 / 字符的每个路径中分离该字符时会发出这些消息。该选项在许多最新实现中都被忽略。选择性列出前面的归档的输出如清单35 所示。
清单 35. 使用 cpio 列出和恢复所选文件
[ian@echidna lpi103-2]$ cpio -i --list "*backup*" < ../lpicpio.1
backup
backup/text1.bkp.1
backup/text1.bkp.2
3 blocks
[ian@echidna lpi103-2]$ cpio -i --list absolute-filenames "*text1*" < ../lpicpio.2
/home/ian/lpi103-2/text10
/home/ian/lpi103-2/backup/text1.bkp.1
/home/ian/lpi103-2/backup/text1.bkp.2
/home/ian/lpi103-2/text1
4 blocks
清单 36 展示了如何将路径中含有 “text1” 的所有文件恢复到一个临时子目录中。其中一些文件位于子目录中。与 tar
不同,如果目录树不存在的话,您将需要明确地指定 -d
或 --make-directories
选项。此外,cpio
不会使用归档副本替换文件系统中任何较新的文件,除非您指定了 -u
或 --unconditional
选项。
清单 36. 使用 cpio 恢复所选文件
[ian@echidna lpi103-2]$ mkdir temp
[ian@echidna lpi103-2]$ cd temp
[ian@echidna temp]$ cpio -idv "*f1*" "*.bkp.1" < ../../lpicpio.1
f1a
f1
backup/text1.bkp.1
3 blocks
[ian@echidna temp]$ cpio -idv "*.bkp.1" < ../../lpicpio.1
cpio: backup/text1.bkp.1 not created: newer or same age version exists
backup/text1.bkp.1
3 blocks
[ian@echidna temp]$ cpio -id --no-absolute-filenames "*text1*" < ../../lpicpio.2
cpio: Removing leading `/' from member names
4 blocks
./home/ian/lpi103-2/backup/text1.bkp.1
./home/ian/lpi103-2/backup/text1.bkp.2
./home/ian/lpi103-2/text1
./backup/text1.bkp.1
[ian@echidna temp]$ cd ..
[ian@echidna lpi103-2]$ rm -rf temp # You may remove these after you have finished
有关其他选项的更多细节,请参考手册页。
dd 命令
就其最简单形式而言,dd
命令将一个输入文件复制到一个输出文件。您已经了解过 cp
命令,因此您可能希望知道为什么还要用另外一个命令复制文件。dd
命令可以完成很多常规 cp
命令无法办到的事情。特别是,它可以对文件执行转换,比如将小写转换为大写,或将 ASCII转换为EBCDIC。它还可以重新阻塞(reblock)一个文件,当将文件传输给磁带时可能需要这样做。它还可以跳过或只包括所选的文件块。最后,它可以读取和写入原始设备,比如/dev/sda,这允许您创建和恢复作为完整分区映像的文件。写入到设备通常需要根权限。
我们将首先来看一个使用 conv
将文件转换为大写的简单例子,如清单 37 所示。我们使用 if
选项指定输入文件,而不是使用默认的 stdin。类似的 of
选项可用于覆盖 stdout 默认输出。为了演示的目的,我们使用 ibs
和 obs
选项指定了不同的输入和输出块大小。对于较大的文件,在进行磁盘之间的传输时,使用较大的块大小加速操作会比较方便。另外,块大小最常用于磁带。注意,清单末尾的三行状态代码表示有多少完整的和局部的块被读取和写入,以及被传输的数据的总量。
清单 37. 使用 dd 将文本转换为大写
[ian@echidna lpi103-2]$ cat text6
1 apple
2 pear
3 banana
9 plum
3 banana
10 apple
1 apple
2 pear
3 banana
9 plum
3 banana
10 apple
[ian@echidna lpi103-2]$ dd if=text6 conv=ucase ibs=20 obs=30
1 APPLE
2 PEAR
3 BANANA
9 PLUM
3 BANANA
10 APPLE
1 APPLE
2 PEAR
3 BANANA
9 PLUM
3 BANANA
10 APPLE
4+1 records in
3+1 records out
98 bytes (98 B) copied, 0.00210768 s, 46.5 kB/s
任何文件都可能是一个原始设备。这种情况通常出现在磁带中,但是一个完整的磁盘分区,比如 /dev/hda1 或 /dev/sda2,可以被备份到一个文件或磁带中。理想情况下,设备上的文件系统应当被解除挂载,或至少为只读挂载,从而确保数据在备份期间不会发生修改。清单39 展示了一个示例,其中输入文件是一个原始设备dev/sda3,而输出文件是位于根用户的主目录中的文件backup-1。要将文件转储到磁带或软盘中,将进行 of=/dev/fd0
或 of=/dev/st0
等指定。
清单 38. 使用 dd 备份分区
[root@echidna ~]# dd if=/dev/sda2 of=backup-1
1558305+0 records in
1558305+0 records out
797852160 bytes (798 MB) copied, 24.471 s, 32.6 MB/s
注意,797,852,160 字节的数据被复制,而输出文件也包含这么多的数据,即使实际上只会使用 3% 的分区。除非您使用硬件压缩将文件复制到磁盘,否则您将希望对数据进行压缩。清单39 展示了实现此目的的一种方法,以及 ls
和 df
命令的输出,向您展示了文件的大小和/dev/sda3 上的文件系统的使用百分比。
清单 39. 使用 dd 实现压缩备份
[root@echidna ~]# dd if=/dev/sda2 |gzip >backup-2
1558305+0 records in
1558305+0 records out
797852160 bytes (798 MB) copied, 23.4617 s, 34.0 MB/s
[root@echidna ~]# ls -l backup-[12]
-rw-r--r--. 1 root root 797852160 2009-09-25 17:13 backup-1
-rw-r--r--. 1 root root 995223 2009-09-25 17:14 backup-2
[root@echidna ~]# df -h /dev/sda2
Filesystem Size Used Avail Use% Mounted on
/dev/sda2 755M 18M 700M 3% /grubfile
gzip 压缩将文件大小减小到未压缩大小的 20%。然而,未使用的块可能包含任何数据,因此即使压缩过的备份也可能要比分区的总数据大。
如果按照处理的记录的数量对总字节进行划分,您将看到 dd
将写入多个包含 512 字节的数据块。当复制到磁带之类的原始输出设备中时,这将导致产生非常低效的操作。如前所述,指定 obs
选项来改变输出的大小,或用 ibs
选项来指定输入块大小。您还可以仅指定 bs
来将输入和输出块大小同时指定为一个常用值。在使用磁带时,记住使用相同的块大小来读取和写入磁带。
如果需要多个磁带或其他可移动存储来保存备份,那么需要使用 split
之类的工具将它们分为更小的部分。如果需要跳过磁盘或磁带标签之类的块,可以使用 dd
完成。参见手册页中的示例。
dd
命令无法被文件系统感知,因此需要恢复一个分区的转储才能知道分区的内容。清单40 展示了如何将清单39 中转储的文件恢复到分区/dev/sdc,该分区创建在可移动USB 驱动上,专门用于这个用途。
清单 40. 使用 dd 恢复分区
[root@echidna ~]# gunzip backup-2 -c | dd of=/dev/sdc7
1558305+0 records in
1558305+0 records out
797852160 bytes (798 MB) copied, 30.624 s, 26.1 MB/s
您可能对一些使用 dd
命令在幕后执行实际的设备写入工作的CD 和 DVD刻录应用程序感兴趣。如果您使用的工具提供了大量实际使用的命令,并且您现在对 dd
有了更多的了解,那么您会发现查看日志会大有裨益。实际上,如果您将一个ISO 映像刻录到一张CD 或 DVD磁盘,一种确定没有错误的方法就是使用 dd
回读磁盘并通过 cmp
工具传递结果。清单 41 使用我们在本文中创建的备份文件解释了这一常见技巧,而不是使用ISO 映像。注意,我们使用映像的文件大小计算读取的块的数量。
清单 41. 比较映像和文件系统
[root@echidna ~]# ls -l backup-1
-rw-r--r--. 1 root root 797852160 2009-09-25 17:13 backup-1
[root@echidna ~]# echo $(( 797852160 / 512 )) # calculate number of 512 byte blocks
1558305
[root@echidna ~]# dd if=/dev/sdc7 bs=512 count=1558305 | cmp - backup-1
1558305+0 records in
1558305+0 records out
797852160 bytes (798 MB) copied, 26.7942 s, 29.8 MB/s
流、管道和重定向
本文帮助您巩固重定向标准 I/O 流的基础 Linux 技术。您将学习如何:
重定向标准 I/O 流:标准输出和标准错 误 通过管道将一个命令的输出导入到另一个命令的输 入 将输出发送到 stdout 和文件 中 将命令输出用作另一个命令的参 数本文帮助您准备 Linux Professional Institute's Junior Level Administration(LPIC-1) 考试101 的主题103 下的考核目标103.4。该考核目标的权值为4。
在本文中,我们将使用在文章 “学习 Linux,101:文本流和过滤器” 中创建的一些文件练习命令。即使您没有阅读那篇文章或者没有保存所创建的文件,也一样能顺利学习本文。我们首先在您的主目录下创建一个名为lpi103-4 的子目录并在其中创建必要的文件。为此,在作为当前目录的主目录下打开一个文本窗口,将清单1 中的内容复制到文本窗口并运行命令。完成之后就创建了您将要使用的lpi103-4 子目录和文件。
清单 1. 创建示例文件
mkdir -p lpi103-4 && cd lpi103-4 && {
echo -e "1 apple\n2 pear\n3 banana" > text1
echo -e "9\tplum\n3\tbanana\n10\tapple" > text2
echo "This is a sentence. " !#:* !#:1->text3
split -l 2 text1
split -b 17 text2 y; }
您的窗口应该类似于清单 2,并且当前的目录为新创建的 lpi103-4 目录。
清单 2. 创建示例文件 - 输出
[ian@echidna ~]$ mkdir -p lpi103-4 && cd lpi103-4 && {
> echo -e "1 apple\n2 pear\n3 banana" > text1
> echo -e "9\tplum\n3\tbanana\n10\tapple" > text2
> echo "This is a sentence. " !#:* !#:1->text3
echo "This is a sentence. "
"This is a sentence. " "This is a sentence. ">text3
> split -l 2 text1
> split -b 17 text2 y; }
[ian@echidna lpi103-4]$
Linux shell(比如 Bash)接收或发送序列和字符串流 形式的输入或输出。每个字符都独立于与之相邻的字符。字符没有被组织成结构化记录或固定大小的块。不管实际的字符串流进入或来自文件、键盘、显示窗口或其他I/O 设备,都使用文件I/O 技术来访问流。Linuxshell 使用3 种标准的I/O 流,每种流都与一个文件描述符相关联:
stdout 是标准输出流,它显示来自命令的输出。它的文件描述符为 1 。 stderr 是标准错误流,它显示来自命令的错误输出。它的文件描述符为 2 。 stdin 是标准输入流,它为命令提供输入。它的文件描述符为 0 。输入流通常通过终端击键为程序提供输入。输出流通常向终端输出文本字符。最初的终端是 ASCII 打字机或显示终端,但现在更多是指图形桌面上的文本窗口。
如果您已经学习了文章 “学习 Linux,101:文本流和过滤器”,那么就熟悉本文的部分内容。
可以通过两种方法将输出重定向到文件:
n>
将输出从文件描述符 n 重定向到文件。您必须具有该文件的写权限。如果该文件不存在,将创建它。如果该文件已经存在,通常将覆盖所有现有内容,并且没有任何警告。
n>>
还可以将输出从文件描述符 n 重定向到一个文件中。这里也一样要求您具有该文件的写权限。如果该文件不存在,将创建它。如果该文件已经存在,输出将附加到现有的内容后面。
在 n> 或 n>> 中的 n 引用文件描述符。如果省略它,将执行标准输出。清单 3 在我们先前在lpi103-4 目录中创建的文件中使用重定向将标准输出和标准错误从 ls
命令分离出来。我们还显示将输出附加到现有文件中。
清单 3. 输出重定向
[ian@echidna lpi103-4]$ ls x* z*
ls: cannot access z*: No such file or directory
xaa xab
[ian@echidna lpi103-4]$ ls x* z* >stdout.txt 2>stderr.txt
[ian@echidna lpi103-4]$ ls w* y*
ls: cannot access w*: No such file or directory
yaa yab
[ian@echidna lpi103-4]$ ls w* y* >>stdout.txt 2>>stderr.txt
[ian@echidna lpi103-4]$ cat stdout.txt
xaa
xab
yaa
yab
[ian@echidna lpi103-4]$ cat stderr.txt
ls: cannot access z*: No such file or directory
ls: cannot access w*: No such file or directory
使用 n> 的输出重定向通常覆盖现有的文件。您可以使用 set
内置控件的 noclobber
选项对此进行控制。如果该选项已经设置,您可以使用n>| 覆盖它,如清单4 所示。
清单 4. 带有 noclobber 选项的输出重定向
[ian@echidna lpi103-4]$ set -o noclobber
[ian@echidna lpi103-4]$ ls x* z* >stdout.txt 2>stderr.txt
-bash: stdout.txt: cannot overwrite existing file
[ian@echidna lpi103-4]$ ls x* z* >|stdout.txt 2>|stderr.txt
[ian@echidna lpi103-4]$ cat stdout.txt
xaa
xab
[ian@echidna lpi103-4]$ cat stderr.txt
ls: cannot access z*: No such file or directory
[ian@echidna lpi103-4]$ set +o noclobber #restore original noclobber setting
在某些情况下,您可能想要将标准输出和标准错误都重定向到一个文件中。这通常为自动进程或后台作业而执行的,以便以后可以查看输出。使用&> 或&>> 同时将标准输出和标准错误重定向到同一个文件中。另一种方法是,首先重定向文件描述符 n,然后使用 m>&n 或 m>>&n 将文件描述符 m 重定向到同一个文件。例如,command 2>&1 >output.txt
不同于command >output.txt 2>&1
在第一种情况中,stderr被重定向到stdout 的当前位置,然后在将stdout 重定向到output.txt,但第二次重定向仅影响stdout,不影响stderr。在第二种情况中,stderr被重定向到stdout 的当前位置,即output.txt。我们在清单5 中显示了这些重定向。注意,在最后一个命令中先重定向标准错误在重定向标准输出,所以标准错误输出仍然打印在终端窗口中。
清单 5. 将两个流重定向到一个文件中
[ian@echidna lpi103-4]$ ls x* z* &>output.txt
[ian@echidna lpi103-4]$ cat output.txt
ls: cannot access z*: No such file or directory
xaa
xab
[ian@echidna lpi103-4]$ ls x* z* >output.txt 2>&1
[ian@echidna lpi103-4]$ cat output.txt
ls: cannot access z*: No such file or directory
xaa
xab
[ian@echidna lpi103-4]$ ls x* z* 2>&1 >output.txt # stderr does not go to output.txt
ls: cannot access z*: No such file or directory
[ian@echidna lpi103-4]$ cat output.txt
xaa
xab
不过,有时候您可能想要完全忽略标准输出或标准错误。为此,将选择的流重定向到空文件 /dev/null。清单 6 显示了如何从 ls
命令忽略错误输出,同时也使用 cat
命令显示 /dev/null 是空的。
清单 6. 使用 /dev/null 忽略输出
[ian@echidna lpi103-4]$ ls x* z* 2>/dev/null
xaa xab
[ian@echidna lpi103-4]$ cat /dev/null
就像可以重定向 stdout 和 stderr 流一样,我们也可以使用 < 操作符从文件重定向 stdin。如果您已经学习了文章 “学习 Linux,101:文本流和过滤器”,那么您可能还会记得我们在 sort 和 uniq 小节中使用 tr
命令将 text1 文件中的空格替换成制表符。在那个例子中我们使用来自 cat
命令的输出为 tr
命令创建标准输入。现在,我们没有必要调用 cat
,而是使用输入重定向将空格转换成制表符,如清单7 所示。
清单 7. 输入重定向
[ian@echidna lpi103-4]$ tr ' ' '\t'<text1
1 apple
2 pear
3 banana
Shell(包括 bash)还有存在 here-document的概念,它是另一种输入重定向形式。它将<< 和一个单词(比如END)结合构成一个标记,用来表示输入端。我们在清单8 中对此进行演示。
清单 8. 使用 here-document 的输入重定向
[ian@echidna lpi103-4]$ sort -k2 <<END
> 1 apple
> 2 pear
> 3 banana
> END
1 apple
3 banana
2 pear
您可能很想知道可不可以仅输入 sort -k2
和数据,然后按 Ctrl-d表明输入端。最简单的答案是,您可以这样做,但您必须先了解here-documents。详细的答案是,here-documents通常用于shell 脚本(脚本没有其他方式能够表明应该将脚本的哪一行看作输入)。因为shell 脚本通过广泛使用制表符来提供缩进,所以here-documents 还有另一个特点。如果您使用 <<- 而不是 <<,那么将消除前面的制表符。
在清单 9 中,我们使用命令行替换创建了一个强制制表符,然后创建了一个包含两个 cat
命令的 shell 脚本,这两个命令都从 here-document 读取数据。注意,我们使用 END 作为从终端读取的here-document 的标记。如果我们在该脚本中也使用END 作为标记,将导致提前结束输入。因此我们使用EOF 作为标记。在创建好脚本之后,我们使用点号 .
命令导入它,即在当前的shell 上下文中运行它。
清单 9. 使用 here-document 的输入重定向
[ian@echidna lpi103-4]$ ht=$(echo -en "\t")
[ian@echidna lpi103-4]$ cat<<END>ex-here.sh
> cat <<-EOF
> apple
> EOF
> ${ht}cat <<-EOF
> ${ht}pear
> ${ht}EOF
> END
[ian@echidna lpi103-4]$ cat ex-here.sh
cat <<-EOF
apple
EOF
cat <<-EOF
pear
EOF
[ian@echidna lpi103-4]$ . ex-here.sh
apple
pear
在本系列的后续文章中,我们将更详细地介绍命令替换和脚本。查看我们的 学习 Linux,101:LPIC-1 路线图 获得本系列所有文章的简介和链接。
在文章 吧学习 Linux,101:文本流和过滤器” 中,我们这样描述文本过滤:接收文本输入流并对文本执行一些转换,然后在发送到输出流的过程。这种过滤通常是通过构造命令管道线 来完成的,其中来自一个命令的输出被导入 或重定向 为下一个命令的输入。管道的这种使用方式并不局限于文本流,尽管这是它的最常见用法。
通过管道将 stdout 导入到 stdin
在两个命令之间使用管道 | 操作符将的一个命令的 stdout 指向第二个命令的 stdin。您可以通过添加更多的命令和管道操作符来构造更长的管道线。任何命令都可能包含选项或参数。许多命令使用连字符 (-) 取代文件名作为一个参数,用于表示输入来自stdin 而不是文件。查看手册页确保正确使用命令。构造由多个命令(每个命令都有特定的功能)组成的长管道线是在Linux 和 UNIX® 中用于完成任务的常见方法。在清单 10 的假设管道线中,command2
和 command3
都带有参数,但 command3
仅使用 -
参数表示来自 stdin 的输入。
清单 10. 通过管道从几个命令导出输出
command1 | command2 paramater1 | command3 parameter1 - parameter2 | command4
需要说明的是,管道线仅将stdout 导向stdin。您不能使用2| 单独导出stderr,至少使用我们目前所了解的工具还不能这样做。如果stderr 已被重定向到stdout,那么两个流都会被通过管道导出。在清单11 中,我们展示了一个不太现实的 ls
命令,它有 4 个不是按字母顺序出现的通配符参数,然后使用一个管道对包含正常和错误输出内容的进行分类。
清单 11. 使用管道导出两个输出流
[ian@echidna lpi103-4]$ ls y* x* z* u* q*
ls: cannot access z*: No such file or directory
ls: cannot access u*: No such file or directory
ls: cannot access q*: No such file or directory
xaa xab yaa yab
[ian@echidna lpi103-4]$ ls y* x* z* u* q* 2>&1 |sort
ls: cannot access q*: No such file or directory
ls: cannot access u*: No such file or directory
ls: cannot access z*: No such file or directory
xaa
xab
yaa
yab
Linux 和 UNIX 系统中的管道的优点之一是,与其他流行的操作系统不同,它们的管道不涉及到中间文件。第一个命令的stdout 没有写到一个文件中,然后再由第二个命令读取。在文章“学习 Linux,101:文件和目录管理” 中,您学习了如何使用 tar
命令在一个步骤中归档和压缩文件。即使您使用的UNIX 系统的 tar
命令不支持使用 -z
(gzip)或 -j
(bzip2)进行压缩也不成问题。您可以使用这样的管道bunzip2 -c somefile.tar.bz2 | tar -xvf -
完成该任务。
使用文件而不是 stdout开始管道线
在以上的管道线中,我们在开始时使用一些生成输出的命令,然后通过管道线的每个阶段导出输出。如果我们要以现有的文件开始,应该怎么办呢?许多命令都接受stdin 或文件作为输入,因此这不成问题。如果您有要求来自stdin 的输出的过滤器,那么可以考虑使用 cat
命令将文件复制到stdout。不过,您可以对第一个命令使用输入重定向,然后在剩余的管道下中导出该命令的输出,这是更加常见的解决方案。仅需使用< 操作符将第一个命令的stdin 重定向到需要处理的文件。
在前面对管道线的讨论中,您学习了如何接受一个命令的输出,并将它用作另一个命令的输入。反过来,假设您想将一个命令或文件的内容作为另一个命令的参数而不是输入。管道线不能用于实现该目的。三种常见的解决办法是:
xargs
命
令
带有
-exec
选项的
find
命
令
命令替
换
您将首先了解第一个解决办法。我们曾经在清单 9 中创建了一个强制制表符,您可以从中看到命令替换的例子。可以在命令行上使用命令替换,但在脚本中使用它则更常见;您将在本系列的后续文章中更多地了解它和脚本。查看我们的 学习 Linux,101:LPIC-1 路线图 获得本系列所有文章的简介和链接。
使用 xargs
命令
xargs
命令读取标准的输入,然后使用参数作为输入构建和执行命令。如果没有给出命令,那么将使用 echo
命令。清单 12 是使用我们的 text1 文件的基础例子,它包含 3 个行,每行只有两个单词。
清单 12. 使用 xargs
[ian@echidna lpi103-4]$ cat text1
1 apple
2 pear
3 banana
[ian@echidna lpi103-4]$ xargs<text1
1 apple 2 pear 3 banana
为什么 xargs
只有一行输出?默认情况下,xargs
在空格处中断输出,并且每个生成的标记都成为一个参数。不过,当 xargs
构建命令时,它将一次传递尽可能多的参数。您可以使用 -n
覆盖该行为,或使用 --max-args
参数。在清单13 中,我们使用了这两种方法,并为使用 xargs
添加一个显式的 echo
调用。
清单 13. 使用 xargs
和 echo
[ian@echidna lpi103-4]$ xargs<text1 echo "args >"
args > 1 apple 2 pear 3 banana
[ian@echidna lpi103-4]$ xargs --max-args 3 <text1 echo "args >"
args > 1 apple 2
args > pear 3 banana
[ian@echidna lpi103-4]$ xargs -n 1 <text1 echo "args >"
args > 1
args > apple
args > 2
args > pear
args > 3
args > banana
如果输入包含由单引号或双引号保护的空格,或使用了斜杠进行转义,那么 xargs
将不在遇到这些空格时中断。清单 14 显示了这些空格点。
清单 14. 使用带引号的 xargs
[ian@echidna lpi103-4]$ echo '"4 plum"' | cat text1 -
1 apple
2 pear
3 banana
"4 plum"
[ian@echidna lpi103-4]$ echo '"4 plum"' | cat text1 - | xargs -n 1
1
apple
2
pear
3
banana
4 plum
到目前为止,已经在命令的末尾添加了所有参数。如果您需要在这些参数后面再使用其他参数,可以使用 -I
选项指定一个替换字符串。如果 xargs
将要执行的命令包含有替换字符串,那么将使用参数替换它。进行了替换之后,仅将参数传递给每个命令。不过,将从一整行输出创建参数,而不仅是一个标记。您还可以使用 xargs
的 -L
选项让命令将行当作参数看待,而不是默认的以单个空格分隔的标记。使用 -I
选项表示 -L 1
。清单 15 显示了使用 -I
和 -L
选项的例子。
清单 15. 使用带有输入行的 xargs
[ian@echidna lpi103-4]$ xargs -I XYZ echo "START XYZ REPEAT XYZ END" <text1
START 1 apple REPEAT 1 apple END
START 2 pear REPEAT 2 pear END
START 3 banana REPEAT 3 banana END
[ian@echidna lpi103-4]$ xargs -IX echo "<X><X>" <text2
<9 plum><9 plum>
<3 banana><3 banana>
<10 apple><10 apple>
[ian@echidna lpi103-4]$ cat text1 text2 | xargs -L2
1 apple 2 pear
3 banana 9 plum
3 banana 10 apple
尽管我们的例子为了便于演示使用了简单的文本文件,您很少看到包含这样的输入的 xargs
。您通常需要处理某些命令生成的大量文件,这些命令包括 ls
、find
或 grep
。清单 16 显示了一种通过 xargs
将目录清单传递到命令(比如 grep
)的方法。
清单 16. 使用带有多个文件的 xargs
[ian@echidna lpi103-4]$ ls |xargs grep "1"
text1:1 apple
text2:10 apple
xaa:1 apple
yaa:1
如果上一个例子中的一个或多个文件名包含空格,那么会发生什么呢?如果您像清单 16 那样使用该命令,那么将得到一个错误。在实际情况中,文件列表可能来自一些源,比如定制脚本或命令,而不是 ls
,或者您希望通过其他管道线阶段传递它,以进一步进行过滤。所以您应该使用 grep "1" *
取代以上构造。
对于 ls
命令,您可以使用 --quoting-style
选项强制给导致问题的文件名加上引号或进行转义。另外一种更好的解决办法是使用 xargs
的 -0
选项,从而使用 null 字符串 (\0) 分隔输入参数。尽管 ls
没有提供使用 null 字符串分隔的文件名作为输出的选项,但许多命令都提供这样的选项。
在清单 17 中,我们首先将 text1 复制到 “text 1”,然后显示一些在 xargs
命令中使用包含空格的文件名列表的方法。这些示例仅为了演示概念,因为 xargs
可能更加复杂。尤其是在最后一个例子中, 如果一些文件名已经包含新行字符串,那么将新行字符串转换成 null 字符串将导致错误。在本文的下一个部分中,我们将查看另外一个更加健壮的解决方案,即使用 find
命令生成合适的以 null 字符串分隔的输出。
清单 17. 文件名中包含空格的 xargs
[ian@echidna lpi103-4]$ cp text1 "text 1"
[ian@echidna lpi103-4]$ ls *1 |xargs grep "1" # error
text1:1 apple
grep: text: No such file or directory
grep: 1: No such file or directory
[ian@echidna lpi103-4]$ ls --quoting-style escape *1
text1 text\ 1
[ian@echidna lpi103-4]$ ls --quoting-style shell *1
text1 'text 1'
[ian@echidna lpi103-4]$ ls --quoting-style shell *1 |xargs grep "1"
text1:1 apple
text 1:1 apple
[ian@echidna lpi103-4]$ # Illustrate -0 option of xargs
[ian@echidna lpi103-4]$ ls *1 | tr '\n' '\0' |xargs -0 grep "1"
text1:1 apple
text 1:1 apple
xargs
命令不会构建任意长度的命令。在Linux 内核2.26.3 之前,命令的长度是受限制的。针对某个包含大量名称很长的文件的目录的命令,比如 rm somepath/*
,可能会失败,返回的消息表明参数列表太长。在更旧的Linux 系统或UNIX 系统上仍然存在该限制,因此了解如何使用 xargs
以处理这种问题非常有用。
您可以使用 --show-limits
选项显示 xargs
的默认限制,然后使用 -s
选项将输出命令的长度限制在允许的最大字符串数量之内。查看手册页了解其他未能再次讨论的选项。
使用带有 -exec
选项或 xargs
的 find
命令
在文章 “学习 Linux,101:文件和目录管理” 中,您学习例如如何使用 find
命令根据名称、修改时间、大小或其他特征查找文件。找到匹配的文件集之后,您通常希望对它们执行某些操作:删除、移动和重命名它们等。现在我们看一下 find
命令的 -exec
选项,其功能类似于使用 find
并通过管道将输出指向 xargs
。
清单 18. 使用 find
和 -exec
[ian@echidna lpi103-4]$ find text[12] -exec cat text3 {} \;
This is a sentence. This is a sentence. This is a sentence.
1 apple
2 pear
3 banana
This is a sentence. This is a sentence. This is a sentence.
9 plum
3 banana
10 apple
与前面学习的 xargs
命令相比,它有几个不同之处。
尝试运行 findtext[12] |xargs cat text3
亲自看看区别在哪里。
现在,我将话题转回到文件名中的空格。在清单 19 中我们尝试使用带有 -exec
的 find
,而不是带有 xargs
的 ls
。
清单 19. 对包含空格的文件名使用 find
和 -exec
[ian@echidna lpi103-4]$ find . -name "*1" -exec grep "1" {} \;
1 apple
1 apple
到目前为止,一切进展顺利。但是不是缺少了什么?哪个文件包含 grep
找到行?缺少了文件名,因为 find
为每个文件调用 grep
一次,而 grep
非常智能,能够知道您是不是仅提供文件名,您不需要它告诉您是哪个文件。
我们也可以改为使用 xargs
,但我们已经看到了文件名中包含空格时出现的问题。我们还提到 find
可以生成一个以 null 分隔符分隔的文件名列表,这是 -print0
选项所起的作用。新的 find
可能使用加号(+)取代分号(;)作为分隔符,这允许 find
在一次调用命令时传递尽可能多的名称,类似于 xargs
。在这种情况中,仅能使用{} 一次,并且它必须是该命令的最后一个参数。清单20 显示了这两种方法。
清单 20. 对包含空格的文件名使用 find
和 xargs
[ian@echidna lpi103-4]$ find . -name "*1" -print0 |xargs -0 grep "1"
./text 1:1 apple
./text1:1 apple
[ian@echidna lpi103-4]$ find . -name "*1" -exec grep "1" {} +
./text 1:1 apple
./text1:1 apple
一般而言,两种方法都是有效的,选择哪种方法由您决定。记住,使用管道导出包含未受保护的空格的内容将导致问题,因此如果您要使用管道将输出导出到 xargs
,请使用将 -print0
选项和 find
结合使用,并使用 -0
选项告诉 xargs
接收使用 null 分隔符分隔的输入。其他命令,包括 tar
,也支持使用 -0
选项并用 null 分隔符分隔的输入,因此应该对支持该选项的命令使用它,除非您能确保您的输入列表不会造成问题。
最后,我们介绍对文件列表进行操作。在执行删除或重命名文件等重要操作之前,最好彻底地测试列表和仔细测试命令。进行良好的备份也是非常有价值的。
这个小节简单地讨论另一个命令。有时候,您可能希望在屏幕上看到输出,同时保留一个副本。尽管您可以将命令输出重定向到一个窗口中的文件,然后使用 tail -fn1
在另一个屏幕中跟踪输出来实现该目的,但使用 tee
命令要简单得多。
您可以将 tee
和管道一起使用。对标准输出而言,参数是一个或多个文件。-a
选项附加而非覆盖文件。在前面关于管道的讨论中可以看到,必须先将 stderr 重定向到 stdout ,然后再重定向到 tee
,如果您需要同时保存两者的话。清单21 显示 用于将输出保存到文件 f1 和 f2 中的 tee
。
清单 21. 使用 tee 分离 stdout
[ian@echidna lpi103-4]$ ls text[1-3]|tee f1 f2
text1
text2
text3
[ian@echidna lpi103-4]$ cat f1
text1
text2
text3
[ian@echidna lpi103-4]$ cat f2
text1
text2
text3