目录
一、getline 函数
1. 从输入数据流中读取
2. 从文件中读取
3. 将输入赋给一个变量
4. 从管道读取输入
二、close() 函数
三、system() 函数
四、基于菜单的命令生成器
五、直接向文件和管道输出
1. 直接输出到一个文件
2. 直接输出到一个管道
3. 处理多个文件
六、生成柱状报告
七、调试
1. 制作副本
2. 前像和后像
3. 找出问题的出处
4. 利用注释排除干扰
5. Slash and Burn
6. 为脚本设置防御措施
八、限制
awk-toc" style="margin-left:0px;">九、使用 #! 语法调用 awk
一、getline 函数
getline 函数用于从输入中读取下一行。getline 函数不仅能读取正常的输入数据流,而且也能处理来自文件和管道的输入。
1. 从输入数据流中读取
getline 函数类似于 awk 中的 next 语句,两者都是导致下一个输入行被读取。next 语句将控制传递回脚本的顶部;getline 函数得到下一行但不改变脚本的控制。可能的返回值为:
- 1:如果能够读取一行。
- 0:如果到了文件末尾。
- -1:如果遇到错误。
注意:尽管 getline 被称为一个函数并且返回了一个值,但它的语法类似于一个语句。不能写成 getline(),它的语法不允许有圆括号。
下面的例子匹配了标题“Name”,读取下一行,并打印它的第一个字段:
# getline.awk -- 测试 getline 函数
/^\.SH "?Name"?/ {getline # 取得下一行print $1 # 打印新行的 $1 值
}
这个模式匹配任意以“.SH”且其后跟有“Name”的行(Name 可能被包围在引号中)。一旦一行被匹配,使用 getline 读取下一个输入行。当读取新行后,getline 将它赋给 $0 并将它分解成字段,同时设置系统变量 NF、NR 和 FNR。因此新行变成当前行,这时可以引用 $1 并检索第一个字段。注意前面的行不再被看做是变量 $0。然而如果需要,可以用 getline 读取这行并将它赋给一个变量,从而避免改变 $0。
下面显示了该脚本如何工作:
$ awk -f getline.awk test
XSubImage
可以改写上一篇最后演示的 sorter.awk 脚本,使用 getline 读取标题“Related Commands”后面的所有行。下面这段脚本代替了 sorter 程序中的前两个过程:
# 匹配 "Related Commands" 并收集它们
/^\.SH "?Related Commands"?/ {printwhile (getline > 0)commandList = commandList $0
}
通过在 while 循环中测试 getline 的返回值来读取若干输入行。当 getline 成功读取一行时,表达式“getline > 0”为真。当到达文件末尾时,getline 返回 0 并退出循环。
2. 从文件中读取
getline 函数除了能读取正常的输入流外,还可以从一个文件或管道中读取。例如下面的语句从文件 data 中读取了一行:
getline < "data"
尽管文件名可以通过一个变量来提供,但它通常被指定为字符串常量,这个字符串必须用引号括起来。符号“<”和 shell 程序中的输入重定向符号一样。可以用 while 循环从文件中读取所有的行,测试到文件结束时循环退出。下面的例子打开文件 data 并打印它的所有行:
while ( (getline < "data") > 0 )print
这里加上括号以防止混乱,“<”表示重定向,而“>”是对返回值的一个比较。输入也可以来自标准输入,在提示用户输入信息后使用 getline:
BEGIN { printf "Enter your name: "getline < "-"print
}
执行情况:
$ awk 'BEGIN { printf "Enter your name: "getline < "-"print
}'
Enter your name: Tom
Tom
3. 将输入赋给一个变量
getline 函数允许将输入记录赋给一个变量,变量名作为一个参数来提供。下面的语句从输入中读取下一行并赋给变量 input:
getline input
将输入赋给一个变量不会影响当前的输入行,也就是说对 $0 没有影响。新的输入行没有被分解成字段,因此对变量 NF 也无影响。但它递增了记录计数器 NR 和 FNR。
可以按下面的方式,将用户响应赋给变量 name:
BEGIN { printf "Enter your name: "getline name < "-"print name
}
注意下面将输入数据赋给变量的语法是错误的:
name = getline
这条语句是将 getline 的返回值赋给变量 name。
4. 从管道读取输入
可以执行一个命令将输出结果用管道输送到 getline。下面的例子将 who am i 命令的输出结果赋给 $0:
$ awk 'BEGIN {"who am i" | getline; print}'
root pts/0 2024-11-04 08:59 (10.1.1.2)
这个行被分解为字段并设置了系统变量 NF。同样也可以将结果赋给一个变量:
$ awk 'BEGIN {"who am i" | getline me; print me}'
root pts/0 2024-11-04 08:59 (10.1.1.2)
通过将输出结果赋给一个变量可以避免设置 $0 和 NF,但输入行没有被分解为字段。
下面的脚本将一个命令的输出结果用管道输送给 getline。它使用 who am i 命令的输出结果得到用户名,然后在 /etc/passwd 中查找这个名字,打印出那个文件的第五个字段(用户全名):
awk '# getname - 从 /etc/passwd 文件中打印用户全名
BEGIN { "who am i" | getlinename = $1FS = ":"
}
name ~ $1 { print $5 }
' /etc/passwd
注意:FS 在执行 getline 之后设置,否则将影响到对命令的输出字段的分解。
当一个命令的输出结果被用管道输送给 getline 且包含多个行时,getline 一次读取一行。要读取输出的所有行,就必须创建一个循环来执行 getline,直到不再有输出为止。下面的例子使用 while 循环来读取输出的每一行并将它赋给数组 who_out 的下一个元素,然后在循环中打印所有行,在循环外打印指定行:
$ awk 'BEGIN {while ("ls -l test*" | getline) {who_out[++i] = $0; print} print who_out[3]}'
-rw-r--r-- 1 root root 208 10月 30 17:49 test
-rw-r--r-- 1 root root 14 10月 30 10:10 test1
-rw-r--r-- 1 root root 13 9月 20 15:36 test2
-rw-r--r-- 1 root root 51 10月 31 10:53 test3
-rw-r--r-- 1 root root 45 10月 21 10:41 test.do
-rwxr-xr-x 1 root root 76 9月 3 11:36 testsed
-rw-r--r-- 1 root root 13 9月 20 15:36 test2
每次调用 getline 函数时,读取输出的下一行。然而,其中的 ls -l test* 命令只执行一次。
下一个例子是在一个文档中查找“@date”,并用当天的日期替换它:
# subdate.awk -- 用当天日期替换 @date
/@date/ {"date +'%a., %h %d, %Y'" | getline todaygsub(/@date/, today)
}
{ print }
可用该脚本在格式化信件中插入日期:
$ cat subdate.test
To: Peabody
From: Sherman
Date: @dateI am writing you on @date to
remind you about our special offer.$ awk -f subdate.awk subdate.test
To: Peabody
From: Sherman
Date: Mon., Nov 04, 2024I am writing you on Mon., Nov 04, 2024 to
remind you about our special offer.
除了包含“@date”的行,“@date”被当天日期替换外,其余的所有行都按原样输出。
二、close() 函数
close() 函数用于关闭打开的文件或管道。使用它有以下几个原因:
- 为了在一个程序中能够打开所希望的数量的管道,必须用 close() 函数关闭一个用过的管道(通常是当 getline 返回 0 或 -1 时)。它用一个语句实现,和用于创建管道的表达式相同,例如:
close("who")
- 关闭一个管道使得可以运行同一个命令两次,例如可以用两次 date 来定时一个命令。
- 为了得到一个输出管道来完成它的工作,使用 close() 可能是必要的。例如:
{ some processing of $0 | "sort > tmpfile" } END {close("sort > tmpfile")while ((getline < "tmpfile") > 0) {do more work} }
- 为了保证同时打开的文件数不超过系统的限制,有必要关闭打开的文件。
本篇后面的“处理多个文件”会看到关于 close() 函数的例子。
三、system() 函数
system() 函数执行一个以表达式方式给出的命令,返回被执行命令的退出状态。脚本等待这个命令完成后才继续执行。下面的例子执行 mkdir 命令:
BEGIN { if (system("mkdir dale") != 0)print "Command Failed" }
这里在一个 if 语句中调用 system() 函数,来测试一个非零的退出状态。运行这个程序两次,结果是一次成功、一次失败:
$ awk -f system.awk
$ ls dale
$ awk -f system.awk
mkdir: 无法创建目录"dale": 文件已存在
Command Failed
下面用 awk 脚本实现 soelim 命令的功能:
/^\.so/ { gsub(/"/, "", $2)system("cat " $2)next}
{ print }
这个脚本查找在行的开始处的 .so,去掉所有的引号,然后用 system() 来执行 cat 命令并输出文件的内容。这些输出和文件中剩余的行合并,如下面的例子所示:
$ cat soelim.test
This is a test
.so test1
This is a test
.so test2
This is a test.$ cat test1
first:second
one:two$ cat test2
three:four
five:six$ awk -f soelim.awk soelim.test
This is a test
first:second
one:two
This is a test
three:four
five:six
This is a test.
这里没有显式地测试命令的退出状态,因此如果指定的文件不存在,出错信息将和输出融合在一起:
$ rm test2
$ awk -f soelim.awk soelim.test
This is a test
first:second
one:two
This is a test
cat: test2: 没有那个文件或目录
This is a test.
下面的例子是一个函数,用来提示输入一个文件名。它使用 system() 函数来执行 test 命令以验证文件存在并且可读:
$ cat getFilename.awk
# getFilename 函数 -- 提示用户输入文件名,验证文件名存在,并返回绝对路径
function getFilename( file) {while (! file) {printf "Enter a filename: "getline < "-" # 获取用户响应file = $0# 检查文件存在并可读,如果文件不存在则返回 1if (system("test -r " file)) {print file " not found"file = ""}}if (file !~ /^\//) {"pwd" | getline # get current directoryclose("pwd")file = $0 "/" file}return file
}
BEGIN {print getFilename()}$ awk -f getFilename.awk
Enter a filename: tt
tt not found
Enter a filename: test
/root/sed_awk/test
getFilename 函数返回了由用户指定的文件的绝对路径名。它把提示信息和验证序列放在 while 循环中,以便于用户在前一个文件非法时可以重新输入。BEGIN 过程中只有一条语句,调用并打印函数 getFilename 的返回值。调用函数时没有传参,使得初始的 file 为缺省的空串。
如果文件存在且可读,test -r 命令将返回 0,否则返回 1。一旦确定文件名正确,就测试文件名是否以“/”开始,斜杠表明用户提供了一个绝对路径名。如果测试失败,则使用 getline 函数获得 pwd 命令的输出结果,然后将它与文件名拼接起来。注意 getline 函数的两个用法:第一个是获得用户响应,第二个获取 pwd 命令的返回值。
四、基于菜单的命令生成器
system() 函数和 getline 函数的一个常见应用是建立基于菜单的命令生成器。菜单用于将要执行的命令的描述提示给用户,允许用户使用数字在菜单中选择要执行的任务。
这个程序被设计成一种解释程序,它从文件中读取要出现在菜单中的描述和要执行的实际命令行。菜单命令文件的格式是在文件中将菜单标题作为第一行,后面的行包括两个字段:第一个是要执行的命令的描述;第二个是要执行的命令行。例如:
$ cat uucp_commands
UUCP Status Menu
Look at files in PUBDIR:find /var/spool/uucppublic -print
Look at recent status in LOGFILE:tail /var/spool/uucp/LOGFILE
Look for lock files:ls /var/spool/uucp/*.LCK
下面是 invoke 程序的全部代码:
awk -v CMDFILE="uucp_commands" '# invoke -- 基于菜单的命令生成器# CMDFILE 中的第一行是菜单的标题
# 后面的行包含:$1 - 描述; $2 - 要执行的命令BEGIN { FS = ":"
# 处理 CMDFILE,将条目读入菜单数组if ((getline < CMDFILE) > 0)title = $1elseexit 1while ((getline < CMDFILE) > 0) {# 输入数组++sizeOfArray# 菜单项数组menu[sizeOfArray] = $1# 与菜单项相关的命令数组command[sizeOfArray] = $2}# 调用函数显示菜单项和提示符display_menu()
}# 处理用户响应
{# 测试用户响应的值if ($1 > 0 && $1 <= sizeOfArray) {# 打印执行的命令printf("Executing ... %s\n", command[$1])# 然后执行它system(command[$1])printf("<Press RETURN to continue>")# 在再次显示菜单前等待输入getline}elseexit# 重新显示菜单display_menu()
}function display_menu() {# 清屏system("clear")# 打印标题、项目列表、退出项目和提示符print "\t" titlefor (i = 1; i <= sizeOfArray; ++i)printf "\t%d. %s\n", i, menu[i]printf "\t%d. Exit\n", iprintf("Choose one: ")
}' -
创建基于菜单的命令生成器的第一步是读取菜单命令文件。首先读取文件的第一行并将它赋给变量 title。余下的行包括两个字段并被读到两个数组中,一个用于生成菜单项,另一个用于提供要执行的命令。在一个 while 循环中使用 getline 从这个文件中一次读取一行。
仔细观察使用 if 语句和 while 循环测试的表达式的语法:
(getline < CMDFILE) > 0
变量 CMDFILE 的菜单命令文件的名字,被作为一个命令行参数传递赋值。符号“<”被 getline 解释为输入重定向操作符,然后测试 getline 的返回值是否大于(“>”)0。在表达式中使用括号的目的是使表达式更清楚,即先执行“getline < CMDFILE”,然后再将它的结果与 0 比较。
这个过程被放在 BEGIN 模式中,下面的命令不会将文件名赋给 CMDFILE 变量,因为直到第一个输入行被读取后变量 CMDFILE 才有定义:
awk script CMDFILE="uucp_commands" -
有两种方法解决这个问题。第一是使用 awk 提供的 -v 选项,使得变量被立即设置并在 BEGIN 模式中可用,本例使用此方法:
awk -v CMDFILE="uucp_commands" script
第二种是更为通用的方法,就是将 CMDFILE 的值作为 shell 变量来传递。创建一个 shell 程序来执行 awk,并在该脚本中定义 CMDFILE,然后修改 invoke 脚本中读取 CMDFILE 的行:
while ((getline < '"$CMDFILE"') > 0 ) {
一旦菜单命令文件被装载后,程序必须显示菜单并提示用户,这是通过 display_menu 函数实现的。需要在两个地方调用它:从 BEGIN 模式中调用它以向用户提供初始提示;在处理了用户响应后再调用它以便进行下个选择。该函数先使用 system() 执行清屏命令,然后打印标题和编号列表中的每项,最后一项始终是“Exit”,最后提示用户选择。
程序中读取菜单命令文件不属于处理输入流(在 BEGIN 中)。这个程序接收标准输入,这使得用户对提示的响应将被作为输入的第一行。主程序部分就是对用户的选择做出响应并执行命令。
首先检查用户输入,如果超出范围则退出程序。如果是有效输入,则从 command 数组中检索相应的命令,显示它,并使用 system() 函数执行它。用户将在屏幕上看到命令的执行结果,后面跟有信息“<Press RETURN to continue>”。显示这个信息的目的是在清屏并重新显示菜单前等待用户完成操作。getline 函数使程序等待用户响应,但对这个响应不做任何事情。函数 display_menu 在这个过程的末尾被调用,重新显示菜单并提示输入选择。
执行这个程序时显示如下的输出:
UUCP Status Menu1. Look at files in PUBDIR2. Look at recent status in LOGFILE3. Look for lock files4. Exit
Choose one:
用户被提示输入菜单选项的编号。1、2、3 以外的任何输入都将退出程序,否则执行相应命令。例如输入“1”将显示下面的结果:
Executing ...find /var/spool/uucppublic -print
/var/spool/uucppublic
/var/spool/uucppublic/dale
/var/spool/uucppublic/HyperBugs
<Press RETURN to continue>
再按 RETURN 键时,菜单将重新显示。用户可以选择“4”来退出这个程序。
这个程序实际上就是一个用以执行其它命令的 shell,任何命令(甚至是其它 awk 程序)都可以通过修改菜单命令文件来被执行。换句话说,程序中最可能需要修改的部分被提取出来,并通过维护一个独立的文件来完成修改,便于改变和扩展菜单列表。
五、直接向文件和管道输出
1. 直接输出到一个文件
任何 print 和 printf 语句可以用输出重定向操作符“>”或“>>”直接将输出结果写入一个文件中,例如下面的语句将当前记录写到文件 data.out 中:
print > "data.out"
文件名可以是任何能产生合法文件名的表达式。第一次使用重定向操作符将打开文件,随后使用重定向操作符将数据追加到文件中。“>”和“>>”之间的区别和 shell 中的重定向操作符之间的区别相同:“>”在打开一个文件时截断它(清空原有内容后重新写入),“>>”保存文件中包含的任何内容并向文件中追加数据。
因为重定向操作符“>”和关系操作符的大于号一样,为避免用表达式作为 print 命令的参数时可能产生混淆,规定当“>”出现在任何打印语句的参数列表中时被看做是重定向操作符。要想使“>”出现在表达式的参数列表中时被看做是关系操作符,可以用圆括号将表达式或参数列表括起来,例如:
print "a =", a, "b =", b, "max =", (a > b ? a : b) > "data.out"
2. 直接输出到一个管道
也可以将输出直接写入一个管道,命令为:
print | command
该命令在第一次执行时打开一个管道,并将当前记录作为输入输送给 command。这里的命令只执行一次,但每执行一次 print 命令将提供下一个输入行。
下面的脚本计算文件中有多少个单词:
{# words.awk - 去掉宏,然后得到单词数
sub(/^\.../,"")
print | "wc -w"
}
执行情况:
$ cat test
.SH "Name"
XSubImage - create a subimage from part of an image.
.
.
.
.SH "Related Commands"
XDestroyImage, XPutImage, XGetImage,
XCreateImage, XGetSubImage, XAddPixel,
XPutPixel, XGetPixel, ImageByteOrder. $ awk -f words.awk test
25
注意,每次只能打开一定数量的管道,使用 close() 函数关闭用过的管道。 大多数情况下是使用 shell 脚本将 awk 命令的输出结果用管道输送到另一个命令,而不是在 awk 脚本中来处理:
$ cat words.sh
awk '{ # words.sh
sub(/^\.../,"")
print
}' $* |
# 得到单词数量
wc -w$ words.sh test
25
3. 处理多个文件
当读写文件时文件被打开。每个操作系统对一个正在运行的程序能够同时打开的文件的数量有一定的限制,每个 awk 实现对打开文件的数量也都有内部限制,这个数字可能比系统限制要小。为避免打开过多文件,awk 提供了 close() 函数用于关闭打开的文件。关闭已经处理完的文件可使程序打开更多的文件。
直接向文件写入输出的一个常见方法,是将一个大文件分割成几个小文件。尽管 UNIX 提供的 split 和 csplit 命令可以完成相同的工作,但它们不能为一个新文件指定一个有用的文件名。
类似地,也可以用 sed 来写入文件,但必须指定一个固定的文件名。在 awk 中可以用变量来指定文件名,或从文件的模式中挑选一个值作为文件名。例如,如果 $1 提供了一个可以作为文件名的字符串,则可以编写一个脚本将每个记录输出到它对应的文件中:
print $0 > $1
如果不关闭文件,那么这个程序最终将用完可以打开的文件数而不得不中止。
下面的脚本用于将一个包含大量帮助页的大文件分割为小文件。每个帮助页以一个注册编号开始并以一个空行结束:
.nr X 0
提供文件名的行如下所示:
.if \nX=0 .ds x} XDrawLine "" "Xlib - Drawing Primitives"
这行的第五个字段,“XDrawLine”作为文件名。将这些行写入一个数组直到找到文件名,一旦找到文件名就输出这个数组,并从这一位置开始将每个输入行写入该新文件中。下面是 man.split 脚本:
# man.split -- 分隔包含 x 的帮助页的文件
BEGIN { file = 0; i = 0; filename = "" }# 新的帮助页第一项是 “.nr X 0”,最后一行为空
/^\.nr X 0/,/^$/ {# 这个条件收集行直到得到一个文件名if (file == 0)line[++i] = $0else # 输出文件名所在行后面的行,包括最后的空行print $0 > filename# 匹配提供文件名的行if ($4 == "x}") {# 现在有一个文件名filename = $5file = 1# 将文件名输出到屏幕print filename# 打印收集的所有行for (x = 1; x <= i; ++x){print line[x] > filename}i = 0}# 关闭文件,重新初始化变量if ($0 ~ /^$/) {close(filename)filename = ""file = 0i = 0}
}
这里使用变量 file 作为标记表示是否找到文件名。file 初始为 0,找到文件名前,当前输入行被存储在一个数组中,变量 i 是该数组的下标计数器。当遇到设置文件名的行时,将 file 设置为 1(此时该行已经被写入数组)。打印文件名是为了用户能从程序执行过程中得到一些反馈。然后循环访问数组,并将它输出到文件中。当读取下一个输入行时,file 已被设置为 1,else 分支中的 print 语句把当前行输出到被命名的文件中。直到遇到空行时,重新初始化变量(此时空行也已被写入文件)。下面演示该脚本的执行情况。
文件 a 内容如下:
.nr X 0
aaa
bbb
.if \nX=0 .ds x} XDrawLine "" "Xlib - Drawing Primitives"
ccc
ddd.nr X 0
aaa1
bbb1
.if \nX=0 .ds x} XDrawLine1 "" "Xlib - Drawing Primitives"
ccc1
ddd1
执行脚本:
$ awk -f man.split a
XDrawLine
XDrawLine1
查看输入文件的内容:
$ cat XDrawLine
.nr X 0
aaa
bbb
.if \nX=0 .ds x} XDrawLine "" "Xlib - Drawing Primitives"
ccc
ddd$ cat XDrawLine1
.nr X 0
aaa1
bbb1
.if \nX=0 .ds x} XDrawLine1 "" "Xlib - Drawing Primitives"
ccc1
ddd1
六、生成柱状报告
下面是客户订单文件中的两个记录样本:
Charlotte Webb
P.O N61331 97 Y 045 Date: 03/14/97
#1 3 7.50
#2 3 7.50
#3 1 7.50
#4 1 7.50
#7 1 7.50Martin S. Rossi
P.O NONE Date: 03/14/97
#1 2 7.50
#2 5 6.75
每个订单包含多行,用一个空行将不同订单分开。前面两行提供了客户姓名、订单编号和订货日期。后面的每行用编号表示一个条目,包括数量和单价。下面的脚本为每个订单条目增加金额字段(数量乘以单价):
awk '/^#/ {amount = $2 * $3printf "%s %6.2f\n", $0, amountnext
}
{ print }' $*
以 # 开头的条目行,计算并多打印金额字段,其它行原样输出。printf 格式转换符“%f”用于打印一个浮点型数据,“6.2”指定最小输出宽度为 6 且精度(小数位数)为 2,%f 的默认值是 6。脚本执行结果如下:
$ addem orders
Charlotte Webb
P.O N61331 97 Y 045 Date: 03/14/97
#1 3 7.50 22.50
#2 3 7.50 22.50
#3 1 7.50 7.50
#4 1 7.50 7.50
#7 1 7.50 7.50Martin S. Rossi
P.O NONE Date: 03/14/97
#1 2 7.50 15.00
#2 5 6.75 33.75
下面设计一个程序读取多个记录,并累计订单信息,显示每个条目总的数量和金额,并产生所有订单的合计数量和金额。以下是整个程序:
$ cat addemup
#! /bin/bash
# addemup -- 合计客户订单
awk 'BEGIN { FS = "\n"; RS = "" }
NF >= 3 {for (i = 3; i <= NF; ++i) {sv = split($i, order, " ")if (sv == 3) {title = order[1]copies = order[2]price = order[3]amount = copies * pricetotal_vol += copiestotal_amt += amountvol[title] += copiesamt[title] += amount} else print "Incomplete Record"}
}
END {printf "%5s\t%10s\t%6s\n\n", "TITLE", "COPIES SOLD", "TOTAL"for (title in vol)printf "%5s\t%10d\t$%7.2f\n", title, vol[title], amt[title]printf "%s\n", "-------------"printf "\t%s%4d\t$%7.2f\n", "Total ", total_vol, total_amt
}' $*
BEGIN 过程中设置字段分隔符为“\n”并且记录分隔符为空,实际上相当于做了一个将每个订单的多行转为一行多列的操作。每个记录的字段数依赖于该订单的条目数。首先检查输入记录至少有三个字段,然后用一个 for 循环从第三个字段开始读取所有字段。split 函数将一个字段中的子值(编号、数量、单价)分割到数组 order 的元素中:order[1] 存编号,order[2] 存数量,order[3] 存单价。split() 函数的返回值如果是 3 则进行计算和累加,否则打印错误信息。END 过程用于打印报告,其中的 for 循环遍历存储每个编号的合计数量和金额的两个数组。 订单报告生成器 addemup 执行结果如下:
$ addemup orders
TITLE COPIES SOLD TOTAL#1 5 $ 37.50#2 8 $ 56.25#3 1 $ 7.50#4 1 $ 7.50#7 1 $ 7.50
-------------Total 16 $ 116.25
七、调试
本节说明 awk 脚本的调试方法,并给出当 awk 程序不能如期工作时,应如何修改 awk 程序的建议。程序一般有逻辑和语法两类问题,逻辑问题没有出错信息,但结果不对,是程序思路上的错误(bug),而语法错误通常会产生错误信息以引导找到问题所在。
1. 制作副本
调试程序前先备份!对程序的修改很多是没有效果或可能产生新的错误,这时就需要将所做的修改进行恢复。
建议把创建程序的步骤看成几个阶段,将设置每个核心工作作为一个单一的阶段。一旦完成了这些功能并且测试了它们后,在进入下一阶段设计新的功能之前为程序做一个备份。这样当增加的新代码产生问题时就能恢复到前面的状态。
2. 前像和后像
在 awk 调试中的困难是并不是总知道程序执行过程中发生了什么。可以检查输入和输出,但没办法中止程序执行并检查它的状态。(一句话就是 awk 脚本程序没有单步跟踪或 debug 调试器。)
通常的问题是确定程序在什么时间或什么地方对变量赋值。第一个方法是使用 print 语句在程序中的不同位置打印变量的值。使用一个变量作为标志来确定产生了某种特定的条件是很通用的。例如在程序开始部分标志被设置为 0,在程序的一个或多个点设置标志为 1。如果想在程序的特定部分检查这个标志,在赋值前后应用 print 语句。例如:
print flag, "before"
if (! $1) {...flag = 1
}
print flag, "after"
如果不能确定一个替换命令或函数产生的结果,在调用函数前后打印相应的字符串:
print $2
sub(/ *\(/, "(", $2)
print $2
在替换命令之前打印值是为了确认输入的值。前面的命令可能已经改变了那个变量,问题将可能是输入记录的格式可能不是所想象的。仔细检查输入是调试的一个重要步骤,尤其是可以是使用 print 语句证明输入的各个字段与所期望的一致。
3. 找出问题的出处
一个脚本越是模块化,可以分割成单独的部分就越多,测试和调试就越容易。编写函数可以将函数内部所做的处理隔离开,并在不影响程序其它部分的情况下测试它。
如果一个程序有几个分支,而输入行是通过一个分支传递的,要测试输入到达程序的哪部分。例如,想知道包含单词“retrieving”的条目在程序的特定部分是否被处理,可以将下面的行插入到认为该单词在程序中应该出现的地方:
if ($0 ~ /retrieving/) print ">> retrieving" > "/dev/tty"
当程序运行时,如果遇到字符串“retrieving”,它将打印一个信息。有时可能不知道哪个 print 语句引起的问题,可以在 print 语句中插入标识符,以提示执行了哪个 print 语句,例如:
if (PRIMARY)print (">>PRIMARY:", PRIMARY)
elseif (SECONDARY)print (">>SECONDARY:", SECONDARY)elseprint (">>TERTIARY:", TERTIARY)
这项技术也被用来检查程序的某个部分究竟是否被执行。如果一个 awk 程序是其它几个程序的管道的一部分,甚至是其它的 awk 程序的一部分,可以用 tee 命令将输出重定向到一个文件,例如:
$INDEXDIR/input.idx $FILES |
sort -bdf -t: +0 -1 +1 -2 +3 -4 +2n -3n | uniq |
$INDEXDIR/pagenums.idx | tee page.tmp |
$INDEXDIR/combine.idx |
$INDEXDIR/format.idx
tee 命令通常用于将命令的输出保存到文件中,同时在终端上显示输出结果。通过添加“tee page.tmp”,能够将 pagenums.idx 程序的输出结果捕获到文件 page.tmp 中。相同的输出通过管道输送到 combine.idx。
4. 利用注释排除干扰
另一个简单的技术是注释掉一系列可能引起问题的行,看它们是否真的有问题。这里建议使用连续的两个字符符号,例如“#%”来临时注释掉这些行。这样在后续的编辑中就能注意到它们,并做相应的处理。也便于将这些注释符号去掉并用一个编辑命令来恢复这些行,而不至于影响程序的正常注释。例如下面的语句将条件注释掉,使得 print 语句能够无条件被执行:
#% if ( thisFails )print "I give up"
5. Slash and Burn
当所有的方法都失败时,可以删除部分程序直到错误消失。当然要对程序做一个备份并在临时备份上删除行。这是一个非常笨拙的技术,却是在全部放弃或重新从头开始之前一个有效的办法。有时当仅有的结果是核心程序中断时,这是唯一可以用来查找出现什么问题的方法。其思路也是将可能引起问题的部分孤立起来。例如,删除一个函数或 for 循环,看它是否是引起问题的原因。确信删除的是完整的单元。
用“slash and burn”来解释程序是如何工作的。首先使用样本输入运行原始程序,并保存输出。从不理解的部分程序代码开始删除,然后在样本输入上运行修改过的程序并和原始输出相比较,看发生了什么变化。
6. 为脚本设置防御措施
各种类型的输入错误和不一致会使脚本的运行出现问题,一个好的建议是将核心程序用“防御”过程包围起来,以捕获不一致的输入记录和防止程序意外失败。例如,在处理每个记录前对它进行验证,确保存在正确的字段数,或指定字段具有所希望的数据类型。另一个与防御技术有关的方面是错误处理,在某些情况下可以使程序继续运行,在另一些情况下要打印出错信息和/或使程序终止。
编写防御程序耗时且乏味,而 awk 只限定在固定领域,没必要要求 awk 程序具有专业质量。
八、限制
下表列出了特定 awk 实现中的限制,但对大多数系统是一个好的参考值。(与表中的数据不同,大多数 awk 允许打开的管道数大于 1。)
项目 | 约束 |
每个记录中字段的个数 | 100 |
每个输入记录的字符个数 | 3000 |
每个输出记录的字符个数 | 3000 |
每个字段的字符个数 | 1024 |
每个printf字符串的字符个数 | 3000 |
字面字符串中的字符个数 | 400 |
字符类中的字符个数 | 400 |
打开的文件数 | 15 |
打开的管道数 | 1 |
对于数值型数据,awk 使用双精度类型,浮点型数据的长度限制由硬件架构决定。超过这些限制将使脚本产生无法预测的问题。gawk 的限制比表中的大,例如,一个记录中的字段数目最大不能超过 long 类型所能表示的范围,记录可以比 3000 个字符长,也允许打开更多的文件和管道。
sed 实现也有限制,但没在文档中说明。实践表明,大多数 UNIX 版本的 sed 对替换(s)命令数的限制是 99 或 100。
awk">九、使用 #! 语法调用 awk
“#!”是从 shell 脚本中调用 awk 的可选语法,其优点是可以在 shell 脚本的命令行中指定 awk 的参数和文件名。运用这个语法最好的方法是将下面一行作为 shell 脚本的第一行:
#!/bin/awk -f
“#!”后面跟的是 awk 所在路径,然后是 -f 选项。这行之后可以写 awk 脚本:
#!/bin/awk -f
{ print $1 }
脚本不必使用引号,在第一行后面的所有行都能像在单独的脚本文件中一样被执行。
下面的脚本执行失败:
$ cat t.sh
#!/bin/awk
{ print $1 }$ t.sh test
awk: cmd. line:1: t.sh
awk: cmd. line:1: ^ syntax error
awk: cmd. line:1: t.sh
awk: cmd. line:1: ^ unterminated regexp
失败的原因是 awk 将脚本文件作为输入文件而不是脚本文件解释,因为没有给出脚本,所以 awk 产生语法错误。换句话说,脚本是按下面的语句执行:
/bin/awk t.sh
如果将程序改为添加 -f 选项,执行如下:
$ cat t.sh
#!/bin/awk -f
{ print $1 }$ t.sh test
.SH
XSubImage
.
.
.
.SH
XDestroyImage,
XCreateImage,
XPutPixel,
和输入下面的语句一样执行:
/bin/awk -f t.sh test
注意:在“#!”行只能有一个参数。这行将由 UNIX 内核处理,而不是由 shell 处理,因此不能包含任意 shell 构件。
“#!” 语法允许创建 shell 脚本,来将命令行参数透明地传递给 awk,即可以通过调用 shell 脚本的命令行来给 awk 传递参数。例如通过改变 awk 脚本来传递参数 n:
{ print $1*n }
可以按下面的方式调用该程序:
$ myscript n=4 myfile
执行情况:
$ cat myscript
#!/bin/awk -f
{ print $1*n }$ cat myfile
1
2
3
4
5$ myscript n=4 myfile
4
8
12
16
20