捐赠?

Harley Hahn
个人主页

给 Harley
发送留言


来自 Harley Hahn 的
个人寄语

Unix 之书
主页

搜索

章节列表

目录

插图列表

各章...
   1   2   3
   4   5   6
   7   8   9
  10  11  12
  13  14  15
  16  17  18
  19  20  21
  22  23  24
  25  26

术语表

各附录...
  A  B  C
  D  E  F
  G  H

命令
摘要...

• 按字母顺序
• 按类别

Unix-Linux
年表

互联网
资源

错误与
勘误

推荐语


教师
与学生
资料...

主页
与概述

练习
与答案

Unix 模范
课程与
课程大纲

教师用
PowerPoint 文件

第 25 章...

文件操作 (Working With Files)

这是讨论 Unix 文件的三章中的最后一章。在第 23 章里, 我们详细讨论了 Unix 文件系统。当时我解释过,文件有 三种类型:目录、普通文件和伪文件。对于日常工作来说, 最重要的是目录和普通文件,所以我要确保你掌握与这两类 文件相关的所有基本技能。在第 24 章里,我们谈过如何 使用目录。在本章中,我们将讨论操作普通文件的种种细节。

整章当中,当我使用“文件”这个词时,指的都是普通文件。 因此严格说来,本章的标题其实应该是“操作普通文件” (Working With Ordinary Files)。

本章的安排如下:首先,我会教你怎样创建、复制、移动 和重命名普通文件。接着我们讨论权限,也就是让用户得以 共享文件的那些属性。然后我们会谈谈幕后发生的事情,你 会看到,操作文件实际上就是在处理“链接”。最后,我会 解释如何搜索文件,以及如何对搜索中找到的文件进行 处理。听起来内容很多 —— 确实很多 —— 但我向你保证, 等你读完这一章,一切都会变得清晰明了。

跳至页首

创建文件 (Creating a File): touch

你怎样创建一个文件?说来奇怪,你其实并不需要创建。 需要的时候,Unix 会自动替你创建文件;你自己很少需要 新建一个文件。

有三种常见情况,文件会在其中被自动为你创建。第一, 在需要时,许多程序会自动为你创建一个文件。例如,假设 你用下面这条命令启动 vi 编辑器(第 22 章):

vi essay

这条命令说明你要编辑一个名为 essay 的文件。 如果 essay 并不存在,那么在你第一次保存成果时, vi 就会替你把它创建出来。这个例子里我用的是 vi,但同样的道理也适用于许多其他程序。

第二,当你把输出重定向到一个文件时(见第 15 章), 如果该文件尚不存在,Shell 就会为你创建它。例如,假设 你想把 ls 命令的输出保存到一个名为 listing 的文件里。你输入 ls 命令,并对 输出做重定向:

ls > listing

如果 listing 还不存在,Shell 就会为你创建它。

最后,当你复制一个文件时,复制程序会创建一个新文件。 例如,假设你想把文件 data 复制为一个名为 extra 的文件。你输入下面这条命令:

cp data extra

如果文件 extra 不存在,它就会被自动创建。 (cp 命令在本章后面会讲解。)

不过,假设由于某种原因,想亲手创建一个全新的 空文件。最简单的办法是什么?在第 24 章里,我解释过用 mkdir 命令新建一个目录。那么有没有一个类似的 命令用来新建普通文件呢?答案是没有,但有一条命令有个 副作用:它会创建一个空文件。这条命令叫 touch, 下面说说它是怎么工作的。

在第 24 章里,我解释过如何显示文件的修改时间 (ls -l)或访问时间(ls -lu)。 修改时间是文件最后一次被改动的时间;访问时间是文件 最后一次被读取的时间。touch 的主要用途,是在不 改动文件内容的情况下改变文件的修改时间和访问时间。 想象你伸出手去,小心翼翼地碰了一下这个文件(这正是 它名字的由来)。语法如下:

touch [-acm] [-t time] file...

其中 time 是形如 [[YY]YY]MMDDhhmm[.ss] 的时间与日期。

默认情况下,touch 会把修改时间和访问时间都设置 为当前的日期和时间。例如,假设一个名为 essay 的 文件最后一次修改是在 7 月 8 日下午 2:30。你输入:

ls -l essay

输出是:

-rw-------  1 harley staff  4883 Jul  8 14:30 essay

现在是 12 月 21 日上午 10:30。你输入:

touch essay

这时再输入同样的 ls 命令,你会看到:

-rw-------  1 harley staff  4883 Dec 21 10:30 essay

什么时候你会用到 touch 呢?假设你准备分发一组 文件 —— 音乐、软件,什么都行 —— 而你希望它们拥有 完全相同的日期和时间。进入存放这些文件的目录,然后 输入:

touch *

所有被 * 通配符匹配到的文件(见第 24 章),现在 都有了相同的修改时间和访问时间。

如果你只想修改修改时间,请使用 -m 选项;如果 只想修改访问时间,请使用 -a。如果想使用一个 指定的日期和时间而不是当前时间,请使用 -t,后面 跟一个形如 [[YY]YY] MMDDhhmm[.ss] 的时间。下面 是两个例子。假设今天是 8 月 31 日。第一条命令把修改 时间(仅此一项)改为当天的 17:29;第二条命令把访问 时间(仅此一项)改为 2008 年 12 月 21 日上午 10:30:

touch -m -t 08311729 file1
touch -a -t 200812211030 file2

说实在的,你很少需要去改动一个文件的修改时间或访问 时间。然而,touch 有一个非常重要的副作用:如果 你指定的文件不存在,touch 就会为你创建它。因此, 你可以随时用 touch 创建全新的空文件。例如,要 创建一个名为 newfile 的文件,只需输入:

touch newfile

如果愿意,你也可以一次创建多个新文件:

touch data1 data2 data3 temp extra

当你用 touch 创建新文件时,修改时间和访问时间 都会是当前的日期和时间。如果这不合你的意,你可以在 创建文件的同时使用上面讲过的选项,把时间设成你指定的 值。

最后还有一个选项:如果你要更新一批文件的修改时间或 访问时间,而希望 touch 创建任何新文件, 请使用 -c(不创建)选项。例如,下面这条命令会 更新指定文件的时间;但如果有哪个文件不存在,它不会被 创建:

touch -c backup1 backup2 backup3 backup4

— 提示 —

大多数时候,你并不需要借助 touch 来创建新文件, 因为 —— 正如我们已经讨论过的 —— 新文件几乎总是在 需要的时候被自动创建出来。

touch 真正派上用场的场合,是你需要快速弄出几个 临时文件的时候,比如说要拿文件命令做实验。这种情况下, 用 touch 是创建一组全新空文件最快的办法,例如:

touch test1 test2 test3

跳至页首

为文件命名 (Naming a File)

在文件命名方面,Unix 是相当宽松的。基本的规则只有 两条:

1. 文件名最长可达 255 个字符。(*)

* 脚注

严格说来,文件名的最大长度是由文件系统决定的,而不是 由 Unix 或 Linux 决定的。大多数现代文件系统默认允许的 文件名长度上限是 255 个字符。不过,有些文件系统更灵活 一些。例如,如果你是个文件系统迷,很容易就能修改 ext2ext3ext4 文件系统,让一个 文件名最多可以使用 1012 个字符。

2. 文件名中可以包含除 /(斜杠)和空字符之外的 任何字符。

这完全合情合理。正如你在第 24 章中所知,/ 字符在路径名中被用作分隔符,所以当然不能把它用在文件 名里。空字符是由全零位组成的字符(见第 23 章)。这个 字符在 C 语言中被用作字符串结束符,你通常绝不会把它 用在文件名里。

在这两条规则之外,我还要加上第三条我自己定的规则。

3. 创建对你来说有意义的文件名。

举个例子,与更具描述性的 chemlab-experiment-2008-12-21 相比,名字 data 说明不了什么。没错,长名字复杂、敲起来费 时间,但一旦你学会了文件名补全的用法(见第 13 章), 你就很少需要把整个名字打完。比如对于上面这个长名字, 你可能只需输入 ch<Tab>,让 Shell 替你补全 剩下的部分。

我的建议是:在创建文件的当下,就为你的所有文件挑选 有意义的名字。否则,日积月累,你会攒下许多文件,它们 里面也许有价值的数据,也许什么也没有。如果你和其他人 一样,我相信你一定想过,总有一天你会把所有文件整理 一遍,删掉不需要的。然而,如果你和其他人一样,你多半 永远也不会真的去做。(*)

* 脚注

如果需要说服力,不妨问问自己:“此时此刻,我的电脑里 有多少照片是我打算哪天抽空整理一番的?”

— 提示 —

防止目录里堆积垃圾文件的最好办法,就是在创建文件时 给它们起有意义的名字。

Unix 允许你创建包含各种古怪字符的文件名:退格符、 标点、控制字符,甚至空格和制表符。显然,这样的文件名 会惹麻烦。例如,如果你用 ls -l 命令列出一个 名为 info;date 的文件的信息,会怎么样:

ls -l info;date

Unix 会把分号解释成两个命令之间的分隔:

ls -l info
date

再举一个例子。假设你有一个名为 -jokes 的文件。 在命令中使用这个名字会很麻烦,例如:

ls -jokes

Unix 会把 -(连字符)解释为选项的标志。

一般来说,只要名字里含有带特殊含义的字符 (<>|! 等等), 你就会遇到麻烦。最好的主意是把自己限制在不会被误解的 字符范围内。这些字符列在图 25-1 中。连字符是可以用 的,只要你别把它放在名字的开头。

Figure 25-1
文件名中可以安全使用的字符

Unix 允许你在文件名中使用任何字符,除了 / (斜杠)和空字符。不过,如果你只用字母、 数字、点、连字符(但不放在名字开头)和下划线,你的 日子会好过得多。

a, b, c...小写字母
A, B, C...大写字母
0, 1, 2...数字
.
-连字符
_下划线

如果你真的弄出了一个包含空格或其他古怪字符的名字, 有时可以靠给名字加引号来让它正常工作。(引号在第 13 章讲解。)下面是一个非常花哨的例子:

ls -l 'this is a bad filename, but it does work'
ls -l this\ is\ a\ bad\ filename\,\ but\ it\ does\ work

本节结束之前,我要说明三条重要的文件命名约定。第一, 正如我们在第 14 章和第 24 章讨论过的,名字以 .(点)开头的文件称为点文件(dotfile)或隐藏 文件。当你使用 ls 时,只有指定了 -a (全部)选项,这类文件才会被列出。按照约定,我们只在 存放配置数据或初始化命令的文件上使用以点开头的名字。 (图 24-5 列出了常见的点文件。)

第二,我们经常使用以点加一个或多个字母结尾的文件名, 用来表示文件的类型。例如,C 源文件的文件名以 .c 结尾,如 myprog.c;MP3 音乐文件的 文件名以 mp3 结尾;用 gzip 程序压缩过的 文件以 .gz 结尾;如此等等。在这种情况下,后缀 被称为扩展名(EXTENSION)。这样的扩展名有成百上千种。 它们很方便,因为你可以用通配符(见第 24 章)来指代 一组文件。例如,你可以用下面的命令列出目录中所有 C 源文件的名字:

ls *.c

最后,你应当还记得,Unix 区分大写和小写。因此,名字 infoInfoINFO 是三个完全不 同的名字。Unix 用户一般就用 info。现在看看下面 这几个名字,它们都可能是你为一个存放程序或 Shell 脚本的目录挑的名字:

Program Files
ProgramFiles
programfiles
program-files
program_files
bin

第一个名字是 Windows 用的。作为 Unix 用户,我们立刻 就会把它否掉,因为它含有空格。第二个名字里有两个大写 字母,敲起来费劲,所以也否掉。可是下面三个名字呢? 它们不含空格、大写字母或古怪字符。不过,对于重要的 目录来说,名字短、好打是很方便的,这就是我们把票投给 bin 的原因。

在 Unix 世界里,我们有一条约定:以大写字母开头的名字 保留给在某些方面特别重要的文件。例如,当你下载以一组 文件形式发布的软件时,常常会发现一个名为 README 的文件。由于在 ASCII 码中大写排在小写之前(第 19 章和 第 20 章),这样的名字在目录列表里会排在最前面,因而 格外醒目(*)。正因为如此,作为一般规则,我建议你在给 文件和目录命名时只使用小写字母。

* 脚注

前提是使用 C 语言环境(locale)。如果你用的是 en_US 语言环境,情况就不是这样(见第 19 章)。

— 提示 —

如果你是程序员,时不时就会忍不住把正在开发的程序或 Shell 脚本的文件名取作 test。别这么干。

巧的是,Shell 有一个名为 test 的内置命令,用来在 Shell 脚本中比较取值。如果你把自己的某个程序命名为 test,那么每当你键入名字想运行它时,得到的都会 是那个 Shell 内置命令。看上去好像什么也没发生,而你会 白费许多时间去琢磨问题出在哪里。

(如果你想知道 test 到底是干什么的,去在线手册里 查查它。)

跳至页首

复制文件 (Copying a File): cp

要复制一个文件,使用 cp(copy,复制)命令。 语法是:

cp [-ip] file1 file2

其中 file1 是一个已存在文件的名字,file2 是目标文件的名字。

这条命令用起来很直截了当。例如,如果你有一个名为 data 的文件,想复制出一份名为 extra 的 拷贝,就用:

cp data extra

再举一个例子。你想复制一份系统口令文件(见第 11 章)。 副本要叫 pword,并且要放在你的主目录里。正如 我们在第 24 章讨论过的,~ 字符代表你的主目录, 所以你可以用:

cp /etc/passwd ~/pword

