第9章 awk的惊人表现

本章定位:shell脚本中awk工具完整学习。awk专门做文本处理,POSIX标准工具,各UNIX/Linux都自带;常见实现:nawk/gawk/mawk
核心思想:数据驱动编程,自动循环读取输入记录(行),模式匹配成功则执行操作。

9.1 awk命令行

调用语法

两种使用方式:直接写程序片段;把awk代码放到文件用-f读取

1
2
3
4
5
# 方式1:命令行直接写awk代码
awk [-F fs] [-v var=value...] 'program' [--] [var=value...] [file(s)]

# 方式2:awk代码存文件,-f加载程序文件
awk [-F fs] [-v var=value...] -f programfile [--] [var=value...] [file(s)]
选项 作用
-F fs 字段分隔符,fs是正则表达式;等价修改内建变量FS
-v var=value 处理输入文件之前初始化变量,必须写在awk程序代码前面
-f file 从文件读取awk程序,可以多次使用,用于代码库复用
-- 标志:后面全部内容不再解析为选项,全部当做参数/文件名

重要规则&示例

  1. 不写输入文件,则读取标准输入(stdin);特殊文件名-代表标准输入。
  2. -v必须放在awk程序字符串前面;放在后面会被当成文件名。
  3. 可以在文件名位置赋值变量,文件处理的时候动态改变变量:
1
2
# 第一次处理 *.tex Pass=1;第二次处理 *.tex Pass=2
awk '{print Pass, $0}' Pass=1 *.tex Pass=2 *.tex
awk '{print Pass, $0}' Pass=1 *.tex Pass=2*.tex 解析

拆解这条命令

1
awk '{print Pass, $0}' Pass=1 *.tex Pass=2 *.tex

先把语法格式重新拆开看:

1
awk [-F fs] [-v var=value...] 'program' [--] [var=value...] [file(s)]
  1. 'program' 这一整块,就是awk程序本体

{print Pass, $0} 全部包裹在单引号 ' ' 里面,属于 program 的内容!,$0 属于 awk 语法:代表当前整行记录。
{ } 大括号是 awk语言内部语法,不属于shell命令行选项语法。

shell层面:单引号把一整块字符串传给awk,shell不解析里面的大括号。 awk拿到字符串之后,awk解释器才解析 { print Pass, $0 }

整行命令分段拆分

1
2
awk      '{print Pass, $0}'     Pass=1     *.tex     Pass=2     *.tex
# ^命令     ↑ program程序        ↑赋值       ↑文件      ↑赋值       ↑文件

按照语法模板:

1
awk   'program'   [var=value...]   [file(s)]
  • program = {print Pass, $0}
  • 后面跟着一堆参数,分两类:
    1. var=value 格式:这是运行时变量赋值,不是文件!
    2. 其他:当做输入文件名。

执行顺序(非常关键)

awk处理后面参数是从左到右逐个处理

  1. 遇到 Pass=1 → 设置awk变量 Pass=1
  2. 遇到 *.tex → 把这些tex文件当做输入,读取处理;此时Pass=1生效
  3. 遇到 Pass=2 → 修改变量 Pass=2
  4. 遇到 *.tex → 再次读取同样一批tex文件;此时Pass=2生效

效果:同一批文件处理两遍
第一遍:Pass=1,打印 1 原始行内容
第二遍:Pass=2,打印 2 原始行内容

示例输出:

1
2
3
4
1 第一行文本
1 第二行文本
2 第一行文本
2 第二行文本

区分两个完全不同层面,很多人混淆在这里

① Shell命令行层面(操作系统shell,bash/sh)

1
awk  'xxxx'  a b c
  • awk:shell要执行的外部命令。
  • 单引号 'xxxx':shell的引号,保护里面所有内容原封不动,完整传给awk程序

shell完全看不懂{}{}对shell来说就是普通字符。shell不解析awk语法!

② awk语言层面(awk程序,单引号里面的内容)

{print Pass, $0} 交给awk之后,awk解释器来解析:

1
2
3
{                # action操作大括号
    print Pass, $0   # 打印变量Pass的值,再打印整条记录$0(当前行)
}
  • { ... }:awk语法,代表操作action块;
  • Pass:awk变量,不是shell变量;
  • $0:awk内置变量,当前输入的一整行记录。

✔记住分界线:单引号内部全部属于awk语言世界;外面是shell世界。

