Shell 脚本神器 xargs

2024-11-15 ⏳4.6分钟(1.8千字) 🕸️

最近工作中要大批量操作线上机器。因为是历史遗留系统,无法使用 Ansible 等现成的框架,只能自己手写 Shell 脚本来处理。这中间就不免会涉及到批量生成列表和并发执行命令的场景。这类工作可以通过 xargs 命令来实现。本篇就来分享具体的实现技巧。

xargs简单来说就是从标准输出读取一批数据,然后使用这些数据作为参数来调用指定的程序。下面给出一个简单的例子:

$ seq 1 3 | xargs -I % echo %
1
2
3

seq也是 UNIX 命令,可以根据参数生成一组数列。seq 1 3会依次输出1,2,3,每个数占一行。seq的输出内容会通过管道|传递给xargs命令。这里我用xargs来调用 echo %命令,其中的%是参数占位符,由前面-I参数设置。xargs会从stdin中依次读取1,2,3,并依次执行echo 1; echo 2; echo 3;。占位符不一定是%,大家可以根据实际情形灵活指定。

好了,到这里基本就介绍完xargs的主要功能了。等等,这么简单的东西有什么用呢?其实大有用处。UNIX 哲学是做一件事,并把它做到极致。然后我们可以通过管道把不同的功能组合起来,完成各类复杂的任务。而且xargs在这种组合过程中发挥了关键作用。

现在介绍几种常见的使用范式。

我们可以复用xargs生成各类连续型列表。比如我最近的工作中要大量生成机器名:

cat ids.txt|xargs -I % echo demo-%-db-1

这里我事先把对应的 ID 保存到ids.txt文件中。然后生成一系列形如demo-1-db-1 这样的机器名。

有朋友可能会说用sed也可以实现。比如上例可以改写成

cat ids.txt|sed 's/\(.*\)/demo-\1-db-1/'

sed涉及到正则表达式,相对来说没有xargs直观。

以上是为每个🆔生成一条数据的例子。有时候我们还需要一次生成多条数据。就比如我最近工作的情景,每组业务服务有两台机器,分别使用-1-2后缀,需要同时处理。这时我们可以这样生成:

cat ids.txt | xargs -I % printf "demo-%-gs-1\ndemo-%-gs-2\n"

注意,核心变化是改用了printf命令,它类似C语言中的printf()函数,可以输出转义字符。这里我用了\n来输出换行。假如xargs读到数字1,它就会输出:

demo-1-gs-1
demo-1-gs-2

除了用xargs指定参数占位符外,在某些特定的场景,我们也可以不指定。此时它的行为会有些差异。它会把读到的内容用空格合成一行再输出,比如:

$ seq 1 3 | xargs echo
1 2 3

我们可以利用这个特性处理一些简单的任务。比如说批量停止深信服 EasyConnect 的进程1

ps aux | grep EasyCon | awk '{print $2}' | xargs sudo kill

这里使用psgrep查出 EasyConnect 相关的进程信息,然后用awk '{print $2}' 提取ps输出结果的第二列,也就是进程号;最后利用xargs执行sudo kill打死进程。

假如说 EasyConnect 的进程号分号是1001/1002/1003,那么上面的脚本等价于:

sudo kill 1001 1002 1003

xargs这种行列转换在有些场景🎬下非常有用。比如我司内部系统支持通过正则表达式来搜索主机资源,但是每次搜索都会重置列表。如果你要查的主机是连续编号,比如gs[1-9],这种还好处理。否则,就需要多次打开搜索列表,多次搜索并提交,很不方便。

后来我索性把所有主机ID都加到一个大的正则里,一口气匹配出所有目标:

cat ids | xargs echo | sed 's/ /|/g'

注意最后的sed,它使用了g参数,表示要把一行内所有的空格都替换成|

网上还有另外一个有名的例子,批量删除 Ubuntu 下的包配置。Ubuntu 下某个包被删除后,它的配置文件可能还保存在磁盘中。像我这样有强迫症的人肯定是不能忍,非得清理掉不成。

具体可以选通过dpgk -l|grep ^rc来查出所有的遗留配置包。然后再通过awk提取包名,最后通过xargs批量删除。

dpkg -l | grep ^rc | awk '{pritn $1}` | xargs sudo apt-get purge -y

我这次使用xargs调用sudo apt-get purge命令,而且指定了-y参数。默认apt-get 会输出确认删除提示,用户输入y表示确认后才真正删除。但我们是批量操作,无法执行这样的交互式确认,所以要指定-y参数。

以上命令也可以改为

dpkg -l | grep ^rc | awk '{pritn $1}` | xargs -I % sudo apt-get purge -y %

xargs会针对每一个包名多次调用sudo apt-get purge,效果跟上面那条没有差别。

那为什么xargs要有两种执行模式呢~我想第一个原因是有些程序不支持输入多个参数。像kill 1001; kill 10002可以简化成kill 1001 1002,但像是dig就不能同时查询两个域名。

不过我也不认为这是主要原因。更重要的原因是xargs需要实现并发执行的效果。我们可以把发送给xargs的内容看成是任务,每一行一个任务。xargs不但可以逐个触发对应的处理程序,而且还可以通过-P参数来并行处理。给大家举个列子:

seq 1 5 | xargs -P 2 -I % curl -O https://example.com/files/%.txt

其效果等价于以下脚本,也就是下载五个 txt 文件:

curl -O https://example.com/files/1.txt
curl -O https://example.com/files/2.txt
curl -O https://example.com/files/3.txt
curl -O https://example.com/files/4.txt
curl -O https://example.com/files/5.txt

如果没有指定-P参数,xargs会依次执行上面的五条命令。但我们指定为-P 2,那么 xargs会每次并改执行两条命令,如果改为-P 5,那就会同时下载五个文件。这样可以大大缩减任务执行耗时。

比如,我曾经使用五并发来批量查询 DNS 记录😜

seq 1 100 | xargs -I % echo gs%.example.net | xargs -P 10 -I % q CNAME % @119.29.29.29

再比如我还有十并发来批量备份 MongoDB 数据:

cat dbs.txt | xargs -P 10 -I % mongodump mongodb://root:${pass}@%1.mongodb.rds.aliyuncs.com:3717/% \
  --authenticationDatabase admin \
  --gzip --archive=/backup/db/%.archive

同样的技巧也可以用于并行处理更复杂的任务。比如 Adam 基于xargs用单机处理数据2,吞吐量达到了 270MB/s,甚至比 Hadoop 还要快。

xargs无疑是非常老旧的工具,但它的设计思想却永不过时。UNIX下一次只做一件事并做到极致,然后再将不同的工具组合起来。这种哲学不但启迪了后来的函数式编程,在单机性能日益增强的现代将发挥越来越重要的作用。希望大家都能使用xargs来提升自己的工作效率。


  1. 深信服的 EasyConnect 非常恶心,具体可以看我的专门文章 ../easyconnect-in-docker.html↩︎

  2. https://adamdrake.com/command-line-tools-can-be-235x-faster-than-your-hadoop-cluster.html↩︎