如何使用awk调用外部程序
1. 概述
awk 命令是一个非常强大的文本处理工具。 使用它,我们可以在 Linux 命令行中解决各种文本处理问题。
在本教程中,我们将了解如何从awk脚本调用外部程序。
2. 从awk调用外部命令
尽管awk是一个强大的实用程序,但有时我们需要外部命令的帮助来解决一些问题。 例如:
- *awk + sendmail:*读取包含电子邮件地址和消息的 CSV 文件,并处理和发送每条消息
- *awk + cp:*读取文件列表的输入,并将文件复制到具有定义名称模式的所需目的地
- *awk + md5sum:*读取包含文件名列表的输入,输出文件名和文件的MD5哈希
当我们从awk调用外部命令时 ,根据要求,通常我们希望获得返回的状态或命令的输出。
比如上面的awk + cp例子中,我们想要获取 cp命令的返回状态,以了解复制是否成功。在awk + md5sum示例中,我们需要md5sum命令的输出,以便我们的脚本可以产生正确的输出。
在后面的部分中,我们将通过示例了解如何处理这两种情况。
3. 获取外部命令的执行状态
我们将通过解决文件备份问题来解决如何获取外部命令的返回状态。让我们看看我们的输入文件:
$ cat /tmp/test/file_list.csv
Id,Filename,Create_date
1,"/tmp/test/source/file1.txt",2020-06-01
2,"/tmp/test/source/file2.pdf",2020-05-01
3,"/tmp/test/source/file3.txt",2020-06-03
4,"/tmp/test/source/file4.zip",2020-06-07
正如名称file_list.csv所说,它是一个 CSV 文件。它在第二个字段中包含文件名。根据这个文件,我们有*/tmp/test/source*目录下的文件:
$ ls -l /tmp/test/source
-rw-r--r-- 1 kent kent 30 Jun 7 00:13 file1.txt
-rw-r--r-- 1 kent kent 36 Jun 7 00:13 file2.pdf
-rwx------ 1 root root 1752 Jun 7 00:10 file3.txt
-rw-r--r-- 1 kent kent 37 Jun 7 00:13 file4.zip
要求是,我们将第二个字段中的文件复制到*/tmp/test/backup并在 CSV 文件中添加一个名为“ Backup_status ”的新字段来记录相应文件的备份状态是“成功”*还是“失败”。
如果我们仔细阅读上面的 ls输出,我们会注意到file3.txt的权限为700。如果我们尝试由普通用户读取,则会收到“permission denied”错误。因此,我们期望file3.txt的备份状态在输出中应该是“失败”。
** awk的*system(cmd)*函数可以调用外部命令并获取退出状态。**这个功能是解决问题的关键。
首先,让我们看看问题是如何解决的:
kent$ awk -F',' -v OFS=',' -v toDir="/tmp/test/backup" \
'NR==1{print $0,"Backup_status"; next}
{ backup_cmd = "cp " $2 " " toDir " >/dev/null 2>&1"
st = system(backup_cmd)
print $0, ( st==0? "Success" : "Failed" ) }' /tmp/test/file_list.csv
Id,Filename,Create_date,Backup_status
1,"/tmp/test/source/file1.txt",2020-06-01,Success
2,"/tmp/test/source/file2.pdf",2020-05-01,Success
3,"/tmp/test/source/file3.txt",2020-06-03,Failed
4,"/tmp/test/source/file4.zip",2020-06-07,Success
我们执行awk命令后,所有备份状态为“ Success ”的文件都已复制到预期的目录:
$ ls /tmp/test/backup
file1.txt file2.pdf file4.zip
现在,让我们逐行浏览awk代码,了解它是如何工作的:
- 第 1 行:由普通用户kent启动**awk命令并设置所需的变量,例如*FS 和OFS *
- 第 2 行:通过添加一个新字段来扩展标题:Backup_status并打印标题
- 第 3 行:构造复制命令并通过将stdout和 stderr都重定向到 /dev/null来丢弃所有输出
- 第 4 行:调用 *system()*函数并将退出状态保存在变量 st中
- 第 5 行:打印带有备份状态信息的输出(st==0表示成功)
4. 获取外部命令的输出
我们已经了解了如何调用外部程序并从awk代码中获取退出状态。但是,有时我们想使用外部命令的输出来做进一步的处理。
一个命令可以产生单行输出或多行输出。在本节中,我们将讨论如何处理这两种情况。
4.1. 从外部命令获取单行输出
让我们也从一个问题开始。
我们将重用相同的输入文件*/tmp/test/file_list.csv*。这次,我们要在 CSV 文件中添加一个新列“ MIME_type ”,以显示每个文件的MIME 类型 。
要获取 MIME 类型,我们可以使用file 命令。例如,我们可以这样获取文件*/tmp/test/source/file1.txt*的MIME类型:
$ file -b --mime-type file1.txt
text/plain
好的。到目前为止,解决我们的问题唯一缺少的部分是如何调用file命令并从我们的awk脚本中获取输出。
在awk中,有一个名为getline 的多功能命令。我们可以通过管道将构造的命令传递给 getline命令,并使用以下语法将命令的输出保存到变量中:
"an external command" | getline variable
例如,如果我们想将file命令的输出保存到awk变量 result中,我们可以这样写:
"file -b --mime-type file1.txt" | getline result
现在,让我们组装这些东西并解决我们添加 MIME 类型的问题:
kent$ awk -F',' -v OFS=',' \
'NR==1{print $0,"MIME_type"; next}
{ cmd = "file -b --mime-type " $2
cmd | getline result
close(cmd)
print $0, result }' /tmp/test/file_list.csv
Id,Filename,create_date,MIME_type
1,"/tmp/test/source/file1.txt",2020-06-01,text/plain
2,"/tmp/test/source/file2.pdf",2020-05-01,application/pdf
3,"/tmp/test/source/file3.txt",2020-06-03,regular file, no read permission
4,"/tmp/test/source/file4.zip",2020-06-07,application/zip
由于我们使用普通用户kent执行该命令,因此当我们在file3.txt上调用file命令时收到错误消息。此错误消息也会添加到输出中。
通过将命令传递给getline命令来获得单行输出非常简单。
我们还能用同样的方法得到多行输出吗?让我们在下一节中找出答案。
4.2. 从外部命令获取多行输出
某些命令可能会产生多行输出。让我们试试是否可以使用cmd |获得完整的输出。getline v方法:
$ awk 'BEGIN{cmd="seq 10"; cmd | getline out; close(cmd); print out}'
1
哎呀!我们知道命令seq 10将产生十行输出。然而,我们的awk只从输出中获取第一行。这是因为这种形式的getline命令一次从管道中读取一条记录。
getline命令本身有一个返回值。 如果仍然有来自管道的输出,则返回 1。否则,getline命令将返回 0:
$ awk 'BEGIN{cmd="seq 10";
for(i=1;i<=11;i++) {
retValue = cmd | getline out
printf "getline returns: %s; cmd output: %s\n", retValue, retValue?out:"Null"
}
close(cmd)}'
getline returns: 1; cmd output: 1
getline returns: 1; cmd output: 2
getline returns: 1; cmd output: 3
getline returns: 1; cmd output: 4
getline returns: 1; cmd output: 5
getline returns: 1; cmd output: 6
getline returns: 1; cmd output: 7
getline returns: 1; cmd output: 8
getline returns: 1; cmd output: 9
getline returns: 1; cmd output: 10
getline returns: 0; cmd output: Null
我们编写了一个循环来运行 getline 11 次。我们知道命令seq 10将输出十行。因此,我们在上面的输出中有十个getline 返回:1。
但是,在打印cmd output: 10之后,管道不再包含数据。现在,如果我们运行 getline命令并再次尝试从管道读取,该命令将返回 0。
因此,我们可以编写一个 while循环来获取外部命令的完整输出:
$ awk 'BEGIN{cmd="seq 10";
cmd_out=""
while(cmd | getline step_out){
cmd_out=cmd_out (cmd_out=="" ? "" : "\n") step_out
}
print cmd_out
close(cmd)}'
1
2
3
4
5
6
7
8
9
10
4.3. 不要忘记close(cmd)
我们已经看到了使用cmd |获取外部命令输出的示例。获取线变量。 值得一提的是,我们在**调用cmd | getline之后必须调用close(cmd)来关闭管道。否则,我们的awk脚本可能会产生错误的结果。**
让我们看看如果我们不关闭管道会发生什么。例如,假设我们有一个文本文件:
$ cat close_test.txt
"Awk is cool!"
"Sed is cool!"
"Awk is cool!"
"Sed is cool!"
在文件中,第一行和第三行是相同的,第二行和最后一行也是。
现在我们要为输入文件中的每一行调用外部*md5sum 命令以在每一行附加 MD5 哈希值。我们期望相同的行应该得到相同的 MD5 哈希值。在第一次尝试中,我们不调用close(cmd)*来关闭管道:
$ awk '{cmd="md5sum <<<"$0 ; cmd|getline md5; print $0,"MD5:" md5}' close_test.txt
"Awk is cool!" MD5:04cbd36582f5c11cce032ec44ec476d8 -
"Sed is cool!" MD5:f1844ba1dd262ecbbf798f7c38180693 -
"Awk is cool!" MD5:f1844ba1dd262ecbbf798f7c38180693 -
"Sed is cool!" MD5:f1844ba1dd262ecbbf798f7c38180693 -
输出显示最后三行的 MD5 哈希值相同。显然,这是一个错误的输出。
让我们简要解释一下为什么会发生这种情况。
**如果我们不关闭管道,每次将相同的外部命令管道传输到getline时,它将不会再次执行该命令。相反,它将尝试从上次执行的输出中读取下一条记录。**我们知道cmd | getline var命令将返回 1 或 0。如果管道中没有数据,它将返回 0,并且不会设置var变量。
让我们为每个输入行重置md5变量并打印出getline状态以更容易理解问题:
$ awk '{md5=""; cmd="md5sum <<<"$0
status=cmd|getline md5;
print $0,"getline status:"status,"MD5:" md5}' close_test.txt
"Awk is cool!" getline status:1 MD5:04cbd36582f5c11cce032ec44ec476d8 -
"Sed is cool!" getline status:1 MD5:f1844ba1dd262ecbbf798f7c38180693 -
"Awk is cool!" getline status:0 MD5:
"Sed is cool!" getline status:0 MD5:
问题的解决方法是在我们从管道读取输出后调用close(cmd) :
$ awk '{cmd="md5sum <<<"$0 ; cmd|getline md5;close(cmd); print $0,"MD5:" md5}' close_test.txt
"Awk is cool!" MD5:04cbd36582f5c11cce032ec44ec476d8 -
"Sed is cool!" MD5:f1844ba1dd262ecbbf798f7c38180693 -
"Awk is cool!" MD5:04cbd36582f5c11cce032ec44ec476d8 -
"Sed is cool!" MD5:f1844ba1dd262ecbbf798f7c38180693 -