对比 -v 和 后置 var=value 的区别(重点!)

  1. -v var=value
1
awk -v Pass=1 '{print Pass, $0}'  file.txt

‑v 在读取任何输入文件之前就完成赋值,属于初始化,写在awk程序'...'的前面。

  1. 写在程序后面的 Pass=1 file.txt
1
awk '{print Pass, $0}' Pass=1 file.txt

这个赋值发生在处理该参数的时候,是处理文件的过程中动态修改变量。
⚠不能写在 'program'前面!只能写在program之后。

❌错误:

1
2
awk Pass=1 '{print Pass,$0}' file.txt
# Pass=1 写在program前面,awk会把Pass=1当成输入文件名!

✅正确:var=value必须写在 'program' 的后面。

再看语法模板

1
2
awk [-F fs] [-v var=value...] 'program' [--] [var=value...] [file(s)]
#                                  ↑program      ↑这里才允许写var=value

[-v ...] 在program之前[var=value...] 在program之后

极简小测试,复制运行体会

准备一个简单测试文件 test.txt

1
2
apple
banana

执行这条命令:

1
awk '{print "Pass="Pass, $0}' Pass=1 test.txt Pass=2 test.txt

输出:

1
2
3
4
Pass=1 apple
Pass=1 banana
Pass=2 apple
Pass=2 banana

可以看到:文件test.txt被读了两遍,第一遍Pass=1,第二遍Pass=2。

总结

  1. 语法模板里没有看到{},但是代码里有'{print Pass, $0}'
  • {print Pass,$0}全部在shell单引号' '内部,属于模板中的program
  • {}awk语言的语法,不是shell命令行选项;shell只负责把整串字符串交给awk。
  1. Pass=1 *.tex Pass=2 *.tex看不懂
  • 在awk程序字符串'...'之后,可以写变量=值,这不是文件名;awk从左到右处理参数;
  • 先赋值Pass=1,处理tex文件;再赋值Pass=2,再次处理tex文件;
  • 效果:同一套文件处理两轮,每轮变量值不一样。

记忆口诀

-v 在awk程序前面,全局初始化;
var=value 在awk程序后面,处理文件时动态修改;
单引号 ' '里面:全部归awk管,大括号、$0都是awk语法,shell不碰。

示例:

1
2
3
4
5
# -F 指定制表符\t作为字段分隔符
awk -F'\t' '{print $1}' test.txt

# FS变量设置字段分隔符
awk '{print $1}' FS="[:;]" test.txt

9.2 awk程序模型

awk核心:模式(pattern) {操作(action)}
awk自动:读取每一条记录(默认一行),拿记录匹配模式;匹配成功执行大括号里面操作。

4种基础写法

写法 说明
pattern { action } 模式匹配 → 执行操作
pattern 省略操作,匹配成功,默认打印整条记录($0)
{ action } 省略模式,对每一条记录都执行操作
什么都不做

两个特殊模式:BEGIN / END

  1. BEGIN { ... } 读取任何输入文件之前执行,只运行一次。适合做初始化:设置变量、打印表头。
  2. END { ... } 全部输入处理完成之后执行,只运行一次。适合输出统计汇总报告。

注意:可以写多个BEGIN、END块,按照代码从上到下顺序依次执行。

示例:

1
2
3
4
5
6
7
8
BEGIN {
    FS=","   # 在读取数据前设置字段分隔符
    print "name\tage"
}
{print $1 "\t" $2}
END {
    print "处理完成!一共",NR,"行记录"
}

9.3 程序元素

awk支持:字符串、数字、变量(标量、关联数组)、表达式、分支、循环、函数。

9.3.1 注释与空白

  1. # 行注释,从#到行结尾。
  2. 语句换行:一行放不下,行末尾加\换行。
1
2
3
# 这是注释
print "hello \
world"

9.3.2 字符串与字符串表达式

  1. 字符串常量使用双引号 "abc";不支持单引号字符串。
  2. 转义:\t tab,\n换行;gawk支持\xHH十六进制;POSIX awk只支持八进制\0xx
  3. 字符串自动拼接:两个字符串紧挨着写,自动合并,不需要任何运算符。
1
2
s = "AB" "CD"   # s = "ABCD"
t = s s         # t = "ABCDABCD"
  1. 字符串比较运算符:== != < <= > >=
  2. 正则匹配运算符:
    • ~ 匹配正则
    • !~ 不匹配正则
