GNU 和 UNIX 命令

 

使用命令行

本节讨论初级管理(LPIC-1)考试 101 的主题 1.103.1 的内容。这个主题的权值是 5。

在本节中,学习以下主题:

  • 使用命令行与 shell 和命令进行交互
  • 有效的命令和命令序列
  • 定义、引用和导出环境变量
  • 命令历史和编辑设施
  • 调用路径中和路径外的命令
  • 使用命令替换
  • 在目录树中递归地应用命令
  • 使用 man(手册)页了解命令

本节简要地介绍 bash shell 的一些主要特性,重点是那些对于认证很重要的特性。但是这个 shell 是一个非常丰富的环境,我们鼓励您进一步探索它。有许多优秀的书籍讨论 UNIX 和 Linux shell,尤其是 bash shell。

bash shell

bash shell 是 Linux 可以使用的几种 shell 之一。它也称为 Bourne-again shell,这个名字来自 Stephen Bourne,以前的一个 shell(/bin/sh)的创建者。bash 大体上与 sh 兼容,但是在函数和编程功能方面提供了许多改进。它结合了来自 Korn shell(ksh)和 C shell(csh)的特性,是一种符合 POSIX 的 shell。

在深入研究 bash 之前,要记住 shell 是一种接受并执行命令的程序。它还支持编程构造,允许从比较小的部分构建出复杂的命令。这些复杂的命令(即脚本)可以保存为文件,就成了新的命令。实际上,典型 Linux 系统上的许多命令 就是 脚本。

shell 有一些内置的 命令,比如 cdbreakexec。其他命令是外部的

shell 还使用三种标准 I/O

  • stdin标准输入流,它向命令提供输入。
  • stdout标准输出流,它显示来自命令的输出。
  • stderr标准错误流,它显示来自命令的错误输出。

输入流向命令提供输入,输入通常来自终端键盘。输出流输出文本字符,一般是在终端上。终端原来是 ASCII 打字机或显示终端,但是现在常常是图形桌面上的窗口。关于如何对这些标准 I/O 流进行重定向的更多细节,在本教程后面的 流、管道和重定向 一节中讨论。本节主要在较高层面上讨论重定向。

对于本教程的其余部分,假设您知道如何获得 shell 提示符。如果您不知道,developerWorks 文章 “Basic tasks for new Linux developers” 会教您如何执行这个任务和其他基本任务。

如果使用没有图形桌面的 Linux 系统,或者在图形桌面上打开了终端窗口,那么就会进入提示符,可能像清单 1 所示的这样。


清单 1. 一些典型的用户提示符


[db2inst1@echidna db2inst1]$
ian@lyrebird:~>
$

如果作为根用户(即超级用户)登录,那么提示符可能像清单 2 所示的一样。


清单 2. 超级用户(根)提示符示例


[root@echidna root]#
lyrebird:~ #
#

