第十章 底部抽屉

getline 函数

getline:手动读取下一行输入。对比next:

  • next:读完下一行,直接跳回脚本开头,不再执行本行剩余代码。
  • getline:读取一行,读取后停在当前位置继续往下执行后续代码,可以拿到读到的内容。

注意:getline是语句,不要写括号 getline()。

返回值:

返回值 含义
1 成功读到一行
0 到达文件末尾EOF
-1 发生读取错误

用法1:读取主输入流(标准输入键盘),覆盖$0、更新NF/NR/FNR

语法:getline 读取下一行,把内容存入$0,自动拆分字段,更新NF、NR、FNR。

示例:匹配.SH "Name",读取紧跟的下一行,打印第一个字段

1
2
3
4
/^\.SH "?Name"?/ {
    getline      # 读取下一行,覆盖$0
    print $1
}

while循环读取连续多行:读取Related Commands标题之后所有行,拼接字符串

1
2
3
4
5
/^\.SH "?Related Commands"?/ {
    print
    while (getline > 0)   #读到返回0(文件结束)停止
        commandList = commandList $0
}

用法2:从指定文件读取输入

语法:getline < "文件名" <是重定向符号,不是小于比较运算符。不会修改NR/FNR。

示例读取data文件所有行

1
2
3
while ((getline < "data") > 0) {
    print
}

读取标准输入,做交互提示

1
2
3
4
5
BEGIN {
    printf "Enter your name: "
    getline < "-"   # "-"代表标准输入
    print $0
}

写了变量名 getline xxx < "file" → 内容进变量 xxx,不碰 $0;
不写变量名 getline < "file" → 读到的行直接覆盖 $0。
两种外部文件读取方式,都不会改动 NR FNR。

用法3:读到自定义变量(不改变$0,不拆分字段)

语法:getline 变量名

正确:getline input
错误:input = getline,这是把函数返回值数字赋值给变量,不是读入的文本!

特点:

  • 读到的内容存到变量,不会修改$0,不会拆分字段,NF不变
  • NR、FNR仍然会+1
1
2
3
4
5
BEGIN {
    printf "Enter name: "
    getline name  # 用户输入存入name,$0不受影响
    print name
}

用法4:从管道读取shell命令输出

语法:"shell命令" | getline 执行shell命令,把命令输出交给getline。

  1. 直接覆盖$0:
1
2
"who am i" | getline
name = $1
  1. 读到自定义变量,不修改$0
1
2
"who am i" | getline me
print me

循环读取命令输出全部行:

1
2
3
4
5
6
BEGIN{
    i=0
    while ("who" | getline ){
        who_out[++i] = $0
    }
}

实战示例:替换文本中@date为系统当前日期

1
2
3
4
5
/@date/ {
    "date +'%a., %b %d, %y'" | getline today
    gsub(/@date/, today)
}
{ print }

重要提醒:管道/文件用完,建议close()关闭。

close() 函数

语法:close(字符串),字符串和当初打开的文件名/管道命令字符串完全一致。

作用:

  1. 释放已经用完的文件、管道;规避系统同时打开文件、管道数量上限。
  2. 同一个命令想要重复执行,必须先close关闭管道,否则命令不会重新运行。

示例关闭管道:

1
2
"who am i" | getline
close("who am i")

示例,输出到sort,读完后关闭管道,再读取临时文件:

1
2
3
4
5
6
7
{ print $0 | "sort > tmpfile" }
END {
    close("sort > tmpfile")   #必须关闭,保证sort写完数据
    while ((getline < "tmpfile")>0){
        print
    }
}

system() 函数

语法:system("shell命令字符串")

  • 执行shell命令;命令输出直接打印到屏幕,不会交给awk脚本处理。
  • 返回值:命令退出状态,0代表执行成功,非0代表失败。

示例,创建目录,判断是否成功:

1
2
3
4
5
BEGIN {
    if(system("mkdir dale") !=0 ){
        print "Command Failed"
    }
}

实现简化版soelim,解析troff .so包含文件:

1
2
3
4
5
6
/^\.so/ {
    gsub(/"/,"",$2)
    system("cat " $2)
    next
}
{ print }

区分记忆:

  • 管道 | getline:拿到命令输出给awk内部变量$0。
  • system():只执行命令,输出直接打印,awk拿不到输出内容。

基于菜单的命令生成器

综合案例:结合system()、getline。

  1. -v CMDFILE=xxx 在BEGIN块提前传入菜单配置文件名。
  2. getline < CMDFILE读取菜单配置文件。
  3. 数组保存菜单描述和对应的shell命令。
  4. system()执行用户选择的命令。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
awk -v CMDFILE="uucp_commands" '
BEGIN {
    FS = ": "
    if ((getline < CMDFILE) > 0)
        title = $1
    else
        exit 1
    while ((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
    } else {
        exit
    }
    display_menu()
}
function display_menu() {
    system("clear")
    print "\t" title
    for(i=1;i<=sizeOfArray;++i)
        printf "\t%d. %s\n", i, menu[i]
    printf "\t%d. Exit\n", i
    printf("Choose one: ")
}
' -

配置文件uucp_commands示例:

1
2
3
4
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

直接向文件和管道输出

  • print > "文件名" :截断方式打开,覆盖原有内容。
  • print >> "文件名":追加模式,保留原有内容,末尾新增。

注意:>在这里是重定向,不是大于号。表达式需要比较大小要用括号包裹。

1
print "max=", (a>b?a:b) > "data.out"

print输出到管道

语法:print | "shell命令"
把print输出,作为输入送给shell命令。

1
2
3
4
{
    sub(/^\.\../,"")
    print | "wc -w"
}

awk打开管道有数量上限;用完记得close("wc -w")关闭管道。
大多数场景,优先使用shell管道 awk '...' | wc -w,比awk内部管道可读性更好。

处理多个文件

系统对程序同时打开的文件、管道数量有限制。处理大量不同文件,用完必须close(),否则会耗尽资源脚本异常终止。

典型场景:按字段的值分割大文件,每行输出到不同文件。

1
2
3
4
{
    print $0 > $1
    close($1)   #立刻关闭,防止打开过多文件
}

man.split脚本示例,把一个大的man手册文件,拆分为多个独立小文件,使用数组缓存头部行,close(filename)及时关闭输出文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# man.split -- 拆分man帮助页大文件
BEGIN { file = 0; i = 0; filename = "" }
/^\.nr x 0/,/^$/ {
    if (file == 0)
        line[++i] = $0
    else
        print $0 > filename

    if ($4 == "x}") {
        filename = $5
        file = 1
        print filename
        for(x=1;x<=i;++x)
            print line[x] > filename
        i=0
    }
    if ($0 ~ /^$/){
        close(filename)
        filename=""
        file=0
        i=0
    }
}

生成柱状报告(业务综合案例)

需求:RS="" 空行分隔记录,每条记录为客户订单;统计各个商品总销量、总金额,输出汇总报表。
输入数据记录之间空行隔开。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/sh
# 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 * price
            total_vol += copies
            total_amt += amount
            vol[title] += copies
            amt[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 "\tTotal %4d\t$%7.2f\n", total_vol, total_amt
}
' $*

调试

awk没有内置调试器,依靠手工调试手段。两类bug:

  1. 语法错误:awk直接报错,容易定位。
  2. 逻辑错误:脚本运行完成无报错,输出结果不对,最难排查。

调试手段汇总

  1. 插入print打印中间变量,观察变量在关键点的值。
1
2
3
4
5
print "flag before=", flag
if(cond){
    flag=1
}
print "flag after=", flag
  1. 打印输入字段,确认输入数据和预想格式一致。
  2. 特殊标记打印,判断代码分支有没有走到:
1
2
3
if($0 ~ /retrieving/){
    print ">>> DEBUG 命中目标行" > "/dev/tty"
}
  1. 注释掉部分代码,缩小出错范围(临时注释标记#%方便批量恢复)
1
2
#% if( test )
    print "debug text"
  1. 删减代码(slash‑and‑burn):备份脚本,一点点删除代码,定位哪段代码引发异常。
  2. 管道中间使用tee保存中间输出,观察流水线每一步结果。
  3. 防御式编程:校验字段数量、数据合法性,捕获异常输入。

约束(awk内置限制)

POSIX传统awk有硬限制;gawk/mawk现代版本限制宽松很多。

表10‑1 awk传统版本约束

项目 约束上限
每个记录字段个数 100
单条输入记录字符数 3000
单条输出记录字符数 3000
单个字段字符数 1024
printf格式字符串字符数 3000
字面字符串字符数 400
字符类[]内字符数量 400
同时打开文件数 15
同时打开管道数 1

Bell Labs awk支持 -mf N 修改最大字段数;‑mr N 修改记录最大长度,规避限制。

使用#!语法调用awk脚本

脚本文件第一行写解释器指令,脚本直接可执行,不需要每次敲awk -f。

正确写法:

1
2
#!/bin/awk -f
{ print $1 }

必须带上‑f,否则awk会把脚本文件名当成输入数据,报语法错误。

使用:

1
2
chmod +x script.awk
./script.awk data.txt

注意:‑v参数不能写在#!行;var=value变量赋值,仍然可以在命令行传给脚本。

第十章核心速记清单

  1. getline系列
    • getline:读入下一行,覆盖$0,更新NF/NR/FNR。
    • getline var:读到变量,不改$0,不拆分字段;禁止写var=getline。
    • getline < "file":从文件读;<是重定向不是小于号。
    • "cmd" | getline:执行shell命令读取输出到awk。
    • 返回值:1成功;0 EOF;‑1错误。
  2. close("名字"):关闭已经用完的文件/管道;同一个管道想要再次运行命令,必须先close。
  3. system("cmd"):执行shell命令,输出直接打印;返回退出码;拿不到输出给awk内部。想要拿到输出,用管道+getline。
  4. 输出重定向:>覆盖,>>追加;print | "cmd"输出到管道;打开文件/管道记得close,防止到达系统上限。
  5. 调试:awk无调试器,依靠插入print打印中间变量、注释删减代码、tee保存中间结果。
  6. 传统awk存在记录长度、字段数、打开文件管道数量硬限制;gawk/mawk限制放宽。
  7. #!/bin/awk -f脚本头,直接可执行awk脚本,‑f不可省略。

容易混淆对比表

写法 行为
next 读取下一行,直接跳回脚本开头,后面代码不再执行
getline 读取下一行,读完继续向下执行当前脚本剩余代码
system() 执行命令,输出直接打印,awk拿不到输出
"cmd" | getline 执行命令,把输出交给 awk 变量 $0 处理