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文件,所以我们需要一种控制目录查找层级的东西,他们就是-mindepthmaxdepth。这两个参数和前面的不一样,他们属于Global options。什么是Global options呢,就是他们的作用是全局性的,并且它们总是返回true,也就是它们和其他的选项连起来一起查找的时候,只用考虑其他选项的条件。并且,为了凸显它们的全局性,它们在命令书写时,必须写在最前面,否则会触发警告。如下,就是写在了-name "*.h"前面。

find -maxdepth 1 -name "*.h"

这两个参数有点反直觉,可以这样理解——最多查找到哪里,有个最多,就是maxdepth,反过来就是从哪里才开始查找,才就是mindepth

聊完了目录结构,名字这些显眼的部分,文件还有访问(access)创建(create)、修改(modiffy)时间,权限(permission),大小(size)这些没有涉及到,而这些也同样可以作为find的查找条件,在开始之前,有一些小规则可以对这些选项做个快速分组——选项会以属性的英文首字母作为开始,如

  • 时间相关的选项有timemin,分别表示某个事件发生的前n天和n分钟,这里的某个事件可以用访问aaccessc创建(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个字节,也就是kb
  • M:1024 * 1024个字节,也就是Mb
  • c: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接收到一条信息,就会将它作为构建命令的参数,就好像我们手动输入了命令那样,构建完成后还会自动执行。最终的结果就是,没产生一个输出,就会产生一条以这个输出为参数的命令,并且这条命令还自动执行,最终的效果就是实现了一条命令实现了多个功能。

findxargs结合起来 #

现在我们挑战升级了,有个需求,需要提取目录下的所有头文件到另一个目录。这个需求可以分为两部分,一部分是找到头文件,这可以用find命令完成。另一部分是复制找到的头文件,这就需要xargs的参与了。

首先是找到头文件。头文件就是以.h结尾的文件(暂不考虑.hpp),这个后缀是出现在名字里的,所以我们可以使用-name "*。h"选项,同时为了避免某些目录名的干扰,我们把类型也做个限定-type f,只查找文件。这第一步就完成了。 第二布就是复制文件啦。复制文件的标准写法是

cp [OPTION]... SOURCE... DIRECTORY

根据这个命令格式,我们需要确定几个参数,源文件当然就是我们查找到的文件啦,这暂时按下不表。目标文件夹,就是我们拷贝到哪里,我们这里暂时就新建一个test的目录吧,目标文件夹就是test。这就结束了吗,还没有。头文件往往需要和他的父目录组成一个依赖路径,所以我们把所有的头文件直接一股脑都拷贝到test目录下是不可取的,这会打乱头文件的依赖关系,我们还得拷贝和头文件相关的父目录。恰巧的是,cp提供了这样的一个选项-parents——它可以拷贝源文件的完成文件名,也就是包含目录。所以问题的关键就来到了源文件这个参数上了。