根用户有相当大的能力,所以使用它要谨慎。在 具有根特权时,大多数提示符的末尾有一个磅符号(#)。一般用户特权常常用另一个字符表示,通常是美圆符号($)。您机器上的实际提示符可能看起来与本教 程中的例子不一样。您的提示符可能包含用户名、主机名、当前目录、输出提示符的日期或时间等等。



回页首


本教程中的约定

针对 LPI 101 和 102 考试的这些 developerWorks 教程包含一些取自真实 Linux 系统的代码示例,使用了这些系统的默认提示符。我们的根提示符的末尾有 #,所以可以将它们与一般用户提示符(末尾有 $)区分开。在关于这个主题的许多书中都采用这种约定。在任何例子中都要仔细注意提示符。



回页首


命令和序列

现在有提示符了,我们来看看可以用它做什么。shell 的主要功能是解释用户的命令,从而使用户可以与 Linux 系统进行交互。在 Linux(和 UNIX)系统上,命令有一个命令名,然后是选项参数。一些命令没有选项和参数,一些命令有选项但没有参数,其他命令没有选项但有参数。

如果一行包含 # 字符,那么此行上的所有其余字符都被忽略。所以 # 字符既可以表示注释,也可以代表根提示符。具体意义应该很容易从上下文判断出来。

echo

echo 命令将它的参数输出(即回显)到终端上,如清单 3 所示。


清单 3. echo 示例


[ian@echidna ian]$ echo Word
Word
[ian@echidna ian]$ echo A phrase
A phrase
[ian@echidna ian]$ echo Where are my spaces?
Where are my spaces?
[ian@echidna ian]$ echo "Here are my spaces." # plus comment
Here are my spaces.

在清单 3 的第 3 个示例中,所有多余的空白在输出中压缩成单一空格。要避免这种情况,需要引用 字符串,可以使用双引号(")或单引号(')。bash 使用空白 (比如空格、制表符和新行字符)将输入行分割为记号(token),然后传递给命令。对字符串进行引用会保留多余的空白,并使整个字符串成为一个记号。在上面的例子中,命令名后面的每个记号都是一个参数,所以分别有 1、2、4 和 1 个参数。

echo 命令有两个选项。一般情况下,echo 将在最后在输出中附加一个新行字符。可以使用 -n 选项抑制这种行为。使用 -e 选项使某些用反斜线进行转义的字符具有特殊意义。表 3 显示了一部分转义字符。

表 3. echo 和转义的字符
转义
序列
功能
/a 警报(铃声)
/b 退格
/c 抑制末尾的新行字符(与 -n 选项的功能相同)
/f 换页(在视频显示器上就会清空屏幕)
/n 新行
/r 回车
/t 水平制表符

转义和行延续

在 bash 中使用反斜线有一个小问题。如果反斜线字符(/)不在引号中,那么它就作为转义,让 bash 保留后面字符的字面意义。这对于特殊的 shell 元字符是必要的,这个问题稍后讨论。这个规则有一个例外:反斜线后面跟着新行会使 bash 把这些字符序列视为行延续请求。这对于将长的行进行分割很方便,尤其是在 shell 脚本中。

要想让 echo 命令或使用相似转义控制字符的众多命令能够正确地处理上面描述的序列,必须将转义序列包含在引号中,或者作为引号中的字符串的一部分,否则就要使用第二个反斜线,让 shell 保留序列的字面意义。清单 4 显示 / 的各种用法。


清单 4. 更多的 echo 示例


[ian@echidna ian]$ echo -n No new line
No new line[ian@echidna ian]$ echo -e "No new line/c"
No new line[ian@echidna ian]$ echo "A line with a typed
> return"
A line with a typed
return
[ian@echidna ian]$ echo -e "A line with an escaped/nreturn"
A line with an escaped
return
[ian@echidna ian]$ echo "A line with an escaped/nreturn but no -e option"
A line with an escaped/nreturn but no -e option
[ian@echidna ian]$ echo -e Doubly escaped//n//tmetacharacters
Doubly escaped
metacharacters
[ian@echidna ian]$ echo Backslash /
> followed by newline /
> serves as line continuation.
Backslash followed by newline serves as line continuation.

注意,如果在输入的行中有不匹配的引号,那么 bash 会显示一个特殊的提示符(>)。输入字符串延续到第二行并包含一个新行字符。

bash shell 元字符和控制操作符

bash 有几个元字符, 如果不在引号中的话,这些字符也将输入分割为单词。 除了空格之外,还有 '|'、'&'、';'、'('、')'、'<' 和 '>'。在本教程的其他几节中将详细讨论其中一部分元字符。目前要注意,如果希望在文本中包含元字符,就必须将它放在引号中,或者用反斜线(/)进 行转义,如清单 4 所示。

新行字符和某些元字符或元字符对也作为控制操作符。 这包括 '||'、'&&'、'&'、';'、';;'、'|'、'(' 和 ')'。这些控制操作符中的一部分允许创建命令的序列列表

最 简单的命令序列只是由分号(;)分隔的两个命令。每个命令依次执行。在任何可编程环境中,命令都会返回一个表示成功或失败的标志;Linux 命令常常返回零值来表示成功,返回非零值表示失败。可以使用 && 和 || 控制操作符在命令列表中引入一些条件处理。如果用控制操作符 && 分隔两个命令,那么第二个命令当且仅当第一个命令返回值为零的退出码时执行。如果用 || 分隔命令,那么第二个命令当且仅当第一个命令返回非零退出码时执行。清单 5 显示了使用 echo 命令的一些命令序列。这里没有什么有意思的事儿,因为 echo 只是返回 0,但是在后面使用更多命令时您将看到更多的例子。


清单 5. 命令序列


[ian@echidna ian]$ echo line 1;echo line 2; echo line 3
line 1
line 2
line 3
[ian@echidna ian]$ echo line 1&&echo line 2&&echo line 3
line 1
line 2
line 3
[ian@echidna ian]$ echo line 1||echo line 2; echo line 3
line 1
line 3

退出

可以使用 exit 命令终止 shell。还可以可选地通过参数提供退出码。如果在图形桌面上的终端窗口中运行 shell,那么窗口将关闭。同样,如果使用 ssh 或 telnet 等命令连接到了远程系统,那么连接将终止。在 bash shell 中,还可以按住 Ctrl 键并按下 d 键来退出。

我 们来看看另一个控制操作符。如果将命令或命令列表放在圆括号中,那么命令或命令序列会在一个子 shell 中执行,所以 exit 命令会退出子 shell,而不是退出您当前工作的 shell。清单 6 显示用 && 和 || 进行组合的一个例子。


清单 6. 子 shell 和序列


[ian@echidna ian]$ (echo In subshell; exit 0) && echo OK || echo Bad exit
In subshell
OK
[ian@echidna ian]$ (echo In subshell; exit 4) && echo OK || echo Bad exit
In subshell
Bad exit

本教程中稍后会使用更多的命令序列。



回页首


环境变量

在 bash shell 中运行时,许多东西组成了环境,比如提示符的形式、主目录、工作目录、shell 的名称、已经打开的文件、已经定义的函数等等。环境包含许多变量,可能由 bash 设置,也可能由用户设置。bash shell 还允许有 shell 变量,可以将这些变量导出 到环境中,供 shell 中运行的其他进程使用,或者供从当前 shell 产生的其他 shell 使用。

环境变量和 shell 变量都有名称。通过在变量名前面加 '$' 来引用变量值。表 4 显示了一些常见的 bash 环境变量。

表 4. 一些常见的 bash 环境变量
名称 功能
USER 已登录用户的名称
UID 已登录用户的数字用户 id
HOME 用户的主目录
PWD 当前工作目录
SHELL shell 的名称
$ 进程 id(即正在运行的 bash shell 进程或其他进程的 PID
PPID 启动这个进程的进程的进程 id(即父进程的 id)
? 最后一个命令的退出码

清单 7 显示了通过这些常见的 bash 变量可以看到什么。


清单 7. 环境和 shell 变量


[ian@echidna ian]$ echo $USER $UID
ian 500
[ian@echidna ian]$ echo $SHELL $HOME $PWD
/bin/bash /home/ian /home/ian
[ian@echidna ian]$ (exit 0);echo $?;(exit 4);echo $?
0
4
[ian@echidna ian]$ echo $$ $PPID
30576 30575

如果不使用 bash,应该怎么办?

bash shell 是大多数 Linux 发行版上的默认 shell。如果不是运行在 bash shell 之下,那么可以考虑用以下方式之一对 bash shell 进行实践。

  • 使用
    chsh -s /bin/bash
    命令来改变默认 shell。这一修改将在下一次登录时生效。
  • 使用
    su - $USER -s /bin/bash
    命令创建另一个进程,作为当前 shell 的子进程。新进程将是使用 bash 的登录 shell。
  • 创建一个默认采用 bash shell 的 id,以便准备 LPI 考试。

创建或设置 shell 变量的方式是输入名称,后面直接跟着等号(=)。变量是大小写敏感的,所以 var1 和 VAR1 是不同的变量。按照约定,变量(尤其是导出的变量)是大写的,但这不是必须的。从技术上说,$$ 和 $? 是 shell 参数 而不是变量。只能引用它们,不能赋值。

在创建 shell 变量时,常常希望将它导出 到环境中,使得从这个 shell 启动的其他进程可以使用它。导出的变量对于父 shell 是 可用的。使用 export 命令导出变量名。作为 bash 中的快捷方式,可以在一步中同时对变量进行赋值和导出。

为了说明赋值和导出,我们在 bash shell 中运行 bash 命令,然后从这个新的 bash shell 运行 Korn shell(ksh)。我们将使用 ps 命令来显示正在运行的命令的相关信息。在本教程后面学习 进程状态 时,将进一步了解 ps


清单 8. 更多的环境和 shell 变量


[ian@echidna ian]$ ps -p $$ -o "pid ppid cmd"
PID PPID CMD
30576 30575 -bash
[ian@echidna ian]$ bash
[ian@echidna ian]$ ps -p $$ -o "pid ppid cmd"
PID PPID CMD
16353 30576 bash
[ian@echidna ian]$ VAR1=var1
[ian@echidna ian]$ VAR2=var2
[ian@echidna ian]$ export VAR2
[ian@echidna ian]$ export VAR3=var3
[ian@echidna ian]$ echo $VAR1 $VAR2 $VAR3
var1 var2 var3
[ian@echidna ian]$ echo $VAR1 $VAR2 $VAR3 $SHELL
var1 var2 var3 /bin/bash
[ian@echidna ian]$ ksh
$ ps -p $$ -o "pid ppid cmd"
PID PPID CMD
16448 16353 ksh
$ export VAR4=var4
$ echo $VAR1 $VAR2 $VAR3 $VAR4 $SHELL
var2 var3 var4 /bin/bash
$ exit
$ [ian@echidna ian]$ echo $VAR1 $VAR2 $VAR3 $VAR4 $SHELL
var1 var2 var3 /bin/bash
[ian@echidna ian]$ ps -p $$ -o "pid ppid cmd"
PID PPID CMD
16353 30576 bash
[ian@echidna ian]$ exit
[ian@echidna ian]$ ps -p $$ -o "pid ppid cmd"
PID PPID CMD
30576 30575 -bash
[ian@echidna ian]$ echo $VAR1 $VAR2 $VAR3 $VAR4 $SHELL
/bin/bash

注意:

  1. 在这个序列开始时,bash shell 的 PID 为 30576。
  2. 第二个 bash shell 的 PID 为 16353,它的父 shell 的 PID 是 30576,也就是原来的 bash shell。
  3. 我们在第二个 bash shell 中创建 VAR1、VAR2 和 VAR3,但是只导出 VAR2 和 VAR3。
  4. 在 Korn shell 中,创建 VAR4。echo 命令只显示了 VAR2、VAR3 和 VAR4 的值,这证明 VAR1 没有导出。尽管提示符改变了,但是 SHELL 变量的值没有改变。您觉得吃惊吗?不能总是依赖于 SHELL 来了解正在运行哪个 shell,但是 ps 命令可以指出实际的命令。注意,ps 在第一个 bash shell 前面放了一个连字符(-),表示这是登录 shell
  5. 回到第二个 bash shell,可以看到 VAR1、VAR2 和 VAR3。
  6. 最后,在返回原来的 shell 时,没有新变量仍然存在。

前 面讨论引用时提到,可以使用单引号,也可以使用双引号。这两种引号之间有一个重要的差异。shell 将双引号(")之间的 shell 变量展开,但是在使用单引号(')时不进行展开。在前面的示例中,我们在 shell 中启动另一个 shell,并获得新的进程 id。使用 -c 选项,可以将命令传递给另一个 shell,这个 shell 将执行命令并返回。如果将带引号的字符串作为命令传递,那么外层 shell 将去掉引号并传递字符串。如果使用双引号,那么变量展开发生在传递字符串 之前,所以结果可能不是您希望的。shell 和命令将运行在另一个进程中,所以它们有不同的 PID。清单 9 演示了这些概念。顶层 bash shell 的 PID 突出显示。


清单 9. 引用和 shell 变量


[ian@echidna ian]$ echo "$SHELL" '$SHELL' "$$" '$$'
/bin/bash $SHELL 19244 $$
[ian@echidna ian]$ bash -c "echo Expand in parent $$ $PPID"
Expand in parent 19244 19243
[ian@echidna ian]$ bash -c 'echo Expand in child $$ $PPID'
Expand in child 19297 19244

到 目前为止,所有的变量引用都以空格结束,所以变量名在哪里结束是很明显的。实际上,变量名只能由字母、数字或下划线字符组成。当找到另一个字符 时,shell 就知道变量名结束了。有时候,可能需要在含义不明确的表达式中使用变量。在这种情况下,可以使用花括号使变量名明确,见清单 10。


清单 10. 对变量名使用花括号


[ian@echidna ian]$ echo "-$HOME/abc-"
-/home/ian/abc-
[ian@echidna ian]$ echo "-$HOME_abc-"
--
[ian@echidna ian]$ echo "-${HOME}_abc-"
-/home/ian_abc-

env

env 命令如果不带任何选项或参数,就显示当前的环境变量。还可以使用它在定制的环境中执行命令。-i(或只是 -)选项在运行命令之前清理当前环境,而 -u 选项清除您不希望传递的环境变量。

清单 11 显示了不带任何参数的 env 命令的部分输出,然后是三个不用父环境调用不同 shell 的例子。在讨论之前,先认真看看这些例子。


清单 11. env 命令


[ian@echidna ian]$ env
HOSTNAME=echidna
TERM=xterm
SHELL=/bin/bash
HISTSIZE=1000
SSH_CLIENT=9.27.89.137 4339 22
SSH_TTY=/dev/pts/2
USER=ian
...
_=/bin/env
OLDPWD=/usr/src
[ian@echidna ian]$ env -i bash -c 'echo $SHELL; env'
/bin/bash
PWD=/home/ian
SHLVL=1
_=/bin/env
[ian@echidna ian]$ env -i ksh -c 'echo $SHELL; env'

_=/bin/env
PATH=/bin:/usr/bin
[ian@echidna ian]$ env -i tcsh -c 'echo $SHELL; env'
SHELL: Undefined variable.

注意,bash 已经设置了 SHELL 变量,但是没有将它导出到环境中,尽管 bash 在环境中创建了另外三个变量。在 ksh 示例中,有两个环境变量,但是试图回显 SHELL 变量的值时只得到了一个空行。最后,tcsh 没有创建任何环境变量,并在我们试图引用 SHELL 的值时产生一个错误。

清除和设置

清单 11 显示了几种 shell 在处理变量和环境方面的行为差异。本教程主要关注 bash,但是您应该明白所有 shell 并不采用相同的处理方式。另外,根据 shell 是否是登录 shell,处理方式也会有差异。在目前,我们只需知道登录 shell 就是在系统上进行登录时获得的 shell;如果愿意,可以作为登录 shell 启动其他 shell。上面使用 env -i 启动的三个 shell 不是登录 shell。请尝试将 -l 选项传递给 shell 命令本身,从而体会登录 shell 的行为有什么不同。

现在,我们来研究在这三个非登录 shell 中尝试显示 SHELL 变量值的结果:

  1. 在 bash 启动时,设置了 SHELL 变量,但是没有将它自动导出到环境中。
  2. 在 ksh 启动时,没有设置 SHELL 变量。但是引用未定义的环境变量相当于引用具有空值的环境变量。
  3. 在 tcsh 启动时,没有设置 SHELL 变量。在这种情况下,默认行为与 ksh(和 bash)的不同之处在于,在尝试使用变量时会报告一个错误。

可以使用 unset 命令将变量从 shell 变量列表中清除。如果变量已经导出到环境中,那么也会从环境中删除它。可以使用 set 命令控制 bash(或其他 shell)的工作方式的许多方面。set 是 shell 内置的命令,所以各种选项是与 shell 相关的。在 bash 中,-u 选项让 bash 在遇到未定义变量时报告错误,而不是像对待具有空值的已定义变量一样。可以在 set 中使用 - 打开各种选项,使用 + 关闭它们。可以使用 echo $- 显示当前设置的选项。


清单 12. unset 和 set


[ian@echidna ian]$ echo $-
himBH
[ian@echidna ian]$ echo $VAR1

[ian@echidna ian]$ set -u;echo $-
himuBH
[ian@echidna ian]$ echo $VAR1
bash: VAR1: unbound variable
[ian@echidna ian]$ VAR1=v1
[ian@echidna ian]$ VAR1=v1;echo $VAR1
v1
[ian@echidna ian]$ unset VAR1;echo $VAR1
bash: VAR1: unbound variable
[ian@echidna ian]$ set +u;echo $VAR1;echo $-

himBH

如果不带任何选项使用 set 命令,那么它显示所有 shell 变量及其值(如果有的话)。还有另一个命令,declare,可以使用它创建、导出和显示 shell 变量的值。可以使用手册页研究其他 set 选项和 declare 命令。在本节后面我们将讨论 手册页。

exec

本节中研究的最后一个命令是 exec。可以使用 exec 命令运行另一个程序来 替代 当前 shell。清单 13 启动一个子 bash shell,然后使用 exec 用 Korn shell 替代它。在退出 Korn shell 时,会回到原来的 bash shell(在这个例子中,PID 是 22985)。


清单 13. 使用 exec


[ian@echidna ian]$ echo $$
22985
[ian@echidna ian]$ bash
[ian@echidna ian]$ echo $$
25063
[ian@echidna ian]$ exec ksh
$ echo $$
25063
$ exit
[ian@echidna ian]$ echo $$
22985



回页首


命令历史

如果您一边阅读本教程,一边输入命令,那么可能会发现常常要多次使用同一个命令,要么是完全一样,要么是只有细微差异。好消息是 bash shell 可以维护命令的历史。 在默认情况下,历史功能是打开的。可以使用 set +o history 命令关闭它,使用 set -o history 重新打开。环境变量 HISTSIZE 告诉 bash 保留多少历史行。还有许多其他设置可以控制历史如何工作以及如何管理历史。完整的细节请参考 bash 手册页。

可以通过历史设施使用的一些命令如下:

history
显示全部历史
history N
显示历史中最后 N
history -d N
从历史中删除第 N 行;例如,如果这一行包含密码,就可能需要这么做
!!
最近的命令
!N
N 个历史命令
!-N
历史中倒数第 N 个命令(!-1 相当于 !!)
!#
正在输入的当前命令
!string
string 开头的最近的命令
!?string?
包含 string 的最近的命令
还可以使用冒号(:)后面跟着某些值来访问或修改一个历史命令的某些部分。清单 14 演示了一些历史功能。
清单 14. 操作历史


[ian@echidna ian]$ echo $$
22985
[ian@echidna ian]$ env -i bash -c 'echo $$'
1542
[ian@echidna ian]$ !!
env -i bash -c 'echo $$'
1555
[ian@echidna ian]$ !ec
echo $$
22985
[ian@echidna ian]$ !en:s/$$/$PPID/
env -i bash -c 'echo $PPID'
22985
[ian@echidna ian]$ history 6
1097 echo $$
1098 env -i bash -c 'echo $$'
1099 env -i bash -c 'echo $$'
1100 echo $$
1101 env -i bash -c 'echo $PPID'
1102 history 6
[ian@echidna ian]$ history -d1100

清单 14 中的命令做了下面这些事:

  1. 回显当前 shell 的 PID
  2. 在新的 shell 中运行 echo 命令并回显 shell 的 PID
  3. 重新运行最后一个命令
  4. 重新运行以 'ec' 开头的最后一个命令;这会重新运行这个例子中的第一个命令
  5. 重新运行以 'en' 开头的最后一个命令,但是用 '$PPID' 替代 '$$',所以会显示父进程的 PID
  6. 显示历史中最后 6 个命令
  7. 删除历史项 1100,即最后一个 echo 命令

还可以交互式地编辑历史。bash shell 使用 readline 库来管理命令编辑和历史。在默认情况下,用来在历史中进行移动和编辑的键和键组合与 GNU Emacs 编辑器中的相似。Emacs 键组合常常表示成 C-xM-x,其中的 x 是常规键,CM 分别是控制 键。在典型的 PC 系统上,Ctrl 键作为 Emacs 控制键,Alt 键作为元键。表 5 总结了一些历史编辑功能。除了表 5 所示的键组合以外,光标移动键(比如右、左、上和下箭头)以及 Home 和 End 键常常按照符合逻辑的方式工作。在手册页中可以找到其他功能以及如何使用 readline init 文件(常常是主目录中的 inputrc)来定制这些选项。

表 5. 用 emacs 命令进行历史编辑
命令 常用 PC 键 说明
C-f 右箭头 向右移动一格
C-b 左箭头 向左移动一格
M-f Alt-f 移动到下一个单词的开头;GUI 环境常常用这个键组合来打开窗口的 File 菜单
M-b Alt-b 移动到前一个单词的开头
C-a Home 移动到行的开头
C-e End 移动到行的末尾
Backspace Backspace 删除光标前面的字符
C-d Del 删除光标后面的字符(Del 和 Backspace 功能可能配置为相反的意义)
C-k Ctrl-k 删除(kill)到行的末尾并保存删除的文本供以后使用
M-d Alt-d 删除(kill)到单词的末尾并保存删除的文本供以后使用
C-y Ctrl-y 取回由删除命令删除的文本

如果您喜欢使用与 vi 相似的编辑模式操作历史,那么可以使用命令 set -o vi 切换到 vi 模式。使用 set -o emacs 返回到 emacs 模式。在 vi 模式中获取命令时,最初处于 vi 的插入模式中。关于 vi 编辑器的更多细节在 用 vi 进行文件编辑 一节中介绍。



回页首


路径

一些 bash 命令是内置的,其他命令是外部的。我们现在来看看外部命令,如何运行它们,以及如何知道命令是否是内部的。

shell 在哪里寻找命令?

外部命令只是文件系统中的文件。本教程后面的一节 基本文件管理 和针对主题 104 的教程讨论了更多细节。在 Linux 和 UNIX 系统上,所有文件都作为一个大型树结构的一部分,这个树结构的根是 /。在到目前为止提供的示例中,我们的当前目录都是用户的主目录。非根用户常常在 /home 目录中有自己的主目录,比如我的主目录是 /home/ian。根用户的主目录常常是 /root。如果输入命令名,那么 bash 会在您的路径 中寻找这个命令,路径是在 PATH 环境变量中指定的以分号分隔的目录列表。

如果想知道在输入某一字符串时执行的是什么命令,使用 whichtype 命令。清单 15 显示我的默认路径以及几个命令的位置。


清单 15. 寻找命令位置


[ian@echidna ian]$ echo $PATH
/usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin:/home/ian/bin
[ian@echidna ian]$ which bash env zip xclock echo set ls
alias ls='ls --color=tty'
/bin/ls
/bin/bash
/bin/env
/usr/bin/zip
/usr/X11R6/bin/xclock
/bin/echo
/usr/bin/which: no set in (/usr/local/bin:/bin:/usr/bin:/usr/X11R6/b
in:/home/ian/bin)
[ian@echidna ian]$ type bash env zip xclock echo set ls
bash is /bin/bash
env is /bin/env
zip is /usr/bin/zip
xclock is /usr/X11R6/bin/xclock
echo is a shell builtin
set is a shell builtin
ls is aliased to `ls --color=tty'

注意,路径中的所有目录都以 /bin 结尾。这是一种常见的约定,但不是要求。which 命令报告 ls 命令是一个别名(alias),而 set 命令无法找到。在这种情况下,可以认为它不存在或者是内置的命令。type 命令报告 ls 命令是一个别名,但是它识别出 set 命令是一个 shell 内置命令。它还报告有一个内置的 echo 命令以及 /bin 目录中的命令,which 也找到了这些命令。这两个命令还以不同的次序产生输出。

我们看到 ls 命令(用于列出目录内容)是一个别名。别名可以方便地将某些命令配置为使用不同的默认设置集,或者为命令提供替代名。在我们的示例中,--color=tty 选项使得目录列表按照文件或目录类型以不同颜色显示。尝试运行 dircolors --print-database,从而了解如何控制颜色编码以及用哪种颜色表示哪种文件。

这些命令都有其他选项。根据需要,可以使用其中任意一个命令。如果我确定要寻找的是可执行文件并只需要它的完整路径,那么倾向于使用 which。我发现 type 会提供更精确的信息,有时需要在 shell 脚本中使用。

运行其他命令

在清单 15 中看到,可执行文件的完整路径以 /(根目录)开头。例如,xclock 程序实际上是 /usr/X11R6/bin/xclock,即 /usr/X11R6/bin 目录中的一个文件。如果命令 在 PATH 设置中,那么仍然可以通过指定路径以及命令名来运行它。可以使用两种类型的路径:

  • 绝对 路径是以 / 开头的路径,比如在清单 15 中看到的那些(/bin/bash、/bin/env 等等)。
  • 相对 路径是相对于当前工作目录 的(当前工作目录由 pwd 命令报告)。这些路径不以 / 开头,但是至少包含一个 /。

使用绝对路径时可以不管当前工作目录是什么,但是只有当命令在当前目录中时才可能使用相对路径。假设您正在主目录的 mytestbin 子目录中开发传统 “Hello World!” 程序的一个新版本。可以使用相对路径以 mytestbin/hello 的形式运行这个命令。在路径中可以使用两个特殊名称;一个点(.)是指当前目录,两个点(..)是指当前目录的父目录。因为主目录常常不在 PATH 中(而且一般来说应该不在),对于希望从主目录运行的任何可执行文件,需要显式地提供路径。例如,如果在主目录中有 hello 程序的副本,那么可以使用命令 ./hello 运行它。可以在绝对路径中使用 . 和 ..,尽管单个 . 在这种情况下不太有用。还可以使用波浪号(~)表示自己的主目录,用 ~username 表示名为 username 的用户的主目录。清单 16 中给出了一些示例。


清单 16. 绝对和相对路径


[ian@echidna ian]$ /bin/echo Use echo command rather than builtin
Use echo command rather than builtin
[ian@echidna ian]$ /usr/../bin/echo Include parent dir in path
Include parent dir in path
[ian@echidna ian]$ /bin/././echo Add a couple of useless path components
Add a couple of useless path components
[ian@echidna ian]$ pwd # See where we are
/home/ian
[ian@echidna ian]$ ../../bin/echo Use a relative path to echo
Use a relative path to echo
[ian@echidna ian]$ myprogs/hello # Use a relative path with no dots
-bash: myprogs/hello: No such file or directory
[ian@echidna ian]$ mytestbin/hello # Use a relative path with no dots
Hello World!
[ian@echidna ian]$ ./hello # Run program in current directory
Hello World!
[ian@echidna mytestbin]$ ~/mytestbin/hello # run hello using ~
Hello World!
[ian@echidna ian]$ ../hello # Try running hello from parent
-bash: ../hello: No such file or directory

改变工作目录

正如可以从系统中的各个目录执行程序,也可以使用 cd 命令改变当前的工作目录。cd 的参数必须是目录的绝对路径或相对路径。对于这个命令,可以在路径中使用 .、..、~ 和 ~username。如果不带参数使用 cd,那么当前工作目录将改变为主目录。用一个连字符(-)作为参数意味着恢复原来的工作目录。主目录存储在 HOME 环境变量中,原来的工作目录存储在 OLDPWD 变量中,所以 cd 相当于 cd $HOMEcd - 相当于 cd $OLDPWD。通常我们会说改变目录,而不完整地说改变当前工作目录

对于这个命令,还有一个环境变量 CDPATH,它包含在解析相对路径时应该搜索的以分号分隔的路径集(除了当前工作目录之外)。如果解析使用了来自 CDPATH 的路径,那么 cd 将输出得到的目录的完整路径。在正常情况下,成功的目录改变不会产生输出,只是提示符可能会改变。清单 17 给出了一些示例。


清单 17. 改变目录


[ian@echidna home]$ cd /;pwd
/
[ian@echidna /]$ cd /usr/X11R6;pwd
/usr/X11R6
[ian@echidna X11R6]$ cd ;pwd
/home/ian
[ian@echidna ian]$ cd -;pwd
/usr/X11R6
/usr/X11R6
[ian@echidna X11R6]$ cd ~ian/..;pwd
/home
[ian@echidna home]$ cd ~;pwd
/home/ian
[ian@echidna ian]$ export CDPATH=~
[ian@echidna mytestbin]$ cd /;pwd
/
[ian@echidna /]$ cd mytestbin
/home/ian/mytestbin



回页首


递归地应用命令

许多 Linux 命令可以递归地应用于一个目录树中的所有文件。例如,ls 命令有一个 -R 选项用于递归地列出目录内容,cpmvrmdiff 命令都有 -r 选项用于递归地应用它们。基本文件管理 一节将详细地讨论命令的递归应用。



回页首


命令替换

bash shell 有一种非常强大的功能,允许将一个命令的结果用作另一个命令的输入;这称为命令替换。实现方法是将您希望使用其结果的命令封闭在反单引号(`)中。这仍然是常用方法,但是有另一个方法可以更容易处理多个嵌套的命令,即将命令封闭在 $( 和 ) 之间。

在前一个教程 “LPI 101 考试准备(主题 102):Linux 安装与包管理” 中,我们看到 rpm 命令可以指出一个命令来自哪个包;我们使用命令替换功能作为简化技术。现在您知道我们当时究竟在做什么了。

在 shell 脚本中,命令替换是一种很有价值的工具,在命令行上也有用。清单 18 给出了一些示例,它们从一个相对路径获得绝对路径,寻找哪个 RPM 提供了 /bin/echo 命令,并(作为根用户)列出了硬盘上三个分区的标签。最后一个示例使用 seq 命令产生一系列整数。


清单 18. 命令替换


[ian@echidna ian]$ echo '../../usr/bin' dir is $(cd ../../usr/bin;pwd)
../../usr/bin dir is /usr/bin
[ian@echidna ian]$ which echo
/bin/echo
[ian@echidna ian]$ rpm -qf `which echo`
sh-utils-2.0.12-3
[ian@echidna ian]$ su -
Password:
[root@echidna root]# for n in $(seq 7 9); do echo p$n `e2label /dev/hda$n`;done
p7 RH73
p8 SUSE81
p9 IMAGES



回页首


手册页

本教程这一节中的最后一个主题是,如何通过手册页和其他文档来源获得 Linux 命令的文档。

手册页和小节

文档的主要(且传统的)来源是手册页,可以使用 man 命令访问手册页。图 1 展示了 man 命令本身的手册页。使用命令 man man 来显示这一信息。


图 1. man 命令的手册页

图 1 显示了手册页中的一些典型项目:

  • 标题,即命令名后面跟着圆括号中的小节号
  • 命令名和在同一手册页中描述的任何相关命令
  • 这个命令可以应用的选项和参数的摘要
  • 对命令的简短描述
  • 关于每个选项的详细信息

可能会找到使用方法、如何报告 bug 、作者信息以及相关命令列表等其他信息。例如,man 的手册页告诉我们相关命令(及其手册页小节)是:

apropos(1)、whatis(1)、less(1)、groff(1) 和 man.conf(5)。

有 8 个常用的手册页小节。在安装包时常常会安装手册页,所以如果还没有安装某个包,就可能还没有它的手册页。同样,某些手‹‹册页小节可能是空的,或者几乎是空的。常用手册页小节以及一些内容示例如下:

  1. 用户命令(env、ls、echo、mkdir、tty)
  2. 系统调用或内核功能(link、sethostname、mkdir)
  3. 库例程(acosh、asctime、btree、locale、XML::Parser)
  4. 与设备相关的信息(isdn_audio、mouse、tty、zero)
  5. 文件格式描述(keymaps、motd、wvdial.conf)
  6. 游戏(注意,现在的许多游戏是图形化的,除了手册页系统之外,还有图形化的帮助)
  7. 杂项命令(arp、boot、regex、unix utf8)
  8. 系统管理(debugfs、fdisk、fsck、mount、renice、rpm)

可能有的其他小节包括 9(Linux 内核文档)、n(新文档)、o(旧文档)和 l(本地文档)。

一些项目会在多个小节中出现。我们的示例说明 mkdir 在小节 1 和 2 中都出现了,tty 也同时出现在小节 1 和 4 中。可以指定某个小节,例如 man 4 ttyman 2 mkdir,还可以指定 -a 选项来列出所有可应用的手册页小节。

在图中可能会注意到,man 有许多选项,可以自己研究。目前,让我们看看与 man 相关的一些“参见”命令。

参见

man 相关的两个重要命令是 whatisaproposwhatis 命令搜索您提供的名称的手册页并显示来自适当手册页的名称信息。apropos 命令对手册页进行关键词搜索并列出包含关键词的手册页。清单 19 演示这些命令。


清单 19. whatis 和 apropos 示例


[ian@lyrebird ian]$ whatis man
man (1) - format and display the on-line manual pages
man (7) - macros to format man pages
man [manpath] (1) - format and display the on-line manual pages
man.conf [man] (5) - configuration data for man
[ian@lyrebird ian]$ whatis mkdir
mkdir (1) - make directories
mkdir (2) - create a directory
[ian@lyrebird ian]$ apropos mkdir
mkdir (1) - make directories
mkdir (2) - create a directory
mkdirhier (1x) - makes a directory hierarchy

顺便说一下,如果无法找到 man.conf 的手册页,可以试着运行 man man.config

man 命令使用分页程序让输出在显示器上分页显示。在大多数 Linux 系统上,分页程序可能是 less 程序。另一个选择是比较老的 more 程序。如果希望打印手册页,那么指定 -t 选项来对手册页进行格式化,以便使用 grofftroff 程序进行打印。

less 分页程序有几个命令,可以帮助在显示的输出中搜索字符串。请使用 man less 来进一步了解 /(向前搜索)、?(向后搜索)、n(重复前一次搜索)以及其他命令。

其他文档来源

除了可以从命令行访问的手册页之外,Free Software Foundation 创建了许多 info 文件,这些文件用 info 程序处理。这些文件提供了丰富的导航功能,包括跳到其他小节。请用 man infoinfo info 了解更多信息。并不是所有命令都有 info 文档,所以如果您成为 info 用户,也常常需要使用手册页。

手册页还有一些图形化界面,比如 xman(来自 XFree86 项目)和 yelp(Gnome 2.0 帮助浏览器)。

如果无法找到某一命令的帮助,可以试着带 --help 选项运行这个命令。这可能会提供此命令的帮助,或者告诉您如何获得需要的帮助。

下一节讨论使用过滤器处理文本流。



回页首

文本流和过滤器

本节讨论初级管理(LPIC-1)考试 101 的主题 1.103.2 的内容。这个主题的权值是 6。

在本节中,学习以下主题:

  • 通过文本工具过滤器发送文本文件和输出流,从而修改输出
  • 使用 GNU textutils 包中的标准 UNIX 命令

文本过滤

文本过滤 就是获取文本输入流,在文本上执行某些转换,然后将它发送到输出流的过程。尽管输入或输出可以是文件,但是在 Linux 和 UNIX 环境中,进行过滤的最常用方式是构造命令的管道,也就是对一个命令的输出进行管道连接(即 重定向),用作下一个命令的输入。管道和重定向将在 流、管道和重定向 一节中更完整地讨论,目前,我们来看看使用 | 和 > 操作符的管道和基本输出重定向。

用 | 构造管道

前面一节提到过,shell 使用三种标准 I/O

  • stdin标准输入流,它向命令提供输入。
  • stdout标准输出流,它显示来自命令的输出。
  • stderr标准错误流,它显示来自命令的错误输出。

在 本教程中到目前为止,输入都是来自我们提供给命令的参数,输出都显示在终端上。许多文本处理命令(过滤器)可以从标准输入流或文件获得输入。要想将一个命 令(command1)的输出用作一个过滤器(command2)的输入,应该使用管道操作符(|)连接这两个命令,如清单 20 所示。


清单 20. 通过管道将 command1 的输出连接到 command2 的输入


command1 | command2

正如在本节后面会看到的,这两个命令都可能有 选项或参数。还可以使用 | 将这个管道中 command2 的输出重定向到另一个命令,command3。将功能有限的多个命令连接成长的管道是在 Linux 和 UNIX 上完成任务的常用方式。有时还会看到连字符(-)用来替代文件名作为命令参数,这意味着输入应该来自 stdin 而不是文件。

用 > 进行输出重定向

能够用几个命令创建管道并在终端上看到输出固然很不错,但是有时候希望将输出保存到文件中。这要使用输出重定向操作符(>)来完成。

对于本节的其余部分,我们将使用一些小文件,所以先创建一个称为 lpi103 的目录,然后进入这个目录。然后使用 > 把 echo 命令的输出重定向到 text1 文件。这些操作见清单 21。注意,输出没有显示在终端上,因为它被重定向到文件了。


清单 21. 将 echo 命令的输出重定向到文件


[ian@echidna ian]$ mkdir lpi103
[ian@echidna ian]$ cd lpi103
[ian@echidna lpi103]$ echo -e "1 apple/n2 pear/n3 banana">text1

既然我们已经有了进行管道连接和重定向的基本工具,就来看一些常用的 UNIX 和 Linux 文本处理命令和过滤器。本节只展示一些基本功能;请通过适当的手册页进一步了解这些命令。



回页首


cat、tac、od 和 split

既然已经创建了 text1 文件,您可能想看看其中的内容。使用 catcatenate 的简写)命令将文件的内容显示在 stdout 上。清单 22 检查上面创建的文件的内容。


清单 22. 用 cat 显示文件内容


[ian@echidna lpi103]$ cat text1
1 apple
2 pear
3 banana

如果不指定文件名(或者指定 - 为文件名),那么 cat 命令从 stdin 接收输入。让我们使用这种方式和输出重定向来创建另一个文本文件,如清单 23 所示。


清单 23. 用 cat 创建文本文件


[ian@echidna lpi103]$ cat>text2
9 plum
3 banana
10 apple

在清单 23 中,cat 从 stdin 读取输入,直到文件的末尾。使用 Ctrl-d(按住 Ctrl 并按下 d)组合键来表示文件的末尾。这个组合键也用来退出 bash shell。还要注意,制表符键帮助将这些水果名按列排列起来。

有时候,可能希望按相反的次序显示文件。很自然,也有一个用于此目的的文本过滤器,称为 taccat 的反序)。清单 24 按相反的次序显示新的 text2 文件和原来的 text1 文件。注意显示中如何简单地连接这两个文件。


清单 24. 用 tac 进行反序显示


[ian@echidna lpi103]$ tac text2 text1
10 apple
3 banana
9 plum
3 banana
2 pear
1 apple

现在,假设使用 cat 或 tac 显示这两个文本文件并注意到对齐方式不一样。要了解造成这个问题的原因,需要看到文件中的控制字符。因为这些控制字符的作用是进行文本显示输出,而控制字符本身没有显示,所以需要将文件转储 为另一种格式,从而能够寻找和解释这些特殊字符。GNU 文本实用程序包含的 od(即 Octal Dump)命令用于这个目的。

od 有几个选项,比如 -A 选项用来控制文件偏移量的基数,-t 选项用来控制显示的文件内容的形式。基数可以指定为 o(八进制 - 默认)、d(十进制)、x(十六进制)或 n(不显示偏移量)。可以将输出显示为八进制、十六进制、十进制、浮点、用反斜线转义的 ASCII 或命名的字符(nl 表示新行,ht 表示水平制表符,等等)。清单 25 显示一些可以用来对 text2 示例文件进行转储的格式。


清单 25. 用 od 对文件进行转储


[ian@echidna lpi103]$ od text2
0000000 004471 066160 066565 031412 061011 067141 067141 005141
0000020 030061 060411 070160 062554 000012
0000031
[ian@echidna lpi103]$ od -A d -t c text2
0000000 9 /t p l u m /n 3 /t b a n a n a /n
0000016 1 0 /t a p p l e /n
0000025
[ian@echidna lpi103]$ od -A n -t a text2
9 ht p l u m nl 3 ht b a n a n a nl
1 0 ht a p p l e nl

注意:

  1. cat-A 选项也可以用来查看制表符和行末的位置。更多信息参见手册页。
  2. 如果您具有大型机背景,就可能对 hexdump 实用程序感兴趣,这是另一个实用程序集的一部分。这里不讨论它,请参考手册页。

我们的示例文件非常小,但是有时候有大型文件需要分割为比较小的块。例如,可能希望将大文件分割为 CD 容量的块,以便将它写到 CD 中。split 命令会完成这个任务,可以使用 cat 命令轻松地重新创建文件。在默认情况下,split 命令产生的文件的名称前面有前缀 'x',后面有后缀 'aa'、'ab'、'ac'、...'ba'、'bb' 等等。可以用选项控制这些默认前缀和后缀。还可以控制输出文件的大小,以及产生的文件是包含整行,还是只按字节计数。清单 26 演示将我们的两个文本文件进行分割,输出文件具有不同的前缀。我们将 text1 分割为最多包含两行的文件,将 text2 分割为最多包含 18 字节的文件。然后使用 cat 单独显示一些片段,并使用 globbing 显示完整的文件,这在本教程后面的 通配符和 globbing 一节中讨论。


清单 26. 用 split 和 cat 进行分割和重新组合


[ian@echidna lpi103]$ split -l 2 text1
[ian@echidna lpi103]$ split -b 18 text2 y
[ian@echidna lpi103]$ cat yaa
9 plum
3 banana
10[ian@echidna lpi103]$ cat yab
apple
[ian@echidna lpi103]$ cat y*
9 plum
3 banana
10 apple

注意,分割产生的文件 yaa 并不以新行字符结束,所以在用 cat 显示它之后提示符发生了偏移。



回页首


wc、head 和 tail

cattac 显示整个文件。这对于我们的示例这样的小文件是合适的,但是对于大文件就不合适了。那么,可能希望先用 wcWord Count)命令看看文件有多大。wc 命令显示文件中的行数、单词数和字节数。还可以使用 ls -l 了解字节数。清单 27 显示以长格式列出两个文本文件的目录清单,以及 wc 的输出。


