在实际工作中因为各种各样的原因,可能需要对文件及文件内容进行批量化的操作。
在 Linux 环境中,使用 xargs
命令可以方便地批量操作文件。
xargs
是给命令传递参数的一个过滤器,可以用于组合多个命令。
xargs
可以将管道或者标准输入数据转换成命令行参数,这正是文件处理过程中需要的一个桥梁。
关于 xargs
命令的更多细节及参数,可以在命令行中输入 man xargs
查看。
接下来举例一些可能遇到的场景。
> 将文件夹下前100个文件,拷贝到目标文件夹中。
拷贝一个文件方式比较简单,可以使用 cp
命令直接完成:
cp [file_name] [target_dir]
拷贝目录下所有文件的方式也不难,因为 cp
命令后面可以用 *
取代 [file_name]
表示所有文件:
cp * [target_dir]
然而如果要更加灵活地拷贝特定数量或特定格式的文件,可能需要利用其它的命令。
比如 ls
可以列举当前目录下的所有文件:
➜ ls
0001.txt 0002.txt 0003.txt 0004.txt 0005.txt
结合管道及 head
命令可以列举当前目录下前若干个文件:
➜ ls | head -n 3
0001.txt
0002.txt
0003.txt
此时使用 xargs
将上述命令的输出结果,传递到 cp
命令中,即可实现 拷贝前xx个文件
的需求:
ls | head -n 100 | args -i cp {} [target_dir]
其中参数 -i
表示将输入的每一行赋值给 {}
,这样就能实现针对每个文件的拷贝操作。
> 将文件下所有文件的.txt
后缀,批量重命名为.md
后缀。
针对具有特定格式的文件名,我们可以通过 grep
的方式过滤出来。此时如果还要进行批量操作,依然可以使用 xargs
命令完成。
在场景1中,可以结合 xargs
命令的 -i
参数和 {}
标识符,对管道的输入(前一个操作的输出)进行操作。现在需要更加进阶版,因为操作需要对 {}
中的内容进行修改。
首先可以使用 echo
命令来观察操作结果的正确性:
➜ ls | xargs -i echo abcd {} efgh {}
abcd 0001.txt efgh 0001.txt
abcd 0002.txt efgh 0002.txt
abcd 0003.txt efgh 0003.txt
abcd 0004.txt efgh 0004.txt
abcd 0005.txt efgh 0005.txt
可以看出针对每行输入,都按照命令的字符串组织格式,打印出一行输出。
为了实现修改文件名的操作,我们需要对输入的文件名 {}
进行替换操作。可以使用 sed
命令完成。
➜ ls | xargs -i echo abcd {} efgh {} | sed 's/.txt/.md/g'
abcd 0001.md efgh 0001.md
abcd 0002.md efgh 0002.md
abcd 0003.md efgh 0003.md
abcd 0004.md efgh 0004.md
abcd 0005.md efgh 0005.md
可以看出 sed
针对管道输入的字符串 abcd 0001.txt efgh 0001.txt
进行了关键词替换操作。
其中的 s/[old_string]/[new_string]/g
是常用的替换操作,在 Vim
等文件编辑器中也经常会使用到,这里不再复述。其中最后的 g
表示全局替换。
如果希望只替换第二个输入,可以将 g
修改为 2g
:
➜ ls | xargs -i echo abcd {} efgh {} | sed 's/.txt/.md/2g'
abcd 0001.txt efgh 0001.md
abcd 0002.txt efgh 0002.md
abcd 0003.txt efgh 0003.md
abcd 0004.txt efgh 0004.md
abcd 0005.txt efgh 0005.md
可以看出已经非常接近我们最终希望实现的功能了,整理相关命令可得:
➜ ls | xargs -i echo mv {} {} | sed 's/.txt/.md/2g'
mv 0001.txt 0001.md
mv 0002.txt 0002.md
mv 0003.txt 0003.md
mv 0004.txt 0004.md
mv 0005.txt 0005.md
最后,将这些输出通过管道重定向到 `sh` 脚本中执行,即可完成批量重命名操作。
➜ ls
0001.txt 0002.txt 0003.txt 0004.txt 0005.txt
➜ ls | xargs -i echo mv {} {} | sed 's/.txt/.md/2g' | sh
➜ ls
0001.md 0002.md 0003.md 0004.md 0005.md
需要注意的是,如果不先 echo
出命令再输入到 sh
的话,mv {} {}
会直接执行,这样就无法输入到后一个管道中进行 sed
替换操作。
综上所述,进行批量重命名的命令可以归纳为:
ls | xargs -i echo mv {} {} | sed 's/[old_string]/[new_string]/2g' | sh
比如以下操作将当前目录下所有文件的文件名中的 .png
替换为 .txt
:
ls | xargs -i echo mv {} {} | sed 's/.png/.txt/2g' | sh
这里我们掌握了一个实用的技能,就是先使用 echo
将希望得到的命令打印出来,最后再输入到 sh
脚本中执行。
这个技能在对一些管道操作还未熟练时,进行摸索和调试非常有帮助。
场景2总结了如何针对特定格式的文件名进行批量修改和替换,这里顺带提一下批量修改文件内容的方式。
假定文件 0001.txt
包含的内容为:
➜ cat 0001.txt
a
ab
abc
abcd
abcde
abcdef
使用 sed
命令可以对文件内容进行输出并修改,比如将内容中所有字符 a
替换成字符 x
:
➜ sed 's/a/x/g' 0001.txt
x
xb
xbc
xbcd
xbcde
xbcdef
需要注意的是,这里并没有修改文件 0001.txt
中的内容,只是将内容输出并替换后打印出来。
如果希望对文件进行直接修改,可以使用 -i
参数,官方的说明是:
> -i extension Edit files in-place, saving backups with the specified extension. If a zero-length extension is given, no backup will be saved. It is not recommended to give a zero-length extension when in-place editing files, as you risk corruption or partial content in situations where disk space is exhausted, etc.
大致含义是,通过 -i
参数可以指定一个扩展名,文件将被原地修改,原内容会被备份到指定扩展名文件中。如果没有给定扩展名参数,则不会保存备份。不建议不给出扩展名参数,因为这风险比较大。
修改 0001.txt
并备份
➜ sed -i .backup 's/a/x/g' 0001.txt
➜ cat 0001.txt
x
xb
xbc
xbcd
xbcde
xbcdef
➜ cat 0001.txt.backup
a
ab
abc
abcd
abcde
abcdef
可以看到文件 0001.txt
中的内容被直接批量替换了。
在实际使用中,可以在验证修改正确后,再删除对应扩展名的文件:
rm *.backup
有了上述知识储备,我们可以对目录下所有文件内容进行相关替换操作,比如:
sed -i .backup 's/[old_string]/[new_string]/g' *.txt
这一招在某些时候批量处理那些包含绝对路径或相对路径的数据文件时还挺常用到的。
有时候会遇到一些奇葩的逻辑需求,比如为了迎合某种数据读取方式,需要将目录下所有文件按照特定组织结构放到相应文件夹中。
比如当一个目录下文件比较多时,可能会要求按照文件的前n位创建一个目录,并将所有文件名前n位相同的文件移动到对应目录中。
这里可能需要引入另外一个强大的命令 awk
,使用 awk
甚至可以实现许多Shell编程的功能。
使用 awk
的基本方式也可以将输出重新进行操作,比如打印:
➜ ls | awk '{print $1}'
0001.txt
0002.txt
0003.txt
0004.txt
0005.txt
打印更丰富一些:
➜ ls | awk '{print "cp "$1" "$1}'
cp 0001.txt 0001.txt
cp 0002.txt 0002.txt
cp 0003.txt 0003.txt
cp 0004.txt 0004.txt
cp 0005.txt 0005.txt
使用 substr
截取字符串:
➜ ls | awk '{print "cp "$1" "substr($1,1,4)}'
cp 0001.txt 0001
cp 0002.txt 0002
cp 0003.txt 0003
cp 0004.txt 0004
cp 0005.txt 0005
结合上述知识,可以打印以文件前若干字符创建文件夹并归类
的操作命令:
➜ ls | awk '{print "mkdir "substr($1,1,4)" && mv "$1" "substr($1,1,4)}'
mkdir 0001 && mv 0001.txt 0001
mkdir 0002 && mv 0002.txt 0002
mkdir 0003 && mv 0003.txt 0003
mkdir 0004 && mv 0004.txt 0004
mkdir 0005 && mv 0005.txt 0005
将上述命令重定向到 sh
执行:
➜ ls | awk '{print "mkdir "substr($1,1,4)" && mv "$1" "substr($1,1,4)}' | sh
命令执行后发现目录下创建了5个文件夹,每个文件夹内有对应的文件。
创建文件夹并挪动的命令可以归纳为:
ls | awk '{print "mkdir "f($1)" && mv "$1" "f($1)}'
其中 f($1)
表示对输入参数(即 ls
列出的文件名)进行相关字符串操作。