Linux批量文件操作——基于find,xargs
前言 #
在项目初创阶段,经常会遇到各种文件操作,拷贝头文件,库,批量重命名等。文件结构一复杂,这就将是个无聊的工作。
查找文件 #
find
可以在目录结构中搜索文件,这是它在man
里面的作用描述。那么怎么搜索呢?有多种方式,按文件时间,大小,按文件名,路径名,按文件类型,权限,按用户。而这些方式又可以通过与或非的逻辑相互组合,完成更苛刻的查找工作,简直是文件查找的福音。
通常介绍一种命令都会以命令形式开始,find
的格式如下
find [-H] [-L] [-P] [-D debugopts] [-Olevel] [starting-point...] [expression]
[-H] [-L] [-P] [-D debugopts] [-Olevel]这一些统统不重要,
[-H] [-L] [-P]是针对软连接的,不常用。[-D debugopts]是显示运行期间的额外信息,信息太乱太杂,用处不大。[-Olevel]则是用于优化查找的,默认的已经够用,所以也没必要深究。
find
的最大魔法在最后的[expression]
,下面将以实例的形式讲解这个[expression]
到底该怎么玩,原始的文件结构如下
├── alice.h
├── andy
│ ├── jack
│ │ └── mary.h
│ ├── mark.cpp
│ ├── mark.h
│ └── pony.txt
├── andy.c
├── bill.cpp
├── bill.h
├── mark.h
└── mary
现在,我想找到以andy
命名的文件,命令该怎么写呢。直觉告诉我们应该是
find andy
但是直觉对吗,我们来看输出
andy
andy/mark.cpp
andy/jack
andy/jack/mary.h
andy/pony.txt
andy/mark.h
它只找到了andy
目录,甚至都没找到andy.c
,那么看来我们需要一种方式告诉find
,我们找的东西是文件,不是目录,这个选项就是-type
。
-type
后面需要紧跟一个参数,常用取值是d
代表目录,f
代表文件。现在我们需要找到文件,那么就应该加个-type f
的选项。但这就够了吗?执行命令会发现报错了,因为后面的andy
被认为是路径,而我们要找的是文件名啊。所以,这又需要另一个选项的帮忙,-name
。-name
后面可以跟具体的名字或者正则。结合这两个条件,我们得出了最终的命令
find -type f -name "andy*"
这里有两点值得注意,首先-type
和-name
其实是两个独立选项,可以单独使用,也可以联合使用,当联合使用时,他们之间不使用操作符(-o
(Or),-a
(AND),-not
)连接时,就会把-a
单做连接符,也就是所有的条件都满足才回出现在最终的结果中。由此,可以延伸出一种反向的查找方法,
find -type f -not -name "andy*"
这个命令就会查找出所有不以andy
开头的文件。
./andy/mark.cpp
./andy/jack/mary.h
./andy/pony.txt
./andy/mark.h
./bill.h
./bill.cpp
./mark.h
./alice.h
另一个值得注意的点是"andy*"
加了双引号,因为*
是特殊字符,所以需要用双引号转椅一下,假如没有特殊字符是不需要添加双引号的。回到最初的命令,为啥第一条我们想当然的命令竟然没有找到我们期盼的目标呢?因为find
是严格匹配的,我们只写了andy
,而遗漏了后缀.c
,这是最容易发生错误的地方。
其实,到这里,我们已经学习了这个命令的50%,那剩下的有什么内容呢?记得find
的主要功能吗,里面提到了个目录结构。对的,find
还可以控制查找范围。
新需求来了,怎样找到某个目录下所有的直接子.h
文件呢?这里的直接和子合起来的意思是查找范围只能是当前目录,不能查找到当前目录的子目录。在解决这个问题前,我们需要知道一个前置知识——两个目录之间存在两种相互关系,兄弟或者父子。兄弟目录深度相同,父子目录深度相差1。知道了这点,再来看需求——.h
文件很简单,使用-name "*.h"
就能满足。但是,这样会找到andy
目录下的.h
文件,所以我们需要一种控制目录查找层级的东西,他们就是-mindepth
,maxdepth
。这两个参数和前面的不一样,他们属于Global options
。什么是Global options
呢,就是他们的作用是全局性的,并且它们总是返回true
,也就是它们和其他的选项连起来一起查找的时候,只用考虑其他选项的条件。并且,为了凸显它们的全局性,它们在命令书写时,必须写在最前面,否则会触发警告。如下,就是写在了-name "*.h"
前面。
find -maxdepth 1 -name "*.h"
这两个参数有点反直觉,可以这样理解——最多查找到哪里,有个最多,就是maxdepth
,反过来就是从哪里才开始查找,才就是mindepth
。
聊完了目录结构,名字这些显眼的部分,文件还有访问(access
)创建(create
)、修改(modiffy
)时间,权限(permission
),大小(size
)这些没有涉及到,而这些也同样可以作为find
的查找条件,在开始之前,有一些小规则可以对这些选项做个快速分组——选项会以属性的英文首字母作为开始,如
- 时间相关的选项有
time
和min
,分别表示某个事件发生的前n
天和n
分钟,这里的某个事件可以用访问a
(access
)c
创建(create
)、m
修改(modiffy
)替代,它们组合起来就是一个完整的选项,如mmin n
就表示查找修改时间在n
分钟以内的文件。 - 以此类推,
i
代表大小写敏感,如iname
,l
代表link
文件
当然,这些都可能用得不多,实际使用到再查可能还更方便点,但是有两个很好用的选项不得不讲。
考虑以下情况,某天老本发来一堆用户日志文件,让你给这些用户根据使用频次分级,你该怎么办呢?首先,我们可以把日志文件大小作为依据,根据最大最小划分好区间,如(0-100M),然后用定好的级别(如5级)划分出每级区间(0-20,20-40,…),这样我们多次运行命令,就能得到所有的分级情况了。想法是美好的,但是find
提供了这种选项了吗?它提供了-size n
。我们来试着查找一下0-20区间的文件
find -size 20M
回车,你会发现结果貌似不完全正确,它可能确实找出了一些满足条件的文件,但是有一些满足的却没有找出,问题出现在哪里呢?原来-size n
中的n
是严格匹配,就是你输入的是20M,它就只找到恰好是20M的文件,而不是我们期望的20M和20M一下的文件。那么有解决方法吗,当然有,就是数字前面加上+
,-
号,+
代表大于等于这个值,-
代表小于。所以我们查找20M以下的文件的命令应该是
find -size -20M
解决了符号问题,还有单位问题值得注意,也就是-20M
中的M
。其实-size
的标准形式是-size [+-]n[cwbkMG]
。[+-]
和n
都说过了,后面的则单位。它们是以大小递增顺序排列的,说明如下
c
:字节w
:双字节,也就是word
b
:512字节构成的块,数字n
后面没加单位的话,这个是默认值k
:1024个字节,也就是kbM
:1024 * 1024个字节,也就是Mbc
:1024 * 1024 * 1024个字节,也就是Gb
说完了单位的事,我们接着来出来20——40的分级,直接把20改为40吗?当然不,改为40找到的就是小于等于40M的文件了,所以我们需要一种区间标定法,find
没有提供直接的选项支持,但是前面说过,选项是可以组合的,也就是我们可以重复使用-size
来标识一个区间。也就是
find -size +20M -size -40M
按照这种方法,多次改变值,就可以完成任务啦。
其实上面的方案还有一点小纰漏,就是没有找出直接没用过的用户,也就是size为0的,那把数字改为0,可以吗?答案是可以,但是假如我们想找的是空目录,而不是空文件呢,-size
就不能解决了,因为通常空目录的大小不为0.所以,find
又提供了个检测文件是否为空的选项-empty
,它不仅可以找到空文件,还可以找到空目录。在我们的实例中,使用 find -size 0
找到的结果是
./andy/mark.cpp
./andy/jack/mary.h
./andy/pony.txt
./andy/mark.h
./andy.c
./alice.h
没有找出空目录mary
。而使用find -empty
查找后,结果是
./mary
./andy/mark.cpp
./andy/jack/mary.h
./andy/pony.txt
./andy/mark.h
./andy.c
./alice.h
不仅找到了mary
空目录,其他的空文件也找出来了。
至此,find
相关的东西已经了解得差不多了。但是,很多时候仅仅找到并不能完全满足我们的需求,我们可能需要把找到的文件复制到其他地方或者删除之类的,能否把这些操作合起来呢?这就要请我们的xargs
登场了。
xargs #
xargs
只有一个简单的功能,就是从标准输入读入内容,构建并执行命令。怎么理解呢?假设我们在执行find
命令,find
命令执行肯定是有过程,有逻辑的。按照一定的逻辑和过程,find
对文件进行逐一评估,假如满足条件,就输出结果。随着命令的执行,结果可能越来越多。假如我们需要对产生的每个结果都执行一条命令呢,这该怎么办?按照一般的思路,当然是将结果保存起来,然后再写个脚本,读取每一条记录,然后执行相应。但是有了xargs
,我们不用这么麻烦了,可以一步到位。我们利用管道符将结果从终端连接到xargs
中,xargs
接收到一条信息,就会将它作为构建命令的参数,就好像我们手动输入了命令那样,构建完成后还会自动执行。最终的结果就是,没产生一个输出,就会产生一条以这个输出为参数的命令,并且这条命令还自动执行,最终的效果就是实现了一条命令实现了多个功能。
将find
和xargs
结合起来 #
现在我们挑战升级了,有个需求,需要提取目录下的所有头文件到另一个目录。这个需求可以分为两部分,一部分是找到头文件,这可以用find
命令完成。另一部分是复制找到的头文件,这就需要xargs的参与了。
首先是找到头文件。头文件就是以.h
结尾的文件(暂不考虑.hpp
),这个后缀是出现在名字里的,所以我们可以使用-name "*。h"
选项,同时为了避免某些目录名的干扰,我们把类型也做个限定-type f
,只查找文件。这第一步就完成了。
第二布就是复制文件啦。复制文件的标准写法是
cp [OPTION]... SOURCE... DIRECTORY
根据这个命令格式,我们需要确定几个参数,源文件当然就是我们查找到的文件啦,这暂时按下不表。目标文件夹,就是我们拷贝到哪里,我们这里暂时就新建一个test
的目录吧,目标文件夹就是test
。这就结束了吗,还没有。头文件往往需要和他的父目录组成一个依赖路径,所以我们把所有的头文件直接一股脑都拷贝到test
目录下是不可取的,这会打乱头文件的依赖关系,我们还得拷贝和头文件相关的父目录。恰巧的是,cp
提供了这样的一个选项-parents
——它可以拷贝源文件的完成文件名,也就是包含目录。所以问题的关键就来到了源文件这个参数上了。