Shell 查找并列出文件,find
是Unix/Linux命令行工具箱中最棒的工具之一。该命令在命令行和shell脚本编写方面都能发挥功效。同cat
和ls
一样,find
也包含大量特性,多数用户都没有发挥出它的最大威力。本章讨论了find
的一些常用的查找功能。
预备知识
find
命令的工作方式如下:沿着文件层次结构向下遍历,匹配符合条件的文件,执行相应的操作。默认的操作是打印出文件和目录,这也可以使用-print
选项来指定。
实战演练
要列出给定目录下所有的文件和子目录,可以采用下面的语法:
$ find base_path
bash_path
可以是任意位置(例如/home/slynux),find
会从该位置开始向下查找。例如:
$ find . -print
.history
Downloads
Downloads/tcl.fossil
Downloads/chapter2.doc
…
.
指定当前目录,..
指定父目录。这是Unix文件系统中的约定用法。
print
选项使用\n
(换行符)分隔输出的每个文件或目录名。而-print0
选项则使用空字符'\0'
来分隔。-print0
的主要用法是将包含换行符或空白字符的文件名传给xargs
命令。随后会详细讨论xargs
命令:
$> echo "test" > "file name"
$> find . -type f -print | xargs ls -l
ls: cannot access ./file: No such file or directory
ls: cannot access name: No such file or directory
$> find . -type f -print0 | xargs -0 ls -l
-rw-rw-rw-. 1 user group 5 Aug 24 15:00 ./file name
补充内容
上面的例子演示了如何使用find
列出文件层次中所有的文件和目录。find
命令能够基于通配符或正则表达式、目录树深度、文件日期、文件类型等条件查找文件。
- 根据文件名或正则表达式进行搜索
-name
选项指定了待查找文件名的模式。这个模式可以是通配符,也可以是正则表达式。在下面的例子中,'*.txt'
能够匹配所有名字以.txt结尾的文件或目录。
注意
*.txt
两边的单引号。shell会扩展没有引号或是出现在双引号("
)中的通配符。单引号能够阻止shell扩展*.txt
,使得该字符串能够原封不动地传给find
命令。
$ find /home/slynux -name '*.txt' -print
find命令有一个选项-iname
(忽略字母大小写),该选项的作用和-name
类似,只不过在匹配名字时会忽略大小写。例如:
$ ls
example.txt EXAMPLE.txt file.txt
$ find . -iname "example*" -print
./example.txt
./EXAMPLE.txt
find
命令支持逻辑操作符。-a
和-and
选项可以执行逻辑与(AND)操作,-o
和-or
选项可以执行逻辑或(OR)操作。
$ ls
new.txt some.jpg text.pdf stuff.png
$ find . \( -name '*.txt' -o -name '*.pdf' \) -print
./text.pdf
./new.txt
上面的命令会打印出所有的.txt和.pdf文件,因为这个find命令能够匹配所有这两类文件。\
(以及\
)用于将 -name '*.txt' -o -name '*.pdf'
视为一个整体。
下面的命令演示了如何使用-and
操作符选择名字以s开头且其中包含e的文件:
$ find . \( -name '*e*' -and -name 's*' \)
./some.jpg
-path
选项可以限制所匹配文件的路径及名称。例如,$ find /home/users -path '*/slynux/*' -name '*.txt' –print
能够匹配文件/home/users/slynux/readme.txt,但无法匹配/home/users/slynux.txt。
regex
选项和path
类似,只不过前者是基于正则表达式来匹配文件路径的。
正则表达式比通配符更复杂,能够更精确地进行模式匹配。使用正则表达式进行文本匹配的一个典型例子就是识别E-mail地址。E-mail地址通常采用name@host.root这种形式,所以可以将其一般化为[a-z0-9]+@[a-z0-9]+\.[a-z0-9]+
。中括号中的字符表示的是一个字符组。在这个例子中,该字符组中包含a-z
和0-9
。符号+
指明在它之前的字符组中的字符可以出现一次或多次。点号是一个元字符(就像通配符中的?
),因此必须使用反斜线对其转义,这样才能匹配到E-mail地址中实际的点号。这个正则表达式可以理解为:一系列字母或数字,然后是一个@
,接着是一系列字母和数字,再跟上一个点号,最后以一系列字母和数字结尾。
下面的命令可以匹配.py或.sh文件:
$ ls
new.PY next.jpg test.py script.sh
$ find . -regex '.*\.(py\|sh\)$'
./test.py
script.sh
-iregex选项可以让正则表达式在匹配时忽略大小写。例如:
$ find . -iregex '.*\(\.py\|\.sh\)$'
./test.py
./new.PY
./script.sh
- 否定参数
find
也可以用!
排除匹配到的模式:
$ find . ! -name "*.txt" -print
上面的find命令能够匹配所有不以.txt结尾的文件。该命令的运行结果如下:
$ ls
list.txt new.PY new.txt next.jpg test.py
$ find . ! -name "*.txt" -print
.
./next.jpg
./test.py
./new.PY
- 基于目录深度的搜索
find
命令在查找时会遍历完所有的子目录。默认情况下,find
命令不会跟随符号链接。-L
选项可以强制其改变这种行为。但如果碰上了指向自身的链接,find
命令就会陷入死循环中。
-maxdepth
和–mindepth
选项可以限制find
命令遍历的目录深度。这可以避免find
命令没完没了地查找。
/proc
文件系统中包含了系统与当前执行任务的信息。特定任务的目录层次相当深,其中还有一些绕回到自身(loop back on themselves)的符号链接。系统中运行的每个进程在proc
中都有对应的子目录,其名称就是该进程的进程ID。这个目录下有一个叫作cwd
的链接,指向进程的当前工作目录。
下面的例子展示了如何列出运行在含有文件bundlemaker.def的目录下的所有任务:
$ find -L /proc -maxdepth 1 -name 'bundlemaker.def' 2>/dev/null
- -L选项告诉
find
命令跟随符号链接 - 从/proc目录开始查找
-maxdepth 1
将搜索范围仅限制在当前目录-name 'bundlemaker.def'
指定待查找的文件2>/dev/null
将有关循环链接的错误信息发送到空设备中
-mindepth
选项类似于-maxdepth
,不过它设置的是find
开始进行查找的最小目录深度。这个选项可以用来查找并打印那些距离起始路径至少有一定深度的文件。例如,打印出深度距离当前目录至少两个子目录的所有名字以f开头的文件:
$ find . -mindepth 2 -name "f*" -print
./dir1/dir2/file1
./dir3/dir4/f2
即使当前目录或dir1和dir3中包含以f开头的文件,它们也不会被打印出来。
maxdepth
和mindepth
应该在find
命令中及早出现。如果作为靠后的选项,有可能会影响到find
的效率,因为它不得不进行一些不必要的检查。例如,如果-maxdepth
出现在-type
之后,find
首先会找出-type
所指定的文件,然后再在匹配的文件中过滤掉不符合指定深度的那些文件。但是如果反过来,在-type
之前指定目录深度,那么find
就能够在找到所有符合指定深度的文件后,再检查这些文件的类型,这才是最有效的搜索之道。
- 根据文件类型搜索
类Unix系统将一切都视为文件。文件具有不同的类型,例如普通文件、目录、字符设备、块设备、符号链接、硬链接、套接字以及FIFO等。
find
命令可以使用-type
选项对文件搜索进行过滤。借助这个选项,我们可以告诉find
命令只匹配指定类型的文件。
只列出所有的目录(包括子目录):
$ find . -type d -print
将文件和目录分别列出可不是件容易事。不过有了find就好办了。例如,只列出普通文件:
$ find . -type f -print
只列出符号链接:
$ find . -type l -print
表2-1列出了find能够识别出的类型与参数。
表 2-1
文件类型 | 类型参数 |
---|---|
普通文件 | f |
符号链接 | l |
目录 | d |
字符设备 | c |
块设备 | b |
套接字 | s |
FIFO | p |
- 根据文件的时间戳进行搜索
Unix/Linux文件系统中的每一个文件都有3种时间戳,如下所示:
- 访问时间(
-atime
):用户最近一次访问文件的时间。 - 修改时间(
-mtime
):文件内容最后一次被修改的时间。 - 变化时间(
-ctime
):文件元数据(例如权限或所有权)最后一次改变的时间。
Unix默认并不保存文件的创建时间。但有一些文件系统(
ufs2
、ext4
、zfs
、btrfs
、jfs
)会选择这么做。可以使用stat
命令访问文件创建时间。
鉴于有些应用程序通过先创建一个新文件,然后再删除原始文件的方法来修改文件,文件创建时间未必准确。
-atime
、-mtime
和-ctime
可作为find
的时间选项。它们可以用整数值来指定天数。这些数字前面可以加上-
或+
。-
表示小于,+
表示大于。
考虑下面的例子。
- 打印出在最近7天内被访问过的所有文件。
$ find . -type f -atime -7 -print
- 打印出恰好在7天前被访问过的所有文件。
$ find . -type f -atime 7 -print
- 打印出访问时间超过7天的所有文件。
$ find . -type f -atime +7 -print
-mtime
选项会根据修改时间展开搜索,-ctime
会根据变化时间展开搜索。
-atime
、-mtime
以及-ctime
都是以“天”为单位来计时的。find
命令还支持以“分钟”为计时单位的选项。这些选项包括:
-amin
(访问时间);-mmin
(修改时间);-cmin
(变化时间)。
打印出7分钟之前访问的所有文件:
$ find . -type f -amin +7 -print
–newer
选项可以指定一个用于比较修改时间的参考文件,然后找出比参考文件更新的(更近的修改时间)所有文件。
例如,找出比file.txt修改时间更近的所有文件:
$ find . -type f -newer file.txt -print
find命令的时间戳处理选项有助于编写系统备份和维护脚本。
- 基于文件大小的搜索
可以根据文件的大小展开搜索:
# 大于2KB的文件
find . -type f -size +2k
# 小于2KB的文件 find . -type f -size -2k
# 大小等于2KB的文件
$ find . -type f -size 2k
除了k
之外,还可以用其他文件大小单位。
b
:块(512字节)。c
:字节。w
:字(2字节)。k
:千字节(1024字节)。M
:兆字节(1024K字节)。G
:吉字节(1024M字节)。
- 基于文件权限和所有权的匹配
也可以根据文件权限进行文件匹配。列出具有特定权限的文件:
$ find . -type f -perm 644 -print
# 打印出权限为644的文件
-perm
选项指明find
应该只匹配具有特定权限值的文件。文件权限会在3.5节进行讲解。
以Apache Web服务器为例。Web服务器上的PHP文件需要具有合适的执行权限。我们可以用下面的方法找出那些没有设置好执行权限的PHP文件:
$ find . -type f -name "*.php" ! -perm 644 –print
PHP/custom.php
$ ls -l PHP/custom.php
-rw-rw-rw-. root root 513 Mar 13 2016 PHP/custom.php
我们也可以根据文件的所有权进行搜索。用选项 -user USER
就能够找出由某个特定用户所拥有的文件。
参数USER
可以是用户名或UID。
例如,可以使用下面的命令打印出用户slynux拥有的所有文件:
$ find . -type f -user slynux -print
- 利用find执行相应操作
find
命令能够对其所查找到的文件执行相应的操作。无论是删除文件或是执行任意的Linux命令都没有问题。
(1) 删除匹配的文件
find
命令的-delete
选项可以删除所匹配到的文件。下面的命令能够从当前目录中删除.swp文件:
$ find . -type f -name "*.swp" -delete
(2) 执行命令
利用-exec
选项,find
命令可以结合其他命令使用。
在上一个例子中,我们用-perm
找出了所有权限不当的PHP文件。这次的任务也差不多,我们需要将某位用户(比如root)所拥有的全部文件的所有权更改成另一位用户(比如Web服务器默认的Apache用户www-data),那么可以用-user
找出root拥有的所有文件,然后用-exec
更改所有权。
你必须以root用户的身份执行
find
命令才能够更改文件或目录的所有权。
find
命令使用一对花括号{}
代表文件名。在下面的例子中,对于每一个匹配的文件,find
命令会将{}
替换成相应的文件名并更改该文件的所有权。如果find
命令找到了root所拥有的两个文件,那么它会将其所有者改为slynux:
# find . -type f -user root -exec chown slynux {} \;
注意该命令结尾的\;。必须对分号进行转义,否则shell会将其视为
find
命令的结束,而非chown
命令的结束。
为每个匹配到的文件调用命令可是个不小的开销。如果指定的命令接受多个参数(如chown
),你可以换用加号(+
)作为命令的结尾。这样find
会生成一份包含所有搜索结果的列表,然后将其作为指定命令的参数,一次性执行。
另一个例子是将给定目录中的所有C程序文件拼接起来写入单个文件all_c_files.txt。各种实现方法如下:
$ find . -type f -name '*.c' -exec cat {} \;>all_c_files.txt
$ find . -type f -name '*.c' -exec cat {} > all_c_files.txt \;
$ fine . -type f -name '*.c' -exec cat {} >all_c_files.txt +
我们使用 >
操作符将来自find
的数据重定向到all_c_files.txt文件,没有使用>>
(追加)的原因是find
命令的全部输出就只有一个数据流(stdin
),而只有当多个数据流被追加到单个文件中时才有必要使用>>
。
下列命令可以将10天前的 .txt文件复制到OLD目录中:
$ find . -type f -mtime +10 -name "*.txt" -exec cp {} OLD \;
find命令还可以采用类似的方法与其他命令结合使用。
我们无法在
exec
选项中直接使用多个命令。该选项只能够接受单个命令,不过我们可以耍一个小花招。把多个命令写到一个shell脚本中(例如command.sh),然后在-exec
中使用这个脚本:
exec ./commands.sh {} \;
-exec
可以同printf
搭配使用来生成输出信息。例如:
$ find . -type f -name "*.txt" -exec printf "Text file: %s\n" {} \;
Config file: /etc/openvpn/easy-rsa/openssl-1.0.0.cnf
Config file: /etc/my.cnf
- 让find跳过特定的目录
在find
的执行过程中,跳过某些子目录能够提升性能。例如,在版本控制系统(如Git)管理的开发源代码树中查找特定文件时,文件系统的每个子目录里都会包含一个目录,该目录中保存了和版本控制相关的信息。这些目录通常跟我们没什么关系,所以没必要去搜索它们。
在搜索时排除某些文件或目录的技巧叫作修剪。下面的例子演示了如何使用-prune
选项排除某些符合条件的文件:
$ find devel/source_path -name '.git' -prune -o -type f -print
-name ".git" –prune
是命令中负责进行修剪的部分,它指明了.git目录应该被排除在外。-type f –print
描述了要执行的操作。