如果目标文件不存在,cp 会创建它。如果目标文件 已经存在,cp 会把它替换掉。一旦发生替换,被替换 掉的数据就再也找不回来了。看看第一个例子:

cp data extra

如果文件 extra 不存在,它就会被创建。然而,如果 文件 extra 本来已经存在,它就会遭到替换。这时, 原文件中的数据就永远丢失了,没有任何办法找回。(请把 最后一句再读一遍。)

要把数据追加到文件末尾,不要用 cp。你应当用 cat 程序并重定向输出(见第 16 章)。例如,下面 这条命令把 data 的内容追加到 extra 的 末尾。这种情况下,extra 原来的内容被 保留下来。

cat data >> extra

由于 cp 很容易就把一个文件的内容冲掉,如果你想 格外小心,可以使用 -i(interactive,交互式) 选项:

cp -i data extra

-i 选项告诉 cp:在替换一个已存在的文件 之前先征求你的许可。例如,你可能会看到:

cp: overwrite extra (yes/no)?

如果你的回答以“y”或“Y”(代表 yes)开头,cp 就会替换该文件。如果你给出任何其他回答 —— 比如直接按 <Return> 键 —— cp 就不会做替换。

还有一个我认为你应当了解的选项,就是 -p (preserve,保留)。这个选项让目标文件拥有与源文件相同 的修改时间、访问时间和权限。(权限我们在本章后面讨论。)

跳至页首

把文件复制到别的目录
(Copying Files to a Different Directory): cp

cp 命令可以用来把一个或多个文件复制到另一个 目录。语法是:

cp [-ip] file... directory

其中 file 是已存在文件的名字,directory 是已存在目录的名字。-i(交互式)和 -p (保留)选项的用法与前一小节所述相同。

举个例子。要把文件 data 复制到名为 backups 的目录,用:

cp data backups

要把 data1data2data3 三个 文件复制到 backups 目录,用:

cp data1 data2 data3 backups

再来一个例子,稍微复杂一点。你的工作目录是 /home/harley/work/bin。你想把文件 adventure 从目录 /home/harley/bin 复制到 工作目录。要指代源目录,我们用 ../../bin;要指代 工作目录,我们单用一个 .。命令是:

cp ../../bin/adventure .

— 提示 —

你常常可以用通配符一次指定多个文件名(见第 24 章)。 例如,要把 data1data2data3 三个文件复制到 backups 目录,你可以用:

cp data[123] backups

如果没有其他文件的名字以 data 开头,你可以用:

cp data* backups

如果没有其他文件的名字以 d 开头,你可以用:

cp d* backups

跳至页首

把一个目录复制到另一个目录
(Copying a Directory to Another Directory): cp -r

使用 -r 选项,你就可以用 cp 把一个目录 及其全部文件复制到另一个目录。语法是:

cp -r [-ip] directory1... directory2

其中 directory1 是已存在目录的名字, directory2 是目标目录的名字。-i(交互式) 和 -p(保留)选项的用法与本章前面所述相同。 -r(recursive,递归)选项告诉 cp 复制 整棵子树。

举个例子。假设在你的工作目录里有两个子目录: essaysbackups。在 essays 目录 里,有许多文件和子目录。你输入:

cp -r essays backups

现在 backups 里就有了一份 essays 的拷贝,包括它 所有的文件和子目录。当你使用 -r 时,cp 命令会在需要时自动创建新目录。

— 提示 —

要复制一个目录中的所有文件,用 cp 加上 * 通配符(见第 24 章),例如:

cp documents/* backups

要复制目录本身,连同它所有的文件和子目录,用 cp 加上 -r 选项,例如:

cp -r documents backups

跳至页首

移动文件 (Moving a File): mv

要把文件移动到另一个目录,使用 mv(move,移动) 命令。语法是:

mv [-if] file... directory

其中 file 是已存在文件的名字,directory 是目标目录的名字。

mv 命令会把一个或多个文件移动到一个已存在的 目录里。(要创建目录,请使用 mkdir 命令,见 第 24 章。)下面两个例子。第一条命令把一个名为 data 的文件移动到名为 archive 的目录:

mv data archive

你必须当心。如果名为 archive 的目录并不存在, mv 会以为你想重命名这个文件(见下文)。下一个 例子把 data1data2data3 三个 文件移动到 archive 目录:

mv data1 data2 data3 archive

与大多数文件命令一样,你可以使用通配符。例如,上一条 命令可以简写为:

mv data[123] archive

如果你要移动到的目标已经存在,源文件会取代目标文件。 在这种情况下,目标文件原本的内容将会丢失,而且无法 找回,所以要小心。如果你担心丢数据,请使用 -i (交互式)选项。例如:

mv -i data archive

这是在告诉 mv:替换已存在的文件之前先征求你的 许可。如果你键入的回答以字母 y 或 Y(代表 yes)开头, mv 就会替换该文件。如果你给出任何其他回答 —— 比如直接按 <Return> 键 —— mv 就不会做替换。 在这个例子中,mv 会在替换名为 archive/data 的文件之前征求你的许可。

与之相反的选项是 -f(force,强制)。它会迫使 mv 不跟你确认就替换文件。-f 选项既能 覆盖 -i 选项,也能越过文件权限所加的限制(本章 后面会解释)。-f 要谨慎使用,而且只有在你完全 清楚自己在做什么的时候才用。

跳至页首

重命名文件或目录 (Renaming a File or Directory): mv

要重命名一个文件或目录,使用 mv(move,移动) 命令。语法是:

mv [-if] oldname newname

其中 oldname 是已存在的文件或目录的名字,newname 是新的名字。-i(交互式)和 -f(强制)选项 的用法与上一小节所述相同。

重命名文件或目录是很简单的事。例如,要把一个文件从 unimportant 改名为 important,用:

mv unimportant important

如果目标(此例中是 important)已经存在,它会被 替换。原目标文件中的所有数据都会丢失,并且无法找回, 所以要小心。你可以使用上一小节介绍的 -i-f 选项来控制替换:-i 告诉 mv 在替换文件之前先问你;-f 则无论如何都强制替换。

正如你所料,你可以用 mv 同时完成改名和移动。 例如,假设 incomplete 是一个文件,而 archive 是一个目录。下面这条命令把 incomplete 移动到目录 archive 中(该目录 必须已经存在)。作为移动的一部分,这个文件会被改名为 complete:

mv incomplete archive/complete

最后,想想看,如果你对名为 old 的目录这样使用 mv,会发生什么:

mv old new

如果不存在名为 new 的目录,old 目录就会被 改名为 new。然而,如果确实有一个名为 new 的目录,old 目录就会被移动进去,成为 new 的一个 子目录。(花点时间想一想这一点。)

跳至页首

删除文件 (Deleting a File): rm

要删除一个文件,使用 rm(remove,移除)命令。 语法是:

rm [-fir] file...

其中 file 是你要删除的文件的名字。

(注意,这条命令的名字是“remove”,而不是“delete”。 等我们在本章后面谈到链接时,这一点就说得通了。)

要删除一个文件,只要指出它的名字即可。下面是一些例子。 第一条命令删除工作目录中名为 data 的文件。第二条 命令删除主目录中名为 essay 的文件。下一条命令删除 名为 bin 的目录(它位于你的工作目录中)里名为 spacewar 的文件。

rm data
rm ~/essay
rm bin/spacewar

与所有文件命令一样,你可以使用通配符(见第 24 章)。 这里有两个例子。第一条命令删除工作目录中的 data1data2data3 三个文件。第二条命令删除 工作目录中的所有文件,点文件除外。(显然,这是一条威力 极大的命令,所以不要拿它做实验。)

rm data[123]
rm *

第一条命令删除工作目录中的 data1data2data3 三个文件。第二条命令威力很大:它会删除 你工作目录中的所有文件,点文件除外。(使用这条命令之前 请三思。)

文件一经删除,就永远消失了。被删除的文件无法找回, 所以要小心。

当你把 rm 与通配符一起使用时,最好先用一条 ls 命令检验一下,看看匹配到了哪些文件。举一个 例子。你想删除 data.backupdata.olddata.extra 三个文件。你考虑使用通配符 data*,它会匹配所有名字以 data 开头的文件。 不过为了稳妥,你输入下面这条命令来检查这个通配符:

ls data*

输出是:

data.backup  data.extra  data.important  data.old

你发现自己把 data.important 这个文件给忘了。 如果你用 rm 加上 data*,就会失去这个文件。 相反,你可以用:

rm data.[beo]*

这样只会匹配你真正想删除的那些文件。

在你用 rm 加上通配符之前,先用 ls 检验 一下,确认会匹配到哪些文件。

跳至页首

如何避免删错文件
(How to Keep From Deleting the Wrong Files): rm -if

正如我在上一小节提到的,在使用 rm 命令之前, 先用 ls 检查一遍通配符是个好习惯。然而,即使你 用 ls 检查过了,在键入 rm 命令时仍然可能 把通配符打错。这里介绍一种万无一失的解决办法。

在第 13 章讨论别名时,我演示过如何定义一个名为 del 的别名,它用与前一条 ls 命令相同的 参数来运行 rm 命令。对于 Bourne shell 家族(Bash、 Korn shell),定义这个别名的命令是:

alias del='fc -s ls=rm'

对于 C-Shell 家族(Tcsh、C-Shell),命令是:

alias del 'rm \!ls:*'

(细节在第 13 章解释。)要把 del 别名永久定义 下来,只需把相应的命令放进你的环境文件(第 14 章)。 别名一旦定义好,用起来就很简单。先输入一条 ls 命令,配上描述你要删除的文件的那个通配符。例如:

ls data.[beo]*

看一眼这份文件列表。如果确实是你想删除的那些,就输入:

del

这会使用前一条 ls 命令中的文件名来执行 rm 命令。如果文件列表不是你想要的,那就改改 通配符。

一个方便可行的替代办法是使用 -i(交互式)选项。 它告诉 rm:在删除每个文件之前都先征求你的许可。 例如,你可以输入:

rm -i data*

rm 程序会为每个文件显示一条消息,请求你的许可 再继续,例如:

rm: remove regular file `data.backup'?

如果你键入的回答以“y”或“Y”(代表 yes)开头, rm 就会删除该文件。如果你给出任何其他回答 —— 比如直接按 <Return> 键 —— rm 就不会碰这个 文件。

人们常常创建一个别名,让自己每次使用 rm 命令时 都自动带上 -i 选项。下面是这样的别名。第一个用于 Bourne Shell 家族;第二个用于 C-Shell 家族:

alias rm='rm -i'
alias rm 'rm -i'

有些系统管理员会把这样的别名放进系统级的环境文件里, 自以为是在为用户做好事。

这种做法有两个理由值得反对。第一,Unix 的设计宗旨是 简洁而精确。每次想删除一个文件都得键入“y”,这会拖慢 你的思路。自动加上 -i 选项会导致思考上的懈怠, 因为用户会开始依赖它。

如果你想就此争辩,不妨这样想:没错,在头一周里,还不 习惯 rm 命令的新用户可能误删一两个文件,而且找不 回来。但这段经历很重要,要不了多久,他就会学会谨慎地 使用这条命令。我相信,从长远看,磨练自己的技能永远比 被人捧在手里更好。事实是,尽管 rm 潜力惊人, 经验丰富的 Unix 用户很少误删文件,因为他们养成了好的 习惯。

我不希望你自动使用 -i 选项的第二个理由是:迟早 你会用到不止一台 Unix 或 Linux 系统。如果你习惯了那条 慢吞吞、别扭、每删一个文件都要问你的 rm 命令, 你就会忘记大多数 Unix 系统并不是这样工作的。某一天,你 会发现自己正处在另一套系统上,那时犯下一个毁灭性的错误 易如反掌。你的手指是有记忆的,一旦习惯了键入 rm 而不是 rm -i,这个习惯就很难改掉了。

因此,如果你一定要为 rm -i 创建一个别名, 请给它起一个别的名字,例如:

alias erase='rm -i'
alias erase 'rm -i'

最后一点。本章后面我们会讨论文件权限。到那时你会看到, 权限有三种:读、写和执行。我现在不展开细节,只说明一点: 没有写权限,你就不被允许删除一个文件。如果你试图删除 一个你没有写权限的文件,rm 会请求你的许可来越过 这层保护机制。

例如,假设文件 data.important 的文件权限是 400。(“400”的含义到后面就清楚了。简单说,它 意味着你有读权限,但没有写权限和执行权限。)你输入:

rm data.important

你会看到这个问题:

rm: remove write-protected regular file `data.important'?

如果你键入的回答以“y”或“Y”(代表 yes)开头, rm 就会删除该文件。如果你给出任何其他回答 —— 比如直接按 <Return> 键 —— rm 就不会碰这个 文件。如果你足够小心,可以让 rm 不去询问许可就 执行删除 —— 不管文件权限如何 —— 办法是使用 -f(强制)选项:

rm -f data.important

在某些系统上,-f 还会覆盖 -i 选项。

— 提示 —

当你用 rm 删除文件时,-f(强制)选项会 越过文件权限,且(在某些系统上)还会越过 -i 选项。因此,只有在你确信自己知道在做什么时才使用 -f

跳至页首

删除整棵目录树 (Deleting an Entire Directory Tree): rm -r

要删除整棵目录树,请在 rm 命令中使用 -r (递归)选项,并指定一个目录的名字。这是告诉 rm:不仅要删除该目录,还要删除该目录之内的全部 文件和子目录。例如,假设你有一个名为 extra 的 目录。这个目录里有许多文件和子目录,每个子目录里又有 更多的文件和子目录。要一次把它们全部删除,输入:

rm -r extra

再举一个例子,看着简单,实则威力巨大。要删除工作目录 之下的一切,用:

rm -r *

显然,rm -r 可能是一条危险的命令,所以哪怕 你有那么一丁点不确定自己是在干什么,-r 就是一 个最好忘掉的选项。退一万步,至少考虑同时使用 -i(交互式)选项。这是告诉 rm:删除每个 文件和目录之前都先征求许可,例如:

rm -ir extra

要不问文件权限、又快又安静地删除整棵目录树,你可以使用 -f 选项:

rm -fr extra

记住,在某些系统上,-f 也会覆盖 -i 选项, 所以请务必小心。

— 提示 —

在使用带 -r 选项的 rm 删除整棵目录树之前, 务必先停一停,用 pwd 显示你的工作目录。想想下面 这条命令如果你正好在错误的目录里执行,会发生什么:

rm -rf *

在结束对 rm 命令的讨论之前,我们快速看一眼抹掉 你所有文件是多么容易。假设你的主目录里有许多子目录, 那是你好几个月辛勤工作的成果。你想删除 extra 目录之下的所有文件和目录。

碰巧你现在不在自己的主目录里。正确的做法是先进入主目录, 然后再输入 rm 命令:

cd
rm -fr extra

然而你心里想:“犯不着敲两条命令,一条命令就能搞定。” 你本来打算输入:

rm -fr ~/extra

(记住,正如我们在第 24 章讨论的,波浪号 ~ 代表 你的主目录。)只是不巧,你正赶时间,于是在斜杠前误打了 一个空格:

rm -fr ~ /extra

这样一来,你实际上是输入了一条删除两棵目录树中所有文件 的命令:~(你的主目录)和 /extra

一旦你按下 <Return>,就别再费劲去按 ^C 或 <Delete>(看哪个是你的 intr 键)想要中止 这条命令了。计算机比你快得多,一条失控的 rm 命令是拦不住的。等你回过神来,你的文件已经全部消失, 连点文件也不剩。(这条命令我替你测试过了:请相信我。)

正如我们在第 4 章讨论的,当你以 root 身份登录时, 你就成了超级用户。作为超级用户,你几乎无所不能,包括 删除整个系统里的任何文件或目录。想想看,如果你以超级 用户登录并输入下面这条命令,会发生什么?

rm -fr /

(除非你拿到了妈妈的特许条,否则千万别在家试这个。)

— 技术提示 —

当把 rm -fr 与变量一起使用时,你必须格外 小心。例如,假设你有一个 Shell 脚本,像下面这样使用了 变量 $HOME$FILE(见第 12 章):

rm -fr $HOME/$FILE

如果由于某种原因,这两个变量都没有被定义,这条命令就 变成了:

rm -fr /

最好的情况下,你会删掉自己所有的文件,包括所有(隐藏的) 点文件。如果你以超级用户身份运行这个脚本,就会酿成一场 灾难。加上 -i 选项也救不了你,因为正如我之前 解释的,在许多系统上 -f 会覆盖 -i

跳至页首

已删除的文件有可能恢复吗?
(Is It Possible to Restore a File That Has Been Deleted?)

没有可能。

跳至页首

文件权限 (File Permissions)

Unix 为每个文件维护着一组文件权限(FILE PERMISSIONS, 常简称为权限 PERMISSIONS)。这些权限控制哪些用户 ID(userid)可以访问该文件、以何种方式访问。权限有三 种:读权限(READ PERMISSION)、写权限(WRITE PERMISSION)和执行权限(EXECUTE PERMISSION)。这三种 权限彼此独立。例如,你的用户 ID 可能对某个文件拥有读 和写权限,但没有执行权限。很重要的一点是要理解:权限是 与用户 ID 相关联的,而不是与人相关联。例如,如果有人用 你的用户 ID 登录,他对你文件的访问权限就和你自己完全 一样。

文件权限的确切含义取决于文件的类型。对于普通文件,权限 的含义很直白:读权限让一个用户 ID 能够读取该文件;写 权限让一个用户 ID 能够写入该文件;执行权限让一个用户 ID 能够执行该文件。当然,除非文件是可执行的,否则试图 执行它是没有意义的。一般规则是:如果文件是某种程序或 脚本,它就是可执行的。例如,Shell 脚本里含有等待 Shell 去执行的命令。

三种权限各不相同,但会协同工作。例如,要编辑一个文件, 你需要同时具备读权限和写权限。要运行一个 Shell 脚本, 你需要同时具备读权限和执行权限。

正如你稍后在本章会看到的,你能够为自己拥有的文件设置和 修改权限。你这样做有两个原因:

• 限制其他用户的访问

限定哪些用户 ID 可以访问你的文件,以一种简单直接的方式 为你的数据提供了安全性。

• 防备你自己的失误

如果你想保护一个文件不被误删,你可以确保该文件没有写 权限。许多会替换或删除数据的命令,在修改没有写权限的 文件之前都会先请求确认。(本章前面讨论的 rmmv 命令就是这样。)

对于目录,权限的含义与普通文件略有不同。读权限让一个 用户 ID 能够读取目录中的名字;写权限让一个用户 ID 能够 对目录做出修改(创建、移动、复制、删除);执行权限让 一个用户 ID 能够在目录中查找。

如果你只有读权限,你可以列出目录中的名字,但也仅此而已。 除非你还有执行权限,否则你无法查看某个文件的大小、无法 到子目录里去看、也无法用 cd 命令切换目录。

想想下面这个不寻常的组合。如果你对一个目录拥有写权限和 执行权限,但没有读权限,那会是什么情形?你将能够访问和 修改这个目录,却读不了它。因此,你无法列出其中的内容, 但如果你知道某个文件的名字,你就能把它删掉。

供查阅之用,图 25-2 总结了适用于普通文件和目录的 文件权限。

Figure 25-2
文件权限小结

文件权限控制哪些用户 ID 可以访问一个文件。每个文件都有 三组权限:属于所有者的、属于用户组的、以及属于所有其他 人的。每组权限包含三个部分:读权限、写权限和执行权限。 这些权限对普通文件和对目录的含义略有不同。

普通文件
从文件读取
写入文件
执行执行该文件
目录
读取目录
创建、移动、复制或删除条目
执行在目录中查找
 

— 提示 —

刚学习目录权限时,你可能觉得有点绕。本章后面你会了解到, 目录条目里只包含一个文件名和一个指向文件的指针,而不是 文件本身。

一旦明白这一点,目录权限就完全说得通了。读权限意味着你 可以读取目录条目;写权限意味着你可以修改目录条目;执行 权限意味着你可以使用目录条目。

跳至页首

Setuid 权限 (Setuid)

在你的 Unix 系统内部,你这个人并不存在。你以某个特定的 用户 ID 登录,然后运行程序来替你办事。作为生活在外部 世界的人,你的角色仅限于提供输入和阅读输出。真正干活 的是你的程序。例如,要排序数据,你用 sort 程序; 要重命名文件,你用 mv 程序;要显示文件,你用 less;如此等等。

作为一般规则(有一个例外,我们马上就会讨论),每当你 运行一个程序,该程序都是在你的用户 ID 的主持下运行的。 这就是说,你的程序拥有的特权与你的用户 ID 完全相同。 例如,假设你的用户 ID 对一个名为 secrets 的文件 没有读权限。你想看看文件里有什么,于是输入:

less secrets

既然你的用户 ID 读不了这个文件,你请来替你干活的程序 也读不了它。结果,你会看到这条消息:

secrets: Permission denied

如果你确实想窥探 secrets 文件的内部,你有三个 选择。第一,你可以修改它的文件权限(本章后面我会解释 怎么做)。第二,你可以用一个对该文件已有读权限的用户 ID 登录。第三,如果你知道 root 口令,你可以以超级 用户身份登录,这几乎让你绕过所有限制。

换句话说 —— 除非你是超级用户 —— 你的程序受限于你的 用户 ID 的种种限制,只有一个例外。有时确实有必要让 普通用户 ID 以特殊特权运行某个程序。为了让这成为可能, 有一种特殊的文件权限设置,它允许其他用户 ID 访问某个 文件,就如同他们是该文件的所有者(创建者)一样。这个 特殊权限称为 SETUID(读作“set U-I-D”)或 SUID。这个名字 的意思是“set userid”(设置用户 ID)。

大多数情况下,setuid 用于让普通用户 ID 运行某些由 root 拥有的选定程序。这意味着无论哪个用户 ID 运行该程序,它都以 root 特权执行。这就使该程序 能够完成通常只有超级用户才能完成的任务。例如,要修改 你的口令,你使用 passwd 程序。然而,要修改你的 口令,这个程序必须修改口令文件和影子文件(见第 11 章), 而这需要超级用户的特权。因此,passwd 程序本身 存放在一个由 root 拥有、并且打开了 setuid 的 文件里。

你怎么看出一个文件具有 setuid 文件权限呢?当你显示 长格式列表时,你会看到文件权限中的某个“x”变成了字母 “s”。例如,你输入:

ls -l /usr/bin/passwd

这会显示 passwd 程序的长格式列表。输出是:

-r-s--x--x  1 root root  21944 Feb 12 2009 /usr/bin/passwd

文件权限中的“s”(从左数第 4 个字符)表示 setuid 权限。这个“s”取代了原本应当是“x”的位置。

显然,这样的权限会带来安全风险。毕竟,一个以超级用户 特权肆意妄为的程序,可以被用来入侵系统或造成破坏。因此, setuid 的使用受到严格控制。作为一般规则,setuid 只用于 让普通用户 ID 为了完成某项特定任务而临时以特权运行一个 程序。

跳至页首

Unix 如何维护文件权限
(How Unix Maintains File Permissions): id, groups

在贝尔实验室创建第一个 Unix 系统的程序员们(见第 1 章), 用一种至今仍在使用的方式组织了文件权限。在 Unix 开发的 年代,贝尔实验室的人们以小组为单位工作,组内共享程序和 文档。因此,Unix 的开发者们设计了三个类别:用户、用户的 工作组,以及系统上的所有人。随后他们设计 Unix,为每个 文件维护三组权限。具体是这样的。

创建文件的那个用户 ID 就成为该文件的所有者(OWNER)。 所有者是唯一能修改文件权限的用户 ID(*)。第一组文件 权限描述所有者可以如何访问该文件。每个用户 ID 都属于 一个用户组(group,下面解释)。第二组权限适用于与所有者 同组的所有其他用户 ID。第三组权限适用于系统上其余的 用户 ID。这就是说,对于你的每个文件和目录,你可以为 你自己、为你工作组里的人、以及为所有其他人分别指派读、 写和执行权限。

* 脚注

唯一的例外是超级用户,他几乎无所不能,可以修改任何文件 的权限。必要时,超级用户还可以分别用 chownchgrp 命令修改文件的所有者和用户组。

举个例子。你和一群人合作开发一个程序。存放程序的文件 位于你的某个个人目录中。你可以这样设置文件权限:你和你 的组都拥有读、写和执行权限,而系统上其他所有用户只有读 和执行权限。这意味着,虽然任何人都能运行这个程序,但 只有你或你组里的成员能修改它。

再举个例子。你有一份不想让其他任何人看的文档。只要 给你自己读和写权限,而不给你的组、也不给所有其他人任何 权限即可。(*)

* 脚注

不过要记住,你什么也瞒不过超级用户。

很重要的一点是要理解:给“所有人”的权限并不包括你自己 或你组里的成员。想象一个奇怪的情形:你把某个文件的读 权限给了所有人,却不给你所在的组任何权限。那么你组里的 成员将无法读取这个文件,而所有其他人都能读。此外,如果 你的网络上有用户能访问你的文件系统,他们也属于“所有人” 这一类,即使他们在你这台特定的系统上并没有账号。

那么谁在你的组里?当你的系统管理员创建你的账号时,他 同时也把你指派到一个用户组(GROUP)。正如每个用户都有 一个叫做用户 ID 的名字,每个组也有一个叫做组 ID(GROUPID, 读作“group-I-D”)的名字。你系统里所有组 ID 的列表保存在 文件 /etc/group 中,你随时可以自由查看:

less /etc/group

你的组名记录在口令文件 /etc/passwd 中(第 11 章 有介绍),与它一起的还有你的用户 ID、你的主目录名以及 其他信息。显示你的用户 ID 和组 ID 最简单的办法,是使用 id 命令。(只键入命令名本身,不加任何选项。)

在一个非常具体的场合里,id 命令格外好用。你在做 一些系统管理工作,时不时需要在自己的用户 ID 和 root(超级用户)之间切换。如果你搞混了、记不起 自己当前用的是哪个用户 ID,你随时可以输入 id 命令。(我每个月就要碰上好几回。)

问:假设一位系统管理员正穿过一间计算机机房,他看到一台 机器被人留在已登录 Unix 系统的状态。他会怎么做?

答:他做的第一件事就是输入 id 命令,看看登录的 是谁。他记下这个用户 ID —— 以便去找这个人谈谈 —— 然后 键入 exit 退出登录。

在 Unix 早期,每个用户 ID 只能属于一个组。然而现代 Unix 系统允许用户同时属于多个组。对每个用户 ID 而言,记录在 口令文件里的那个组称为主用户组(PRIMARY GROUP)。如果 该用户 ID 还属于其他组,它们就称为附属用户组 (SUPPLEMENTARY GROUPS)。你可以用两种方式来显示你的 用户 ID 所属的全部组。第一,使用 id 程序(在 Solaris 上,你必须用 id -a):

id

下面是一些示例输出:

uid=500(harley) gid=500(staff) groups=500(staff),502(admin)

在这个例子中,用户 ID harley 属于两个组:主用户组 staff,以及一个附属组 admin

显示你所有组的另一种方法是使用 groups 程序。 语法是:

groups [userid...]

其中 userid 是一个用户 ID。

默认情况下,groups 显示当前用户 ID 所属各组的 名字。如果你指定了一个或多个用户 ID,groups 会 告诉你他们各自属于哪些组。在你的系统上试试下面两条命令。 第一条显示你的所有组;第二条显示超级用户(用户 ID root)所属的组:

groups
groups root

用户组有多重要?在 1970 年代,组非常重要。大多数 Unix 用户是研究者,工作在一个可信的、没有与外部网络相连的 环境里。把每个用户 ID 放进一个组,研究者们就能与同事 共享工作、协同合作。然而今天,对普通用户来说,组有时 会被忽略,原因有两个。

第一,大多数人都有自己的 Unix 或 Linux 计算机,而当系统 上只有你一个人用时,没有谁可以共享。第二,即便是在共享 的系统或大型网络上,系统管理员常常也觉得不值得去维护 那些小到真正管用的组。例如,如果你是一所大学的本科生, 你的用户 ID 可能属于一个大得离谱的组(比如全体社会科学 专业学生),与这样一群人共享文件毫无意义。

话虽如此,你也应当知道,有些机构确实会费心维护用户组, 以便共享数据文件或可执行程序。例如在大学里,选修某门 课程的学生可能被赋予属于专为本课设置的组的用户 ID。这样 一来,教师创建的文件就只有那些学生能够访问。

— 提示 —

除非你确实需要与组里其他用户 ID 共享文件,否则最好干脆 把用户组这个概念放到一边。设置文件权限时(本章后面讲解), 直接把“组”设成和“所有人”一样的权限就行。

跳至页首

显示文件权限 (Displaying File Permissions): ls -l

要显示一个文件的文件权限,请在 ls 命令中使用 -l(长格式列表)选项。权限显示在输出的最左边。 要显示某个目录的权限,把 -d 选项和 -l 一 起用。(ls 命令连同这些选项在第 24 章中有解释。)

举个例子。你输入下面这条命令查看工作目录中的文件:

ls -l

输出是:

total 109
-rwxrwxrwx 1 harley staff 28672 Sep 5 16:37 program.allusers
-rwxrwx--- 1 harley staff  6864 Sep 5 16:38 program.group
-rwx------ 1 harley staff  4576 Sep 5 16:32 program.owner
-rw-rw-rw- 1 harley staff  7376 Sep 5 16:34 text.allusers
-rw-rw---- 1 harley staff  5532 Sep 5 16:34 text.group
-rw------- 1 harley staff  6454 Sep 5 16:34 text.owner

这份输出的大部分内容我们在第 24 章已经讨论过。简单说, 文件名在最右边。往左看,依次是最后修改的时间和日期、 大小(以字节计),以及所有者的组和用户 ID。在这个例子 中,文件的所有者是用户 ID harley,组是 staff。所有者左边是链接数(本章后面我会讨论)。 最左边,每行的第一个字符是文件类型标志。普通文件用 -(连字符)标记;目录(本例中没有)用 d 标记。

这里我们关注的是文件类型标志右边的那 9 个字符。它们的 含义如下:

r  =  读权限
w  =  写权限
x  =  执行权限
-  =  未授予权限

要分析一个文件的权限,只需把这 9 个字符分成三组,每组 3 个。从左到右,这三组分别表示文件所有者、用户组,以及 系统上所有其他用户 ID 的权限。我们把例子中所有文件都 分析一遍:

所有者其他文件
rwxrwxrwxprogram.allusers
rwxrwx---program.group
rwx------program.owner
rw-rw-rw-text.allusers
rw-rw----text.group
rw-------text.owner

现在我们可以精确地看到每一项权限是如何指派的。比如, 文件 text.owner 的所有者有读和写权限,而组和其他 任何人都没有任何权限。

跳至页首

文件模式 (File Modes)

Unix 用一个紧凑的三位数字代码来表示一整套文件权限。 这个代码称为文件模式(FILE MODE),或更简单地称为模式 (MODE)。举例说,上一个例子中文件 text.owner 的 模式是 600

在一个模式中,每个数字代表一组权限。第一个数字代表拥有 该文件的用户 ID 的权限;第二个数字代表组内用户 ID 的 权限;第三个数字代表系统上所有其他用户 ID 的权限。用 我刚提到的那个例子,我们得到:

6  =  所有者的权限
0  =  组的权限
0  =  所有其他用户 ID 的权限

这个代码是这样工作的。我们先给各种权限赋如下数值:

4  =  读权限
2  =  写权限
1  =  执行权限
0  =  无权限

对每一组权限,只要把相应的数字相加即可。例如,要表示 读加写权限,就把 42 加起来。 图 25-3 列出了每一种可能的组合及其数值。

Figure 25-3: 文件权限组合的数值

文件权限有三种:读权限、写权限和执行权限。这些权限的值 由 3 个不同的数字表示,按下表所示相加。详见正文。

执行 组成 合计
0 + 0 + 00
0 + 0 + 11
0 + 2 + 02
0 + 2 + 13
4 + 0 + 04
4 + 0 + 15
4 + 2 + 06
4 + 2 + 17

我们来做一道例题。如果一个文件满足下面的条件,它的模式 是什么:

• 所有者拥有读、写和执行权限?
• 用户组拥有读和写权限?
• 所有其他用户 ID 只有读权限?

所有者:读 + 写 + 执行=4+2+1=7
组:读 + 写=4+2+0=6
其他:读=4+0+0=4

因此,模式是 764。现在我们回过头来看上一节的 那些例子:

所有者其他模式文件
rwx = 7 rwx = 7 rwx = 7 777 program.allusers
rwx = 7 rwx = 7 --- = 0 770 program.group
rwx = 7 --- = 0 --- = 0 700 program.owner
rw- = 6 rw- = 6 rw- = 6 666 text.allusers
rw- = 6 rw- = 6 --- = 0 660 text.group
rw- = 6 --- = 0 --- = 0 600 text.owner

现在我们来做一个反向的例子。文件模式 540 是什么 意思?借助图 25-3,我们看到:

所有者:5=读 + 执行
组:4=
其他:0=什么都没有

因此,所有者可以读取和执行这个文件。用户组只能读取该 文件。所有其他人没有任何权限。

跳至页首

修改文件权限 (Changing File Permissions): chmod

要修改一个文件的权限,使用 chmod(change file mode,修改文件模式)命令。语法是:

chmod mode file...

其中 mode 是新的文件模式,file 是文件或 目录的名字。

只有所有者或超级用户才能修改一个文件的文件模式。正如我 前面提到的,你创建的每个文件,其所有者自动就是你的用户 ID。

下面是几个你可能怎样使用 chmod 的例子。第一条 命令把指定文件的模式改为:所有者拥有读和写权限,用户组 和所有其他人只有读权限。这样的权限适合一个你愿意让任何 人阅读、但不愿让人修改的文件。

chmod 644 essay1 essay2 document

下一条命令给所有者读、写、执行权限,给用户组和所有其他 人读和执行权限。这样的权限适合这样一个文件:里面是一个 你愿意让别人运行、但不愿让人修改的程序。

chmod 755 spacewar

一般说来,除非有别的理由,限制权限是更稳妥的做法。下面 几条命令演示如何只为所有者设置权限,而不给用户组或所有 其他人任何权限。首先,只设置读和写权限:

chmod 600 homework.text

接着,设置读、写和执行权限:

chmod 700 homework.program

当你创建一个 Shell 脚本或程序时,默认它只有读和写权限。 要执行这个脚本,你必须再加上执行权限。用 chmod 700(如果想共享,就用 chmod 755)。

— 提示 —

为避免出问题,不要把执行权限给一个并不可执行的文件。

跳至页首

Unix 如何为新文件指派权限
(How Unix Assigns Permissions to a New File): umask

当 Unix 创建一个新文件时,它从一个文件模式出发:

666: 用于非可执行的普通文件
777: 用于可执行的普通文件
777: 用于目录

Unix 会从这一初始模式中减去用户掩码(USER MASK)的值。 用户掩码是由你自己设定的一个模式,表示你想要限制 哪些权限。要设置用户掩码,使用 umask 命令。语法是:

umask [mode]

其中 mode 指定你想限制哪些权限。

把一条 umask 命令放进你的登录文件是个好主意,这样 每次登录时你的用户掩码就会被自动设好。事实上,在我给 你看过的第 14 章的登录文件样例里,就会看到一条 umask 命令。

你的用户掩码该是什么?我们看几个例子。先假设你希望把你 的组和其他所有人的写权限扣住不发。用一个 022 模式:

umask 022

这个用户掩码让你的文件可以被共享,但不允许任何人改动。 多数情况下,尽可能保持私密是更稳妥的做法。要做到这一点, 你可以把组和其他所有人的全部权限 —— 读、写、执行 —— 都 扣住不发。用一个 077 模式:

umask 077

要显示用户掩码的当前值,你可以在不带参数的情况下输入 umask 命令:

umask

注意:umask 是一条内置命令,这意味着它的具体行为 取决于你所用的 Shell。有些 Shell 不显示前导零。例如, 如果你的用户掩码是 022,你可能看到 22; 如果你的用户掩码是 002,你可能看到 2。 如果你的 Shell 是这样,就当那些零存在好了。

— 提示 —

除非有很好的理由不这么做,否则请在登录文件里用 umask 077,让你的文件完全私密。如果你想共享, 可以用 chmod 逐个文件地处理。

跳至页首

抹去文件的内容 (Wiping Out the Contents of a File): shred

正如本章前面讨论过的,文件一经删除,就没有办法找回。 不过,该文件占用的实际磁盘空间并没有被清除干净。系统只是 把它标记为“可再使用”,供文件系统调配。迟早这块磁盘空间 会被重新使用,旧数据会被新数据覆盖。在一台繁忙的大型 Unix 系统上,这可能几秒钟内就发生了。然而,这究竟何时 发生并没有任何保证,有时旧数据会在磁盘未使用的部分里 藏上相当长的时间。事实上,确实存在一些特殊的“反删除” (undelete)工具,能够查看磁盘未使用的部分,把旧数据恢复 出来。

不仅如此,即使数据已被覆盖,在极端情况下,只要数据没有被 覆盖一次以上,仍然是可能被恢复的。如果你能把一块硬盘拿到 一间装有昂贵数据恢复设备的实验室,那么从磁盘的磁表面上 察觉旧数据的残留痕迹并非不可能。

所以,对于真正疑心重重的人来说,让数据永久消失的最好办法 是毁掉存储介质,比如说把它熔化。对 CD 或软盘来说这相对 容易,但对硬盘就不那么轻松了,尤其是当你只想抹去几个文件 而不是整块磁盘的时候。因此,在这少数“简单删除文件还不够” 的场合,GNU 实用工具集(见第 2 章)提供了一个叫 shred 的程序。虽然 shred 并非随处可见,但 你在大多数 Linux 系统上都能找到它。语法是:

shred -fvuz [file...]

其中 file 是文件的名字。

shred 的目标是把已有数据反复覆盖许多次,多到让 世界上最昂贵的数据恢复设备在读磁痕迹时也会自觉汗颜。你 要做的只是指定一个或多个文件的名字,shred 会自动 完成剩下的工作。如果你像我常做的那样加上 -v (verbose,详细)选项,shred 会在推进过程中显示 消息:

shred -v datafile

默认情况下,shred 会把数据覆盖许多次,最后留下 随机数据。当然,随机数据恰恰是一个信号,表明这个文件被 “粉碎”过了。要掩盖这一点,你可以使用 -z 选项,它 告诉 shred 在收尾时用全零填满文件。更进一步,如果 你想在处理完之后删除文件,请使用 -u 选项。最后, 要越过严格的文件权限限制,你可以使用 -f(强制) 选项。

那么,这就是那条终极 shred 命令。它会越过现有的 文件权限,通过反复覆盖抹去所有数据,用全零填充文件,并且 把残余部分删掉:

shred -fuvz datafile

当然,shred 程序能做的终究有限。如果一个文件已被 自动备份到另一台系统,或者被复制到了镜像站点,那么再怎么 粉碎也去不掉远处的副本。此外,shred 并非在所有 文件系统上都有效。例如,在 ZFS 文件系统(由 Sun Microsystems 开发)上更新一个文件时,新数据会被写到磁盘上 另一个位置。旧数据不会被替换,除非磁盘上那一块被另一个文件 重新使用。

跳至页首

链接的概念 (The Idea of a Link): stat, ls -i

Unix 创建文件时会做两件事。第一,它在存储设备上划出空间 用来存放数据。第二,它创建一个称为索引节点(INDEX NODE) 或 inode(“I 节点”)的结构,用来保存关于该文件的基本信息。 inode 中含有文件系统使用这个文件所需的全部信息。图 25-4 小结了在典型 Unix 文件系统中一个 inode 里通常能找到什么。 普通用户不必知道 inode 里装的是什么,因为文件系统会自动 处理这些细节。在 Linux 系统上,用 stat 命令很容易 就能查看某个文件内部的 inode。只要键入 stat,后面 跟一个文件的名字:

stat filename

Figure 25-4
inode(索引节点)的内容

索引节点或 inode 中包含有关一个文件的信息。下面列出的是 Unix 文件系统的 inode 中通常存放的信息。确切的内容会因 文件系统的不同而略有差异。

• 以字节计的文件长度
• 存放该文件的设备的名字
• 所有者的用户 ID
• 组 ID
• 文件权限
• 最后修改时间
• 最后访问时间
• inode 最后一次被改动的时间
• 指向该文件的链接数目
• 文件类型(普通文件、目录、特殊文件、符号链接……)
• 分配给该文件的块数

文件系统把所有 inode 存放在一张称为 inode 表(INODE TABLE)的大表里。在 inode 表中,每个 inode 由一个称为 索引号(INDEX NUMBER)或 inumber(“I 号”)的数字标识。例如, 假设某个特定的文件由 inode #478515 描述,我们就说这个文件 的 inumber 是 478515。要显示一个文件的 inumber,请在 ls 中使用 -i 选项。例如,下面这条命令显示 名为 xyzzyplugh 的两个文件的 inumber(*):

ls -i xyzzy plugh

* 脚注

有空的时候,上网查查这两个名字的来历。

下面两条命令显示当前目录中所有文件的 inumber:

ls -i
ls -il

我们使用目录时,说话的口气就好像目录里真的装着文件。 例如,你可能会听某人说他的 bin 目录里有一个名为 spacewar 的文件。然而,该目录并不真的包含这个文件。 实际上,目录里只有文件的名字和它的 inumber。因此,一个目录 的内容其实很小:只是一串名字的列表,每个名字对应一个 inumber。

我们来看一个例子。当你在 bin 目录中创建一个名为 spacewar 的文件时,会发生什么?首先,Unix 在磁盘上 划出存放该文件的空间。接着,Unix 在 inode 表中找一个空闲 的 inode。假设找到的是 inode #478515。Unix 把这个新文件 相应的信息填进这个 inode。最后,Unix 在 bin 目录中 放入一个条目。这个条目包含名字 spacewar,以及 inumber 478515。每当某个程序需要使用这个文件时,做法都很 简单:在目录里查出这个名字,用相应的 inumber 找到 inode, 再用 inode 中的信息去访问该文件。

文件名与其 inode 之间的这种联系称为链接(LINK)。从概念上 说,链接把一个文件名与文件本身连接起来。这就是为什么 —— 正如你从图 25-4 中看到的 —— inode 里并不包含文件名。 事实上,你马上就会看到,一个 inode 可以被多个文件名 引用。

跳至页首

同一个文件的多个链接 (Multiple Links to the Same File)

Unix 文件系统最优雅的特性之一,就是它允许同一个文件拥有 多个链接。换句话说,一个文件可以有多个名字。这怎么可能? 因为文件的唯一标识是它的 inumber,而不是它的名字。 所以,没有理由不允许多个文件名引用同一个 inumber。举一个 例子。

假设你的主目录是 /home/harley。在主目录里,你有一个 名为 bin 的子目录。你在 bin 目录里创建了一个 名为 spacewar 的文件。恰好这个文件的 inumber 是 478515。你用 ln 命令(本章后面会讲)在同一个目录里 创建了另一个名为 funky 的文件,使它的 inumber 与 spacewar 相同。由于 spacewarfunky 拥有相同的 inumber,它们实质上就是同一个文件的两个不同 名字。

现在,假设你切换到主目录,又创建一个名为 extra 的 文件,同样使用这个 inumber。然后你切换到朋友的主目录 /home/weedly,再创建第四个文件 myfile,也用 同一个 inumber。到这里,你依然只有一个文件 —— 那个由 inumber 478515 标识的文件 —— 但它有四个不同的名字:

/home/harley/bin/spacewar
/home/harley/bin/funky
/home/harley/extra
/home/weedly/myfile

你为什么要这么做?一旦你习惯了链接这个概念,就会发现大量 使用它的机会。基本思想 —— 等你经验更丰富、理解更深时会更 清楚 —— 是同一个文件可以有不同的含义,取决于它被使用的 上下文。例如,让不同的用户以不同的名字访问同一个文件,常常 很方便。

不过,链接背后的思想远不止这些。我要你理解它们如何工作的 原因是:正是链接支撑着基本文件命令的运作:cp(复制)、 mv(移动)、rm(移除)和 ln(链接)。 如果你做的只是死记这些命令怎么用,你永远无法真正理解发生了 什么,使用文件系统的种种规则也永远说不通。

一会儿我们就会来讨论这句话的含义。在此之前,我想请你思考 一个问题。假设一个文件有不止一个链接;也就是说,这个文件 可以用不止一个名字访问。这些名字里哪个最重要?最初的那个 名字有什么特殊意义吗?答案是:Unix 把所有链接同等看待。 文件最初叫什么名字并不重要。新链接与旧链接一样重要。

在 Unix 内部,文件不是由名字或位置来控制的。文件是由 所有权和权限控制的。

跳至页首

创建新链接 (Creating a New Link): ln

每当你创建一个文件,文件系统会自动在文件名与文件之间建立 一个链接。然而,有时你会想给一个已存在的文件再建一个新 链接。为此,你使用 ln(link,链接)命令。这条命令有 两种形式。第一,给单个文件新建一个链接,语法是:

ln file newname

其中 file 是已存在的普通文件的名字,newname 是你要给这个链接起的名字。

例如,假设你有一个名为 spacewar 的文件,想再建一个 名为 funky 的链接,用:

ln spacewar funky

结果你会得到两个文件名,它们都指向同一个文件(也就是同一个 inumber)。新链接一旦建立,在功能上就与原来的目录条目完全 一样。

使用 ln 的第二种方式,是为一个或多个普通文件新建 链接,并把它们放进指定的目录。语法是:

ln file... directory

其中 file 是已存在的普通文件的名字,directory 是你想把新链接放进去的那个目录的名字。

举个例子。你的主目录是 /home/harley。在这个目录里, 你有两个文件:data1data2。你的朋友使用 主目录 /home/weedly。在他的这个目录里,有一个名为 work 的子目录。这个目录的文件权限允许你的用户 ID 创建文件。你想为你的两个文件建立链接,并把它们放进朋友的 目录。用这条命令:

ln /home/harley/data1 /home/harley/data2 /home/weedly/work

为了简化这条命令,你可以使用通配符(见第 24 章):

ln /home/harley/data[12] /home/weedly/work

简化这条命令的另一种办法,是在输入 ln 命令之前先 切换到你的主目录:

cd; ln data[12] /home/weedly/work

这些新链接一旦建立,两个文件就同时拥有了位于两个不同目录 中的名字。要查看一个文件的链接数,使用 ls -l 命令。链接数显示在文件权限与所有者用户 ID 之间。例如,假设 你输入这条命令:

ls -l music videos

输出是:

-rw------- 1 harley staff  4070 Oct 14 09:50 music
-rwx------ 2 harley staff 81920 Oct 14 09:49 videos

你可以看到,music 只有一个链接,而 videos 有两个链接。

跳至页首

基本文件命令是如何工作的 (How the Basic File Commands Work)

你务必能够从文件名和链接的角度来理解这些基本的文件命令。 下面是几项基本的操作:

1. 创建文件;创建目录 [mkdir]

要创建一个新文件或新目录,Unix 划出存储空间并建立一个 inode。在适当的目录中,Unix 放入一个新条目,内容是你指定的 文件名或目录名,加上新 inode 的 inumber。

2. 复制文件 [cp]

要复制到一个已存在的文件,Unix 用源文件的内容替换目标文件 的内容。没有任何 inumber 发生变化。要复制到一个并不存在的 文件,Unix 先创建一个全新的、拥有自己 inumber 的文件。 (记住,inumber 才是真正标识文件的东西。)随后把旧文件的内容 复制到新文件。复制之后,就有了两个彼此不同的文件:旧文件名 对应旧的 inumber,新文件名对应新的 inumber。

3. 重命名文件或移动文件 [mv]

要重命名或移动一个文件,Unix 改变文件名,或移动目录条目, 或两者都做,但保持 inumber 不变。这就是为什么同一个命令 (mv)既用来改名也用来移动。

4. 创建链接 [ln]

要给一个已存在的文件新建一个链接,Unix 用你指定的文件名 做出一个新的目录条目,指向与原文件相同的 inumber。于是 就有了一个文件、两个文件名,而两个文件名都指向同一个 inumber。

5. 移除链接 [rm, rmdir]

当你移除一个链接时,Unix 通过删除目录条目,消除文件名与 inumber 之间的联系。如果没有更多的链接了,Unix 才删除 文件本身。

很重要的一点是要理解:移除一个链接并不等于删除一个文件。 如果一个文件有不止一个链接,Unix 不会删除该文件,直到 最后一个链接被移除为止。不过在多数情况下,一个文件只有 一个链接,这就是为什么大多数时候 rmrmdir 表现得像删除命令。

这里有一个简单的例子,用来阐明我们刚刚讨论的那些思想。 你有一个名为 spacewar 的文件。你决定给这个文件新建 一个链接,叫它 funky:

ln spacewar funky

现在你把 spacewar 移除:

rm spacewar

尽管第一个文件名已经不在了,原来的文件依然存在。文件本身 不会被删除,直到最后一个链接(funky)被 移除。

跳至页首

符号链接 (Symbolic Links): ln -s

我们前面讨论的那种链接,让多个名字可以指向同一个文件。 然而这类链接有两个限制。第一,你不能给目录创建链接。 第二,你不能给另一个文件系统中的文件创建链接。

要给一个目录、或给另一个文件系统中的文件创建链接,你需要 创建所谓符号链接(SYMBOLIC LINK,又称软链接 symlink)。 为此,你在 ln 程序中使用 -s 选项。符号链接 不包含文件的 inumber,它包含的是原始文件的路径名。每当 你访问一个符号链接时,Unix 就用那个路径名去找到文件。 (从这个意义上说,符号链接类似于 Windows 的快捷方式。顺便 说一句,Windows Vista 确实支持类 Unix 的真正符号链接。)

当你用 ls -l 显示一个符号链接文件的长格式 列表时,你会注意到两件事。第一,文件类型标志(输出的最左 字符)是小写字母 l,代表“link”。第二,行尾右侧显示 的是符号链接实际指向的内容。举一个例子:在某个系统上,文件 /bin/sh 是指向文件 /bin/bash 的符号链接。 你输入命令:

ls -l /bin/sh

输出是:

lrwxrwxrwx  1 root root 4 Sep 11 2008  /bin/sh -> bash

你可以看到,这个文件只有 4 个字节长,恰好装得下真实文件的 路径名(它由 4 个字符组成)。这一点 —— 它是一个符号链接, 而不是,比如说,一个 4 字符的普通文件 —— 记录在该文件的 inode 里。

如果你想看文件本身的长格式列表,你必须指出它的实际名字:

ls -l /bin/bash

这种情况下,输出是:

-rwxr-xr-x 1 root root 720888 Feb 10 2008  /bin/bash

如你所见,这个文件有 720,888 个字节。你大概已经从名字上 猜到了:这个文件存放的是 Bash shell 的程序。

为了区分两种链接,普通链接有时称为硬链接(HARD LINK), 而符号链接有时称为软链接(SOFT LINK)。当我们单独使用 “链接”这个词时,指的是硬链接。

正如我们前面讨论的,要显示一个文件的硬链接数,你使用 ls -l 命令。然而,没有任何办法显示一个文件 有多少个软链接(符号链接)。这是因为连文件系统自己都不 知道存在多少个这样的链接。

问:如果一个文件存在某个符号链接,而你把这个文件删掉了, 会怎样?

答:符号链接不会被删除。事实上,你仍然可以用 ls 列出它。不过,如果你试图使用这个链接,你会得到一条错误 消息。

跳至页首

在目录上使用符号链接 (Using Symbolic Links With Directories)

在第 24 章中,我们讨论过如何使用内置命令 cd 来改变 工作目录、用 pwd 来显示工作目录的名字。这里有一个 问题:当某个目录名其实是指向另一个目录的符号链接时, cdpwd 应当如何表现?有两种选择。第一, 命令可以把符号链接看作一个独立的实体,是真实目录的另一个 名字,就像硬链接之于普通文件那样。另一种可能是,这个链接 不过是一块通往真实目录的垫脚石。

在某些 Shell 中,cd 有两个选项让你控制这种情况。 -L(logical,逻辑)选项告诉 cd 把符号链接 当作自己就是一个真实目录来对待。-P(physical,物理) 选项告诉 cd 用真实目录替换符号目录。举一个例子。

首先,在你的主目录里创建一个名为 extra 的子目录。 接着,为这个目录创建一个符号链接,命名为 backups。 最后,显示这两个文件的长格式列表:

cd
mkdir extra
ln -s extra backups
ls -ld extra backups

下面是一些示例输出:

lrwxrwxrwx 1 harley staff    5 Sep 8 17:52 backups -> extra
drwxrwxr-x 2 harley staff 4096 Sep 8 17:52 extra

看每行的第一个字符,我们可以判断 backups 是一个 链接,而 extra 是一个真实目录。注意 backups 只有 5 个字节长,恰好装得下目标目录的名字。而 extra 目录 有 4,096 个字节长,也就是这个文件系统的块大小(见第 24 章)。

现在,考虑这条命令:

cd -L backups

它把工作目录改为 backups,尽管严格说来 backups 并非真实存在。那么,如果我们改用 -P 选项会怎样?

cd -P backups

这种情况下,Shell 会用实际的目录替换这个符号链接,我们的 工作目录就成了 extra。发生这种事时,我们说 Shell “跟随”(FOLLOWS)了这个链接。

默认情况下,cd 采用 -L,所以你其实从来不必 指定它。然而,如果你要 Shell 跟随一个符号链接,你就必须 指定 -P

pwd(print working directory,打印工作目录)命令 也有同样的两个选项,可用于在你显示工作目录名字的时候。 要检验这一点,请创建上面那个目录和符号链接,然后输入下面 两条命令中的任意一条(它们是等价的):

cd backups
cd -L backups

现在输入这条命令:

pwd -P

-P 选项告诉 pwd 跟随这个链接。输出看上去 会像这样:

/home/harley/extra

现在输入下面两条命令中的任意一条。与 cd 一样, -L 选项是默认的:

pwd
pwd -L

这种情况下,pwd 不跟随链接,所以输出是:

/home/harley/backups

跳至页首

查找与某个 Unix 命令相关的文件
(Finding Files Associated With a Unix Command): whereis

你会遇到许多次想要找到某个特定文件或某组文件的情形。这种 时候,有三个不同的程序可以使用:whereislocatefind。在接下来的几节里,我们依次 讲解每个程序。

whereis 程序用来查找与某个特定 Unix 命令相关的文件: 二进制(可执行)文件、源文件和文档文件。whereis 不去搜索整个文件系统,而只在这类文件很可能所在的目录里 查找:/bin/sbin/etc/usr/share/man 等等。(参见第 23 章对文件系统层次 标准 Filesystem Hierarchy Standard 的介绍。)

whereis 程序的语法是:

whereis [-bms] command...

其中 command 是一个命令的名字。

假设你想找到与 ls 命令相关的文件。只要输入:

whereis ls

下面是一些典型的输出

ls: /bin/ls /usr/share/man/man1p/ls.1p.gz /usr/share/man/man1/ls.1.gz

在这里我们看到,ls 程序本身位于路径名为 /bin/ls 的文件中。这一点很清楚。接下来的两个长 路径名给出了两个不同 man 手册页(压缩格式)所在的位置:

/usr/share/man/man1p/ls.1p.gz
/usr/share/man/man1/ls.1.gz

(.gz 扩展名表示该文件由 gzip 程序压缩过。 这类文件在显示之前会被自动解压。)

上面两行说明第一个 man 手册页在第 1p 节,第二个在第 1 节。 你可以用 man 命令显示其中任何一页(见第 9 章)。由于 第 1 节是默认值,你不必指定它。但其他任何节你都必须指定, 比如 1p。因此,显示这些页面所用的命令是:

man ls
man 1p ls

这个例子说明了 whereis 让我欣赏的一点:它有时会找 到你根本不知道存在的 man 手册页。例如,在生成上面输出的 那台系统上,ls 程序恰好有两个不同的 man 手册页。 事实上,两者的差别大到值得一读再读。然而,如果我们没有用 whereis 查一查,我们就不会知道第二页的存在。

如果你想限制 whereis 的输出,有几个选项可以使用。 要只显示可执行文件的路径名,使用 -b(binary,二进制) 选项;要显示在线手册中的文件,使用 -m 选项;要显示 源文件,使用 -s 选项。举一个例子。下面这条命令显示 十个不同程序的可执行文件的路径名:

whereis -b chmod cp id ln ls mv rm shred stat touch

在你的系统上试试这条命令,看看会得到什么。

跳至页首

通过搜索数据库查找文件
(Finding Files by Searching a Database): locate

有两个不同的 Unix 程序提供通用的“查找文件”服务: locatefind。我希望两个都学会。find 程序古老得多,也要难得多。但它非常强大, 每个 Unix 和 Linux 系统上都有。locate 程序较新、 容易使用,但不如 find 强大。而且,尽管大多数 Linux 和 FreeBSD 系统都自带它,但并非所有 Unix 系统都有(例如 Solaris)。本节我们讲 locate;后面几节我们讨论 find

locate 程序的工作是搜索一个特殊的数据库,里面存有 所有可公开访问的文件的路径名,找出其中包含指定模式的全部 名字。这个数据库由系统自动维护、定期更新。locate 的语法是:

locate [-bcirS] pattern...

其中 pattern 是你要在路径名中寻找的模式。

一个简单的例子。你想找出路径名中包含 “test” 这几个字符的 所有文件。由于这样的文件很可能很多,把命令的输出用管道送给 less(第 12 章)、一屏一屏地显示是个好主意:

locate test | less

除了普通字符之外,如果你加上 -r 选项,就可以使用 正则表达式。在正则表达式里,你可以用 ^$ 分别锚定路径名的开头和结尾。(关于正则表达式, 见第 20 章。)

这里有一个有意思的例子。你想在自己的系统上找照片。一种 办法是查找扩展名为 .jpg.png 的文件,并 把路径名存进一个文件。然后你可以从容地浏览这个文件。要用 的命令是:

locate -r '.jpg$' > photos
locate -r '.png$' >> photos

第一条命令把输出重定向到文件 photos。第二条命令把 输出追加到同一个文件。(关于重定向输出的讨论,见第 15 章。)

由于 locate 常常给你的比你想要的还多,人们通常会 对输出做某种处理。最有威力的组合之一,是把 locate 的输出用管道送给 grep。例如,假设你 正在用一台新系统,想找 Unix 的词典文件(见第 20 章)。多半 情况下,这个文件里既含有 “dict” 这几个字母,也含有 “words” 这几个字母。要找出所有这样的文件,先用 locate 找出 所有包含 “dict” 的文件,再用 greplocate 的输出中搜索包含 “words” 的行。命令是:

locate dict | grep words

在你的系统上试一下,看看得到什么。

要改变 locate 的行为,有几个选项可以使用。首先, -c(count,计数)选项显示匹配到的文件总数,而不是 显示实际的文件名。例如,如果你想知道系统上有多少个 JPG 文件,用这条命令:

locate -cr '.jpg$'

下一个选项是 -i(ignore case,忽略大小写)。它告诉 locate 把大写字母和小写字母当作一样的。例如,如果 你想搜索路径名中包含 “x11” 或 “X11” 的所有文件,可以用下面 两条命令中的任意一条:

locate x11 X11
locate -i x11

如果你愿意,可以把 -i-r 组合起来使用, 从而进行不区分大小写的正则表达式匹配。例如,下面演示如何 查找路径名以 “/usr” 开头、以 “x11” 或 “X11” 结尾的文件:

locate -ir '^/usr*x11$'

你会常常发现,只匹配路径名的最后一部分是很方便的,也就是 我们所说的文件名或基名(参见第 24 章)。要做到这一点, 请使用 -b 选项。例如,要找出所有基名中包含字母 “temp” 的文件,用:

locate -b temp

要找出名字恰好只由字母 “temp” 组成的所有文件,用:

locate -br '^temp$'

最后,要显示你系统上 locate 数据库的信息,请使用 -S(statistics,统计)选项:

locate -S

你可以看到,locate 很好用。只要告诉它你想要什么, locate 就会帮你找到。不过,有一个缺点我一定要让 你明白。

我前面提到过,locate 使用一个特殊的数据库,里面 保存着所有公开可访问文件的路径名。在一个管理良好的系统上, 这个数据库会定期自动更新。但是,当你或其他人新建一个文件时, 它要等到下一次更新才会出现在数据库里。要绕过这个限制,你可以 使用 find(下一节讨论),因为 find 是真正 去搜索目录树的。

跳至页首

通过搜索目录树查找文件: find
(Finding Files by Searching
a Directory Tree: find)

到目前为止,我们已经讨论了两种查找文件的工具:whereislocate。这两个程序都快、都好用了,而且大多数情况下, 它们应该是你查找文件时的首选。

不过,它们都有局限。whereis 程序只查找与某个特定程序 相关的文件(可执行文件、源文件、文档文件)。locate 程序并不真正执行搜索,它只是在一个包含系统上所有公开可访问 文件路径名的数据库里做模式匹配。当你想按需做一次彻底的搜索时, 你就需要使用 find

find 程序是这三个程序中最古老、也是最复杂的。事实上, find 是你将会用到的最复杂的 Unix 工具之一。不过, 相比另外两个程序,它有三大重要优势。第一,它非常强大: find 可以按照各种各样的条件,在任何地方查找任何文件。 第二,find 完成一次搜索之后,可以用几种不同的方式处理 搜索结果。最后,与 locate 不同,find 在所有 Unix 和 Linux 系统上都有,所以你可以在遇到的任何系统上使用它。

find 的完整语法非常复杂。事实上,我连给你看都不打算看。 相反,我们先从概览开始,一步一步往前走。

大致的思路是:find 在一个或多个目录树中搜索符合某些 条件的文件,这些条件由你指定的测试来定义。搜索完成之后, find 会对找到的文件执行某个动作。这个动作可以很简单, 比如显示文件名;也可以更复杂:find 可以删除这些文件、 显示它们的信息,或者把这些文件交给另一个命令做进一步处理。

要运行 find,你要指定三类东西(按这个顺序):目录路径、 测试、动作。一般语法是:

find path... test... action...

你输入命令之后,find 会走一个三步流程:

1. 路径:find 做的第一件事是查看每一个路径,检查它所 代表的整个目录树,包括所有子目录。

2. 测试:对 find 遇到的每一个文件,它都会应用你指定的 测试。目标是列出所有符合你条件的文件。

3. 动作:搜索完成之后,find 就对列表中的每一个文件执行 你指定的动作。

看下面这个简单的例子:

find /home/harley -name important -print

暂时先不深入细节,我们把这条命令拆开来看:

路径: /home/harley
测试: -name important
动作: -print

在这条命令里,我们给了 find 下面这些指示:

1. 路径:从 /home/harley 开始,搜索所有文件和子目录。

2. 测试:对每一个文件,应用测试 -name important。 (这个测试的含义是查找名为 important 的文件。)

3. 动作:对每一个通过测试的文件,执行动作 -print (也就是显示路径名)。

那么上面这条命令做了什么呢?它显示 /home/harley 目录树中所有名为 important 的文件的路径名。

花一点时间想想我们是怎么分析上面这条命令的。你会发现,任何 一条 find 命令 — 不管多复杂 — 都可以用同样的 方式分析,也就是把它拆成三部分:路径、测试、动作。反过来, 当你需要构造一条复杂的 find 命令时,你也可以依次考虑 这三部分,把它一步步搭起来。

跳至页首

find 命令:路径
(The find Command: Paths)

正如我们讨论过的,find 程序的一般格式是:

find path... test... action...

你可以看到,每一条 find 命令的开头都是一个或多个路径。 这些路径告诉 find 到哪里去搜索。指定路径很简单,下面的 例子会说明这一点。(阅读时请注意到,这些例子都没有指定测试或 动作。这些内容我们马上就会讲。)大多数时候,你只会用到一个路径, 所以先从最简单的例子开始:

find backups

在这个例子里,我们告诉 find 从名为 backups 的 目录开始,搜索它下面所有的子孙,既包括文件也包括子目录。 你可以看到,我们用的是相对路径名。你也可以使用绝对路径名、 用 .(点)代表工作目录,或者用 ~(波浪号)代表 主目录。这里再给几条不完整的命令:

find /usr/bin
find /
find .
find ~
find ~weedly

第一个例子告诉 find/usr/bin 目录开始搜索。 第二个例子从根目录开始搜索。(实际上,这等于告诉 find 搜索整个文件系统。)下一个例子从工作目录开始搜索。第四个例子 从主目录开始搜索。最后一个例子从用户 ID 为 weedly 的 主目录开始搜索。(关于如何指定路径名的完整讨论,参见 第 24 章。)

如果你愿意,可以为 find 指定多个待搜索的路径,例如:

find /bin /sbin /usr/bin ~harley/bin

在这个例子里,find 会搜索四棵各自独立的目录树。 搜索结果会被合在一起,当成一个长长的列表来处理。

跳至页首

find 命令:测试
(The find Command: Tests)

我们使用 find 程序来搜索一棵或多棵目录树,找出符合 指定条件的文件,然后对这些文件执行某些动作。要定义这些条件, 我们指定一个或多个测试(TESTS)。命令的一般格式是:

find path... test... action...

到目前为止,学习 find 还挺轻松的。接下来才变复杂。 上一节我们讨论了如何指定路径。这一节我们讨论如何使用各种测试 来指定你想处理哪些文件。测试有很多种,从简单到生僻的都有。 作为参考,我把最重要的测试总结在图 25-5 中。

-- Harley
Figure 25-5
find 程序:测试

find 程序搜索目录树,依据各种测试寻找符合特定条件 的文件。详情见正文。

文件名
-name pattern文件名匹配 pattern
-iname pattern文件名匹配 pattern(不区分大小写)
文件特征
-type [df]文件类型:d = 目录,f = 普通文件
-perm mode文件权限被设置为 mode
-user userid所有者是 userid
-group groupid所属组是 groupid
-size [-+]n[cbkMG]大小为 n [字符(字节)、块、千字节、兆字节、吉字节]
-empty空文件(大小 = 0)
访问时间、修改时间
-amin [-+]nn 分钟前被访问过
-anewer file访问时间比 file 更近
-atime [-+]nn 天前被访问过
-cmin [-+]nn 分钟前状态被改变过
-cnewer file状态改变时间比 file 更近
-ctime [-+]nn 天前状态被改变过
-mmin [-+]nn 分钟前被修改过
-mtime [-+]nn 天前被修改过
-newer file 修改时间比 file 更近
 

find 程序是一个非常古老的工具,基本的测试在各种系统上 都一样。不过,find 的新版本支持一些并非所有系统都有的 测试。图 25-5 总结了你可以配合 GNU 工具集里那个版本的 find 使用的测试,也就是 Linux 自带的那个程序(参见 第 2 章)。如果你用的是另一种 Unix,所有基本的测试都能用, 但一些更冷门的测试可能不被支持。要看你的 find 版本 支持哪些测试的完整列表,请查阅你系统上的手册页 (man find)。

从图 25-5 你可以看到,find 有很多不同的测试。 随着需要出现,你最终会把它们都学会。眼下,我的目标是确保你 理解最重要的那几个测试。其中最重要的两个测试毫无疑问是 -type-name,所以我们就从它们开始。

-type 测试控制 find 应该查看哪些类型的文件。 语法是 -type 后面跟一个字母的代号。最常用的要么是 代表普通文件的 f,要么是代表目录的 d。必要时, 你也可以使用 b(块设备)、c(字符设备)、 p(命名管道)或 l(符号链接)。这里给几个例子。 (注意:在这些例子里,我使用了动作 -print,它只是把 搜索结果显示出。更复杂的其他动作我们稍后在本章讨论。)


find /etc -type d -print
find /etc -type f -print
find /etc -print

这三条命令都是从 /etc 目录开始进行搜索的。第一条命令 只搜索普通文件;第二条只搜索目录;第三条搜索任何类型的文件。

-name 测试告诉 find 查找名字与指定模式匹配的 文件。如果你愿意,可以使用标准的通配符 *?[ ](参见第 24 章)。但如果这么做,你必须把 它们加上引号,这样它们才是交给 find 处理,而不是由 Shell 解释。这里给几个例子。三条命令都从工作目录(.) 开始搜索,并且只搜索普通文件(-type f):

find . -type f -name important -print
find . -type f -name '*.c' -print
find . -type f -name 'data[123]' -print

第一条命令搜索名为 important 的文件。第二条命令搜索 扩展名为 .c 的文件名,也就是 C 源文件。第三条命令只 搜索名为 data1data2data3 的文件。

— 提示 —

我见到的人们在使用 find 时最常犯的错误,就是用 -name 时忘了给通配符加引号。如果你不给通配符加引号, Shell 会自己把它们解释掉,从而导致错误。考虑下面两条 命令:

find . -type f -name '*.c' -print
find . -type f -name *.c -print

第一条命令可以正常工作。但第二条命令可能不行,因为 Shell 会 把表达式 *.c 替换成一串实际的文件名,从而造成语法错误。 你会看到类似下面这样的错误消息:

find: paths must precede expression

和 Unix 的多数功能一样,-name 测试是区分大小写的, 也就是说它区分大写和小写。要忽略大小写的差别,请改用 -iname。比如下面两个例子。两条命令都从 /usr 开始搜索,并且只查找目录:

find /usr -type d -name bin -print
find /usr -type d -iname bin -print

第一条命令只查找名为 bin 的目录。第二条命令使用 -iname,这意味着它会匹配名为 binBinBIN 等等的目录。

除了名字,你还可以根据各种其他特征来挑选文件。我们已经见过 -type 测试,它挑选某种特定类型的文件,通常是代表普通 文件的 f 或代表目录的 d。你还可以用 -perm 搜索具有特定模式的文件,用 -user-group 搜索属于特定用户 ID 或组 ID 的文件。考虑这三个例子,它们都 从你的主目录开始搜索:

find ~ -type d -perm 700 -print
find ~ -type f -user harley -print
find ~ -type f -group staff -print

第一条命令搜索文件模式为 700 的目录。(我们前面在本章 讨论过权限和模式。)第二条命令搜索属于用户 ID harley 的普通文件。最后一条命令搜索组 ID 为 staff 的普通文件。

你还可以根据文件大小来搜索,方法是用 -size 后面跟一个 具体的值。基本格式是一个数字后跟一个字母缩写。这些缩写是: c 表示字符(也就是字节),b 表示 512 字节的块, k 表示千字节,M 表示兆字节,G 表示吉字节。 这里给两个例子,它们都从你的主目录开始搜索普通文件:

find ~ -type f -size 1b -print
find ~ -type f -size 100c -print

第一条命令搜索大小恰好为 1 块的文件。由于这是一个文件的最小 尺寸,这条命令实际上找出了你所有的小文件。第二条命令搜索恰好 包含 100 字节内容的文件。

在我们继续往下走之前,我想花一点时间说明一个重要观点。当你 使用以块、千字节、兆字节或吉字节为单位的大小时,find 假定你谈论的是磁盘空间。这就是为什么 -size 1b 会找出 你所有的小文件。正如我们在第 24 章讨论过的,磁盘空间 的最小分配单位是 1 块。

当你使用以字节为单位的大小时,find 假定你谈论的是文件 的实际内容,而不是它占用了多少磁盘空间。这就是为什么 -size 100c 会查找恰好包含 100 字节数据的文件。事实上, 一个包含 100 字节数据的文件,既会被 -size 100c 找到, 也会被 -size 1b 找到。

每当你使用 -size 时,都可以在数字前面加一个 - (减号)或 +(加号),分别表示“小于”或“大于”。 (这是所有测试中所用数字的通用规则。)例如,下面这条命令找出 你所有大小小于 10 千字节的个人文件。第二条命令找出你所有大小 大于 1 兆字节的文件:

find ~ -type f -size -10k -print
find ~ -type f -size +1M -print

最后一组测试让你能够根据文件的访问时间或修改时间来搜索文件。 这些测试在图 25-5 中已经总结过了,所以我不再逐条细讲。 我只给你几个例子。假设你想找出你最近 30 分钟内被修改过的所有 文件。用 -mmin 测试配上值 -30,例如:

find ~ -mmin -30 -print

假设你想找出 180 天以上没有被使用过的文件。用 -atime 配上值 +180:

find ~ -atime +180 -print

最后,要找出你最近 10 分钟内被改动过的所有文件,用:

find ~ -cmin -10 -print

跳至页首

find 命令:用 ! 运算符对测试取反
(The find Command:
Negating a Test With the ! Operator)

必要时,你可以在一个测试前面加上 !(感叹号)运算符来 对它取反。要做到这一点,只要在测试前输入 !。使用 ! 时,你必须遵守两条规则。第一,你必须在 ! 的两侧各放一个空格,这样它才能被正确解析。第二,你必须给 ! 加上引号,这样它才是交给 find 处理,而不是 由 Shell 解释。(任何时候你想把元字符传给一个程序时,加引号 都是个好习惯。)

举个例子,考虑下面这条命令,它从你的主目录开始搜索,显示所有 扩展名为 .jpg 的普通文件的名字:

find ~ -type f -name '*.jpg' -print

假设我们反过来,想显示那些.jpg 扩展名的 文件的名字。我们要做的只是用 ! 运算符把这个测试反转 过来:

find ~ -type f \! -name '*.jpg' -print

你会注意到,我用反斜杠给 ! 运算符加了引号。如果你 愿意,也可以改用单引号。(关于引号的更多信息,参见 第 13 章。)

find ~ -type f '!' -name '*.jpg' -print

必要时,你可以对多个测试取反。只要确保每个测试都有它自己的 ! 运算符。例如,假设你想看看自己有没有既不是普通文件 也不是目录的文件。也就是说,你想看看自己有没有符号链接、 命名管道、特殊文件等等。要用的命令是:

find ~ \! -type f \! -type d -print

跳至页首

find 命令:处理文件权限错误消息
(The find Command:
Dealing With File Permission Error Messages)

find 程序是在庞大的目录树中搜索的绝佳工具。特别是, 从 /(根目录)开始,你可以搜索整个文件系统。不过, 当你在自己主目录区域之外搜索时,你会发现有些目录和文件是 禁止访问的,因为你的用户 ID 没有访问它们的权限。每当这种 情况发生,find 就会显示一条错误消息。

例如,下面这条命令在整个文件系统中搜索名为 bin 的 目录:

find / -type d -name bin -print

当你运行这条命令时,你很可能会看到许多类似下面的错误消息:

find: /etc/cron.d: Permission denied

在大多数情况下,没有理由去看这些消息,因为它们其实帮不上你 什么忙。事实上,它们唯一的作用就是把你的输出搞得杂乱不堪。 那么你怎么才能把它们去掉呢?

由于错误消息是写到标准错误上的(参见第 15 章),你可以 把标准错误重定向到 /dev/null(那个垃圾筒)从而去掉这些 消息。用 Bourne Shell 家族(Bash、Korn shell),这很容易:

find / -type d -name bin -print 2> /dev/null

用 C-Shell 家族(C-Shell、Tcsh),就稍微复杂一些,但也能做到:

(find / -type d -name bin -print > /dev/tty) >& /dev/null

细节在第 15 章中解释。

跳至页首

find 命令:动作
(The find Command: Actions)

正如我们讨论过的,我们用 find 程序搜索一棵或多棵目录树, 找出符合指定条件的文件,然后对这些文件执行某些动作。命令的一般 格式是:

find path... test... action...

到目前为止,我们讨论了如何指定路径和测试。为了让讨论收尾,我们现在 来谈谈动作。动作(ACTION)告诉 find 拿它找到的文件怎么办。 作为参考,我把最重要的动作总结在图 25-6 中。(完整列表请见你 系统上的 find 手册页。)

Figure 25-6
find 程序:动作

find 程序搜索目录树,依据你指定的测试寻找符合条件的 文件。对每一个找到的文件,find 执行你指定的动作。 详情见正文。

-print把路径名写到标准输出
-fprint file-print 相同;把输出写到 file
-ls显示长的目录列表
-fls file-ls 相同;把输出写到 file
-delete删除文件
-exec command {} \;执行 command,{} 表示匹配到的文件名
-ok command {} \;-exec 相同,但执行 command 前先确认
 

你可以看到,动作和测试一样,都是以 -(短横线)字符开头。 最常用的动作是 -print,它告诉 find 显示它挑选出的 所有文件的路径名。更准确地说,-print 告诉 find 把路径名列表写到标准输出上。(为什么叫 -print?正如我们在 第 7 章讨论过的,出于历史原因,Unix 有一个用 “print” 一词表示“显示”的惯例。)

这里有一个简单明了的例子,它从工作目录开始搜索名为 important 的文件:

find . -name important -print

在大多数 find 版本上,如果你不指定动作,默认会假定是 -print。因此,下面两条命令是等价的:

find . -name important -print
find . -name important

类似地,在 GNU 版本的 find 上,如果你不指定路径,默认 会假定是工作目录。因此,如果你是 Linux 用户,下面三条命令是 等价的:

find . -name important -print
find . -name important
find -name important

继续往下,让我给你看一个更有用的例子。你想在整个文件系统中 搜索 MP3 格式的音乐文件。为此,你用 find 从根目录开始, 搜索所有扩展名为 .mp3 的普通文件。由于你要在整个文件系统 里搜索,你知道 find 会产生一些无关的文件权限错误消息 (见上一节)。因为这个原因,你把标准错误重定向到垃圾筒。完整的 命令是:

find / -type f -name '*.mp3' -print 2> /dev/null

如果 MP3 文件的列表很长,其中大部分会从你的屏幕上滚过去。 如果这样,你有两个选择。第一,你可以把输出用管道交给 less,一屏一屏地显示:

find / -type f -name '*.mp3' -print 2> /dev/null | less

或者,你可以把输出保存到一个文件里,这样你以后可以从容地 慢慢查看。要做到这一点,用 -fprint 动作代替 -print。语法很简单:只要输入 -fprint,后面跟上 文件名。例如,下面这条命令找出系统上所有 MP3 文件的名字, 并把它们存储在一个叫 musiclist 的文件里:

find / -type f -name '*.mp3' -fprint musiclist 2> /dev/null

-print 动作显示路径名。如果你想了解每个文件的更多信息, 可以改用 -ls 动作。这个动作显示的信息类似于带 -dils 选项的 ls 命令。格式是内部完成的。 (find 并没有真的运行 ls 程序)你会看到:

• inode 号
• 以块计的大小
• 文件权限
• 硬链接的数目
• 所有者
• 所属组
• 以字节计的大小
• 最后一次修改的时间
• 路径名

举个例子,下面这条命令从你的主目录开始搜索最近 10 分钟内被 修改过的所有文件和目录。然后用 -ls 显示这些文件的信息:

find ~ -mmin -10 -ls

-fls 动作与 -ls 类似,只是它和 -fprint 一样,把输出写到一个文件里。例如,下面这条命令找出你最近 10 分钟内被修改过的所有文件,把它们的路径名写进一个名为 recent 的文件:

find ~ -mmin -10 -fls recent

下一个动作 -delete 会非常有用。不过,它也很可能反咬你 一口,所以用的时候要小心。-delete 动作会移除搜索中找到 的每一个文件的链接。如果那个链接是唯一的一个,文件就会被删除 (见本章前面关于链接的讨论)。换句话说,使用 -delete 类似于使用 rm 命令。

这里是一个例子。从你的主目录开始搜索,你想移除所有扩展名为 .backup 的文件:

find ~ -name '*.backup' -delete

这里是一个更复杂(也更有用)的例子。下面这条命令移除所有至少 180 天没有被访问过的、扩展名为 .backup 的文件:

find ~ -name '*.backup' -atime +180 -delete

使用 -ls-delete 动作,分别类似于把一次搜索 的输出送给 lsrm 程序。不过,find 提供 的通用性强得多:你可以用 -exec 动作把搜索输出送给你想要 的任何程序。语法是:

-exec command {} \;

其中命令是你想要的任何命令,包括选项和参数。

它是这样工作的。你输入 -exec,后面跟一条命令。你可以指定 任何你想要的命令,以及选项和参数,就像你在命令行上直接键入一样。 在这条命令里,你用字符 {} 来代表 find 找到的一条 路径名。要表示这条命令的结束,你必须用一个 ;(分号)把它 收尾。从上面语法中你可以看到,这个分号必须加引号。这是为了确保它 交给 find 处理,而不是由 Shell 解释。(引号在第 13 章 中解释。)

-exec 动作比其他任何特性都更像是让 find 如此强大 的原因,所以务必学会用好它。为了向你展示它是怎么工作的,我们先从 一个微不足道的例子开始。下面这条命令从你的主目录开始搜索,找出你 所有的目录。搜索的结果一次一个地送给 echo 程序显示出来。

find ~ -type d -exec echo {} \;

在某些 Shell 下,如果你不给花括号加引号就会有问题:

find ~ -type d -exec echo '{}' \;

顺便快速提一句,让我提醒你:如果你愿意,可以用单引号而不是反斜杠 来给分号加引号:

find ~ -type d -exec echo {} ';'
find ~ -type d -exec echo '{}' ';'

搜索过程中找到的每一个项目,-exec 动作都会被执行一次。 在这个例子中,比如有 26 个目录,find 就会执行 26 次 echo 命令。每一次执行 echo 时,{} 字符都会 被替换成另一个目录的路径名。

我把前面那条命令称为微不足道的例子,是因为你其实不需要把路径名 送给 echo 才能显示它们。你可以改用 -print:

find ~ -type d -print

不过,我们的 -exec 动作可以强大得多。比如说,出于安全 考虑,你决定除了你自己之外不应该允许任何人访问你的目录。要落实 这条策略,你需要用 chmod 把你所有目录的文件权限设置为 700。(见本章前面关于文件权限的讨论。)你可以为每个目录 敲一条 chmod,那会花很长时间。相反,用 find 搜索 你的目录,让 -exec 替你干活:

find ~ -type d -exec chmod 700 {} \;

再来一个例子。你用的 find 版本不支持 -ls-delete 动作。你要怎么替代它们?只要用 -exec 去运行 lsrm,例如:

find ~ -name '*.backup' -exec ls -dils {} \;
find ~ -name '*.backup' -exec rm {} \;

为了有更多的控制,-exec 有一个变体,让你自己决定该执行 哪条命令。用 -ok 代替 -exec,每条命令被执行前都会 要求你确认:

find ~ -type d -ok chmod 700 {} \;

跳至页首

处理找到的文件: xargs
(Processing Files That
Have Been Found: xargs)

当你用 find 搜索符合某个特定条件的文件时,有两种方法 处理你的发现。第一,你可以用上一节讲的 -exec 动作。 这让你可以用任何你想要的程序去处理每一个文件。不过,你必须 明白,-exec 会为每个文件生成一条单独的命令。例如, 如果你的搜索找到了 57 个文件,-exec 就会生成 57 条 单独的命令。

对于在少量文件上操作的简单命令,这可能没问题。然而,当一次 搜索产生大量文件时,有一个更好的选择。你可以不用 -exec, 而是把 find 的输出用管道交给一个专为这种情况高效工作 而设计的特殊程序。这个程序叫 xargs(“X-args”),它用 通过标准输入传给它的参数来运行你指定的任何命令。语法是:

xargs [-prt] [-istring] [command [argument...]]

其中 command 是你想运行的命令;string 是一个占位符; argument 是从标准输入读入的一个参数。

我们先看一个简单的例子。你想创建一张你所有普通文件的列表, 显示每个文件用了多少磁盘空间。你告诉 find 从你的主目录 开始搜索普通文件。输出用管道交给 xargs,它运行 ls -s 命令(第 24 章)来显示每个文件的大小。 整个命令看起来像这样:

find ~ -type f | xargs ls -s

这里有一个更复杂的例子,它应该会在你的日常生活中证明自己的 价值。你是一位漂亮、聪明、品味极佳的女士,你想给我打电话, 告诉我你有多喜欢这本书。你记得,一年多以前,一位共同的朋友给 你发过一封电子邮件,里面有我的名字和电话号码,你把它保存到了 一个文件里。从那以后你再也没有看过那个文件,你甚至想不起它的 名字,也不记得它在哪个目录里。你要怎么找到这个电话号码?

解决办法是用 find 整理出你所有最后一次修改在 365 天 以前的普通文件的列表。把输出用管道交给 xargs,并用 grep(第 19 章)在所有文件中搜索包含字符串 “Harley Hahn” 的行。要用的命令是:

find ~ -type f -mtime +365 | xargs grep "Harley Hahn"

在这个例子里,电话号码是在你主目录下一个名为 important 的文件里找到的。输出是:

/home/linda/important: Harley Hahn (202) 456-1111

当你需要处理 find 的输出时,echo 命令可能有用得 让人吃惊。正如你在第 12 章里记得的,echo 会对它的 参数求值,并把结果写到标准输出上。如果你给它一串路径名, echo 会输出一行长文本,包含所有的名字。这一行随后可以 用管道交给另一个程序做进一步处理。

举个例子,假设你想数一数你一共有多少个普通文件和多少个目录。 你用两条独立的命令,一条数文件,另一条数目录:

find ~ -type f | xargs echo | wc -w
find ~ -type d | xargs echo | wc -w

两条命令都用 find 从你的主目录开始搜索。第一条命令用 -type f 只搜索普通文件;第二条命令用 -type d 只搜索目录。两条命令都把 find 的输出用管道交给 xargs,再由它喂给 echoecho 命令的输出 接着用管道交给 wc -w(第 18 章)来数词的个数, 也就是路径名的个数。

有时候,你会希望在同一条命令里多次使用传给 xargs 的参数。 要做到这一点,你使用 -i(insert,插入)选项。它让你可以 用 {} 作为一个占位符,命令运行之前这个占位符会被替换成 那些参数。让我先从一个简单的例子开始,给你看看它是怎么工作的。

考虑下面两条命令,它们都从你的工作目录开始搜索普通文件:

find . -type f | xargs echo
find . -type f | xargs -i echo {} {}

在第一条命令中,echo 把它参数的单份副本写到标准输出上。 在第二条命令中,echo 把它参数的两份副本写到标准输出上。 下面是一个更有用的例子,它把你工作目录里的所有文件移到另一个 目录,同时在移动的过程中给文件改名。

find . -type f | xargs -i mv {} ~/backups/{}.old

这条命令用 find 在你的工作目录里搜索普通文件。文件列表 用管道交给 xargs,它运行一条 mv 命令(本章前面 讲过)。mv 命令把每个文件移到 backups 子目录里, 这个子目录位于你的主目录之下。作为移动操作的一部分,扩展名 .old 会被添加到每个文件名后面。

假设你的工作目录里有三个普通文件:abc。上面那条命令运行之后,工作目录就空了,而 backups 目录里会有 a.oldb.oldc.old

如果你想用 -i,但由于某种原因不想用 {},你可以 指定自己的占位符。只要把它直接写在 -i 后面即可,例如:

find . -type f | xargs -iXX mv XX ~/backups/XX.old

写这样的命令时,你有可能制造出意想不到的问题,因为你看不见 正在发生什么。如果你预感到会有问题,就使用 -p(prompt, 提示)选项。它告诉 xargs 在生成每一条命令时都显示给你看, 并在运行之前征求你的许可。如果你输入一个以 “y” 或 “Y” 开头的 回答,xargs 就运行这条命令。如果你输入任何其他回答 — 比如直接按 <Return> 键 — xargs 就跳过这条命令。

这里有一个你可以自己试一试的例子。用 touch(本章前面讲过) 在你的工作目录里创建几个新文件:

touch a b c d e

现在输入下面这条命令,给这些文件名中的每一个末尾加上扩展名 .junk。用 -p,这样 xargs 就会为每个文件 征求你的同意:

find . -name '[abcde]' | xargs -i -p mv {} {}.junk

如果你想看看生成了哪些命令,但又不希望被征求许可,就用 -t。它让 xargs 在运行每条命令时把它显示出来。 你可以把 -t 理解成“告诉我你在干什么”:

find . -name '[abcde]' | xargs -i -t mv {} {}.junk

重要:千万别把 -i 和其他选项连在一起写。例如,下面这条 命令不能正常工作,因为 xargs 会认为 -i 后面的 p 是一个占位符,而不是一个选项:

find . -name '[abcde]' | xargs -ip mv {} {}.junk

我想让你知道最后一个选项是 -r。默认情况下,xargs 总会至少运行一次指定的命令。-r 选项告诉 xargs, 如果没有输入参数,就不要运行这条命令。例如,下面这条命令搜索 你的工作目录,并把任何空文件的长列表显示出来:

find . -empty | xargs ls -l

然而,假设并没有空文件。这时 ls -l 命令会在没有 参数的情况下运行。这会产出整个目录的长列表,而这并不是你想要 的结果。解决办法是使用 -r 选项:

find . -empty | xargs -r ls -l

现在,只有在有参数时 xargs 才会运行 ls 命令。

你可以看到,把 findxargs 连起来使用,是构建 强大工具的简单方法。不过,我不想让你以为 xargs 只能和 find 搭配使用。事实上,xargs 可以和任何能向它 提供字符串作为参数的程序搭配使用。这里给几个例子。

你正在写一个 Shell 脚本,你想用 whoami(第 8 章) 显示当前的用户 ID,用 date(也是第 8 章)显示时间 和日期。为了好看,你想让所有输出都在同一行上:

(whoami; date) | xargs

你有一个文件 filenames,里面记录着一些文件的名字。你想 用 cat(第 16 章)把这些文件里的所有数据合并起来, 并把输出保存到一个名为 master 的文件里:

xargs cat < filenames > master

最后,你想把当前目录里所有的 C 源文件移到一个名为 archive 的子目录里。首先,如果这个子目录还不存在,就 创建它:

mkdir archive

现在用 lsxargs 列出并移动所有扩展名为 .c 的文件。我加上了 -t 选项,这样 xargs 就会在每条命令执行时把它显示给你看:

ls *.c | xargs -t -i mv {} archive

跳至页首



练习
(Exercises)

复习问题 #1:

你要用哪条命令来创建一个全新的空文件?为什么你很少需要 使用这条命令?

说出三种会自动为你创建文件的常见情形。

复习问题 #2:

逐一考察下面每个字符串,判断它是不是一个好的文件名。 如果不好,说明原因。

data-backup-02
data_backup_02
DataBackup02
DATABACKUP02
Data Backup 02
Data;Backup,02
databackup20
data/backup/20

复习问题 #3:

什么是文件权限?

文件权限的两个主要用途是什么?

哪个程序用来设置或更改文件权限?

文件权限的三种类型是什么?

对每一种类型,说明它应用于 (a) 普通文件 (b) 目录时, 各意味着什么。

复习问题 #4:

什么是链接?

什么是符号链接?

什么是硬链接?什么是软链接?

你如何创建一个链接?

你如何创建一个符号链接?

复习问题 #5:

哪三个程序用来查找一个文件或一组文件?

每种情形下你会用哪一个?

应用你的知识 #1:

在你的主目录里,创建一个名为 temp 的目录并切换进去。 在这个目录里,创建两个名为 daysmonths 的 子目录。在每个目录里,创建两个名为 file1file2 的文件。

提示:使用子 Shell(见第 15 章)来更改工作目录并创建文件。

所有文件都创建好之后,用 tree 程序(第 24 章)显示一幅 目录树示意图,把目录和文件都显示出来。如果你的系统上没有 tree,就改用 ls -R

应用你的知识 #2:

接着上一道练习:

days 目录里,把 file1 改名为 monday, 把 file2 改名为 tuesday。然后把 monday 复制为 friday

months 目录里,把 file1 改名为 december,把 file2 改名为 july

回到 temp,用 tree 再显示一幅目录树示意图。 如果你的系统上没有 tree,就改用 ls -R

december 创建一个到 april 的链接。从 december 创建一个到 may 的符号链接。显示 months 目录的长列表。你注意到了什么?

为了收拾残局,用一条命令删除 temp 目录、它的所有子目录 以及这些目录里的所有文件。最后再用一条命令确认 temp 已经不存在了。

应用你的知识 #3:

用一个文本编辑器创建一个名为 green 的文件。在文件里 输入这一行:

I am a smart person.

保存你的工作并退出编辑器。

为文件 green 创建一个链接,把它叫做 blue。 显示 greenblue 的长列表,记下它们的修改时间。

等 3 分钟,然后用文本编辑器编辑文件 green。把那一行 文字改成:

I am a very smart person.

保存你的工作并退出编辑器。

显示 greenblue 的长列表。为什么它们的修改时间 相同(而且是更新过的),尽管你只编辑了一个文件?如果你改的是 blue 而不是 green,会发生什么?

应用你的知识 #4:

你正在搭建一个网站,其中所有 HTML 文件都放在你主目录下一个 名为 httpdocs 的目录的子目录里。用一条管道找出所有 扩展名为 .html 的文件,并把它们的权限改成下面这样:

所有者: 读 + 写
组: 
其他: 

进一步思考 #1:

大多数基于图形用户界面的文件管理器都会维护一个“垃圾筒”文件夹 来存放被删除的文件,以便必要时可以恢复。在这类系统里,一个文件 只有从“垃圾筒”中被删除之后才算真正消失。为什么 Unix 基于文本的 工具不提供这样的服务?

进一步思考 #2:

想象乘坐时光机回到 1976 年,那一年我开始使用 Unix。你找到了我, 请我带你参观一下我正在使用的 Unix 系统。

让你惊讶的是,你看到了文件、目录、子目录、工作目录、主目录、 特殊文件、链接、inode、权限,等等。你还看到了所有标准的命令: lsmkdirpwdcdchmodcprmmvlnfind。 事实上,你注意到,你在本章学到的几乎每一个思想和工具都是三十 多年以前开发的。

Unix 文件系统的基本设计里,究竟有什么东西使它存活了这么久, 而且到今天仍然如此有用?

这在计算机世界里罕见吗?

进一步思考 #3:

find 程序是一个强大的工具,但也非常复杂。想象这个程序 有一个基于图形用户界面的版本,它能让你从一个大型下拉列表中选择 选项、把文件模式填进表格里、从菜单中挑选各种测试,等等。

这样的程序会比标准的基于文本的版本更好用吗?

find 的图形用户界面版本能取代基于文本的版本吗?

跳至页首