Linux 和 UNIX 系统中的所有文件都可以作为一个大型树型文件系统的一部分访问,这个树型文件系统的根为 /。通过挂载 分支可以将它们添加到树中,通过解除挂载 可以移除它们。挂载和解除挂载的内容将在挂载和解除挂载文件系统 一文介绍。(参见 学习 Linux,101:LPIC-1 路线图)。
在本文中,我们将使用 “学习 Linux,101:文本流和过滤器” 一文中创建的文件来练习命令。如果您完成了上篇文章的练习,那么您应该在您的主目录中创建了一个目录 lpi103-2。如果还没有的话,那么可以使用系统中的另一个目录来练习本文讨论的命令。
文件和目录名可以是绝对 的,这表示名称以 / 开头,也可以相对 于当前工作目录,这表示不是以 / 开头。文件或目录的绝对路径的组成为:在 0 个或多个目录名后附加一个 /,其中每个目录名的后面都有一个 /,然后是一个最终文件名。
至于相对于当前工作目录的文件或目录名,只需要将工作目录的绝对名、/ 和相对名连接在一起。例如,我们在早期文章中在我的主目录 /home/ian 中创建的目录 lpi103-2,它的完整(即绝对)路径为 /home/ian/lpi103-2。
您可以使用 pwd
命令显示当前工作目录的名称。此命令通常也可以用于 PWD 环境变量。清单 1 展示了 pwd
命令的使用,以及通过三种不同的方法使用 ls
命令列出此目录中的文件。
[ian@echidna lpi103-2]$ pwd /home/ian/lpi103-2 [ian@echidna lpi103-2]$ echo "$PWD" /home/ian/lpi103-2 [ian@echidna lpi103-2]$ ls sedtab text1 text2 text3 text4 text5 text6 xaa xab yaa yab [ian@echidna lpi103-2]$ ls "$PWD" sedtab text1 text2 text3 text4 text5 text6 xaa xab yaa yab [ian@echidna lpi103-2]$ ls /home/ian/lpi103-2 sedtab text1 text2 text3 text4 text5 text6 xaa xab yaa yab
可以看到,您可以将一个相对或绝对目录名作为 ls
目录的参数,它将列出该目录中的内容。
在一台存储设备中,文件或目录被包含到一个块(block)组合中。有关文件的信息被包含在一个索引节点(inode)中,其中记录如下信息:所有者、最后一次访问文件的时间、文件大小、是否为目录以及谁可以读取或写入数据。inode 编号也被称为文件序列号(file serial number),并且在一个特定文件系统中是唯一的。我们可以使用 -l
(或 --format=long
)选项来显示存储在 inode 中的某些信息。
默认情况下,ls
命令不会列出特殊文件,这些文件的文件名以点号 (.) 开头。除根目录外的所有目录都至少包含两个特殊条目:目录本身 (.) 和父目录 (..)。根目录没有父目录。
清单 2 使用 -l
和 -a
选项显示所有文件的长格式的列表,包括 . 和 .. 目录条目。
[ian@echidna lpi103-2]$ ls -al total 52 drwxrwxr-x. 2 ian ian 4096 2009-08-11 21:21 . drwx------. 35 ian ian 4096 2009-08-12 10:55 .. -rw-rw-r--. 1 ian ian 8 2009-08-11 21:17 sedtab -rw-rw-r--. 1 ian ian 24 2009-08-11 14:02 text1 -rw-rw-r--. 1 ian ian 25 2009-08-11 14:27 text2 -rw-rw-r--. 1 ian ian 63 2009-08-11 15:41 text3 -rw-rw-r--. 1 ian ian 26 2009-08-11 15:42 text4 -rw-rw-r--. 1 ian ian 24 2009-08-11 18:47 text5 -rw-rw-r--. 1 ian ian 98 2009-08-11 21:21 text6 -rw-rw-r--. 1 ian ian 15 2009-08-11 14:41 xaa -rw-rw-r--. 1 ian ian 9 2009-08-11 14:41 xab -rw-rw-r--. 1 ian ian 17 2009-08-11 14:41 yaa -rw-rw-r--. 1 ian ian 8 2009-08-11 14:41 yab
在清单 2 中,第一行显示所列文件使用的磁盘块的总数(52)。其余行列出了目录的条目。
第一个字段(本例中为 drwxrwxr-x 或 -rw-rw-r--)告诉我们,文件是一个目录 (d) 还是一个普通文件 (-)。对于特殊文件,还会看到符号链接 (l) 或其他值(例如 /dev 文件系统中的文件)。您将在创建和修改硬链接和符号链接 一文(参见 学习 Linux,101:LPIC-1 路线图)中了解到有关符号链接的更多内容。类型之后是针对所有者、所有者所在组的成员、每一个成员的三组特权。这三个值分别表示用户、组、组成员是否拥有读 (r)、写 (w) 或 (x) 执行权限。诸如 setuid 之类的用户将在管理文件权限和所有权(参见 学习 Linux,101:LPIC-1 路线图)一文中介绍。
下一个字段是一个数字,告诉我们文件的硬链接 的数量。我们已经介绍过,inode 包含有关文件的信息。文件的目录条目包含到文件的 inode 的硬链接(或指针),因此列出的每个条目都应该至少拥有一个硬链接。目录条目对 . 条目和每个子目录条目使用另外的硬链接。因此我们可以从清单 2 中可以看到,使用 .. 表示的主目录有大量子目录,因此包含 35 个硬链接。
接下来两个字段分别为文件的所有者和所有者的主组。某些系统,例如 Red Hat 或 Fedora 系统,在默认情况下为每个用户提供单独的组。在其他系统中,所有用户可能位于一个或多个组中。
下一个字段包含文件的长度,以字节为单位。
倒数第二个字段包含最后一次修改的时间戳。
最后一个字段包含文件或目录的名称。
ls
命令的 -i
选项将显示 inode 号。您将在本文后面以及 创建和修改硬链接和符号链接(参见 学习 Linux,101:LPIC-1 路线图)中再次见到有关 inode 的介绍。
您还可以为 ls
命令指定多个参数,其中的每个名称都可能是文件或目录的名称。对于目录名,ls
命令将列出目录的内容,而不是关于目录本身的信息。在我们的示例中,假设当在父目录中列出目录时,我们希望获得有关 lpi103-2 目录条目本身的信息。命令 ls -l ../lpi103-2
将提供类似前例的列表。清单 3 将展示如何添加 -d
选项以列出有关目录条目的信息,而不是目录的内容,以及如何列出多个文件或目录的条目。
[ian@echidna lpi103-2]$ ls -ld ../lpi103-2 sedtab xaa drwxrwxr-x. 2 ian ian 4096 2009-08-12 15:31 ../lpi103-2 -rw-rw-r--. 1 ian ian 8 2009-08-11 21:17 sedtab -rw-rw-r--. 1 ian ian 15 2009-08-11 14:41 xaa
注意,lpi103-2 的修改时间不同于前一个列表中的修改时间。同样,和前一个列表相同,它与该目录中的任何文件的时间戳都不同。这是否就是您所期望的?并不是这样。然而,在撰写本文时,我创建了一些额外的的例子并删除了它们,因此目录时间戳反映了这一更改。稍后在 处理多个文件和目录 中,我们将更详细地讨论文件时间。
默认情况下,ls
将按字母顺序列出文件。可以使用多种选项对输出进行排序。例如,ls -t
将按照修改时间排序(从最新到最旧),而 ls -lS
将生成一个按大小排序的长列表(从最大到最小)。添加 -r
将反向排序。例如,使用 ls -lrt
生成一个按从最旧到最新排序的长列表。参考手册页面,了解有关排列文件和目录的其他方式。
回页首
我们现在已经了解了一些创建文件的方法,但是假设我们希望复制文件、重命名文件、在文件系统层级结构中移动文件,甚至删除它们。我们使用三个简短的命令来实现这些目的。
cp
mv
cp
相同的规则;您可以重命名某个文件或将一组文件移动到一个新目录中。由于名称只是一个链接到某个 inode 的目录条目,因此 inode 号
只有在文件被移动到另一个文件系统才会发生更改就不足为怪了,在这种情况下,移动文件看上去就类似于在复制文件之后删除它。
rm
清单 4 演示了 cp
和 mv
的使用,它们对我们的文本文件执行了一些备份复制。我们使用 ls -i
展示其中一些文件的 inode。
我们首先为 text1 文件生成一个副本 text1.bkp。
然后决定使用 mkdir
命令创建一个备份子目录
我们为文本 1 生成第二个备份副本,这一次是在备份目录中,并显示出所有三个文件都具有不同的 inode。
随后将 text1.bkp 移动到备份目录中,然后将其重命名,使其与第二个备份更加一致。我们本来可以使用一个单个命令完成这些操作,但是为了演示的目的,我们在这里使用了两个命令。
我们再次检查 inode,然后确定 inode 为 934193 的 text1.bkp 不再存在于 lpi103-2 目录,但是该 inode 仍然为备份目录中的 text1.bkp.1 保留下来。
[ian@echidna lpi103-2]$ cp text1 text1.bkp [ian@echidna lpi103-2]$ mkdir backup [ian@echidna lpi103-2]$ cp text1 backup/text1.bkp.2 [ian@echidna lpi103-2]$ ls -i text1 text1.bkp backup 933892 text1 934193 text1.bkp backup: 934195 text1.bkp.2 [ian@echidna lpi103-2]$ mv text1.bkp backup [ian@echidna lpi103-2]$ mv backup/text1.bkp backup/text1.bkp.1 [ian@echidna lpi103-2]$ ls -i text1 text1.bkp backup ls: cannot access text1.bkp: No such file or directory 933892 text1 backup: 934193 text1.bkp.1 934195 text1.bkp.2
一般来说,cp
将在现有副本上复制文件,如果现有文件可写的话。另一方面,如果目标存在,mv
不会移动或重命名文件。有一些有用的选项与cp
和 mv
的这种行为有关。
-f
或 --force
cp
尝试阐释一个现有目标文件,即使它不是可写的
-i
或 --interactive
-b
或 --backup
和前面一样,您需要参考手册页来获得这些和其他复制和移动选项的详细内容。
清单 5 演示了备份复制和文件删除。
[ian@echidna lpi103-2]$ cp text2 backup [ian@echidna lpi103-2]$ cp --backup=t text2 backup [ian@echidna lpi103-2]$ ls backup text1.bkp.1 text1.bkp.2 text2 text2.~1~ [ian@echidna lpi103-2]$ rm backup/text2 backup/text2.~1~ [ian@echidna lpi103-2]$ ls backup text1.bkp.1 text1.bkp.2
注意,rm
命令还接受 -i
(交互式)和 -f
(强制选项)。当您使用 rm
删除文件后,文件系统将不再访问它。某些系统在默认情况下为根用户设置一个别名 alias rm='rm -i'
,以防止出现意外的文件删除。如果您担心会不小心删除文件的话,这对于普通用户来说也是一个好主意。
在结束这些内容的讨论之前,应当注意 cp
命令在默认情况下会为新的文件创建一个新的时间戳。所有者和组均被设置为执行复制的用户的所有者和组。-p
选项可能被用于保存选择的属性。注意,根用户可能为可以保留所有权的唯一用户。参考手册页获得详情。
回页首
我们已经了解了如何使用 mkdir
创建目录。现在我们将进一步查看 mkdir
并介绍 rmdir
,后者用于删除目录。
假设我们希望在 lpi103-2 目录中创建子目录 dir1 和 dir2。和前面介绍过的其他命令一样,mkdir
可以一次处理多个目录创建请求,如清单 6 所示。
[ian@echidna lpi103-2]$ mkdir dir1 dir2
注意,在成功完成后不会产生输出,但是您可以使用 echo $?
来确认退出代码确实为 0。
相反,如果您希望创建一个嵌入式的子目录,比如 d1/d2/d3,那么命令将会失败,因为 d1 和 d2 目录并不存在。幸运的是,mkdir
具有一个 -p
选项,它允许创建任何所需的父目录,如清单 7 所示。
[ian@echidna lpi103-2]$ mkdir d1/d2/d3 mkdir: cannot create directory `d1/d2/d3': No such file or directory [ian@echidna lpi103-2]$ echo $? 1 [ian@echidna lpi103-2]$ mkdir -p d1/d2/d3 [ian@echidna lpi103-2]$ echo $? 0
使用 rmdir
命令删除目录正好与创建过程相反。同样,可以用 -p
选项来删除父目录。只有在目录为空的情况下才可以使用 rmdir
删除目录,因为不存在可以强制删除的选项。我们将在讨论 递归操作 时查看另一种可以完成这一特殊任务的方法。了解了这种方法后,您将很少会在命令行中使用 rmdir
,但是了解该命令仍然是有用的。
为了解释目录删除,我们将 text1 文件复制到目录 d1/d2 中,这样它就不会成为空目录。我们随后使用 rmdir
来删除刚刚用 mkdir
创建的所有目录。可以看到,d1 和 d2 没有被删除,因为 d2 不为空。另一个目录则被删除,当我们从 d2 删除 text1 的副本时,我们只需要调用 rmdir -p
即可删除 d1 和 d2。
[ian@echidna lpi103-2]$ cp text1 d1/d2 [ian@echidna lpi103-2]$ rmdir -p d1/d2/d3 dir1 dir2 rmdir: failed to remove directory `d1/d2': Directory not empty [ian@echidna lpi103-2]$ ls . d1/d2 .: backup sedtab text2 text4 text6 xab yab d1 text1 text3 text5 xaa yaa d1/d2: text1 [ian@echidna lpi103-2]$ rm d1/d2/text1 [ian@echidna lpi103-2]$ rmdir -p d1/d2
回页首
到目前为止,我们使用的命令已经处理了一个单个文件,或者一些个别命名的文件。在本文的其余部分中,我们将查看处理多个文件的各种操作,递归式处理文件树的某一部分,保存或恢复多个文件或目录。
回页首
ls
命令有一个 -R
(注意为大写 “R”)选项,可以列出一个目录及其所有子目录。递归式操作只能应用于目录名;它不会在目录树中查找名为 ‘text1’ 之类的文件。您可以将 -R
与已经介绍过的其他选项结合使用。lpi103-2 目录的递归式列表包括 inode 号,如清单 9 所示。
[ian@echidna lpi103-2]$ ls -iR .: 934194 backup 933892 text1 933898 text3 933900 text5 933894 xaa 933896 yaa 933901 sedtab 933893 text2 933899 text4 933902 text6 933895 xab 933897 yab ./backup: 934193 text1.bkp.1 934195 text1.bkp.2
可以使用 -r
(或 -R
或 --recursive
)选项来使 cp
命令进入到源目录并以递归的方式复制目录。为了防止出现无穷递归,源目录本身可能不会被复制。清单 10 展示了如何将 lpi103-2 目录中的所有内容复制到 copy1 子目录。我们使用 ls -R
展示生成的目录树。
[ian@echidna lpi103-2]$ cp -pR . copy1 cp: cannot copy a directory, `.', into itself, `copy1' [ian@echidna lpi103-2]$ ls -R .: backup copy1 sedtab text1 text2 text3 text4 text5 text6 xaa xab yaa yab ./backup: text1.bkp.1 text1.bkp.2 ./copy1: text2 text3 text5 xaa yaa yab
我们前面提到,rmdir
只能删除空目录。我们可以使用 -r
(或 -R
或 --recursive
)选项来使 rm
命令同时删除文件和目录,如清单 11 所示,我们将删除刚刚创建的 copy1 目录和它包含的内容,包括备份子目录及其内容。
[ian@echidna lpi103-2]$ rm -r copy1 [ian@echidna lpi103-2]$ ls -R .: backup sedtab text1 text2 text3 text4 text5 text6 xaa xab yaa yab ./backup: text1.bkp.1 text1.bkp.2
如果您具有不可写的文件,那么可能需要添加 -f
选项来强制删除。这通常由根用户在清理系统时执行,但是会发出警告,因为您有可能会不小心删除重要的数据。
回页首
通常,您需要对多个文件系统对象执行单一操作,而不需要像前面的递归操作一样对整个树进行操作。例如,您可能想要找出在 lpi103-2 中创建的所有文本文件的修改时间,而不需要列出分散的文件。尽管这很容易在小目录中实现,但是对于大型文件系统则非常困难。
要解决这个问题,可以使用 bash shell 中内置的通配符支持。这种支持也称为 “globbing”(因为它最初被实现为一个名为 /etc/glob 的程序),让您能够使用通配符模式指定多个文件。
包含任何 '?'、'*' 或 '[' 字符的字符串就是一个通配符模式。Globbing 是指 shell(或另一个程序)将这些模式扩展为一组匹配该模式的参数的过程。这种匹配按照如下方式完成:
'*' 和 '?' 字符与它们自身匹配。如果在文件名中使用这些字符,那么需要注意适当的引用或转义。
由于字符串必须是非空的并以 ']' 终止,如果您需要匹配字符串的话,您必须将 ']' 放到字符串的首位。
两个字符之间的 '-' 字符表示一个范围,包括这两个字符和排序序列中介于这两个字符之间的所有字符。例如,[0-9a-fA-F] 表示任何大写或小写十六进制数位。您可以通过将 '-' 放到一个范围的首位或末位来匹配它。
如果范围的首个字符为 '!' 字符,那么它将对范围求余,即它将匹配剩余字符以外的所有字符。例如,[!0-9] 表示除 0 到 9 之间数字的任何字符。将 '!' 放在首位以外的任意位置都可以匹配它本身。注意 '!' 也可以用于 shell history 函数,因此需要小心地对它进行适当的转义。
注意:通配符模式和常规表达式模式具有一些共同点,但是它们是不同的。需要仔细留意。
Globbing 被单独应用到路径名的每个组成中。您无法匹配 '/',也不能把它包含在一个范围中。您可以在指定多个文件或目录名时使用它,例如在 ls
、cp
、mv
或 rm
命令中。在清单 12 中,我们首先创建一对名字奇怪的文件,然后对通配符模式使用 ls
和 rm
命令。
[ian@echidna lpi103-2]$ echo odd1>'text[*?!1]' [ian@echidna lpi103-2]$ echo odd2>'text[2*?!]' [ian@echidna lpi103-2]$ ls backup text1 text2 text3 text5 xaa yaa sedtab text[*?!1] text[2*?!] text4 text6 xab yab [ian@echidna lpi103-2]$ ls text[2-4] text2 text3 text4 [ian@echidna lpi103-2]$ ls text[!2-4] text1 text5 text6 [ian@echidna lpi103-2]$ ls text*[2-4]* text2 text[2*?!] text3 text4 [ian@echidna lpi103-2]$ ls text*[!2-4]* # Surprise! text1 text[*?!1] text[2*?!] text5 text6 [ian@echidna lpi103-2]$ ls text*[!2-4] # Another surprise! text1 text[*?!1] text[2*?!] text5 text6 [ian@echidna lpi103-2]$ echo text*>text10 [ian@echidna lpi103-2]$ ls *\!* text[*?!1] text[2*?!] [ian@echidna lpi103-2]$ ls *[x\!]* text1 text[*?!1] text10 text2 text[2*?!] text3 text4 text5 text6 xaa xab [ian@echidna lpi103-2]$ ls *[y\!]* text[*?!1] text[2*?!] yaa yab [ian@echidna lpi103-2]$ ls tex?[[]* text[*?!1] text[2*?!] [ian@echidna lpi103-2]$ rm tex?[[]* [ian@echidna lpi103-2]$ ls *b* sedtab xab yab backup: text1.bkp.1 text1.bkp.2 [ian@echidna lpi103-2]$ ls backup/*2 backup/text1.bkp.2 [ian@echidna lpi103-2]$ ls -d .* . ..
注意:
结合使用 '*' 会出现一些意外。模式 '*[!2-4]' 匹配名称中不包含 2、3 或 4 的最长的一部分,这部分可以同时被 text[*?!1] 和 text[2*?!] 匹配。现在所有这些意外都应该清楚了。
和前面的 ls
示例一样,如果模式扩展导致一个目录名,并且没有指定 -d
选项,那么该目录的内容将被列出(和前例中的模式 '*b*' 一样)。
如果一个文件名以句点 (.) 开头,那么该字符必须被明确匹配。注意,只有最后一个 ls
命令列出两个特殊的目录条目(. 和 ..)。
请注意,命令中的任何通配符都可以被 shell 扩展,这将导致意外的结果。并且,如果您指定一个不匹配任何文件系统对象的模式,那么 POSIX 要求原始模式字符串被传递给命令。一些早期的实现将一个 null 列表传递给命令,因此您可能会遇到一些表现出异常行为的老脚本。我们将在清单 13 中解释这些内容。