1
2
3
# ^行首锚点,$行尾锚点,+前面的字符,至少出现 1 次,可多次
# 判断字符串是否全部大写
if ("HELLO" ~ /^[A-Z]+$/) { print "全大写" } 

正则两种写法:/正则/ 或者 "正则字符串",推荐斜杠写法。

9.3.3 数字与数值表达式

awk所有数字全部是双精度浮点数,没有int整数类型。

  • 字符串转数字:s="123"; n=0+s 加0触发转换。
  • 数字转字符串:n=123; s="" n 拼接空字符串。

运算符优先级(从高到低)

运算符 说明
++ -- 自增自减(前置/后置)
^ ** 指数,右结合 a^b^c → a^(b^c)
! + - 逻辑非、一元正负号
* / % 乘、除、取模
+ - 加减
< <= > >= == != 比较
&& 逻辑与(短路)
`
?: 三元运算符 cond ? a : b
= += -= *= /= %= ^= 赋值,右结合

示例三元运算符:

1
max = (a > b) ? a : b

内置数学小函数:int() sqrt() sin() cos() log() exp()

1
n = int(3.14)   # 取整数部分 →3

9.3.4 标量变量

  1. 变量不需要声明,第一次使用自动创建;初始值:空字符串,当做数字时等于0。
  2. 命名规则:[A‑Za-z_]开头,后续字母数字下划线;区分大小写 a A是两个变量。
  3. 约定:内置变量全部大写。

awk常用内置标量变量

变量 含义
FS 输入字段分隔符,默认空格;等价‑F
OFS 输出字段分隔符,默认空格
RS 输入记录分隔符,默认换行\n
ORS 输出记录分隔符,默认换行\n
NR 全局总记录号,所有文件累加行号
FNR 当前文件的记录号,每个文件从1重新计数
NF 当前记录字段数量(一行多少列)
FILENAME 当前正在处理的输入文件名

9.3.5 数组(关联数组)

awk是关联数组:索引可以是字符串,不强制只能数字。类似hash字典。

1
2
3
# key是字符串
phone["alice"] = "13800000001"
phone["bob"] = "13900000002"

遍历数组:

1
2
3
for(key in phone){
    print key, phone[key]
}

9.3.6 命令行参数 ARGC / ARGV

  • ARGC:参数总个数
  • ARGV[]数组,保存命令行参数;ARGV[0]是awk程序名字。

ARGCARGVawk 内置自带的全局变量(默认就存在,不用你定义),作用:存放 awk 命令行上除了 awk 选项 (-v/-F/-f 等) 之外的所有参数。

命令行参数 ARGC / ARGV 详解

注意:不是shell的$(@/)$argv,是awk自己独立维护的一套参数数组

1. 哪些参数会进 ARGV?哪些不会?

命令模板:

1
awk 【选项:-v -f -F】 'program'  arg1 arg2 file1 file2

会放进ARGV数组program后面写的所有东西(变量赋值、文件名)
不会进ARGV-v-f-F这类awk命令行选项,以及选项后面跟着的值。

ARGV[0]固定是字符串awk(程序名),从ARGV[1]开始才是后面的参数。
ARGC = ARGV数组里面元素总数量。

拿这条例子拆解

1
awk -v A=1 -f showargs.awk file1.txt file2.txt
  • -v A=1:选项,不进ARGV
  • -f showargs.awk:选项,不进ARGV
  • file1.txtfile2.txt:program后面的参数,进入ARGV

所以awk内部:

1
2
3
4
ARGV[0] = "awk"
ARGV[1] = "file1.txt"
ARGV[2] = "file2.txt"
ARGC = 3   # 一共3个元素:0,1,2

运行showargs.awk,输出:

1
2
3
4
ARGC= 3
0 awk
1 file1.txt
2 file2.txt

2. 再举一个带 var=value 的例子(就是刚才Pass那个)

1
awk '{print Pass,$0}' Pass=1 test.tex Pass=2 test.tex
  • '{print Pass,$0}' 是program代码,不算参数
  • Pass=1test.texPass=2test.tex 全部在program之后,全部进入ARGV

ARGV数组:

1
2
3
4
5
6
ARGV[0] = "awk"
ARGV[1] = "Pass=1"
ARGV[2] = "test.tex"
ARGV[3] = "Pass=2"
ARGV[4] = "test.tex"
ARGC = 5

这就是awk为什么能识别Pass=1这种后置变量赋值:awk遍历ARGV数组,遇到xxx=yyy格式,就当成变量赋值;其余的当做输入文件名

3. 关键细节(书里重点)

  1. ARGV数组可以修改! 你可以在BEGIN代码里删除/修改ARGV的元素,来控制awk读取哪些文件。

很多高级awk脚本靠修改ARGV来动态控制待处理文件列表。

  1. 区分:
    • shell的$0 $1 $@:是shell拿到的命令行参数,awk看不到。
    • awk的ARGV / ARGC:awk启动之后,自己解析、单独保存的参数列表。两者完全独立。
  2. 什么时候读取ARGV? ARGV/ARGCBEGIN块里面就已经准备好了,所以你可以在BEGIN循环打印、修改它们;此时还没有开始读取任何输入文件。

4. 简易对比表

参数类型 是否进入ARGV
-v var=val
-f script.awk
-F:
program代码 {print ...}
Pass=1(program后面的变量赋值)
file.txt(program后面文件名)

5. 小思考题

1
awk -F: '1' a.txt b.txt

ARGC等于多少?ARGV[1]、ARGV[2]是什么?

答案 ARGC=3 ARGV[0]=“awk” ARGV[1]=“a.txt” ARGV[2]=“b.txt” -F:是选项,不进ARGV。

记忆要点:选项(-v/-f/-F)被awk提前吃掉,不会进ARGV;只有program之后的所有参数,才会全部扔进ARGV数组。

示例showargs.awk

1
2
3
4
5
6
BEGIN{
    print "ARGC=" , ARGC
    for(i=0;i<ARGC;i++){
        print i, ARGV[i]
    }
}

执行:

1
awk -v A=1 -f showargs.awk file1.txt file2.txt

9.4 记录与字段

record记录:默认一行;field字段:一行被FS切分出来的列。

9.4.1 RS 输入记录分隔符

  • POSIX awk:RS只能单个字符;默认\n换行;RS为空代表按段落分割(空行分隔记录)
  • gawk/mawk扩展:RS支持正则表达式,可以跨行切分记录;配套RT保存实际匹配到的分隔文本。

9.4.2 FS 字段分隔符

  1. FS默认值 " "(写的是一个空格,特殊模式!)

⚠这是awk的特殊规则:当FS等于单个空格字符串 " ",不是代表“匹配1个空格” 规则:

  1. 一个或者连续多个空格、tab制表符全部当做同一个分隔符;
  2. 开头的空白、行末尾的空白直接全部丢掉,不会产生空字段

测试输入字符串:

1
"   a   b  c  "

肉眼看:前面3个空格,a,3个空格,b,2个空格,c,后面2个空格。

执行命令:

1
echo "   a   b  c  " | awk '{print "NF="NF; print "1=["$1"] 2=["$2"] 3=["$3"]}'

输出:

1
2
NF=3
1=[a] 2=[b] 3=[c]

解释:

  • 开头一堆空格直接扔掉;
  • abc之间不管多少连续空格/tab,统一当成1个分隔;
  • 末尾一堆空格直接扔掉;
  • 最后得到3个有效字段:a b c → 所以NF=3

日常处理日志、表格,绝大多数场景就用默认FS=" ",非常符合人的直觉。

  1. FS="[ ]" (正则,匹配恰好1个空格字符

[ ] 是正则表达式,方括号里面放一个空格:含义只匹配单个空格
此时就没有上面那种特殊智能行为!
每遇到1个空格就切一刀;开头、中间、结尾的空格都算数。

同样输入:a b c
字符串分解(·代表1个空格):
···a···b··c··

每碰到一个空格就切一刀

  1. 第1个·切割 → 字段1:空字符串
  2. 第2个·切割 → 字段2:空字符串
  3. 第3个·切割 → 字段3:a
  4. 第4个·切割 → 字段4:空字符串
  5. 第5个·切割 → 字段5:空字符串
  6. 第6个·切割 → 字段6:空字符串
  7. 第7个·切割 → 字段7:b
  8. 第8个·切割 → 字段8:空字符串
  9. 第9个·切割 → 字段9:c
  10. 第10个·切割 → 字段10:空字符串
  11. 第11个·切割 → 字段11:空字符串

执行这条完整命令:

1
2
3
4
5
6
7
echo "   a   b  c  " | awk -F'[ ]' '{
    print "NF=" NF
    for(i=1;i<=NF;i++){
        printf "$%d=[%s] ", i, $i
    }
    print ""
}'

输出结果:

1
2
NF=11
$1=[] $2=[] $3=[a] $4=[] $5=[] $6=[] $7=[b] $8=[] $9=[c] $10=[] $11=[]

可以看到:NF=11,大量空字段!

  • $1 $2 是空:来自行最前面的两个空格切出来;
  • $4 $5 $6 是空:a和b之间连续多个空格切出来;
  • $10 $11 是空:行末尾的空格切出来。

9.4.3 字段 $1 $2 … $NF $0

  • $0整条原始记录(整行)
  • $1第一个字段,$2第二个,$NF最后一个字段;NF是字段总数。
  • 字段可以赋值! 修改字段会触发用OFS重新拼接$0

示例:

1
2
$ echo "a b c" | awk '{$2="BBB"; print $0}'
a BBB c

⚠ 直接打印$0,不修改字段,OFS不会生效。必须至少修改任意一个字段,才会重建$0。

1
2
3
4
5
6
7
# OFS修改输出分隔符,只print $0,不会生效
echo "a b c" | awk 'BEGIN{OFS="|"} {print $0}'
# a b c

# 修改字段,触发重建记录,OFS生效
echo "a b c" | awk 'BEGIN{OFS="|"} {$1=$1; print $0}'
# a|b|c

9.5 模式与操作

9.5.1 模式pattern

pattern 位置放的就是一个表达式!表达式计算结果非0 → 条件为真,就执行后面大括号的action。

1
2
pattern   { action }
模式        {操作}

常用模式示例

模式 本质表达式 含义说明
NF == 0 (NF == 0) 内置变量NF(当前行字段数)等于0 → 空行,条件成立
NR < 5 (NR < 5) 内置变量NR(全局累计行号)小于5,条件成立
FNR == 3 (FNR == 3) 内置变量FNR(当前文件内行号)等于3,条件成立
$1 ~ /root/ ($1 ~ /root/) 字段$1匹配正则/root/,条件成立
/error/ ($0 ~ /error/) 简写,等价于拿$0匹配正则,条件成立

awk规则: 模式位置可以写任意合法awk表达式:可以用内置变量、自定义变量、运算符、正则匹配。
表达式结果:非数字0、非空字符串 → 真;0或者空字符串 → 假

1
2
# pattern:表达式 NF == 0;action:{print}
NF == 0 { print "发现空行" }

完整逻辑流程,对每一行输入记录重复执行:

  1. 读入一行,自动更新内置变量:$0、$1、$2…、NF、NR、FNR、FILENAME
  2. 计算pattern表达式 NF == 0
    • 如果这一行是空行:NF值是0,0 == 0 → 结果为真 → 执行{ print "发现空行" }
    • 如果不是空行:NF>0,表达式结果假 → 跳过action,不执行大括号

内置变量会每读完一行自动刷新更新
所以每处理新一行,NF/NR/FNR就变成这一行对应的数值,拿来做判断。

逗号,是awk专门的范围模式运算符

  • 逗号左边:起始条件表达式
  • 逗号右边:结束条件表达式

逻辑:

  1. 一开始状态:不选中
  2. 一旦左边表达式为真 → 切换为选中状态,执行action
  3. 后续每一行都执行action,直到右边表达式结果为真;这一行也会执行,然后切换回不选中。
  4. 之后继续等待下一次左边条件触发。
1
(FNR==3),(FNR==10) {print}
  • FNR==3:表达式,内置变量FNR等于3(每个文件的第3行),开启范围
  • FNR==10:表达式,内置变量FNR等于10(每个文件第10行),关闭范围

所以每个文件,输出3~10行。

⚠重点:FNR每个文件单独计数,处理下一个新文件的时候FNR自动重置回1。NR是全局,多个文件持续累加不会重置。

NR 和 FNR 对比小例子

假设有两个文件: fileA(2行),fileB(2行)

1
awk '{print NR,FNR,$0}' fileA fileB

输出

1
2
3
4
1 1 fileA第一行
2 2 fileA第二行
3 1 fileB第一行
4 2 fileB第二行
  • NR:一直往上累加:1,2,3,4
  • FNR:打开新文件就重置为1:fileA是1‑2;fileB重新从1‑2

所以:

  • NR<5:所有文件合起来,总的前4行
  • FNR==3每一个单独文件的第3行

重要提醒:BEGIN / END块里面这些变量是无效

注意BEGIN块内部:$0、NF、NR、FNR、FILENAME未定义;END块$0不一定可靠。

为什么?

  1. BEGIN { ... }还没有读取任何输入行! 一行都没读,没有记录,所以 $0、NF、NR、FNR 没有有效数据。
1
2
3
BEGIN{
    print NR, FNR, NF   # 这里输出0 0 0,没有实际意义!
}
  1. END块:全部行读完。NR/FNR/NF保留最后一行的值;但是$0部分awk实现会清空,不要依赖。

只有在处理输入记录的时候(也就是BEGIN之外,每一行循环的时候),NF、NR、FNR才会被自动填充有效数值。

9.5.2 操作 action

print输出;print expr1,expr2 多个表达式用逗号隔开,输出用OFS分隔,末尾追加ORS换行。

简写小技巧:1 作为pattern,代表条件永远为真,默认执行print

1
2
3
# 等价于 {print} 原样输出全部输入

awk '1' file.txt

9.6 awk单行程序实战案例

awk擅长写短小单行处理,下面直接复制可用。

 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
# 模拟wc统计:行数、单词数、字符数

awk '{W+=NF; C+=length($0)+1} END{print NR,W,C}'

# 打印第1列和它的对数

awk '{print $1, log($1)}' data.txt

# 随机采样5%的行

awk 'rand() < 0.05' data.txt

# 求第3列总和

awk -v COL=3 '{sum += $COL} END{print sum}' data.txt

# grep等价,打印匹配行,带文件名行号

awk '/error/ {print FILENAME ":" FNR ":" $0}' *.log

# 调换第2列第3列,制表符分隔

awk -F'\t' -v OFS='\t' '{print $1,$3,$2,$4}' in.txt > out.txt

# 行末尾增加换行,行变成两行

awk 'BEGIN{ORS="\n\n"} 1' file.txt

# 找出超过72字符的行

awk 'length($0) >72 {print FILENAME ":" FNR ":" $0}' *.f

9.7 语句

9.7.1 顺序执行

语句用分号分隔;大括号{ ... }复合语句块。

1
2

a=1; b=2; print a+b

9.7.2 if条件

1
2
3
4
5
6
7
8

if( expr ){
    ...
}else if(expr2){
    ...
}else{
    ...
}

9.7.3 循环4种

  1. while
1
2

while(cond){ ... }
  1. do‑while
1
2

do { ... } while(cond)
  1. for(初始化;条件;自增)
1
2

for(i=1;i<=10;i++){ print i }

注意:浮点数不要做for循环,浮点精度误差会出bug。

  1. for-in遍历关联数组
1
2
3
4

for(k in arr){
    print k , arr[k]
}

循环控制:

  • break:跳出最内层循环;awk不支持break N,不能跳多层
  • continue:跳到下一轮循环

9.7.4 数组成员测试 key in array

重要:判断key是否存在数组,不要直接拿arr[key] != ""判断,访问不存在key会自动创建该数组元素!

✅正确

1
2

if( "alice" in phone ){ print "存在" }

❌错误,会新增空元素

1
2

if( phone["alice"] != "" ){}

9.7.5 流程控制关键字

  1. next跳过当前记录剩下所有模式匹配,直接读下一行记录
  2. nextfile(gawk扩展):直接关闭当前文件,跳到下一个输入文件。
  3. exit [n]:立刻终止awk程序,返回退出码n给shell。

9.7.6 getline 手动读取输入

awk默认自动循环读记录;getline可以手动读取记录。

语法 作用 返回值
getline 读取下一行到$0 1成功,0文件结束,‑1错误
getline var 读取一行存入变量var,不修改$0 同上
getline < "file.txt" 从文件读记录 同上
cmd | getline 执行shell命令,读取命令输出一行 同上

重点:不是所有字符串都会执行!只有字符串出现在 | getline 左边的时候,awk 才会把这个字符串交给 shell 当做命令运行。 | getline 这个语法触发把字符串当做命令跑

打开文件/管道记得close("文件名"),避免打开文件数耗尽。

示例读取命令输出:

1
2
3
4
5
6

BEGIN{
     "date" | getline now     # 1.启动shell执行date命令;建立管道;读取一行输出存入now
     close("date")            # 2.传入字符串"date",关闭这条管道,回收子进程、释放句柄
     print now                # 3.打印拿到的时间字符串
}

9.7.7 输出重定向

print输出可以重定向到文件、管道。

  • > 覆盖写文件
  • >>追加写文件
  • | cmd输出管道交给shell命令

awk的文件打开之后不会自动关闭,用完务必close()

1
2
3
4
5
6
7
8

# 输出重定向文件

BEGIN{
    print "hello" > "out.txt"
    print "world" >> "out.txt"
    close("out.txt")
}

9.7.8 system()执行外部shell命令

  • system() 是 awk 的内置函数,专门用来调用执行外部 shell 命令
  • system("shell命令字符串"),返回命令退出状态码。
  • system会新开shell进程;输出直接打印,不会捕获到awk变量。
1
2
3
4
5

BEGIN{
    ret = system("ls -l")
    print "返回码:" , ret
}
  1. system("ls -l")
    • awk 启动一个新 /bin/sh shell 子进程,执行字符串里面的 shell 命令 ls -l
    • 命令的标准输出、标准错误,直接打印到屏幕上,直接输出,不会存进 awk 变量!
  2. 返回值 ret:拿到的是 shell 命令的退出状态码(0 代表命令执行成功;非 0 代表出错)。

⚠巨大区别:system()不能捕获命令输出到 awk 变量!输出直接往外跑。 如果你想要拿到命令输出存到 awk 变量,就不能用 system,要用我们刚才学的 "cmd" | getline var 管道写法。

对比两套方案

方案 1:system () —— 执行命令,拿退出码;拿不到输出

1
2
3
4
BEGIN{
    ret = system("date")   # date输出直接打印屏幕
    print "退出码=" ret
}

运行输出:

1
2
Wed Sep 17 14:20:00 CST 2025
退出码= 0

date 的时间直接输出,没有保存到 awk 变量。ret 只存 0(成功)。

方案 2:"date" | getline now —— 捕获命令输出到变量

1
2
3
4
5
BEGIN{
    "date" | getline now
    close("date")
    print "now=" now   # now变量存住时间文本
}

输出不会直接打印,放到 awk 变量now里面,我们自己控制什么时候 print。

简单总结对照表

方式 作用 能不能拿到命令输出到awk变量 得到什么返回值
system("cmd") 执行shell命令 ❌不能,输出直接打印屏幕 得到命令退出状态码
"cmd" | getline var 执行shell命令 ✅可以,输出读到var变量 getline返回1/0/-1(读取成功/EOF/错误)

坑提醒

  1. system()里面写的是 shell 命令字符串,shell 的特殊符号 * > | 都生效。
1
system("echo hello > test.txt") # shell会做重定向,输出写到test.txt
  1. system 每调用一次就新建一个全新 shell 进程;多次 system 之间变量环境互相不传递。
1
2
3
4
BEGIN{
    system("A=100")       # 在这个临时shell设置变量,shell执行完销毁
    system("echo $A")     # 另一个全新shell,拿不到上面的A=100!
}

两个 system 是两个独立子 shell,不能靠 system 在两次调用之间传递变量。

9.8 用户自定义函数

awk支持自定义函数;标量传值,数组传引用
函数参数列表末尾写额外变量作为局部临时变量(awk没有专门local关键字)。

awk 自定义函数局部变量规则

awk 没有 local 关键字,不能显式声明局部变量。
规则:
function 函数名(参数1,参数2, 局部变量1,局部变量2)
逗号分隔,在参数列表里面,多写几个变量名,调用函数的时候不给它传实参;这些变量就变成函数内部的局部变量
习惯写法:前面是真正接收调用传入的参数,后面打两个空格隔开,之后全部是局部临时变量

语法:

1
2
3
4
5
6
7
8

function funcname(a,b,  local1,local2)
{
    # a,b:真正的入参,调用的时候传值进来
    # local1、local2:局部临时变量!调用函数时不传任何东西给它们
    ...
    return val
}

注意逗号:b, local1b后面逗号,然后习惯敲两个空格,视觉上区分【真实入参】和【局部变量】。
local1 local2 并不是调用的时候要传的参数,只是写在这里,变成函数私有的局部变量。

例子演示

写一个函数,求两个数的和,中间需要一个临时变量tmp做中转。

正确写法:参数列表末尾增加临时局部变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function add(x,y,  tmp) {
    # x y:外部调用传入的参数
    # tmp:局部变量!调用函数不会给tmp传值,写在这里就变成局部
    tmp = x + y
    return tmp
}

BEGIN{
    res = add(3,5)   # 只传2个实参:3给x,5给y;tmp不用传!
    print res
}

运行输出:8

调用:add(3,5),只给xy赋值。tmp没有传入任何值,但是它是函数内部私有局部变量,函数外面访问不到 tmp。

示例:查找数组value对应的key

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

function find_key(array, value,  key)
{
    # 查找array[]以寻找value,并返回array[key] == value
    # 的key,如果找不到该值,则返回""
    for (key in array)
        if (array[key] == value)
            return key
    return ""
}
  • arrayvalue:调用传入的真实参数
  • key:放在参数列表末尾,局部临时循环变量;调用的时候不会传 key 进来

调用示例:

1
2
3
4
5
BEGIN{
    phone["alice"]="111"
    phone["bob"]="222"
    print find_key(phone,"222")
}

调用只传两个实参,key是函数内部循环用的局部变量。

支持递归,但注意递归深度,过大递归会栈溢出。

9.9 字符串内置函数

函数 说明 示例 示例输出
length(s) 返回字符串长度 length("abcde") 5
substr(s,start [,len]) 截取子串,下标从1开始 省略len:截取到字符串末尾 substr("abcde",2,3) substr("abcde",3) "bcd" "cde"
tolower(s) 全部转为小写 tolower("AbC123") "abc123"
toupper(s) 全部转为大写 toupper("AbC123") "ABC123"
index(s,t) s里面找子串t,返回起始位置;找不到返回0 index("abcde","cd") index("abcde","xy") 3 0
match(s,regex) 正则匹配;匹配成功设置内置变量RSTART(起始位置)、RLENGTH(匹配到的长度);匹配失败返回0 BEGIN{s="hello123world"; match(s,/[0-9]+/); print RSTART,RLENGTH} 6 3
sub(re,repl,target) 替换第一个匹配到的内容;省略第三个参数默认操作$0 BEGIN{s="a1 b1 c1"; sub(/1/,"9",s); print s} "a9 b1 c1"
gsub(re,repl,target) 全局全部替换所有匹配;省略第三个参数默认操作$0 BEGIN{s="a1 b1 c1"; gsub(/1/,"9",s); print s} "a9 b9 c9"
split(str,arr,regex) 按正则分割字符串,结果存入数组arr;返回分割出来元素的个数 BEGIN{n=split("aa:bb:cc",arr,":"); print n,arr[1],arr[2],arr[3]} 3 aa bb cc
sprintf(fmt,...) 格式化,返回格式化后的字符串,不会打印,存给变量用 BEGIN{s=sprintf("num=%04d",12); print s} "num=0012"
printf(fmt,...) 格式化,直接输出打印,不返回值 BEGIN{printf("num=%04d\n",12)} num=0012

sub/gsub替换字符串中&代表匹配到原文;要字面&写\\&

split示例:

1
2
3
4
5

BEGIN{
    n=split("a:b:c:d",arr,":")
    for(i=1;i<=n;i++) print arr[i]
}

9.10 数值函数

函数 作用
int(x) 取整数部分,截断小数
sqrt(x) 平方根
sin/cos/atan2(y,x) 三角函数,参数弧度
exp(x) log(x) 指数,自然对数
rand() 返回0 ≤ r <1的随机浮点数
srand([seed]) 设置随机种子;不带参数使用系统时间做种子

⚠注意:不调用srand(),每次运行awk得到的随机序列完全一样!

9.11 小结

  1. awk模型:数据驱动,自动遍历记录pattern {action}模式操作对,学习重点。
  2. 内置变量FS/OFS/RS/ORS/NR/FNR/NF是awk灵魂,控制记录字段解析。
  3. 关联数组,索引可以字符串;区分数组key存在判断,不要直接取值判断空。
  4. 字符串拼接不需要运算符;substr/index/match/split/sprintf高频字符串函数。
  5. 可以getline手动读;system()执行外部命令;输出支持重定向文件管道,记得close。
  6. 支持自定义函数;标量传值,数组传引用;没有local,参数列表预留名字充当局部变量。
  7. 大量简短单行awk可以替代sed/grep/cut等多个工具。

坑提醒:

  1. awk全部浮点数,循环不要用小数做条件;
  2. 修改$1等字段才会用OFS重建$0;单纯print $0不会生效OFS;
  3. 打开文件管道必须close,不然耗尽文件句柄;
  4. 数组key in arr才是正确判断key是否存在;直接读取会新增元素。