清单 27. 对文本文件使用 wc


[ian@echidna lpi103]$ ls -l text*
-rw-rw-r-- 1 ian ian 24 Sep 23 12:27 text1
-rw-rw-r-- 1 ian ian 25 Sep 23 13:39 text2
[ian@echidna lpi103]$ wc text*
3 6 24 text1
3 6 25 text2
6 12 49 total

可以使用选项控制 wc 的输出,或者显示其他信息,比如最大行长度。详情请参考手册页。

有两个命令可以显示第一部分()或最后一部分()。这些命令是 headtail 命令。它们可以用作过滤器,也可以以文件名作为参数。在默认情况下,它们显示文件或流的前 10 行(或最后 10 行)。清单 28 使用 dmesg 命令显示引导消息,并使用 wctailhead 来发现共有 177 个消息,然后显示最后 10 个消息,最后显示最后 15 个消息中的前 6 个。在输出中,一些行被截断了(用 ... 表示)。


清单 28. 使用 wc、head 和 tail 来显示引导消息


[ian@echidna lpi103]$ dmesg | wc
177 1164 8366
[ian@echidna lpi103]$ dmesg | tail
i810: Intel ICH2 found at IO 0x1880 and 0x1c00, MEM 0x0000 and ...
i810_audio: Audio Controller supports 6 channels.
i810_audio: Defaulting to base 2 channel mode.
i810_audio: Resetting connection 0
ac97_codec: AC97 Audio codec, id: ADS98 (Unknown)
i810_audio: AC'97 codec 0 Unable to map surround DAC's (or ...
i810_audio: setting clocking to 41319
Attached scsi CD-ROM sr0 at scsi0, channel 0, id 0, lun 0
sr0: scsi3-mmc drive: 0x/32x writer cd/rw xa/form2 cdda tray
Uniform CD-ROM driver Revision: 3.12
[ian@echidna lpi103]$ dmesg | tail -n15 | head -n 6
agpgart: Maximum main memory to use for agp memory: 941M
agpgart: Detected Intel i845 chipset
agpgart: AGP aperture is 64M @ 0xf4000000
Intel 810 + AC97 Audio, version 0.24, 13:01:43 Dec 18 2003
PCI: Setting latency timer of device 00:1f.5 to 64
i810: Intel ICH2 found at IO 0x1880 and 0x1c00, MEM 0x0000 and ...

tail 的另一种常见用法是使用 -f 选项(常常带行计数 1)跟随 一个文件。如果有一个后台进程会在文件中产生输出,而您希望检查这个文件来了解进程正在做什么,那么就可以采用这种做法。在这种模式中,tail 将一直运行到取消它为止(使用 Ctrl-c),每当行被写到文件时就显示它们。



回页首


expand、unexpand 和 tr

在创建 text1 和 text2 文件时,我们使用制表符创建 text2。有时候希望将制表符转换成空格,或者相反。expandunexpand 命令用于此目的。这两个命令的 -t 选项允许设置制表符对应的空格数。清单 29 显示如何将 text2 中的制表符展开为空格,以及用另一个奇特的 expandunexpand 序列使 text2 中的文本不对齐。


清单 29. 使用 expand 和 unexpand


[ian@echidna lpi103]$ expand -t 1 text2
9 plum
3 banana
10 apple
[ian@echidna lpi103]$ expand -t8 text2|unexpand -a -t2|expand -t3
9 plum
3 banana
10 apple

不幸的是,不能使用 unexpand 将 text1 中的空格替换为制表符,因为 unexpand 需要至少两个空格才能转换为制表符。但是,可以使用 tr 命令来完成,这个命令将一个集合(set1)中的字符转换为另一个集合(set2)中对应的字符。清单 30 显示如何使用 tr 将空格转换为制表符。因为 tr 是一个纯粹的过滤器,所以要使用 cat 命令为它产生输出。这个示例还演示通过使用 - 让 cat 使用标准输入。


清单 30. 使用 tr


[ian@echidna lpi103]$ cat text1 |tr ' ' '/t'|cat - text2
1 apple
2 pear
3 banana
9 plum
3 banana
10 apple

如果您不确定最后这两个示例中发生了什么,那么尝试使用 od 依次终止管道的每个阶段;例如
cat text1 |tr ' ' '/t' | od -tc



回页首


pr、nl 和 fmt

pr 命令用于对文件进行格式化以便打印。默认的页眉包括文件名、创建文件的日期和时间以及页号,页脚是两个空行。当从多个文件或标准输入流创建输出时,当前日期和时间替代文件名和创建日期。可以通过选项分栏并列打印文件并控制格式化的许多方面。详情请参考手册页。

nl 命令对行进行编号,这在打印文件时很方便。还可以用 cat 命令的 -n 选项对行进行编号。清单 31 显示如何打印 text1,然后给 text2 加行号并将它打印在 text1 旁边。


清单 31. 加行号和格式化打印


[ian@echidna lpi103]$ pr text1 | head


2005-09-23 12:27 text1 Page 1


1 apple
2 pear
3 banana


[ian@echidna lpi103]$ nl text2 | pr -m - text1 | head


2005-09-26 11:48 Page 1


1 9 plum 1 apple
2 3 banana 2 pear
3 10 apple 3 banana



另一个对于文本格式化有帮助的命令是 fmt 命令,它使文本适合页面界限。可以将几个短行合并以及将长行分割。在清单 32 中,使用 !#:* 历史特性的变体创建具有一长行文本的 text3,它把我们输入的句子保存了四次。还创建 text4,其中每个单词占一行。然后使用 cat 显示没有经过格式化时的情况,包括显示 '$' 字符来表示行末。最后,使用 fmt 将它们格式化为最大宽度 60 个字符。同样,关于其他选项的详情请参考手册页。


清单 32. 按照最大行长度进行格式化


[ian@echidna lpi103]$ echo "This is a sentence. " !#:* !#:1-$>text3
echo "This is a sentence. " "This is a sentence. " "This is a sentenc
e. " "This is a sentence. ">text3
[ian@echidna lpi103]$ echo -e "This/nis/nanother/nsentence.">text4
[ian@echidna lpi103]$ cat -et text3 text4
This is a sentence. This is a sentence. This is a sentence. This i
s a sentence. $
This$
is$
another$
sentence.$
[ian@echidna lpi103]$ fmt -w 60 text3 text4
This is a sentence. This is a sentence. This is a
sentence. This is a sentence.
This is another sentence.



回页首


sort 和 uniq

sort 命令使用系统地区的整理序列(LC_COLLATE)对输入进行排序。sort 命令还可以对已经排序的文件进行合并,以及检查文件是排序的还是未排序的。

清单 33 先将 text1 中的空格转换为制表符,然后使用 sort 命令对两个文本文件进行排序。因为排序次序是按照字符决定的,您可能会对结果感到吃惊。幸运的是,sort 命令既可以按照数字值,也可以按照字符值进行排序。可以针对整个记录,也可以针对每个字段 指定这一选择。除非指定了另一种字段分隔符,否则字段是由空格或制表符分隔的。清单 33 中的第二个示例对第一个字段进行数字式排序,对第二个字段按照整理序列(字母表次序)排序。它还演示了使用 -u 选项消除任何重复的行,只保留独特的行。


清单 33. 字符和数字式排序


[ian@echidna lpi103]$ cat text1 | tr ' ' '/t' | sort - text2
10 apple
1 apple
2 pear
3 banana
3 banana
9 plum
[ian@echidna lpi103]$ cat text1|tr ' ' '/t'|sort -u -k1n -k2 - text2
1 apple
2 pear
3 banana
9 plum
10 apple

注意,仍然有两行都包含水果名“apple”。另一个命令 uniq 使我们能够更进一步控制对重复行的消除。uniq 命令一般在已排序的文件上进行操作,但是无论文件是否已经排序,它都会删除 连续的 相同行。uniq 命令还可以忽略一些字段。清单 34 按照第二个字段(水果名)对两个文本文件进行排序,并消除那些从第二个字段开始的内容都相同的行(也就是说,在测试 uniq 时我们跳过了第一个字段)。


清单 34. 使用 uniq


[ian@echidna lpi103]$ cat text1|tr ' ' '/t'|sort -k2 - text2|uniq -f1
10 apple
3 banana
2 pear
9 plum

排序是按照整理序列进行的,所以 uniq 显示 “10 apple” 这一行而不是 “1 apple”。请尝试添加对第一个字段的数字式排序,从而改变这种情况。



回页首


cut、paste 和 join

现在,我们来看另外三个对文本数据中的字段进行处理的命令。这些命令对于处理表格数据尤其有帮助。第一个命令是 cut 命令,它从文本文件中提取字段。默认的字段分隔符是制表符。清单 35 使用 cut 分隔 text2 的两列,然后使用空格作为输出分隔符,这是将每行中的制表符转换为空格的另一种方式。


清单 35. 使用 cut


[ian@echidna lpi103]$ cut -f1-2 --output-delimiter=' ' text2
9 plum
3 banana
10 apple

paste 命令将来自两个或更多文件的行并列粘贴在一起,这与 pr 命令使用 -m 选项对文件进行合并的方式相似。清单 36 显示粘贴两个文本文件的结果。


清单 36. 粘贴文件


[ian@echidna lpi103]$ paste text1 text2
1 apple 9 plum
2 pear 3 banana
3 banana 10 apple

这些示例只展示了简单的粘贴,但是 paste 可以以几种其他方式从两个或更多文件粘贴数据。详情请参考手册页。

最后一个字段操作命令是 join, 它根据匹配的字段对文件进行联结。文件应该是按照联结字段排序的。因为 text2 没有按照数字次序排序,我们先对它进行排序,然后将具有匹配的联结字段(在这个示例中是 3)的行联结起来。我们还创建一个新文件 text5,创建的办法是按照第二个字段(水果名)对 text1 排序,然后用制表符替换空格。然后对 text2 排序并使用第二个字段与 text5 联结,应该有两个匹配(apple 和 banana)。清单 37 演示这两个联结。


清单 37. 用联结字段联结文件


[ian@echidna lpi103]$ sort -n text2|join -1 1 -2 1 text1 -
3 banana banana
[ian@echidna lpi103]$ sort -k2 text1|tr ' ' '/t'>text5
[ian@echidna lpi103]$ sort -k2 text2 | join -1 2 -2 2 text5 -
apple 1 10
banana 3 3

用来进行联结的字段是分别针对每个文件指定的。例如,可以根据一个文件中的字段 3 和另一个文件中的字段 10 进行联结。



回页首


sed

sed 是 stream editor(流编辑器)。有关于 sed 的几篇 developerWorks 文章和许多书籍(参见 参考资料)。sed 非常强大,它能够完成的任务超出了您的想像。这里的简短介绍应该激发您对 sed 的兴趣,但是并不全面。

与到目前为止看到的许多文本命令一样,sed 可以作为过滤器,也可以从文件接收输入。输出是标准输出流。sed 将来自输入的行装载到模式空间 中,将 sed 编辑命令应用于模式空间的内容,然后将模式空间写到标准输出。sed 可以将模式空间中的几行组合起来,可以写文件、只写选择的输出或者根本不写输出。

sed 使用正则表达式语法(参见本教程后面的 用正则表达式进行搜索)来搜索和选择性地替换模式空间中的文本,以及通过编辑命令集来控制应该操作哪些文本行。保留缓冲区(hold buffer) 为文本提供临时存储。保留缓冲区可以替换模式空间、添加到模式空间中或与模式空间交换。sed 的命令比较有限,但是这些命令与正则表达式语法和保留缓冲区结合起来就可以实现某些令人吃惊的功能。sed 命令集常常称为 sed 脚本

清单 38 显示了三个简单的 sed 脚本。在第一个脚本中,使用 s(替换)命令用大写替换每行上的小写 'a'。这个示例只替换第一个 'a',所以在第二个示例中,添加一个 'g'(全局)标志使 sed 修改文本中出现的所有 'a'。在第三个脚本中,使用 d(删除)命令删除一行。在示例中,使用地址 2 表示只应该删除第二行。用分号(;)分隔命令并使用第二个脚本中使用过的全局替换。


清单 38. 简单的 sed 脚本


[ian@echidna lpi103]$ sed 's/a/A/' text1
1 Apple
2 peAr
3 bAnana
[ian@echidna lpi103]$ sed 's/a/A/g' text1
1 Apple
2 peAr
3 bAnAnA
[ian@echidna lpi103]$ sed '2d;$s/a/A/g' text1
1 apple
3 bAnAnA

除了操作单独的行之外,sed 还可以操作一个范围内的行。范围的开始和结束由一个逗号(,)分隔,可以用行号指定,脱字符(^)表示文件的开头,美圆符号($)表示文件的末尾。给出一 个地址或地址范围,就可以在花括号({ 和 })之间组合几个命令,让这些命令只应用于范围内的行。清单 39 演示了将全局替换只应用于文件的最后两行的两种方式。它还演示了使用 -e 选项将命令添加到模式空间中。在使用花括号时,必须以这种方式分隔命令。


清单 39. sed 地址


[ian@echidna lpi103]$ sed -e '2,${' -e 's/a/A/g' -e '}' text1
1 apple
2 peAr
3 bAnAnA
[ian@echidna lpi103]$ sed -e '/pear/,/bana/{' -e 's/a/A/g' -e '}' text1
1 apple
2 peAr
3 bAnAnA

sed 脚本还可以存储在文件中。实际上,对于频繁使用的脚本,很可能希望这么做。在前面,我们使用 tr 命令将 text1 中的空格改为制表符。现在用一个存储在文件中的 sed 脚本来完成这个任务。将使用 echo 命令创建文件。结果见清单 40。


清单 40. sed one-liner


[ian@echidna lpi103]$ echo -e "s/ //t/g">sedtab
[ian@echidna lpi103]$ cat sedtab
s/ / /g
[ian@echidna lpi103]$ sed -f sedtab text1
1 apple
2 pear
3 banana

有许多像清单 40 这样的方便的 sed one-liner。参见 参考资料 中的链接。

最后一个 sed 示例使用 = 命令打印行号,然后通过 sed 对产生的输出进行过滤,从而模仿 nl 命令对行进行编号的效果。清单 41 使用 = 来打印行号,然后使用 N 命令将第二个输入行读入模式空间,最后删除模式空间中两行之间的新行字符(/n)。


清单 41. 用 sed 对行进行编号


[ian@echidna lpi103]$ sed '=' text2
1
9 plum
2
3 banana
3
10 apple
[ian@echidna lpi103]$ sed '=' text2|sed 'N;s//n//'
19 plum
23 banana
310 apple

结果不太理想!我们其实希望将行号在一栏中对齐,与文件中的行分开。在清单 42 中,我们输入几行命令(注意 > 辅助提示符)。请研究这个示例并参考下面的解释。


清单 42. 用 sed 对行进行编号 - 修正版


[ian@echidna lpi103]$ cat text1 text2 text1 text2>text6
[ian@echidna lpi103]$ ht=$(echo -en "/t")
[ian@echidna lpi103]$ sed '=' text6|sed "N
> s/^/ /
> s/^.*/(....../)/n//1$ht/"
1 1 apple
2 2 pear
3 3 banana
4 9 plum
5 3 banana
6 10 apple
7 1 apple
8 2 pear
9 3 banana
10 9 plum
11 3 banana
12 10 apple

我们采取的步骤是这样的:

  1. 先使用 cat 创建一个有 12 行的文件,其内容来自 text1 和 text2 文件的两个副本。产生 12 行是为了显示两位数的行号,从而表现栏中数字格式化的效果。
  2. bash shell 使用制表符键结束命令,所以在需要真正的制表符时如果可以使用代表制表符的字符,这样会很方便。我们使用 echo 命令实现这个字符表示并将它存储在 shell 变量 'ht' 中。
  3. 像前面一样,创建包含行号和数据行的流,并通过 sed 的第二个副本对它进行过滤。
  4. 将第二行读入模式空间。
  5. 在模式空间的开头(由 ^ 表示)加上我们的行号以及六个空格。
  6. 然 后用新行字符前面的最后六个字符加制表符来替换新行前的所有内容。注意,'s' 命令的左边部分使用 '/(' 和 '/)' 表示在右边部分中要使用的字符。在右边,用 /1 引用第一个这样表示的字符集(在这个示例中只有一个字符集)。注意,命令包含在双引号(")中,所以会对 $ht 进行替换。

sed 最近的版本(version 4)包含 info 格式的文档,还有许多出色的示例。在 version 3.02 中没有这些。GNU sed 在接到 sed --version 命令时会显示版本。



回页首


基本文件管理

本节讨论初级管理(LPIC-1)考试 101 的主题 1.103.3 的内容。这个主题的权值是 3。

在本节中,学习以下主题:

  • 列出目录内容
  • 复制、移动和删除文件和目录
  • 递归地操作多个文件和目录
  • 使用通配符模式操作文件
  • 使用 find 命令根据类型、大小或时间来定位和操作文件

列出目录

在前面关于 使用命令行 的一节中,在讨论路径时说过,Linux 和 UNIX® 系统上的所有文件都是一个大型树结构的文件系统的一部分,这个文件系统的根是 /。

列出目录项

如果您在前面的一节按照我们的说明进行操作,就已经在主目录中创建了一个目录,lpi103。文件和目录名可以是绝对的(意味着它们以 / 开头),也可以是相对当前工作目录 的(意味着它们不以 / 开头)。文件或目录的绝对路径包括一个 /,后面跟着零个或几个目录名,每个目录名后面跟着一个 /,然后是最后一个名称。如果给出了相对于当前工作目录的文件或目录名,只需将工作目录的绝对路径、/ 和这个目录的相对路径组合起来。例如,在前一节中在我的主目录 /home/ian 中创建了 lpi103 目录,所以它的完整(即绝对)路径是 /home/ian/lpi103。清单 43 演示了三种使用 ls 命令列出这个目录中文件的方式。


清单 43. 列出目录项


[ian@echidna lpi103]$ pwd
/home/ian/lpi103
[ian@echidna lpi103]$ echo $PWD
/home/ian/lpi103
[ian@echidna lpi103]$ ls
sedtab text2 text4 text6 xab yab
text1 text3 text5 xaa yaa
[ian@echidna lpi103]$ ls "$PWD"
sedtab text2 text4 text6 xab yab
text1 text3 text5 xaa yaa
[ian@echidna lpi103]$ ls /home/ian/lpi103
sedtab text2 text4 text6 xab yab
text1 text3 text5 xaa yaa

可以看到,可以将目录名作为参数提供给 ls 命令,它会列出这个目录的内容。

列出细节

在存储设备上,文件或目录包含在 的集合中。关于文件的信息包含在 inode 中,记录的信息包括拥有者、最近一次访问文件的时间、文件的大小、它是否是目录以及谁可以读或写它。inode 号也称为文件序列号(file serial number) ,这个编号在特定的文件系统中是惟一的。可以使用 -l(或 --format=long)选项来显示 inode 中存储的一些信息。

在默认情况下,ls 命令不列出特殊文件,即名称以点(.)开头的文件。除根目录之外的每个目录都至少有两个特殊项,目录本身(.)和父目录(..)。根目录没有父目录。

清单 44 使用 -l-a 选项以长格式列出所有文件,包括 . 和 .. 目录项。


清单 44. 长格式目录清单


[ian@echidna lpi103]$ ls -al
total 56
drwxrwxr-x 2 ian ian 4096 Sep 30 15:01 .
drwxr-xr-x 94 ian ian 8192 Sep 27 12:57 ..
-rw-rw-r-- 1 ian ian 8 Sep 26 15:24 sedtab
-rw-rw-r-- 1 ian ian 24 Sep 23 12:27 text1
-rw-rw-r-- 1 ian ian 25 Sep 23 13:39 text2
-rw-rw-r-- 1 ian ian 84 Sep 25 17:47 text3
-rw-rw-r-- 1 ian ian 26 Sep 25 22:28 text4
-rw-rw-r-- 1 ian ian 24 Sep 26 12:46 text5
-rw-rw-r-- 1 ian ian 98 Sep 26 16:09 text6
-rw-rw-r-- 1 ian ian 15 Sep 23 14:11 xaa
-rw-rw-r-- 1 ian ian 9 Sep 23 14:11 xab
-rw-rw-r-- 1 ian ian 18 Sep 23 14:11 yaa
-rw-rw-r-- 1 ian ian 7 Sep 23 14:11 yab

在清单 44 中,第一行显示列出的文件所使用的磁盘块的总数(56)。余下的字段说明文件的情况。

  • 第 一个字段(在这个示例中是 drwxrwxr-x 或 -rw-rw-r--)说明这是目录(d)还是常规文件(-)。还可能看到符号链接(l)或表示某些特殊文件(比如 /dev 文件系统中的文件)的其他值。类型后面是三组权限(比如 rwx 或 r--),分别针对拥有者、拥有者的组中的成员以及所有用户。组中的三个值分别表示拥有者、组或所有用户是否具有读(r)、写(w)或执行(x)权限。后 面会讨论其他使用方式,比如 setuid。
  • 下一个字段是一个数字,说明了文件的硬链接 数量。我们说过,inode 包含关于文件的信息。文件的目录项包含到文件的 inode 的硬链接(即指针),所以列出的每个项应该至少有一个硬链接。目录项还有针对 . 项的硬链接以及针对每个子目录的 .. 项的硬链接。所以从上面的清单中可以看到,我的主目录有许多子目录。
  • 下两个字段是文件的拥有者和拥有者的主组。某些系统(比如 Red Hat 系统)为每个用户默认提供一个单独的组。在其他系统上,所有用户可能属于一个或几个组。
  • 下一个字段包含文件的长度。
  • 倒数第二个字段包含最近一次修改的时间戳。
  • 最后一个字段包含文件或目录的名称。

ls 命令的 -i 选项显示 inode 号。在本教程后面以及在针对主题 104 的教程中讨论硬链接和符号链接时,我们将使用这个命令。

多个文件

还可以为 ls 命令指定多个参数,其中每个参数是文件或目录的名称。如果参数是目录名称,那么 ls 命令列出这个目录的内容,而不是列出关于目录本身的信息。在我们的示例中,假设希望获得关于 lpi103 目录项本身的信息,就像父目录中列出的一样。命令 ls -l ../lpi103 会给出像前一个示例一样的清单。清单 45 显示如何使用 ls -ld 以及如何列出多个文件或目录。


清单 45. 使用 ls -d


[ian@echidna lpi103]$ ls -ld ../lpi103 sedtab xaa
drwxrwxr-x 2 ian ian 4096 Oct 2 18:49 ../lpi103
-rw-rw-r-- 1 ian ian 8 Sep 26 15:24 sedtab
-rw-rw-r-- 1 ian ian 15 Sep 23 14:11 xaa

注意,lpi103 的修改时间与前一个清单中不一样。另外,与前一个清单一样,目录中文件的时间戳不一样。您预料到这种情况了吗?正常情况下不会这样的。但是,在开发本教程时,我创建了一些额外的示例,然后删除了,所以目录时间戳反映了这一事实。在本节后面讨论 搜索文件 时会进一步讨论文件时间。

对输出进行排序

在默认情况下,ls 按照字母表次序列出文件。有许多用于对输出进行排序的选项。例如, ls -t 按照修改时间排序(从最新的到最旧的),而 ls -lS 将按大小次序产生一个长清单(从最大的到最小的)。添加 -r 选项将反转排序次序。例如,使用 ls -lrt 产生从最旧到最新的长清单。列出文件和目录的其他方式请参考手册页。



回页首


复制、移动和删除

我们已经学习了一些创建文件的方式,但是可能希望复制文件、对文件进行重命名、在文件系统层次结构中移动文件,甚至删除文件。使用下面这三个命令来完成这些任务。

cp
用于复制一个或多个文件。 必须 提供至少两个文件名,一个或多个 文件名和一个 目标 文件名。如果指定两个文件名,那么第一个文件被复制到第二个文件中。源和目标文件名都可以包含路径说明。如果将一个目录指定为最后一个参数,那么可以指定 多个要复制进这个目录的文件。将从文件现在的位置复制所有文件,副本的文件名称与原来的文件相同。注意,与 DOS 和 Windows 操作系统不同,并不假设目标目录是当前目录。
mv
用于 移动重命名 一个或多个文件或目录。使用的名称大体上遵循与 cp 命令相同的规则;可以对单个文件进行重命名,或者将一组文件移动到新目录中。因为名称只是一个链接到 inode 的目录项,所以 inode 号应该不变, 除非 文件移动到了另一个文件系统中,在这种情况下,移动文件实际上是复制然后删除原来的文件。
rm
用于 删除 一个或多个文件。稍后我们会讨论如何删除目录。

清单 46 演示如何使用 cpmv 创建文本文件的一些备份副本。还使用 ls -i 显示一些文件的 inode。

  1. 先将 text1 文件复制为 text1.bkp。
  2. 然后使用 mkdir 命令创建一个备份目录。
  3. 再建立 text1 的第二个备份副本,这一次在备份目录中,并显示这三个文件的 inode 是不同的。
  4. 然后将 text1.bkp 移动到备份目录,并将它重命名以便与第二个备份更一致。其实可以在一个命令中完成这两个操作,这里使用两个命令是为了演示。
  5. 再次检查 inode 并确认 inode 为 2129019 的 text1.bkp 已经不在 lpi103 目录中了,这个 inode 现在是备份目录中的 text1.bkp.1。

清单 46. 复制和移动文件


[ian@echidna lpi103]$ cp text1 text1.bkp
[ian@echidna lpi103]$ mkdir backup
[ian@echidna lpi103]$ cp text1 backup/text1.bkp.2
[ian@echidna lpi103]$ ls -i text1 text1.bkp backup
2128984 text1 2129019 text1.bkp

backup:
1564497 text1.bkp.2
[ian@echidna lpi103]$ mv text1.bkp backup
[ian@echidna lpi103]$ mv backup/text1.bkp backup/text1.bkp.1
[ian@echidna lpi103]$ ls -i text1 text1.bkp backup
ls: text1.bkp: No such file or directory
2128984 text1

backup:
2129019 text1.bkp.1 1564497 text1.bkp.2

在正常情况下,cp 命令可以覆盖现有的副本,如果现有的文件是可写的话。另一方面,如果目标文件存在,那么 mv 将不会移动或重命名这个文件。对于 cpmv 的这种行为,有几个有用的相关选项可以进行控制。

-f 或 --force
将使 cp 尝试删除现有的目标文件,即使它是不可写的。
-i 或 --interactive
要求在尝试替换现有文件之前进行确认。
-b 或 --backup
对被替换的任何文件建立备份。
关于这些选项和用于复制和移动的其他选项的完整细节,请参考手册页。

在清单 47 中,演示带备份操作的复制以及文件删除。


清单 47. 备份复制和文件删除


[ian@echidna lpi103]$ cp text2 backup
[ian@echidna lpi103]$ cp --backup=t text2 backup
[ian@echidna lpi103]$ ls backup
text1.bkp.1 text1.bkp.2 text2 text2.~1~
[ian@echidna lpi103]$ rm backup/text2 backup/text2.~1~
[ian@echidna lpi103]$ ls backup
text1.bkp.1 text1.bkp.2

注意,rm 命令还接受 -i(交互式)和 -f(强迫)选项。使用 rm 删除文件之后,文件系统就不能再访问它。一些系统默认设置一个别名 alias rm='rm -i',以便帮助根用户避免意外的文件删除。如果您很担心意外地删除文件,那么也可以这么做。

在结束对这三个命令的讨论之前,还应该注意,cp 命令在默认情况下为新文件创建新的时间戳。拥有者和组也设置为进行复制的用户及其组。可以使用 -p 选项保留选择的属性。注意,根用户是可以保留拥有关系的惟一用户。细节请参考手册页。



回页首


mkdir 和 rmdir

我们已经看到了如何用 mkdir 创建目录。现在进一步讨论 mkdir 以及用于删除目录的命令 rmdir

mkdir

假设我们在 lpi103 目录中,希望创建子目录 dir1 和 dir2。与我们已经讨论过的那些命令一样,mkdir 可以在一个命令中处理多个目录创建请求,如清单 48 所示。


清单 48. 创建多个目录


[ian@echidna lpi103]$ mkdir dir1 dir2

注意,成功完成时并没有输出,但是可以使用 echo $? 检查退出码是否确实是 0。

如果希望创建嵌套的子目录,比如 d1/d2/d3,那么这个操作会失败,因为 d1 和 d2 目录还不存在。幸运的是,mkdir 有一个 -p 选项,它允许创建所需的任何父目录。清单 49 演示了这种做法。


清单 49. 创建父目录


[ian@echidna lpi103]$ mkdir d1/d2/d3
mkdir: cannot create directory `d1/d2/d3': No such file or directory
[ian@echidna lpi103]$ echo $?
1
[ian@echidna lpi103]$ mkdir -p d1/d2/d3
[ian@echidna lpi103]$ echo $?
0

rmdir

使用 rmdir 命令删除目录与创建目录的过程相反。同样,也有 -p 选项用来删除父目录。只有在目录是空的时候,才能使用 rmdir 删除目录,因为没有强迫删除选项。在讨论 递归操作 时,会看到删除目录的另一种方式。学会这种方法之后,就可能不常在命令行上使用 rmdir 了,但是了解这个命令是有好处的。

为了演示目录删除,我们将 text1 文件复制到目录 d1/d2 中,这样这个目录就不空了。然后使用 rmdir 删除刚才用 mkdir 创建的所有目录。可以看到,d1 和 d2 没有被删除,因为 d2 不是空的。另一个目录被删除了。从 d2 中删除了 text1 的副本之后,就可以通过调用一次 rmdir -p 删除 d1 和 d2。


清单 50. 删除目录


[ian@echidna lpi103]$ cp text1 d1/d2
[ian@echidna lpi103]$ rmdir -p d1/d2/d3 dir1 dir2
rmdir: `d1/d2': Directory not empty
[ian@echidna lpi103]$ ls . d1/d2
.:
backup sedtab text2 text4 text6 xab yab
d1 text1 text3 text5 xaa yaa

d1/d2:
text1
[ian@echidna lpi103]$ rm d1/d2/text1
[ian@echidna lpi103]$ rmdir -p d1/d2



回页首


递归操作

在本节余下的部分中,讨论处理多个文件以及递归地操作目录树的一部分的各种操作。

递归地列出信息

ls 命令有一个 -R(注意,是大写的 'R')选项,用于列出一个目录及其所有子目录的内容。递归选项只应用于目录名;不能用来在目录树中搜索具有某一名称(比如 'text1')的所有文件。-R 可以与前面介绍的其他选项一起使用。清单 51 递归地列出 lpi103 目录的信息,包括 inode 号。


清单 51. 递归的目录清单


[ian@echidna lpi103]$ ls -iR ~/lpi103
/home/ian/lpi103:
1564496 backup 2128985 text2 2128982 text5 2128987 xab
2128991 sedtab 2128990 text3 2128995 text6 2128988 yaa
2128984 text1 2128992 text4 2128986 xaa 2128989 yab

/home/ian/lpi103/backup:
2129019 text1.bkp.1 1564497 text1.bkp.2

递归地复制

可以使用 -r(或 -R--recursive)选项让 cp 命令探索源目录并递归地复制内容。为了避免无限地递归,源目录本身可能不被复制。清单 52 显示如何将 lpi103 目录中的所有内容复制到 copy1 子目录中。然后使用 ls -R 显示了产生的目录树。


清单 52. 递归地复制


[ian@echidna lpi103]$ cp -pR . copy1
cp: cannot copy a directory, `.', into itself, `copy1'
[ian@echidna lpi103]$ ls -R
.:
backup sedtab text2 text4 text6 xab yab
copy1 text1 text3 text5 xaa yaa

./backup:
text1.bkp.1 text1.bkp.2

./copy1:
backup text1 text3 text5 xaa yaa
sedtab text2 text4 text6 xab yab

./copy1/backup:
text1.bkp.1 text1.bkp.2

递归地删除

前面提到过,rmdir 只删除空目录。可以使用 -r(或 -R--recursive)选项让 rm 命令同时删除文件 目录。在清单 53 中,删除刚创建的 copy1 目录及其内容,包括 backup 子目录及其内容。


清单 53. 递归地删除


[ian@echidna lpi103]$ rm -r copy1
[ian@echidna lpi103]$ ls -R
.:
backup text1 text3 text5 xaa yaa
sedtab text2 text4 text6 xab yab

./backup:
text1.bkp.1 text1.bkp.2

如果有不可写的文件,那么可能需要添加 -f 选项来进行强迫删除。根用户在清理文件系统时常常这么做,但是如果不小心,就可能丢失有价值的数据。



回页首


通配符和 globbing

常常需要在许多文件系统对象上执行操作,但不是操作整个目录树(就像刚才用递归操作所做的)。例如,可能希望寻找在 lpi103 中创建的所有文本文件的修改时间,但是不列出分割的文件。对于小目录这很容易,但是对于大型文件系统就很困难了。

要解决这个问题,可以使用 bash shell 内置的通配符支持。这种支持也称为 “globbing”(因为它原来是作为 /etc/glob 程序实现的),允许使用通配符模式指定多个文件。

包含字符 '?'、'*' 或 '[' 的字符串是通配符模式。shell(也可能是另一个程序)利用 globbing 过程将这些模式扩展成与模式匹配的路径名列表。匹配规则如下:

?
匹配任何单一字符。
*
匹配任何字符串,包括空字符串。
[
引入一个 字符类。字符类是一个非空字符串,以 ']' 结束。匹配意味着与方括号中的任何字符匹配。有几个特殊的考虑事项。
  • '*' 和 '?' 匹配本身。如果在文件名中使用这些字符,那么需要小心地加引号或进行转义。
  • 因为这个字符串必须非空并以 ']' 结束,所以如果要匹配 ']' 本身,那么必须把它放在字符串中 最前面
  • 在两个其他字符之间加 '-' 字符表示一个范围,包括这两个字符以及整理序列中它们之间的所有字符。例如,[0-9a-fA-F] 表示所有大写或小写的十六进制数字。要匹配 '-' 本身,应该将它放在范围的最前面或最后面。
  • 如 果将 '!' 字符指定为范围的第一个字符,就表示对这个范围进行求反,因此匹配除指定字符外的任何字符。例如,[!0-9] 表示除数字 0 到 9 之外的任何字符。如果 '!' 不在最前面,就匹配本身。记住,'!' 还用于 shell 历史功能,所以需要小心地对它进行转义。

globbing 单独应用于路径名的每个成分。不能匹配 '/',也不能在范围中包含这个字符。可以在能够指定多个文件或目录名的任何地方使用通配符,例如,在 lscpmvrm 命令中。在清单 54 中,先创建两个名字很怪的文件,然后使用带通配符的 lsrm 命令。


清单 54. 通配符模式示例


[ian@echidna lpi103]$ echo odd1>'text[*?!1]'
[ian@echidna lpi103]$ echo odd2>'text[2*?!]'
[ian@echidna lpi103]$ ls
backup text1 text2 text3 text5 xaa yaa
sedtab text[*?!1] text[2*?!] text4 text6 xab yab
[ian@echidna lpi103]$ ls text[2-4]
text2 text3 text4
[ian@echidna lpi103]$ ls text[!2-4]
text1 text5 text6
[ian@echidna lpi103]$ ls text*[2-4]*
text2 text[2*?!] text3 text4
[ian@echidna lpi103]$ ls text*[!2-4]* # Surprise!
text1 text[*?!1] text[2*?!] text5 text6
[ian@echidna lpi103]$ ls text*[!2-4] # More surprise!
text1 text[*?!1] text[2*?!] text5 text6
[ian@echidna lpi103]$ echo text*>text10
[ian@echidna lpi103]$ ls */!*
text[*?!1] text[2*?!]
[ian@echidna lpi103]$ ls *[x/!]*
text1 text2 text3 text5 xaa
text[*?!1] text[2*?!] text4 text6 xab
[ian@echidna lpi103]$ ls *[y/!]*
text[*?!1] text[2*?!] yaa yab
[ian@echidna lpi103]$ ls tex?[[]*
text[*?!1] text[2*?!]
[ian@echidna lpi103]$ rm tex?[[]*
[ian@echidna lpi103]$ ls *b*
sedtab xab yab

backup:
text1.bkp.1 text1.bkp.2
[ian@echidna lpi103]$ ls backup/*2
backup/text1.bkp.2
[ian@echidna lpi103]$ ls -d .*
. ..

注意:

  1. 求反与 '*' 结合使用会产生一些令人吃惊的效果。模式 '*[!2-4]' 匹配名称中后面没有 2、3 或 4 的最长的部分,所以匹配 text[*?!1] 和 text[2*?!] 两者。所以清单中两处令人吃惊的地方就不难理解了。
  2. 与前面的 ls 示例一样,如果模式扩展产生目录名,而且没有指定 -d 选项,那么会列出这个目录的内容(上面的模式 '*b*' 示例就是这种情况)。
  3. 如果文件名以点(.)开头,那么必须显式匹配这个字符。注意,只有最后一个 ls 命令列出了两个特殊目录项目(. 和 ..)。

记 住,命令中的任何通配符都由 shell 负责进行扩展,这可能会产生出乎意料的结果。因此,如果指定的模式不匹配任何文件系统对象,那么 POSIX 要求将原来的模式字符串传递给命令。清单 55 演示了这种情况。一些比较早的实现把一个空列表传递给命令,所以您可能会遇到一些行为异常的旧脚本。清单 55 中也演示了这些问题。


清单 55. 通配符模式的异常情况


[ian@echidna lpi103]$ echo text*
text1 text2 text3 text4 text5 text6
[ian[ian@echidna lpi103]$ echo "text*"
text*
@echidna lpi103]$ echo text[[/!?]z??
text[[!?]z??

关于 globbing 的更多信息,请参考 man 7 glob。需要加小节号是因为在第 3 小节中也有 glob 信息。理解所有 shell 交互的最佳方式是实践,所以只要有机会就尝试使用通配符。记住先用 ls 检查通配符模式的结果,然后再在 cpmvrm 命令中使用,以免产生意外的效果。

回页首


接触文件

现在来看看 touch 命令,它更新文件访问和修改时间或创建空文件。在下一部分中,将看到如何使用这一信息搜索文件和目录。我们将使用本教程前面创建的 lpi103 目录。

touch

不带选项的 touch 以一个或多个文件名作为参数,并更新文件的 修改 时间。这就是一般情况下长目录清单显示的时间戳。在清单 56 中,使用 echo 创建一个小文件 f1,然后使用长目录清单显示修改时间(即 mtime)。在这种情况下,这个时间正好就是创建文件的时间。然后使用 sleep 命令等待 60 秒并再次运行 ls。注意,文件时间戳增加了一分钟。


清单 56. 用 touch 更新修改时间


[ian@echidna lpi103]$ echo xxx>f1; ls -l f1; sleep 60; touch f1; ls -l f1
-rw-rw-r-- 1 ian ian 4 Nov 4 15:57 f1
-rw-rw-r-- 1 ian ian 4 Nov 4 15:58 f1

如果指定一个不存在的文件的文件名,那么 touch 在一般情况下会创建一个空文件,除非指定了 -c--no-create 选项。清单 57 演示了这些命令。注意只创建了 f2。


清单 57. 用 touch 创建空文件


[ian@echidna lpi103]$ touch f2; touch -c f3; ls -l f*
-rw-rw-r-- 1 ian ian 4 Nov 4 15:58 f1
-rw-rw-r-- 1 ian ian 0 Nov 4 16:12 f2

touch 命令还可以使用 -d-t 选项将文件的 mtime 设置为特定的日期和时间。-d 在可接受的日期和时间方面非常灵活,而 -t 选项至少需要 MMDDhhmm 时间,年和秒值是可选的。清单 58 显示一些示例。


清单 58. 用 touch 设置 mtime


[ian@echidna lpi103]$ touch -t 200511051510.59 f3
[ian@echidna lpi103]$ touch -d 11am f4
[ian@echidna lpi103]$ touch -d "last fortnight" f5
[ian@echidna lpi103]$ touch -d "yesterday 6am" f6
[ian@echidna lpi103]$ touch -d "2 days ago 12:00" f7
[ian@echidna lpi103]$ touch -d "tomorrow 02:00" f8
[ian@echidna lpi103]$ touch -d "5 Nov" f9
[ian@echidna lpi103]$ ls -lrt f*
-rw-rw-r-- 1 ian ian 0 Oct 24 12:32 f5
-rw-rw-r-- 1 ian ian 4 Nov 4 15:58 f1
-rw-rw-r-- 1 ian ian 0 Nov 4 16:12 f2
-rw-rw-r-- 1 ian ian 0 Nov 5 00:00 f9
-rw-rw-r-- 1 ian ian 0 Nov 5 12:00 f7
-rw-rw-r-- 1 ian ian 0 Nov 5 15:10 f3
-rw-rw-r-- 1 ian ian 0 Nov 6 06:00 f6
-rw-rw-r-- 1 ian ian 0 Nov 7 11:00 f4
-rw-rw-r-- 1 ian ian 0 Nov 8 2005 f8

如果不确定日期表达式会解析成什么日期,可以使用 date 命令来检查。它还接受 -d 选项并可以解析与 touch 相同的日期格式。

可以使用 -r(或 --reference)选项加上一个参考文件名 来表示 touch(或 date)应该使用一个现有文件的时间戳。清单 59 显示一些示例。


清单 59. 来自参考文件的时间戳


[ian@echidna lpi103]$ date
Mon Nov 7 12:40:11 EST 2005
[ian@echidna lpi103]$ date -r f1
Fri Nov 4 15:58:27 EST 2005
[ian@echidna lpi103]$ touch -r f1 f1a
[ian@echidna lpi103]$ ls -l f1*
-rw-rw-r-- 1 ian ian 4 Nov 4 15:58 f1
-rw-rw-r-- 1 ian ian 0 Nov 4 15:58 f1a

Linux 系统会记录文件修改 时间和文件访问 时间。在创建文件时,这两个时间戳设置为相同的值,在修改文件时同时重新设置这两者。只要访问文件,即使没有修改文件,访问时间也会更新。对于最后一个 touch 示例,我们将看看文件访问 时间。-a(或 --time=atime--time=access--time=use)选项指定访问时间应该更新。清单 60 使用 cat 命令访问 f1 文件并显示它的内容。然后使用 ls -lls -lu 分别显示 f1 和 f1a 的修改时间和访问时间,f1a 是使用 f1 作为参考文件创建的。然后使用 touch -a 将 f1 的访问时间重新设置为 f1a 的访问时间。


清单 60. 访问时间和修改时间


[ian@echidna lpi103]$ cat f1
xxx
[ian@echidna lpi103]$ ls -lu f1*
-rw-rw-r-- 1 ian ian 4 Nov 7 14:13 f1
-rw-rw-r-- 1 ian ian 0 Nov 4 15:58 f1a
[ian@echidna lpi103]$ ls -l f1*
-rw-rw-r-- 1 ian ian 4 Nov 4 15:58 f1
-rw-rw-r-- 1 ian ian 0 Nov 4 15:58 f1a
[ian@echidna lpi103]$ cat f1
xxx
[ian@echidna lpi103]$ ls -l f1*
-rw-rw-r-- 1 ian ian 4 Nov 4 15:58 f1
-rw-rw-r-- 1 ian ian 0 Nov 4 15:58 f1a
[ian@echidna lpi103]$ ls -lu f1*
-rw-rw-r-- 1 ian ian 4 Nov 7 14:13 f1
-rw-rw-r-- 1 ian ian 0 Nov 4 15:58 f1a
[ian@echidna lpi103]$ touch -a -r f1a f1
[ian@echidna lpi103]$ ls -lu f1*
-rw-rw-r-- 1 ian ian 4 Nov 4 15:58 f1
-rw-rw-r-- 1 ian ian 0 Nov 4 15:58 f1a

关于允许使用的许多日期和时间格式的完整信息,请参考 touch 和 date 命令的手册页或 info 页。



回页首


搜索文件

在本节的最后一个主题中,将讨论 find 命令,它用于在一个或多个目录树中搜索文件,搜索条件包括名称、时间或大小等等。同样,还是使用前面创建的 lpi103 目录。

find

find 命令使用完整名称或部分名称,或者根据其他条件(比如大小、类型、文件拥有者、创建日期或最后访问日期)搜索文件或目录。最基本的搜索是根据名称或名称的 一部分进行搜索。在清单 61 给出的示例中,先在 lpi103 目录中搜索名称里有 ‘1’ 或 ‘k’ 的所有文件,然后执行一些路径搜索,这在稍后解释。


清单 61. 按名称搜索文件


[ian@echidna lpi103]$ find . -name "*[1k]*"
./text1
./f1
./backup
./backup/text1.bkp.2
./backup/text1.bkp.1
./f1a
[ian@echidna lpi103]$ find . -ipath "*ACK*1"
./backup/text1.bkp.1
[ian@echidna lpi103]$ find . -ipath "*ACK*/*1"
./backup/text1.bkp.1
[

注意:

  1. 可以使用 shell 通配符模式,就与前面讨论 通配符和 globbing 时看到的一样。
  2. 可以使用 -path 而不是 -name 以匹配完整的路径而不只是基本文件名。在这种情况下,模式 可能 跨越路径成分。
  3. 如果希望进行大小写敏感的搜索(如上面的 ipath 示例所示),那么在搜索字符串或模式的 find 选项前面加一个 ‘i’。
  4. 如果要搜索名称以点开头的文件或目录,比如 .bashrc 或当前目录(.),那么 必须 在模式中最前面指定点。否则,名称搜索会忽略这些文件或目录。

在上面的第一个示例中,找到了两个文件和一个目录(./backup)。使用 -type 参数加上一个字母的类型对搜索进行限制。‘f’ 表示常规文件,‘d’表示目录,‘l’ 表示符号链接。其他类型请参考 find 的手册页。清单 62 显示搜索目录(-type d)的结果。


清单 62. 按类型搜索文件


[ian@echidna lpi103]$ find . -type d
.
./backup
[ian@echidna lpi103]$ find . -type d -name "*"
./backup

注意,如果在 -type d 搜索中没有对名称进行限制,那么就会显示名称以点开头的目录(在这个示例中只有当前目录)。

还可以按文件大小进行搜索,可以针对特定大小(n),也可以针对大于(+n)或小于(-n)给定值的文件。通过给出文件大小的上界和下界,可以搜索大小在给定范围内的文件。在默认情况下,find-size 选项假设单位是 ‘b’,即 512 字节的块。指定 ‘c’ 表示字节,‘k’ 表示 kb。在清单 63 中,先搜索大小为 0 的所有文件,然后搜索大小为 24 或 25 字节的所有文件。注意,指定 -empty 而不是 -size 0 也可以搜索空文件。


清单 63. 按大小搜索文件


[ian@echidna lpi103]$ find . -size 0
./f2
./f3
./f4
./f5
./f6
./f7
./f8
./f9
./f1a
[ian@echidna lpi103]$ find . -size -26c -size +23c -print
./text1
./text2
./text5
./backup/text1.bkp.2
./backup/text1.bkp.1

清单 63 引入了 -print 选项,这是在搜索返回的结果上执行动作 的例子。在 bash shell 中,如果没有指定动作,这就是默认动作。在某些系统上和某些 shell 上,要求指定动作,否则就没有输出。

其他动作包括 -ls(打印文件信息,相当于 ls -lids 命令)或 -exec(对每个文件执行一个命令)。-exec 必须用一个分号终止,这个分号必须进行转义,以避免 shell 解释它。如果在命令中要使用返回的文件,就指定 {}。正如前面所说的,花括号对于 shell 也有特殊意义,所以需要转义或加上引号。清单 64 显示如何使用 -ls-exec 选项列出文件信息。


清单 64. 搜索文件并在结果上操作


[ian@echidna lpi103]$ find . -size -26c -size +23c -ls
2128984 4 -rw-rw-r-- 1 ian ian 24 Sep 23 12:27 ./text1
2128985 4 -rw-rw-r-- 1 ian ian 25 Sep 23 13:39 ./text2
2128982 4 -rw-rw-r-- 1 ian ian 24 Sep 26 12:46 ./text5
1564497 4 -rw-rw-r-- 1 ian ian 24 Oct 4 09:45 ./backup/text1.bkp.2
2129019 4 -rw-rw-r-- 1 ian ian 24 Oct 4 09:43 ./backup/text1.bkp.1
[ian@echidna lpi103]$ find . -size -26c -size +23c -exec ls -l '{}' /;
-rw-rw-r-- 1 ian ian 24 Sep 23 12:27 ./text1
-rw-rw-r-- 1 ian ian 25 Sep 23 13:39 ./text2
-rw-rw-r-- 1 ian ian 24 Sep 26 12:46 ./text5
-rw-rw-r-- 1 ian ian 24 Oct 4 09:45 ./backup/text1.bkp.2
-rw-rw-r-- 1 ian ian 24 Oct 4 09:43 ./backup/text1.bkp.1

-exec 选项可以用于您能够想像得到的任何用途。例如:

find . -empty -exec rm '{}' /;

删除目录树中的所有空文件,

find . -name "*.htm" -exec mv '{}' '{}l' /;

将所有 .htm 文件重命名为 .html 文件。

最后,使用 touch 命令描述的时间戳来搜索具有特定时间戳的文件。清单 65 给出了三个示例:

  1. 使用 -mtime -2 时,find 命令搜索最近两天中修改过的所有文件。在这种情况下,一天是相对于当前日期和时间的 24 小时时间段。注意,如果想要根据访问时间而不是修改时间搜索文件,那么要使用 -atime
  2. 添加 -daystart 选项意味着天是日历天,从午夜开始。现在 f3 文件从列表中排除了。
  3. 最后,使用以分钟(而不是天)为单位的时间范围搜索在一小时(60 分钟)和 10 小时(600 分钟)之间修改过的文件。

清单 65. 按时间戳搜索文件


[ian@echidna lpi103]$ date
Mon Nov 7 14:59:02 EST 2005
[ian@echidna lpi103]$ find . -mtime -2 -type f -exec ls -l '{}' /;
-rw-rw-r-- 1 ian ian 0 Nov 5 15:10 ./f3
-rw-rw-r-- 1 ian ian 0 Nov 7 11:00 ./f4
-rw-rw-r-- 1 ian ian 0 Nov 6 06:00 ./f6
-rw-rw-r-- 1 ian ian 0 Nov 8 2005 ./f8
[ian@echidna lpi103]$ find . -daystart -mtime -2 -type f -exec ls -l '{}' /;
-rw-rw-r-- 1 ian ian 0 Nov 7 11:00 ./f4
-rw-rw-r-- 1 ian ian 0 Nov 6 06:00 ./f6
-rw-rw-r-- 1 ian ian 0 Nov 8 2005 ./f8
[ian@echidna lpi103]$ find . -mmin -600 -mmin +60 -type f -exec ls -l '{}' /;
-rw-rw-r-- 1 ian ian 0 Nov 7 11:00 ./f4

find 命令的手册页可以帮助您学习这里没有提到的众多选项。



回页首

流、管道和重定向

本节讨论初级管理(LPIC-1)考试 101 的主题 1.103.4 的内容。这个主题的权值是 5。

在本节中,学习以下主题:

  • 对标准 IO 流进行重定向:标准输入、标准输出和标准错误
  • 将一个命令的输出通过管道连接到另一个命令的输入
  • 将输出发送到 stdout 和文件
  • 将命令的输出用作另一个命令的参数

对标准 IO 进行重定向

shell 使用三种标准 I/O

  1. stdout标准输出流,显示来自命令的输出。它的文件描述符是 1。
  2. stderr标准错误流,显示来自命令的错误输出。它的文件描述符是 2。
  3. stdin标准输入流,向命令提供输入。它的文件描述符是 0。
输入流向程序提供输入,常常是来自终端键盘。输出流打印文本字符,常常是在终端上。终端原来是 ASCII 打字机或显示终端,但是现在常常是图形桌面上的窗口。

正如在 文本流和过滤器 一节中看到的,可以将标准输出重定向到文件或者另一个命令的标准输入,还可以将标准输入重定向为来自文件或者来自另一个命令的输出。

对输出进行重定向

对输出进行重定向有两种方法:

n>
将来自文件描述符 n 的输出重定向到文件。必须对这个文件有写权限。如果文件不存在,就创建它。如果它存在,现有内容常常会丢失而没有警告。
n>>
也将来自文件描述符 n 的输出重定向到文件。同样,必须对这个文件有写权限。如果文件不存在,就创建它。如果它存在,输出就附加到现有文件后面。
n> 或 n>> 中的 n 是指 文件描述符。如果忽略,就假设是标准输出。清单 66 演示使用重定向将来自 ls 的标准输出和标准错误分别放进两个文件,这些文件是在 lpi103 目录中创建的。还演示了如何将输出附加到现有的文件中。
清单 66. 输出重定向


[ian@echidna lpi103]$ ls x* z*
ls: z*: No such file or directory
xaa xab
[ian@echidna lpi103]$ ls x* z* >stdout.txt 2>stderr.txt
[ian@echidna lpi103]$ ls w* y*
ls: w*: No such file or directory
yaa yab
[ian@echidna lpi103]$ ls w* y* >>stdout.txt 2>>stderr.txt
[ian@echidna lpi103]$ cat stdout.txt
xaa
xab
yaa
yab
[ian@echidna lpi103]$ cat stderr.txt
ls: z*: No such file or directory
ls: w*: No such file or directory

使用 n> 进行输出重定向常常会覆盖现有文件。可以使用内置命令 setnoclobber 选项控制这种行为。如果设置了这个选项,就会在覆盖现有文件时发出警告;如果确实要覆盖,可以使用 n>|,如清单 67 所示。


清单 67. 带 noclobber 选项的输出重定向


[ian@echidna lpi103]$ set -o noclobber
[ian@echidna lpi103]$ ls x* z* >stdout.txt 2>stderr.txt
-bash: stdout.txt: cannot overwrite existing file
[ian@echidna lpi103]$ ls x* z* >|stdout.txt 2>|stderr.txt
[ian@echidna lpi103]$ cat stdout.txt
xaa
xab
[ian@echidna lpi103]$ cat stderr.txt
ls: z*: No such file or directory
[ian@echidna lpi103]$ set +o noclobber #restore original noclobber setting

有时候希望将标准输出和标准错误重定向到同一个文件中。自动化进程或后台作业常常采用这种做法,这样就能够在以后查看输出。使用 &> 或 &>> 将标准输出和标准错误重定向到同一个位置。另一种方法是对文件描述符 n 进行重定向,然后使用 m>&n 或 m>>&n 将文件描述符 m 重定向到同一个位置。对输出进行重定向的次序很重要。例如,
command 2>&1 >output.txt

command >output.txt 2>&1
不一样。清单 68 中演示了这些重定向。注意在最后一个命令中,标准输出在标准错误之后进行重定向,所以标准错误输出仍然发送到终端窗口。


清单 68. 将两个流重定向到一个文件中


[ian@echidna lpi103]$ ls x* z* &>output.txt
[ian@echidna lpi103]$ cat output.txt
ls: z*: No such file or directory
xaa
xab
[ian@echidna lpi103]$ ls x* z* >output.txt 2>&1
[ian@echidna lpi103]$ cat output.txt
ls: z*: No such file or directory
xaa
xab
[ian@echidna lpi103]$ ls x* z* 2>&1 >output.txt
ls: z*: No such file or directory
[ian@echidna lpi103]$ cat output.txt
xaa
xab

有时候希望完全忽略标准输出或标准错误。为此,可以把适当的流重定向到 /dev/null 中。在清单 69 中,演示如何忽略来自 ls 命令的错误输出。


清单 69. 使用 /dev/null 忽略输出


[ian@echidna lpi103]$ ls x* z* 2>/dev/null
xaa xab
[ian@echidna lpi103]$ cat /dev/null

对输入进行重定向

正如可以对 stdout 和 stderr 流进行重定向,也可以使用 < 操作符将 stdin 重定向为来自文件。在前面讨论 sort 和 uniq 时,曾经使用 tr 命令将 text1 文件中的空格替换为制表符。在这个示例中,我们使用来自 cat 命令的输出为 tr 创建标准输入。无需不断地调用 cat,现在可以使用输入重定向将空格转换为制表符,如清单 70 所示。


清单 70. 输入重定向


[ian@echidna lpi103]$ tr ' ' '/t'<text1
1 apple
2 pear
3 banana

shell(包括 bash)还有 here-document 的概念,这是输入重定向的另一种形式。这使用 << 以及一个单词(比如 END),这个单词作为输入结束的标志。清单 71 演示了这种做法。


清单 71. 用 here-document 进行输入重定向


[ian@echidna lpi103]$ sort -k2 <<END
> 1 apple
> 2 pear
> 3 banana
> END
1 apple
3 banana
2 pear

回忆一下在 清单 23 中是如何创建 text2 文件的。您可能会奇怪为什么不能只是输入 sort -k2,输入数据,然后按下 Ctrl-d 来结束输入。简短的答案是可以这么做,但是当时还没有学习 here-document。在 shell 脚本中常常使用 here-document(这在关于 shell、脚本、编程以及编译的主题 109 教程中讨论)。脚本没有其他方法能够判断脚本的哪些行应该作为输入对待。因为 shell 脚本大量使用制表符来进行缩进,从而提高可读性,所以 here-document 还会造成另一种麻烦。如果使用 <<- 而不是 <<,那么前面的制表符会被删除。在清单 72 中,使用与 清单 42 中一样的技术创建制表符的替代字符。然后创建一个非常小的 shell 脚本,它包含两个读取 here-document 的 cat 命令。最后,使用 .(点)命令引用(source) 脚本,这意味着在当前 shell 上下文中运行它。


清单 72. 用 here-document 进行输入重定向


[ian@echidna lpi103]$ ht=$(echo -en "/t")
[ian@echidna lpi103]$ cat<<END>ex-here.sh
> cat <<-EOF
> apple
> EOF
> ${ht}cat <<-EOF
> ${ht}pear
> ${ht}EOF
> END
[ian@echidna lpi103]$ cat ex-here.sh
cat <<-EOF
apple
EOF
cat <<-EOF
pear
EOF
[ian@echidna lpi103]$ . ex-here.sh
apple
pear



回页首


管道

在 文本流和过滤器 一节中指出,文本过滤 过程就是取得文本输入流,在文本上执行某些转换,然后将它发送到输出流中。还指出,过滤常常是通过构造命令的管道 来完成的,也就是对一个命令的输出进行管道连接重定向,作为下一个命令的输入使用。以这种方式使用管道并不只限于文本流,尽管文本流是最常使用这些方式的地方。

将 stdout 管道连接到 stdin

我们已经看到,在两个命令之间使用 |(管道)操作符可以将第一个命令的 stdout 定向到第二个命令的 stdin。可以通过添加更多的命令和更多的管道操作符来构造长管道,如清单 73 所示。


清单 73. 对几个命令的输出进行管道连接


command1 | command2 | command3

要注意的是,管道 只能 将 stdout 管道连接到 stdin。不能使用 2| 对 stderr 进行管道连接,至少用到目前为止学习的工具无法完成这种任务。如果 stderr 已经被重定向到 stdout,那么这两个流都被管道连接。在清单 74 中,使用管道对错误消息和正常输出消息进行排序,这些消息来自一个奇怪的 ls 命令,其中的通配符参数并没有按照字母表的次序排列。


清单 74. 对两个输出流进行管道连接


[ian@echidna lpi103]$ ls y* x* z* u* q* 2>&1 |sort
ls: q*: No such file or directory
ls: u*: No such file or directory
ls: z*: No such file or directory
xaa
xab
yaa
yab

管道中的任何命令都可以有选项或参数。许多命令使用连字符(-)替代文件名参数,这表示输入应该来自 stdin 而不是文件。请查看命令的手册页。将功能有限的多个命令连接成长的管道是在 Linux 和 UNIX 上完成任务的常用方式。

在 Linux 和 UNIX 系统上管道的一个优点是,与某些其他流行的操作系统不同,管道不涉及中间文件。第一个命令的 stdout 并不是 写到文件中,然后再由第二个命令读取。如果您的 tar 版本碰巧不支持使用 bzip2 对压缩的文件进行解压,那么没关系。在主题 102 的教程中可以看到,可以使用管道

bunzip2 -c drgeo-1.1.0.tar.bz2 | tar -xvf -

来完成这个任务。



回页首


输出作为参数

在 使用命令行 一节中,我们学习了命令替换以及如何将命令的输出作为另一个命令的一部分使用。在前面的 基本文件管理 一节中,学习了如何使用 find 命令的 -exec 选项,将 find 命令的输出作为另一个命令的输入使用。清单 75 使用这些技术以三种方式显示 text1 和 text2 文件的内容。


清单 75. 利用命令替换和 find -exec 将输出用作参数


[ian@echidna lpi103]$ cat `ls text[12]`
1 apple
2 pear
3 banana
9 plum
3 banana
10 apple
[ian@echidna lpi103]$ cat $(find . -name "text[12]")
1 apple
2 pear
3 banana
9 plum
3 banana
10 apple
[ian@echidna lpi103]$ find . -name "text[12]" -exec cat '{}' /;
1 apple
2 pear
3 banana
9 plum
3 banana
10 apple

上面这些方法是有效的,但是有一些限制。我们来考虑文件名包含空白(在这个示例中是空格)的情况。看看清单 76 ,试着自己理解每个命令发生的情况,然后再看后面的解释。


清单 76. 处理包含空白的文件名


[ian@echidna lpi103]$ echo grapes>"text sample2"
[ian@echidna lpi103]$ cat `ls text*le2`
cat: text: No such file or directory
cat: sample2: No such file or directory
[ian@echidna lpi103]$ cat "`ls text*le2`"
grapes
[ian@echidna lpi103]$ cat "`ls text*2`"
cat: text2
text sample2: No such file or directory

这里执行的任务如下:

  • 创建一个称为 “text sample2” 的文件,其中只包含一行,只有单词 “grapes”。
  • 尝试使用命令替换来显示 “text sample2” 文件的内容,这个操作失败了,因为 shell 将 两个 参数传递给 cat,即 text 和 sample2。
  • 我们比 shell 聪明,决定在命令替换值外面加上引号。这样操作正常执行了。
  • 最后,我们修改通配符表达式,输出是一个看起来非常奇怪的错误。发生的情况是,shell 向 cat 命令提供 一个 参数,这个参数等于

    echo -e "text2/ntext sample2"

    产生的字符串。如果这看起来很奇怪,请自己试试!

需要有办法清楚地识别各个文件名,无论它们是由单一单词还是由多个单词组成的。我们在前面没有提到过,但是在管道或命令替换中使用命令(比如 ls)的输出时,常常是每行一个项目地提交输出。处理这种情况的一种方法是,在内置命令 while 建立的循环中使用内置命令 read。尽管这超出了本教程的目标,但是我们仍然给出一个示例,以此引发您的兴趣。


清单 77. 在 while 循环中使用 read


[ian@echidna lpi103]$ ls text*2 | while read l; do cat "$l";done
9 plum
3 banana
10 apple
grapes

xargs

在很多时候,我们希望处理文件列表,所以确实需要用某种方式搜索和处理文件。幸运的是,find 命令有一个选项 -print0,它以 null 字符分隔输出的文件名而不是用新行字符。tarxargs 等命令有 -0(或 --null)选项,这使它们能够理解这种形式的参数。我们已经看到了 tar 命令。xargs 命令的工作方式有点儿像 find-exec 选项,但是有一些重要的差异。我们先来看一个示例。


清单 78. 使用带 -0 选项的 xargs


[ian@echidna lpi103]$ find . -name "text*2" -print0 |xargs -0 cat
9 plum
3 banana
10 apple
1 apple
2 pear
3 banana
grapes

注意,现在将 find 的输出管道连接到 xargs。不需要在命令的末尾加分号进行分隔,而且在默认情况下 xargs 将参数附加到命令字符串后面。但是,我们看到了 7 行输出,而不是期望的 4 行。什么地方错了?

再次搜索

可以使用 wc 命令检查我们认为会打印的那两个文件,确实只有 4 行。问题的原因是 find 会搜索备份目录,在那里还会找到 backup/text1.bkp.2,这个文件也与通配符模式匹配。为了解决这个问题,使用 find-maxdepth 选项将搜索的深度限制为一个目录,即当前目录。还有一个对应的 -mindepth 选项,允许更具体地指定在哪里进行搜索。清单 79 演示了最后这个解决方案。


清单 79. 对搜索进行限制,从而只显示 4 行


[ian@echidna lpi103]$ ls text*2
text2 text sample2
[ian@echidna lpi103]$ wc text*2
3 6 25 text2
1 1 7 text sample2
4 7 32 total
[ian@echidna lpi103]$ find . -name "text*2" -maxdepth 1 -print0 |xargs -0 cat
9 plum
3 banana
10 apple
grapes

关于 xargs 的其他问题

xargsfind -exec 之间还有其他一些差异。

  • xargs 命令在默认情况下向命令传递尽可能多的参数。可以使用 -l--max-lines 和一个数字限制输入行的数量。也可以使用 -n--max-args 限制传递的参数数量,或者使用 -s--max-chars 限制参数字符串中使用的最大字符数量。如果命令可以处理多个参数,那么同时处理尽可能多的参数一般来说效率更高。
  • find -exec 一样,如果指定 -i--replace 选项,那么可以使用 '{}'。可以通过为 -i 指定值,改变表示在哪里替换输入参数的默认字符串 '{}'。这意味着 -l 1
对于 xargs 的最后一个示例见清单 80。
清单 80. 更多的 xargs 示例


[ian@echidna lpi103]$ # pass all arguments at once
[ian@echidna lpi103]$ find . -name "text*2" |xargs echo
./text2 ./backup/text1.bkp.2 ./text sample2
[ian@echidna lpi103]$ # show the files we created earlier with the touch command
[ian@echidna lpi103]$ ls f[0-n]*|xargs echo
f1 f1a f2 f3 f4 f5 f6 f7 f8 f9
[ian@echidna lpi103]$ # remove them in one stroke
[ian@echidna lpi103]$ ls f[0-n]*|xargs rm
[ian@echidna lpi103]$ # Use a replace string
[ian@echidna lpi103]$ find . -name "text*2" |xargs -i echo - '{}' -
- ./text2 -
- ./backup/text1.bkp.2 -
- ./text sample2 -
[ian@echidna lpi103]$ # Limit of one input line per invocation
[ian@echidna lpi103]$ find . -name "text*2" |xargs -l1 echo
./text2
./backup/text1.bkp.2
./text sample2
[ian@echidna lpi103]$ # Limit of one argument per invocation
[ian@echidna lpi103]$ find . -name "text*2" |xargs -n1 echo
./text2
./backup/text1.bkp.2
./text
sample2

注意,这里没有使用 -print0。您能解释清单 80 中的示例吗?



回页首


对输出进行转储

本节的最后简短地讨论另一个命令。有时候希望在屏幕上看到输出,同时保存一个副本。可以 在一个窗口中将命令输出重定向到文件,然后在另一个窗口中使用 tail -fn1 跟踪输出,但是使用 tee 命令更容易。

与管道一起使用 tee。参数是作为标准输出的一个文件(或多个文件)。-a 选项要求将输出附加到文件,而不是覆盖文件。与前面讨论管道时看到的一样,如果希望同时保存 stderr 和 stdout,需要将 stderr 重定向到 stdout,然后再管道连接到 tee。清单 81 使用 tee 将输出保存到两个文件 f1 和 f2。


清单 81. 用 tee 对 stdout 进行转储


[ian@echidna lpi103]$ ls text[1-3]|tee f1 f2
text1
text2
text3
[ian@echidna lpi103]$ cat f1
text1
text2
text3
[ian@echidna lpi103]$ cat f2
text1
text2
text3



回页首

创建、监视和杀死进程

本节讨论初级管理(LPIC-1)考试 101 的主题 1.103.5 的内容。这个主题的权值是 5。

在本节中,学习以下主题:

  • 前台和后台作业
  • 启动没有用于 I/O 的终端的进程
  • 监视和显示进程
  • 向进程发送信号
  • 识别和杀死进程

显 然,除了正在运行的终端程序之外,计算机上还运行着许多程序。如果正在使用图形桌面,可能会同时打开多个终端窗口,可能打开文件浏览器、互联网浏览器、游 戏、电子表格或其他应用程序。到目前为止,我们的示例都是在终端窗口中输入命令。命令运行,我们等待它完成,在此之前我们不能做其他任何事情。在 使用命令行 一节中,我们遇到了显示进程状态的 ps 命令,并看到进程有进程 ID(PID)和父进程 ID(PPID)。在本节中,学习如何使用终端窗口同时执行多个任务。

前台和后台作业

在终端窗口中运行命令时,比如在此之前我们给出的那些命令示例,是在前台 运行它。我们的命令都运行得非常快,所以进程不会长期存在。为了研究长期运行的进程,假设我们正在运行一个图形桌面,希望在桌面上显示一个数字化时钟。大多数桌面已经有了这种时钟,这没关系,我们只是以此作为示例。

如果安装了 X Window 系统,就可能有 xclockxeyes 这样的实用程序。这两种程序对于这个练习都适用,但是我们使用 xclock。手册页指出,可以使用以下命令在图形桌面上启动一个数字化时钟

xclock -d -update 1

-update 1 部分要求每秒进行一次更新,如果没有这个选项,时钟每分钟只更新一次。现在在一个终端窗口中运行这个命令。应该会看到图 2 所示的时钟,终端窗口的内容看起来应该像清单 82 这样。如果您没有 xclock 或 X Window 系统,我们稍后会解释如何用终端创建一个简单的数字化时钟,所以您可以先学下去,在创建了那个时钟之后,用它做这些练习。


图 2. 用 xclock 显示数字化时钟


清单 82. 启动 xclock


[ian@echidna ian]$ xclock -d -update 1

不幸的是,终端窗口不再有提示符,所以确实需要重新获得控制。幸运的是,bash shell 有一个暂停 键,Ctrl-z。按下这个组合键会重新出现终端提示符,如清单 83 所示。


清单 83. 用 Ctrl-z 暂停 xclock


[ian@echidna ian]$ xclock -d -update 1

[1]+ Stopped xclock -d -update 1
[ian@echidna ian]$

时钟仍然在桌面上,但是已经停止运行了。它确实已经暂停了。实际上,如果将另一个窗口拖过时钟上面,它甚至不会重新绘制。还会看到一个终端输出消息,表示 “[1]+   Stopped”。这个消息中的 1 是作业号。可以通过输入 fg %1 重新启动时钟。还可以使用命令名或命令名的一部分,即 fg %xclockfg %?clo。最后,如果使用没有参数的 fg,那么就会重新启动最近停止的作业,在这个示例中就是作业 1。用 fg 进行重新启动也将作业带回前台,并不再有 shell 提示符。需要做的事是将这个作业放到后台bg 命令可以完成这个任务,它采用与 fg 命令相同的作业指定方式。清单 84 显示如何使用两种形式的 fg 命令将 xclock 作业带回前台。可以再次暂停它并将它放到后台;时钟继续运行,同时您可以在终端中做其他工作。


清单 84. 将 xclock 放到后台


[ian@echidna ian]$ fg %1
xclock -d -update 1

[1]+ Stopped xclock -d -update 1
[ian@echidna ian]$ fg %?clo
xclock -d -update 1

[1]+ Stopped xclock -d -update 1
[ian@echidna ian]$ bg
[1]+ xclock -d -update 1 &
[ian@echidna ian]$

使用 “&”

您 可能会注意到,在将 xclock 作业放到后台时,消息不再指出 “Stopped”,而是以一个 & 结束。实际上,根本不需要为了将进程放到后台而暂停它。只需要将一个 & 放在命令后面,shell 就会在后台启动命令。我们使用这种方法启动一个模拟时钟,它具有小麦背景和红色的指针。应该会看到图 3 这样的时钟和清单 85 这样的终端输出。


图 3. 用 xclock 显示模拟时钟


清单 85. 用 & 在后台启动 xclock


[ian@echidna ian]$ xclock -bg wheat -hd red -update 1&
[2] 5659

注意,这一次消息有点儿不一样。它代表一个作业号和一个进程 id(PID)。稍后会讨论 PID 和状态。现在,使用 jobs 命令了解正在运行什么作业。添加 -l 选项来列出 PID,会看到作业 2 的 PID 确实是 5659,见清单 86。还要注意,作业 2 在作业号旁边有一个加号(+),这表示它是当前作业。如果使用 fg 命令时没有指定作业,这个作业就会转到前台。


清单 86. 显示作业和进程信息


[ian@echidna ian]$ jobs -l
[1]- 4234 Running xclock -d -update 1 &
[2]+ 5659 Running xclock -bg wheat -hd red -update 1 &

在解决与后台作业相关的其他问题之前,我们先创建一个简单的数字化时钟。使用 sleep 命令产生两秒的延迟,并使用 date 命令打印当前日期和时间。将这些命令放在一个 while 循环中,并使用 do/done 块创建一个无限循环。最后,将所有这些放在圆括号中构成命令列表,并使用 & 让整个命令列表在后台运行。


清单 87. 简单的数字化时钟


[ian@echidna ian]$ (while sleep 2; do date;done) &
[1] 16291
[ian@echidna ian]$ Thu Nov 10 22:58:02 EST 2005
Thu Nov 10 22:58:04 EST 2005
Thu Nov 10 22:58:06 EST 2005
Thu Nov 10 22:58:08 EST 2005
fThu Nov 10 22:58:10 EST 2005
Thu Nov 10 22:58:12 EST 2005
gThu Nov 10 22:58:14 EST 2005

( while sleep 2; do
date;
done )
Thu Nov 10 22:58:16 EST 2005
Thu Nov 10 22:58:18 EST 2005

这个命令列表作为作业 1 运行,PID 是 16291。每隔两秒,date 命令运行并在终端上打印一个日期和时间。用户的输入以粗体显示。对于输入很慢的人,在输入一条完整的命令之前,这个命令列表可能已经产生了几行输出,所以 输入的字符可能与输出混在一起。实际上,我们输入的 ‘f’‘g’ 是 fg 命令,用于将这个命令列表带到前台,但是这个命令被两行输出分开了。当最终输入完 fg 命令时,bash 显示正在 shell 中运行的命令,也就是这个命令列表,之后仍然每两秒打印一次时间。

成功地将作业带到后台之后,可以终止(即杀死)它,或者采取某些其他操作。在这个示例中,使用 Ctrl-c 终止时钟。

标准 IO 和后台进程

在前面的示例中,date 命令的输出与 fg 命令的回显字符混在一起。这就产生了一个有意思的问题。如果进程需要来自 stdin 的输入,那么会怎么样?

用来启动后台应用程序的终端进程称为控制终端。 除非被重定向到其他地方,否则来自后台进程的 stdout 和 stderr 流会输出到控制终端。同样,后台任务期望从控制终端获得输入,但是控制终端无法将您输入的字符转发给后台进程的 stdin。在这种情况下,bash shell 暂停进程,这样它就不再执行了。可以将它带到前台并提供必要的输入。清单 88 演示了一种简单的情况。可以将一个命令列表放到后台执行。过了一会儿,按 Enter,进程停止。将它带到前台,提供一行输入,最后按 Ctrl-d 表示输入文件结束。命令列表完成,我们显示创建的文件。


清单 88. 等待 stdin


[ian@echidna ian]$ (date; cat - >bginput.txt; date)&
[1] 18648
[ian@echidna ian]$ Fri Nov 11 00:03:28 EST 2005


[1]+ Stopped ( date; cat - >bginput.txt; date )
[ian@echidna ian]$ fg
( date; cat - >ginput.txt; date )
input data
Fri Nov 11 00:03:53 EST 2005
[ian@echidna ian]$ cat bginput.txt
input data



回页首


没有终端的作业

在 实践中,可能希望让后台进程的标准 IO 流重定向到文件或者来自文件。有另一个相关问题;如果控制终端关闭了或用户注销了,那么进程会怎么样?答案取决于使用的 shell。如果 shell 发送 SIGHUP(或 hangup)信号,那么应用程序可能关闭。我们稍后讨论信号,但是目前考虑解决这个问题的另一种方式。

nohup

nohup 命令用于启动一个命令,它将忽略 hangup 信号并将 stdout 和 stderr 附加到文件。默认文件是 nohup.out 或 $HOME/nohup.out。如果文件不可写,那么命令将不运行。如果希望将输出发送到别处,那么对 stdout 或 stderr 进行重定向。

nohup 的另一个特点是,它不执行管道或命令列表。在 对标准 IO 进行重定向 一节中,我们看到了如何将一组命令保存在 shell 脚本中并引用(source)它。可以将管道或列表保存在文件中,然后使用 sh(默认 shell)或 bash 命令运行它,但是不能使用前面示例中使用的 . 或 source 命令。本系列中的下一个教程(针对主题 104,讨论设备、Linux 文件系统和 Filesystem Hierarchy Standard)将解释如何使脚本文件可执行,但是目前只通过引用(source)或者通过使用 shbash 命令来运行脚本。清单 89 显示如何用脚本运行刚才创建的数字化时钟。但是,将时间写到文件中其实没什么用,而且文件会随着脚本运行而增大,所以将时钟设置为每 30 秒更新一次而不是每秒一次。


清单 89. 使用 nohup 运行脚本中的命令列表


[ian@echidna ian]$ echo "while sleep 30; do date;done">pmc.sh
[ian@echidna ian]$ nohup . pmc.sh&
[1] 21700
[ian@echidna ian]$ nohup: appending output to `nohup.out'

[1]+ Exit 126 nohup . pmc.sh
[ian@echidna ian]$ nohup sh pmc.sh&
[1] 21709
[ian@echidna ian]$ nohup: appending output to `nohup.out'

[ian@echidna ian]$ nohup bash pmc.sh&
[2] 21719
[ian@echidna ian]$ nohup: appending output to `nohup.out'

如果显示 nohup.out 的内容,会看到第一行说明为什么在第一次尝试时会得到退出码 126。后续的行是来自在后台运行的两个 pmc.sh 版本的输出。清单 90 显示了 nohup 的输出。


清单 90. nohup 进程的输出


[ian@echidna ian]$ cat nohup.out
/bin/nice: .: Permission denied
Fri Nov 11 15:30:03 EST 2005
Fri Nov 11 15:30:15 EST 2005
Fri Nov 11 15:30:33 EST 2005
Fri Nov 11 15:30:45 EST 2005
Fri Nov 11 15:31:03 EST 2005

现在,我们来讨论进程的状态。停下来思考一下,现在有了两个作业,它们在文件系统中创建不断增大的文件。可以使用 fg 命令将它们带到前台,然后使用 Ctrl-c 终止它,但是如果让它们运行时间长一些,我们就可以看看监视它们和与它们进行交互的其他方式。



回页首


进程状态

在本节前面的部分中,我们简单地介绍了 jobs 命令以及如何使用它列出作业的进程 id(PID)。

ps

还有另一个命令,ps 命令,可以使用它显示各种进程状态信息。“ps” 是 “process status” 的首字母缩写。ps 命令接受零个或更多 PID 参数并显示相关的进程状态。如果使用带 -p 选项的 jobs 命令,只输出每个作业的进程组主进程 的 PID。将这个输出用作 ps 命令的参数,如清单 91 所示。


清单 91. 后台进程的状态


[ian@echidna ian]$ jobs
[1]- Running nohup sh pmc.sh &
[2]+ Running nohup bash pmc.sh &
[ian@echidna ian]$ jobs -p
21709
21719
[ian@echidna ian]$ ps `jobs -p`
PID TTY STAT TIME COMMAND
21709 pts/3 SN 0:00 sh pmc.sh
21719 pts/3 SN 0:00 bash pmc.sh

如果使用不带选项的 ps,那么会看到一个进程列表,它们都以我们的终端作为控制终端,如清单 92 所示。


清单 92. 用 ps 显示状态


[ian@echidna ian]$ ps
PID TTY TIME CMD
20475 pts/3 00:00:00 bash
21709 pts/3 00:00:00 sh
21719 pts/3 00:00:00 bash
21922 pts/3 00:00:00 sleep
21930 pts/3 00:00:00 sleep
21937 pts/3 00:00:00 ps

其他选项包括 -f(完整)、-j(作业)和 -l(长),可以控制显示多少信息。如果不指定任何 PID,那么另一个有用选项是 --forest 选项,它在一个树型层次结构中显示运行的命令,显示哪个进程是哪个进程的父进程。例如,可以看到前面列表中的 sleep 命令是后台运行的脚本的子进程。如果碰巧在另一个时候运行这个命令,有可能看到进程状态中列出 date 命令,但是出现这种情况的几率非常小,因为这个命令运行得非常快。清单 93 列出了更多的进程状态信息。


清单 93. 更多的状态信息


[ian@echidna ian]$ ps -f
UID PID PPID C STIME TTY TIME CMD
ian 20475 20474 0 15:02 pts/3 00:00:00 -bash
ian 21709 20475 0 15:29 pts/3 00:00:00 sh pmc.sh
ian 21719 20475 0 15:29 pts/3 00:00:00 bash pmc.sh
ian 21945 21709 0 15:34 pts/3 00:00:00 sleep 30
ian 21953 21719 0 15:34 pts/3 00:00:00 sleep 30
ian 21954 20475 0 15:34 pts/3 00:00:00 ps -f
[ian@echidna ian]$ ps -j --forest
PID PGID SID TTY TIME CMD
20475 20475 20475 pts/3 00:00:00 bash
21709 21709 20475 pts/3 00:00:00 sh
21945 21709 20475 pts/3 00:00:00 /_ sleep
21719 21719 20475 pts/3 00:00:00 bash
21953 21719 20475 pts/3 00:00:00 /_ sleep
21961 21961 20475 pts/3 00:00:00 ps

列出其他进程

前面使用的 ps 命令只是列出从当前终端会话启动的进程(注意清单 93 的第二个示例中的 SID 栏)。要看到所有具有控制终端的进程,应该使用 -a 选项。-x 选项显示没有控制终端的进程,-e 选项显示 每个 进程的信息。清单 94 显示所有具有控制终端的进程的完整信息。


清单 94. 显示其他进程


[ian@echidna ian]$ ps -af
UID PID PPID C STIME TTY TIME CMD
ian 4234 32537 0 Nov10 pts/0 00:00:00 xclock -d -update 1
ian 5659 32537 0 Nov10 pts/0 00:00:00 xclock -bg wheat -hd red -update
ian 21709 20475 0 15:29 pts/3 00:00:00 sh pmc.sh
ian 21719 20475 0 15:29 pts/3 00:00:00 bash pmc.sh
ian 21969 21709 0 15:35 pts/3 00:00:00 sleep 30
ian 21977 21719 0 15:35 pts/3 00:00:00 sleep 30
ian 21978 20475 0 15:35 pts/3 00:00:00 ps -af

注意,这个列表包括两个 xclock 进程,都是在前面从系统的主图形终端(在这里由 pts/0 表示)启动的,显示的其他进程都是与一个 ssh(Secure Shell)连接(在这个示例中是 pts/3)相关联的。

ps 还有许多选项,包括对显示哪些字段以及如何显示字段进行控制的选项。其他选项控制如何选择要显示的进程,例如选择特定用户的进程。完整的细节请参考 ps 的手册页,也可以使用 ps --help 获得简单的总结。

top

如果需要多次运行 ps 来查看变化,那么可能需要使用 top 命令。它显示不断更新的进程列表,以及有用的总结信息。关于选项的完整细节请参考 top 的手册页,包括如何按照内存使用情况或其他条件进行排序。清单 95 显示 top 输出的前几行。


清单 95. 监视进程变化


3:37pm up 46 days, 5:11, 2 users, load average: 0.01, 0.17, 0.19
96 processes: 94 sleeping, 1 running, 0 zombie, 1 stopped
CPU states: 0.1% user, 1.0% system, 0.0% nice, 0.9% idle
Mem: 1030268K av, 933956K used, 96312K free, 0K shrd, 119428K buff
Swap: 1052216K av, 1176K used, 1051040K free 355156K cached

PID USER PRI NI SIZE RSS SHARE STAT %CPU %MEM TIME COMMAND
22069 ian 17 0 1104 1104 848 R 0.9 0.1 0:00 top
1 root 8 0 500 480 444 S 0.0 0.0 0:04 init
2 root 9 0 0 0 0 SW 0.0 0.0 0:00 keventd
3 root 9 0 0 0 0 SW 0.0 0.0 0:00 kapmd
4 root 19 19 0 0 0 SWN 0.0 0.0 0:00 ksoftirqd_CPU0
5 root 9 0 0 0 0 SW 0.0 0.0 0:00 kswapd



回页首


信号

现在看看 Linux 信号,这是与进程进行通信的一种异步方式。我们提到过 SIGHUP 信号并使用过 Ctrl-c 和 Ctrl-z(这是向进程发送信号的另一种方式)。发送信号的一般方式是使用 kill 命令。

使用 kill 发送信号

kill 命令向指定的作业或进程发送信号。清单 96 显示使用 SIGTSTP 和 SIGCONT 信号停止并恢复后台作业。使用 SIGTSTP 信号相当于使用 fg 命令将作业带到前台,然后使用 Ctrl-z 暂停它。使用 SIGCONT 相当于使用 bg 命令。


清单 96. 停止并重新启动后台作业


[ian@echidna ian]$ kill -s SIGTSTP %1
[ian@echidna ian]$ jobs -l
[1]+ 21709 Stopped nohup sh pmc.sh
[2]- 21719 Running nohup bash pmc.sh &
[ian@echidna ian]$ kill -s SIGCONT %1
[ian@echidna ian]$ jobs -l
[1]+ 21709 Running nohup sh pmc.sh &
[2]- 21719 Running nohup bash pmc.sh &

在这个示例中,使用作业指示(%1),但是也可以将信号发送给进程 id(比如,作业 %1 的 PID 21709)。如果在作业 %1 停止时使用 tail 命令,就只有一个进程在更新 nohup.out 文件。

还有许多其他信号,可以使用 kill -l 在系统上显示这些信号。一些信号用于报告错误,比如非法操作代码、浮点异常或试图访问进程无权访问的内存。注意,信号具有编号(比如 20)和名称(比如 SIGTSTP)。在 -s 选项中既可以使用编号,也可以使用名称。应该检查系统上的信号编号,确定哪个编号属于哪个信号。

信号处理程序和进程终止

我们已经见过用 Ctrl-c 终止进程。实际上,这会向进程发送 SIGINT(或 interrupt)信号。如果使用 kill 而不带任何信号名,它就会发送 SIGTERM 信号。对于大多数情况,这两个信号是等效的。

nohup 命令使进程对于 SIGHUP 信号“免疫”。一般情况下,进程可以实现信号处理程序捕获 信号。所以进程可以实现一个信号处理程序来捕获 SIGINT 或 SIGTERM。因为信号处理程序知道信号已经发送了,它可以选择采取什么操作,例如忽略 SIGINT,只在收到 SIGTERM 时终止进程。清单 97 显示如何向作业 %1 发送 SIGTERM 信号。注意,在发送这个信号之后,进程状态显示 “Terminated”。如果发送 SIGINT,会显示 “Interrupt”。过一会儿,发生进程清理,作业将从作业列表中消失。


清单 97. 用 SIGTERM 终止进程


[ian@echidna ian]$ kill -s SIGTERM %1
[ian@echidna ian]$ jobs -l
[1] 21709 Terminated nohup sh pmc.sh
[2]- 21719 Running nohup bash pmc.sh &
[ian@echidna ian]$ jobs -l
[2]+ 21719 Running nohup bash pmc.sh &

信号处理程序为进程提供了很大的灵活性,使进 程可以根据信号执行正常工作和中断,从而实现某些特殊目的。除了允许进程捕获终止请求并采取适当操作,比如关闭文件或检查正在进行的事务,还常常使用信号 让守护进程重新读取它的配置文件,还可能重新启动操作。例如,可以在修改网络参数时向 inetd 进程发送信号,或者在添加新打印机时向行打印机守护进程(lpd)发送信号。

无条件终止进程

某些信号不能被捕获,比如某些硬件异常。SIGKILL 不能被捕获,它会无条件地终止进程。一般来说,只有在其他方式都无法终止进程时才需要发送这个信号。



回页首


注销和 nohup

我们说过,使用 nohup 允许进程在用户注销之后继续运行。现在就这么做,然后重新登录。在重新登录之后,使用 jobsps 检查时钟进程。输出如清单 98 所示。


清单 98. 重新登录之后


[ian@echidna ian]$ jobs
[ian@echidna ian]$ ps -a
PID TTY TIME CMD
4234 pts/0 00:00:00 xclock
5659 pts/0 00:00:00 xclock
27217 pts/4 00:00:00 ps

可以看到这一次我们运行在 pts/4 上,但是没有作业,只有 ps 命令以及原来从图形终端(pts/0)启动的两个 xclock 进程。我们期望的情况不是这样的。但是,所有作业都丢失了。在清单 99 中,显示如何使用 -s 选项和会话 ID 找回失去的作业,在 清单 93 中可以看到会话 ID 是 20475。如果没有会话 ID 可用,请考虑有没有其他方法可以找回作业。


清单 99. 重新登录之后


[ian@echidna ian]$ ps -js 20475
PID PGID SID TTY TIME CMD
21719 21719 20475 ? 00:00:00 bash
27335 21719 20475 ? 00:00:00 sleep

既然已经学习了如何杀死进程,您应该能够使用进程的 PID 和 kill 杀死这些进程。



回页首


进程执行优先级

本节讨论初级管理(LPIC-1)考试 101 的主题 1.103.6 的内容。这个主题的权值是 3。

在本节中,学习以下主题:

  • 进程执行优先级
  • 设置优先级
  • 修改优先级

优先级

在前一节中我们看到,与大多数现代操作系统一样,Linux 可以运行多个进程。为此,Linux 会在进程之间分享 CPU 和其他资源。如果某些进程可以使用 100% 的 CPU,那么别的进程就可能没反应了。在前一节中查看 进程状态 时,我们看到 top 命令的默认输出按照 CPU 使用情况的降序列出进程。如果对我们的简单时钟脚本运行 top 命令,这个进程可能进不了这个列表,因为这个进程在大多数时候不使用 CPU。

系统上可能有许多需要大量使用 CPU 的命令。例如,视频编辑工具以及在不同图像类型或不同声音编码之间进行转换(比如从 mp3 到 ogg)的程序。

我们将创建一个小脚本,它只是使用 CPU,不做有意义的事儿。这个脚本有两个输入,一个计数值和一个标签。它打印标签以及当前日期和时间,然后递减计数值直至为 0,然后再打印标签和日期。这个脚本没有错误检查,也不很健壮,但是它可以帮助我们说明概念。


清单 100. CPU 密集型脚本


[ian@echidna ian]$ echo 'x="$1"'>count1.sh
[ian@echidna ian]$ echo 'echo "$2" $(date)'>>count1.sh
[ian@echidna ian]$ echo 'while [ $x -gt 0 ]; do let x=$x-1;done'>>count1.sh
[ian@echidna ian]$ echo 'echo "$2" $(date)'>>count1.sh
[ian@echidna ian]$ cat count1.sh
x="$1"
echo "$2" $(date)
while [ $x -gt 0 ]; do let x=$x-1;done
echo "$2" $(date)

如果在您自己的系统上运行这个脚本,可能会看到清单 101 所示的输出。这个脚本要使用大量 CPU。如果您不是使用自己的工作站,那么在运行这个脚本之前要确定使用大量 CPU 不会对别人造成不良影响。


清单 101. 运行 count1.sh


[ian@echidna ian]$ sh count1.sh 10000 A
A Mon Nov 14 07:14:04 EST 2005
A Mon Nov 14 07:14:05 EST 2005
[ian@echidna ian]$ sh count1.sh 99000 A
A Mon Nov 14 07:14:26 EST 2005
A Mon Nov 14 07:14:32 EST 2005

到目前为止,还算好。现在使用在本教程中学到的知识创建一个命令列表,从而在后台运行这个脚本,并启动 top 命令来查看这个脚本使用多少 CPU。命令列表如清单 102 所示,top 的输出见清单 103。


清单 102. 运行 count1.sh 和 top


[ian@echidna ian]$ (sh count1.sh 99000 A&);top


清单 103. 使用了大量 CPU


7:20am up 48 days, 20:54, 2 users, load average: 0.05, 0.05, 0.00
91 processes: 88 sleeping, 3 running, 0 zombie, 0 stopped
CPU states: 0.1% user, 0.0% system, 0.0% nice, 0.9% idle
Mem: 1030268K av, 1002864K used, 27404K free, 0K shrd, 240336K buff
Swap: 1052216K av, 118500K used, 933716K free 605152K cached

PID USER PRI NI SIZE RSS SHARE STAT %CPU %MEM TIME COMMAND
8684 ian 20 0 1044 1044 932 R 98.4 0.1 0:01 sh

不坏啊。我们用一个简单的脚本就占用了 98.4% 的 CPU。



回页首


显示和设置优先级

如果有这样的作业长期运行,那么可能发现它会干扰我们(或其他用户)在系统上进行其他工作的能力。Linux 和 UNIX 系统使用一个优先级系统,共有 40 个优先级,范围从 -20(最高优先级)到 19(最低优先级)。

nice

由一般用户启动的进程的优先级常常是 0。nice 命令显示默认优先级。ps 命令也可以显示优先级(nice 或 NI 级别),例如使用 -l 选项。清单 104 显示这种信息,其中突出显示了 nice 值 0。


清单 104. 显示优先级信息


[ian@echidna ian]$ nice
0
[ian@echidna ian]$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
000 S 500 7283 7282 0 70 0 - 1103 wait4 pts/2 00:00:00 bash
000 R 500 9578 7283 0 72 0 - 784 - pts/2 00:00:00 ps

nice 命令还可以用于以不同优先级启动进程。用 -n(或 --adjustment)选项和一个负值来提高优先级,用正值来降低优先级。记住,具有最低优先级值的进程以最高的调度优先级运行。所以可以认为提高优先级值就是对其他进程更谦让。 注意,常常需要具有超级用户(根用户)身份才能指定负的优先级调整值。换句话说,一般用户常常只能让他们的进程更谦让。在清单 105 中,在后台以不同的调度优先级运行 count1.sh 脚本的两个副本。注意,这两个进程的完成时间相差 5 秒。尝试使用不同的 nice 值,或者对第一个进程而不是对第二个进程进行优先级调整,从而体会不同情况的效果。


清单 105. 使用 nice 设置优先级


[ian@echidna ian]$ (sh count1.sh 99000 A&);/
> (nice -n 19 sh count1.sh 99000 B&);/
> sleep 2;ps -l;sleep 20
B Mon Nov 14 08:17:36 EST 2005
A Mon Nov 14 08:17:36 EST 2005
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
000 S 500 7283 7282 0 70 0 - 1104 wait4 pts/2 00:00:00 bash
000 R 500 10765 1 84 80 0 - 1033 - pts/2 00:00:01 sh
000 R 500 10767 1 14 79 19 - 1033 - pts/2 00:00:00 sh
000 R 500 10771 7283 0 72 0 - 784 - pts/2 00:00:00 ps
A Mon Nov 14 08:17:43 EST 2005
B Mon Nov 14 08:17:48 EST 2005

注意,与 nohup 命令一样,不能将命令列表或管道用作 nice 的参数。



回页首


修改优先级

renice

如果启动了一个进程并意识到它应该以不同的优先级运行,那么也有办法在进程启动之后修改它的优先级,也就是使用 renice 命令。为要修改的进程指定绝对的优先级(而不是调整值),如清单 106 所示。


清单 106. 使用 renice 修改优先级


[ian@echidna ian]$ sh count1.sh 299000 A&
[1] 11322
[ian@echidna ian]$ A Mon Nov 14 08:30:29 EST 2005

[ian@echidna ian]$ renice +1 11322;ps -l
11322: old priority 0, new priority 1
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
000 S 500 7283 7282 0 75 0 - 1104 wait4 pts/2 00:00:00 bash
000 R 500 11322 7283 96 77 1 - 1032 - pts/2 00:00:11 sh
000 R 500 11331 7283 0 76 0 - 786 - pts/2 00:00:00 ps
[ian@echidna ian]$ renice +3 11322;ps -l
11322: old priority 1, new priority 3
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
000 S 500 7283 7282 0 75 0 - 1104 wait4 pts/2 00:00:00 bash
000 R 500 11322 7283 93 76 3 - 1032 - pts/2 00:00:16 sh
000 R 500 11339 7283 0 76 0 - 785 - pts/2 00:00:00 ps

可以在手册页中找到关于 nicerenice 的更多信息。



回页首


用正则表达式进行搜索

本节讨论初级管理(LPIC-1)考试 101 的主题 1.103.7 的内容。这个主题的权值是 3。

在本节中,学习以下主题:

  • 正则表达式
  • 使用正则表达式搜索文件和文件系统
  • 通过 sed 使用正则表达式

正则表达式

正则表达式来源于计算机语言理论。大多数计算机科学系的学生都知道,可以用正则表达式表达的语言也就是有穷自动机可以接受的语言。本节讨论的正则表达式是一种表达复杂规则的方式,所以与您在计算机科学课上学到的正则表达式 不是 一回事儿,尽管它们之间确实有继承关系。

正则表达式(也称为 “regex” 或 “regexp”)是一种描述文本字符串(即模式)的方式,它使程序能够针对任意文本字符串匹配 模式,这就提供了非常强大的搜索能力。grep(表示 generalized regular expression processor,通用正则表达式处理程序)是任何 Linux 或 UNIX 程序员或管理员的标准工具。它允许使用正则表达式搜索文件或命令输出。在 文本流和过滤器 一节中,我们介绍了 sedstream editor,流编辑器),这是另一个大量使用正则表达式在文件或文本流中搜索并替换文本的标准工具。本节帮助您更好地理解 grepsed 所使用的正则表达式。另一个大量使用正则表达式的程序是 awk,这个程序属于 LPIC-2 认证考试 201 的范围。

关于正则表达式和计算机语言理论有许多书籍。请参见 参考资料 中的一些学习建议。

在学习正则表达式时,您会发现正则表达式语法与 通配符和 globbing 一节讨论的通配符(即 globbing)语法之间存在相似性。但是,这种相似性只是表面的,这两者没有内在联系。



回页首


基本成分

大多数 Linux 系统上的 GNU grep 使用两种形式的正则表达式语法:基本的扩展的。对于 GNU grep,这两种形式在功能上没有区别。本节描述基本语法,以及它与扩展语法之间的差异。

正则表达式由字符操作符 以及元字符 组成。大多数字符匹配本身,大多数元字符必须使用反斜线(/)进行转义。基本操作如下:

串联
串联两个正则表达式创建一个更长的表达式。例如,正则表达式 a 将匹配字符串 abcdcba 两次(第一个和最后一个 a),正则表达式 b 也是这样。但是, ab 只匹配 abcdcba, ba 只匹配 abcdc ba
重复
Kleene *(即重复操作符)匹配前面正则表达式的零次或更多次出现。所以表达式 a*b 匹配有任意个 a 并以 b 结束的字符串,包括 b 本身。Kleene * 不必进行转义,所以要想在表达式中匹配字面意义的星号(*),就必须对星号进行转义。
选择
选择操作符(|)匹配前面或后面的表达式。在基本语法中必须对它进行转义。例如,表达式 a*/|b*c 匹配有任意个 a 或任意个 b(但是不能同时有)并以一个 c 结束的字符串。同样,单一字符 c 也是匹配的。
常常需要给正则表达式加上引号,以免 shell 将它展开。

我们以前面在 lpi103 目录中创建的文本文件为例。请研究清单 107 中的简单示例。注意,grep 的参数是一个正则表达式(必须有)和要搜索的零个或多个文件的列表。如果没有给出文件,grep 就会搜索 stdin,这使它成为可以在管道中使用的过滤器。如果没有匹配的行,grep 就没有输出,但是可以检查它的退出码。


清单 107. 简单的正则表达式


[ian@echidna lpi103]$ grep p text1
1 apple
2 pear
[ian@echidna lpi103]$ grep pea text1
2 pear
[ian@echidna lpi103]$ grep "p*" text1
1 apple
2 pear
3 banana
[ian@echidna lpi103]$ grep "pp*" text1
1 apple
2 pear
[ian@echidna lpi103]$ grep "x" text1
[ian@echidna lpi103]$ grep "x*" text1
1 apple
2 pear
3 banana
[ian@echidna lpi103]$ cat text1 | grep "l/|n"
1 apple
3 banana
[ian@echidna lpi103]$ echo -e "find an/n* here" | grep "/*"
* here

从这些示例中可以看到,有时候可能会得到令人吃惊的结果,尤其是在使用重复的时候。您可能期望 p* 或至少 pp* 匹配多个 p 字符,但是 p*(和 x*)匹配文件中的每一行,因为 * 操作符匹配前面正则表达式的 零次 或更多次出现。

便捷方式

既然已经有了正则表达式的基本成分,我们来看几种便捷方式。

+
+ 操作符与 * 操作符相似,只是它匹配前面正则表达式的 一次 或更多次出现。对于基本表达式,必须对它进行转义。
?
? 表示前面的表达式是可选的,所以它表示零次或一次出现。这与 globbing 中使用的 ? 不一样。
.
.(点)是一个表示任何字符的元字符。最常用的模式之一是 .*,它匹配包含任何字符(或根本没有字符)的任意长度的字符串。当然,这会用作一个长表达式的一部分。请比较单个点与 globbing 中使用的 ?,以及 .* 与 globbing 中使用的 *。

清单 108. 更多的正则表达式


[ian@echidna lpi103]$ grep "pp/+" text1 # at least to p's
1 apple
[ian@echidna lpi103]$ grep "pl/?e" text1
1 apple
2 pear
[ian@echidna lpi103]$ grep "pl/?e" text1 # pe with optional l between
1 apple
2 pear
[ian@echidna lpi103]$ grep "p.*r" text1 # p, some string then r
2 pear
[ian@echidna lpi103]$ grep "a.." text1 # a followed by two other letters
1 apple
3 banana

匹配行的开头或结尾

^(脱字符)匹配行的开头,$(美圆符号)匹配行的结尾。所以 ^..b 匹配行首的任意两个字符,后面跟着一个 bar$ 匹配任何以 ar 结尾的行。正则表达式 ^$ 匹配空行。

更复杂的表达式

到目前为止,我们见过了对单个字符应用重复。如果希望搜索一个多字符字符串的一次或多次出现,比如在 banana 中两次出现的 an,那么使用圆括号,这在基本语法中必须进行转义。与此相似,可能希望搜索几个字符,而不使用 . 那样宽泛的搜索或者很长的选择。可以将要搜索的字符放在方括号([])中,对于基本语法,这不需要转义。方括号中的表达式组成了一个字符类。除了后面讨论的几种例外情况之外,使用方括号还可以避免对特殊字符(. 和 *)进行转义。


清单 109. 圆括号和字符类


[ian@echidna lpi103]$ grep "/(an/)/+" text1 # find at least 1 an
3 banana
[ian@echidna lpi103]$ grep "an/(an/)/+" text1 # find at least 2 an's
3 banana
[ian@echidna lpi103]$ grep "[3p]" text1 # find p or 3
1 apple
2 pear
3 banana
[ian@echidna lpi103]$ echo -e "find an/n* here/nsomewhere." | grep "[.*]"
* here
somewhere.

对于字符类,有另外几种有意思的情况。

范围表达式
范围表达式是由 -(连字符)分隔的两个字符,比如 0-9 表示数字,0-9a-fA-F 表示十六进制数字。注意,范围依赖于系统的地区设置。
命名的类
几个命名的类提供了常用类的便捷方式。命名的类以 [: 开头,以 :] 结尾。包括:
[:alnum:]
字母数字字符
[:blank:]
空白和制表符字符
[:digit:]
数字 0 到 9(相当于 0-9)
[:upper:] 和 [:lower:]
分别表示大写和小写字母
^(取反)
如果作为方括号中的第一个字符使用,^(脱字符)就对其余字符的意义取反,所以只匹配 不在这个类 中的字符。
由于有特殊意义,如果希望在字符类中匹配字面意义的 -(连字符),就必须将它放在最前面或最后面。如果希望匹配字面意义的 ^(脱字符),那么必须不放在最前面。](右方括号)结束字符类,除非它出现在最前面。

字符类是正则表达式和 globbing 相似的一个方面,但是取反符号不一样(^ 和 !)。清单 110 给出一些字符类示例。


清单 110. 更多的字符类


[ian@echidna lpi103]$ # Match on range 3 through 7
[ian@echidna lpi103]$ echo -e "123/n456/n789/n0" | grep "[3-7]"
123
456
789
[ian@echidna lpi103]$ # Find digit followed by no n or r till end of line
[ian@echidna lpi103]$ grep "[[:digit:]][^nr]*$" text1
1 apple



回页首


通过 sed 使用正则表达式

前面对 sed 的简短介绍中提到过,sed 要使用正则表达式。正则表达式可以在地址表达式和替换表达式中使用。所以表达式 /abc/s/xyz/XYZ/g 意味着 对包含 abc 的行应用替换命令,将每个 xyz 替换为 XYZ。清单 111 给出应用于 text1 文件的两个示例,另一个示例将点(.)前面的最后一个单词替换为字符串 LAST WORD。注意,字符串 First 没有被修改,因为它前面没有空格。


清单 111. sed 中的正则表达式


[ian@echidna lpi103]$ sed -e '//(a.*a/)/|/(p.*p/)/s/a/A/g' text1
1 Apple
2 pear
3 bAnAnA
[ian@echidna lpi103]$ sed -e '/^[^lmnXYZ]*$/s/ear/each/g' text1
1 apple
2 peach
3 banana
[ian@echidna lpi103]$ echo "First. A phrase. This is a sentence." |/
> sed -e 's/ [^ ]*/./ LAST WORD./g'
First. A LAST WORD. This is a LAST WORD.



回页首


扩展的正则表达式

在基本语法中使用某些字符时需要对它们进行转义,扩展的正则表达式语法消除了这些需求,这些字符包括圆括号、‘?’、‘+’、‘|’ 和 ‘{’。这意味着,如果希望将它们解释为一般字符,就必须进行转义。可以使用 grep 的 -E(或 --extended-regexp)选项表示您正在使用扩展的正则表达式语法。也可以使用 egrep 命令。一些比较老的 sed 版本不支持扩展的正则表达式。如果您的 sed 版本支持扩展的正则表达式,那么使用 -r 选项告诉 sed 正在使用扩展的语法。清单 112 给出本节前面使用过的一个示例,以及 egrep 使用的对应的扩展表达式。


清单 112. 扩展的正则表达式


[ian@echidna lpi103]$ grep "an/(an/)/+" text1 # find at least 2 an's
3 banana
[ian@echidna lpi103]$ egrep "an(an)+" text1 # find at least 2 an's
3 banana



回页首


在文件中进行搜索

本节的最后让您体会一下 grepfind 的强大功能,在文件系统中搜索一些东西。同样,这些示例非常简单;它们使用在 lpi103 目录及其子目录中创建的文件。

首先,grep 可以同时搜索多个文件。如果添加 -n 选项,它会指出匹配的行数。如果只希望知道匹配的行数,那么使用 -c 选项;如果只希望列出匹配的文件,那么使用 -l 选项。清单 113 给出一些示例。


清单 113. 用 grep 搜索多个文件


[ian@echidna lpi103]$ grep plum *
text2:9 plum
text6:9 plum
text6:9 plum
yaa:9 plum
[ian@echidna lpi103]$ grep -n banana text[1-4]
text1:3:3 banana
text2:2:3 banana
[ian@echidna lpi103]$ grep -c banana text[1-4]
text1:1
text2:1
text3:0
text4:0
[ian@echidna lpi103]$ grep -l pear *
ex-here.sh
nohup.out
text1
text5
text6
xaa

最后一个示例使用 find 在当前目录及其子目录中搜索所有常规文件,然后使用 xargs 将文件列表传递给 grep,以便判断每个文件中出现 banana 的次数。最后,用另一个 grep 调用对这个输出进行过滤,这一次使用 -v 选项,找出 不包含 搜索字符串的所有文件。


清单 114. 搜索至少包含一个 banana 的文件


[ian@echidna lpi103]$ find . -type f -print0| xargs -0 grep -c banana| grep -v ":0$"
./text1:1
./text2:1
./xab:1
./yaa:1
./text5:1
./text6:4
./backup/text1.bkp.2:1
./backup/text1.bkp.1:1

本节只触及了 Linux 命令行和正则表达式功能的皮毛。请使用手册页进一步学习这些有用的工具。



回页首


用 vi 进行文件编辑

本节讨论初级管理(LPIC-1)考试 101 的主题 1.103.8 的内容。这个主题的权值是 1。

在本节中,学习以下主题:

  • 用 vi 编辑文本

使用 vi

vi 编辑器几乎在所有 Linux 和 UNIX 系统上都存在。实际上,如果系统只有一个编辑器,那么它可能就是 vi,所以了解 vi 的操作方式是有意义的。本节介绍一些基本的 vi 编辑命令。要想全面了解 vi,请阅读 “vi intro -- the cheat sheet method”(参见 参考资料),或者参考手册页或相关书籍。

启动 vi

大多数 Linux 发行版本现在附带 vim(表示 Vi IMproved,改进的 vi)编辑器而不是传统的 vi。vim 向上兼容 vi,并有图形模式(gvim)以及标准的 vi 文本模式界面。vi 命令常常是 vim 程序的别名或符号链接。请回顾前面的主题 shell 在哪里寻找命令?,了解使用的究竟是哪个命令。

在前面的 修改优先级 一节中,我们希望修改正在运行的 count1.sh shell 脚本的优先级。如果您自己尝试这个操作,就会发现这个命令运行得太快了,没有足够的时间可以用 renice 完成优先级修改。所以我们要使用 vi 编辑器在文件的开头添加一行,从而让进程睡眠 20 秒,这样就有时间修改优先级了。

要启动 vi 编辑器,使用 vi 命令并以文件名作为参数。有许多选项可供选用,细节请参考手册页。使用命令

vi count1.sh

应该会看到与清单 115 相似的显示。如果使用 vim,一些单词可能是不同颜色的。vim 有语法突出显示模式(原始的 vi 编辑器没有这种模式),在系统上这种模式可能是默认打开的。


清单 115. 使用 vi 编辑 count1.sh


x="$1"
echo "$2" $(date)
while [ $x -gt 0 ]; do let x=$x-1;done
echo "$2" $(date)
~
~
~
~
~
~
"count1.sh" 4L, 82C

vi 模式

vi 编辑器有两种操作模式:

命令模式
在命令模式中,可以在文件中移动并执行编辑操作,比如搜索文本、删除文本、修改文本等等。常常以命令模式启动。
插入模式
在插入模式中,在插入点上将新文本输入文件。要返回命令模式,按下 Esc 键。

这两个模式决定了编辑器的行为。在开发 vi 的时候,并不是所有的终端键盘上都有光标移动键,所以在 vi 中可以做的所有事情都可以使用标准打字机上的键加上 EscInsert 键来完成。但是,可以将 vi 配置为使用额外的键(如果这些键可用的话);键盘上的大多数键在 vi 中都有作用。vi 是在早期的终端连接时代开发的,所以 vi 使用的命令非常短,因此命令的含义比较含混。

离开 vi

在学习新编辑器时,我喜欢先了解如何退出它,以免意外退出。下面这些退出 vi 的方式包括保存或放弃修改,或者从头重新开始。如果这些命令看起来无效,那么您可能是在插入模式中,所以要按 Esc 离开插入模式并返回命令模式。

:q!
退出文件编辑并放弃所有修改。这是摆脱麻烦的常用方法。
:w!
写文件(无论是否修改了)。尝试覆盖现有文件、只读文件或其他不可写文件。可以提供一个文件名参数,编辑的结果将写到这个文件而不是原来的文件。一般来说,去掉 ! 比较安全,除非您知道自己正在做什么。
ZZ
如果文件已经修改了,就写文件,然后退出。这是正常退出 vi 的常用方法。
:e!
编辑文件的当前磁盘副本。这会重新装载文件,放弃已经做的修改。如果磁盘副本由于某些其他原因改变了,而您希望获得最新版本,那么可以使用这个命令。
:!
运行 shell 命令。输入命令并按 Enter。当命令完成时,会看到输出以及返回 vi 编辑的提示。

注意:

  1. 在输入冒号(:)时,光标将移动到屏幕底部,在这里输入命令和任何参数。
  2. 如果从上面的命令中去掉惊叹号,就可能收到一个错误消息,比如指出修改还未保存或无法写输出文件(例如,正在编辑一个只读文件)。
  3. : 命令有更长的形式(:quit、:write、:edit),但是很少使用长形式。

移动

以下命令用于在文件中到处移动:

h
在当前行向左移动一个字符
j
移动到下一行
k
移动到上一行
l
在当前行向右移动一个字符
w
移动到当前行上的下一个单词
e
移动到当前行上的下一个单词末尾
b
移动到当前行上的前一个单词开头
Ctrl-f
向前滚动一页
Ctrl-b
向后滚动一页

如果在这些命令前面输入一个数字,那么命令将重复执行这个数字指定的次数。这个次数称为重复计数,或简称为计数。例如,5h 将向左移动 5 个字符。对于许多 vi 命令都可以使用重复计数。

移动到行

以下命令用于移动到文件中的特定行:

G
移动到文件中的一个特定行。例如,3G 移动到第 3 行。如果没有参数,G 就移动到文件的最后一行。
H
相对于屏幕上的顶行进行移动。例如,3H 移动到从当前屏幕顶行开始的第 3 行。
L
这与 H 相似,但是移动相对于屏幕的最后一行。所以,2L 移动到从屏幕最后一行开始的第 2 行。

搜索

可以使用正则表达式搜索文件中的文本:

/
使用 / 后面跟着一个正则表达式,在文件中向前搜索。
?
使用 ? 后面跟着一个正则表达式,在文件中向后搜索。
n
使用 n 以任意方向重复上一次搜索。

可以在上面的任何搜索命令前面加上一个数字,表示重复计数。所以,3/x 将搜索从当前点开始的第 3 个 x,这相当于 /x 后面跟着 2n。

修改文本

使用以下命令插入、删除或修改文本:

i
在当前位置的字符前面进入插入模式。输入文本并按 Esc 返回命令模式。使用 I 在当前行的开头进行插入。
a
在当前位置的字符后面进入插入模式。输入文本并按 Esc 返回命令模式。使用 A 在当前行的末尾进行插入。
c
使用 c 修改当前字符并进入插入模式以输入替换字符。
o
在当前行下面打开一个新行来进行文本插入。使用 O 在当前行上面打开一个新行。
cw
删除当前单词的剩余部分并进入插入模式以替换它。使用重复计数替换多个单词。使用 c$ 一直替换到行末。
dw
与上面的 cw(和 c$)一样,但是不进入插入模式。
dd
删除当前行。使用重复计数删除多行。
x
删除光标位置的字符。使用重复计数删除多个字符。
p
将最后删除的文本放在当前字符的后面。使用 P 将它放在当前字符的前面。
xp
x 和 p 的组合是一种有用的命令。这个命令将光标位置的字符与它右边的字符对换。

应用这些操作

我们打算在 count1.sh 文件中添加一行。为了保留原来的文件并将修改的版本保存为 count2.sh,可以在用 vi 打开文件之后,使用下面这些 vi 命令。注意,<Esc> 表示按 Esc 键。


清单 116. 在 count1.sh 中添加一行的编辑器命令


1G
O
sleep 20<Esc>
:w! count2.sh
:q

会者不难,是吧?

本系列中的下一个教程讨论主题 104,涉及设备、Linux 文件系统和 Filesystem Hierarchy Standard(FHS)。



回页首


你可能感兴趣的:(apple,unix,shell,正则表达式,bash,作业)