第 25 章...
文件操作 (Working With Files)
- 创建文件: touch
- 为文件命名
- 复制文件: cp
- 把文件复制到别的目录: cp
- 把一个目录复制到另一个目录: cp -r
- 移动文件: mv
- 重命名文件或目录: mv
- 删除文件: rm
- 如何避免删错文件: rm -if
- 删除整棵目录树: rm -r
- 已删除的文件有可能恢复吗?
- 文件权限
- Setuid 权限
- Unix 如何维护文件权限: id, groups
- 显示文件权限: ls -l
- 文件模式
- 修改文件权限: chmod
- Unix 如何为新文件指派权限: umask
- 抹去文件的内容: shred
- 链接的概念: stat, ls -i
- 同一个文件的多个链接
- 创建新链接: ln
- 基本文件命令是如何工作的
- 符号链接: ln -s
- 在目录上使用符号链接
- 查找与某个 Unix 命令相关的文件: whereis
- 通过搜索数据库查找文件: locate
- 通过搜索目录树查找文件: find
- find 命令:路径
- find 命令:测试
- find 命令:用 ! 运算符否定一个测试
- find 命令:处理文件权限错误信息
- find 命令:动作
- 处理查找到的文件: xargs
- 练习
这是讨论 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 个字符。不过,有些文件系统更灵活 一些。例如,如果你是个文件系统迷,很容易就能修改 ext2、ext3 或 ext4 文件系统,让一个 文件名最多可以使用 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 区分大写和小写。因此,名字 info、Info 和 INFO 是三个完全不 同的名字。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
要把 data1、data2 和 data3 三个 文件复制到 backups 目录,用:
cp data1 data2 data3 backups
再来一个例子,稍微复杂一点。你的工作目录是 /home/harley/work/bin。你想把文件 adventure 从目录 /home/harley/bin 复制到 工作目录。要指代源目录,我们用 ../../bin;要指代 工作目录,我们单用一个 .。命令是:
cp ../../bin/adventure .
— 提示 —
你常常可以用通配符一次指定多个文件名(见第 24 章)。 例如,要把 data1、data2 和 data3 三个文件复制到 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 复制 整棵子树。
举个例子。假设在你的工作目录里有两个子目录: essays 和 backups。在 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 会以为你想重命名这个文件(见下文)。下一个 例子把 data1、data2 和 data3 三个 文件移动到 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 章)。 这里有两个例子。第一条命令删除工作目录中的 data1、 data2 和 data3 三个文件。第二条命令删除 工作目录中的所有文件,点文件除外。(显然,这是一条威力 极大的命令,所以不要拿它做实验。)
rm data[123]
rm *
第一条命令删除工作目录中的 data1、data2 和 data3 三个文件。第二条命令威力很大:它会删除 你工作目录中的所有文件,点文件除外。(使用这条命令之前 请三思。)
文件一经删除,就永远消失了。被删除的文件无法找回, 所以要小心。
当你把 rm 与通配符一起使用时,最好先用一条 ls 命令检验一下,看看匹配到了哪些文件。举一个 例子。你想删除 data.backup、data.old 和 data.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 可以访问你的文件,以一种简单直接的方式 为你的数据提供了安全性。
• 防备你自己的失误
如果你想保护一个文件不被误删,你可以确保该文件没有写 权限。许多会替换或删除数据的命令,在修改没有写权限的 文件之前都会先请求确认。(本章前面讨论的 rm 和 mv 命令就是这样。)
对于目录,权限的含义与普通文件略有不同。读权限让一个 用户 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。这就是说,对于你的每个文件和目录,你可以为 你自己、为你工作组里的人、以及为所有其他人分别指派读、 写和执行权限。
* 脚注
唯一的例外是超级用户,他几乎无所不能,可以修改任何文件 的权限。必要时,超级用户还可以分别用 chown 和 chgrp 命令修改文件的所有者和用户组。
举个例子。你和一群人合作开发一个程序。存放程序的文件 位于你的某个个人目录中。你可以这样设置文件权限:你和你 的组都拥有读、写和执行权限,而系统上其他所有用户只有读 和执行权限。这意味着,虽然任何人都能运行这个程序,但 只有你或你组里的成员能修改它。
再举个例子。你有一份不想让其他任何人看的文档。只要 给你自己读和写权限,而不给你的组、也不给所有其他人任何 权限即可。(*)
* 脚注
不过要记住,你什么也瞒不过超级用户。
很重要的一点是要理解:给“所有人”的权限并不包括你自己 或你组里的成员。想象一个奇怪的情形:你把某个文件的读 权限给了所有人,却不给你所在的组任何权限。那么你组里的 成员将无法读取这个文件,而所有其他人都能读。此外,如果 你的网络上有用户能访问你的文件系统,他们也属于“所有人” 这一类,即使他们在你这台特定的系统上并没有账号。
那么谁在你的组里?当你的系统管理员创建你的账号时,他 同时也把你指派到一个用户组(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 的权限。我们把例子中所有文件都 分析一遍:
| 所有者 | 组 | 其他 | 文件 |
|---|---|---|---|
| rwx | rwx | rwx | program.allusers |
| rwx | rwx | --- | 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 = 无权限
对每一组权限,只要把相应的数字相加即可。例如,要表示 读加写权限,就把 4 和 2 加起来。 图 25-3 列出了每一种可能的组合及其数值。
Figure 25-3: 文件权限组合的数值
文件权限有三种:读权限、写权限和执行权限。这些权限的值 由 3 个不同的数字表示,按下表所示相加。详见正文。
| 读 | 写 | 执行 | 组成 | 合计 |
|---|---|---|---|---|
| — | — | — | 0 + 0 + 0 | 0 |
| — | — | 有 | 0 + 0 + 1 | 1 |
| — | 有 | — | 0 + 2 + 0 | 2 |
| — | 有 | 有 | 0 + 2 + 1 | 3 |
| 有 | — | — | 4 + 0 + 0 | 4 |
| 有 | — | 有 | 4 + 0 + 1 | 5 |
| 有 | 有 | — | 4 + 2 + 0 | 6 |
| 有 | 有 | 有 | 4 + 2 + 1 | 7 |
我们来做一道例题。如果一个文件满足下面的条件,它的模式 是什么:
• 所有者拥有读、写和执行权限?
• 用户组拥有读和写权限?
• 所有其他用户 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 选项。例如,下面这条命令显示 名为 xyzzy 和 plugh 的两个文件的 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 相同。由于 spacewar 和 funky 拥有相同的 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。在这个目录里, 你有两个文件:data1 和 data2。你的朋友使用 主目录 /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 不会删除该文件,直到 最后一个链接被移除为止。不过在多数情况下,一个文件只有 一个链接,这就是为什么大多数时候 rm 和 rmdir 表现得像删除命令。
这里有一个简单的例子,用来阐明我们刚刚讨论的那些思想。 你有一个名为 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 来显示工作目录的名字。这里有一个 问题:当某个目录名其实是指向另一个目录的符号链接时, cd 和 pwd 应当如何表现?有两种选择。第一, 命令可以把符号链接看作一个独立的实体,是真实目录的另一个 名字,就像硬链接之于普通文件那样。另一种可能是,这个链接 不过是一块通往真实目录的垫脚石。
在某些 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
你会遇到许多次想要找到某个特定文件或某组文件的情形。这种 时候,有三个不同的程序可以使用:whereis、 locate 和 find。在接下来的几节里,我们依次 讲解每个程序。
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 程序提供通用的“查找文件”服务: locate 和 find。我希望两个都学会。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” 的文件,再用 grep 在 locate 的输出中搜索包含 “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)
到目前为止,我们已经讨论了两种查找文件的工具:whereis 和 locate。这两个程序都快、都好用了,而且大多数情况下, 它们应该是你查找文件时的首选。
不过,它们都有局限。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 [-+]n | n 分钟前被访问过 |
| -anewer file | 访问时间比 file 更近 |
| -atime [-+]n | n 天前被访问过 |
| -cmin [-+]n | n 分钟前状态被改变过 |
| -cnewer file | 状态改变时间比 file 更近 |
| -ctime [-+]n | n 天前状态被改变过 |
| -mmin [-+]n | n 分钟前被修改过 |
| -mtime [-+]n | n 天前被修改过 |
| -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 源文件。第三条命令只 搜索名为 data1、data2 或 data3 的文件。
— 提示 —
我见到的人们在使用 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,这意味着它会匹配名为 bin、Bin、 BIN 等等的目录。
除了名字,你还可以根据各种其他特征来挑选文件。我们已经见过 -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 执行你指定的动作。 详情见正文。
| 把路径名写到标准输出 | |
| -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 动作,分别类似于把一次搜索 的输出送给 ls 和 rm 程序。不过,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 去运行 ls 或 rm,例如:
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,再由它喂给 echo。echo 命令的输出 接着用管道交给 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 会被添加到每个文件名后面。
假设你的工作目录里有三个普通文件:a、b 和 c。上面那条命令运行之后,工作目录就空了,而 backups 目录里会有 a.old、b.old 和 c.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 命令。
你可以看到,把 find 和 xargs 连起来使用,是构建 强大工具的简单方法。不过,我不想让你以为 xargs 只能和 find 搭配使用。事实上,xargs 可以和任何能向它 提供字符串作为参数的程序搭配使用。这里给几个例子。
你正在写一个 Shell 脚本,你想用 whoami(第 8 章) 显示当前的用户 ID,用 date(也是第 8 章)显示时间 和日期。为了好看,你想让所有输出都在同一行上:
(whoami; date) | xargs
你有一个文件 filenames,里面记录着一些文件的名字。你想 用 cat(第 16 章)把这些文件里的所有数据合并起来, 并把输出保存到一个名为 master 的文件里:
xargs cat < filenames > master
最后,你想把当前目录里所有的 C 源文件移到一个名为 archive 的子目录里。首先,如果这个子目录还不存在,就 创建它:
mkdir archive
现在用 ls 和 xargs 列出并移动所有扩展名为 .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 的目录并切换进去。 在这个目录里,创建两个名为 days 和 months 的 子目录。在每个目录里,创建两个名为 file1 和 file2 的文件。
提示:使用子 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。 显示 green 和 blue 的长列表,记下它们的修改时间。
等 3 分钟,然后用文本编辑器编辑文件 green。把那一行 文字改成:
I am a very smart person.
保存你的工作并退出编辑器。
显示 green 和 blue 的长列表。为什么它们的修改时间 相同(而且是更新过的),尽管你只编辑了一个文件?如果你改的是 blue 而不是 green,会发生什么?
应用你的知识 #4:
你正在搭建一个网站,其中所有 HTML 文件都放在你主目录下一个 名为 httpdocs 的目录的子目录里。用一条管道找出所有 扩展名为 .html 的文件,并把它们的权限改成下面这样:
| 所有者: | 读 + 写 | |
| 组: | 无 | |
| 其他: | 读 |
进一步思考 #1:
大多数基于图形用户界面的文件管理器都会维护一个“垃圾筒”文件夹 来存放被删除的文件,以便必要时可以恢复。在这类系统里,一个文件 只有从“垃圾筒”中被删除之后才算真正消失。为什么 Unix 基于文本的 工具不提供这样的服务?
进一步思考 #2:
想象乘坐时光机回到 1976 年,那一年我开始使用 Unix。你找到了我, 请我带你参观一下我正在使用的 Unix 系统。
让你惊讶的是,你看到了文件、目录、子目录、工作目录、主目录、 特殊文件、链接、inode、权限,等等。你还看到了所有标准的命令: ls、mkdir、pwd、cd、chmod、 cp、rm、mv、ln 和 find。 事实上,你注意到,你在本章学到的几乎每一个思想和工具都是三十 多年以前开发的。
Unix 文件系统的基本设计里,究竟有什么东西使它存活了这么久, 而且到今天仍然如此有用?
这在计算机世界里罕见吗?
进一步思考 #3:
find 程序是一个强大的工具,但也非常复杂。想象这个程序 有一个基于图形用户界面的版本,它能让你从一个大型下拉列表中选择 选项、把文件模式填进表格里、从菜单中挑选各种测试,等等。
这样的程序会比标准的基于文本的版本更好用吗?
find 的图形用户界面版本能取代基于文本的版本吗?
© 本书全部内容,2026 年版权所有,Harley Hahn
完整的商标与版权信息