awk 命令

awk 简述

awk 命令格式
awk [选项] '程序' file ...
awk 命令行选项

-F, --field-separator

设置字段分隔字符,缺省为「空白字符」或制表字符 (Tab 键)。

-v var=value, --asign var=value

将变量 var 设置成 value

记录和字段

AWK 可以处理文本文件和串流 (管道),输入数据分为「记录」和「字段」。awk 一次对一条「记录」进行操作,直到结尾 (EOF)。
「记录」由「记录分隔字符 RS (Record Separator)」来分隔,缺省的 RS 是换行字符,表示文本数据中的每一行都是一条记录,可以使用变量设置不同的 RS
一行记录可由「字段分隔字符 FS (Field Separator)」分隔出不同字段,缺省的 FS 是空格或制表字符 (Tab 键),可采用命令行选项 -F 或变量设置不同的 FS。每个字段可由字段变量 ($) 取得 $1 $2 $3 依此类推,$0 为全部内容,最后一个字段也可以使用变量 $NF

FS (字段分隔字符) 设置为 :,将 /etc/passwd 文件标示出字段如下:
$grep ubuntu /etc/passwd
ubuntu:x:1000:1000::/home/ubuntu:/bin/bash
$1    :$2    :$4   :$6          :$7($NF)
        :$3       :$5
$0----------------------------------------

awk 程序 (program)

「程序 (program)」是由一系列模式动作对组写成:
'pattern {action}'

其中 pattern (模式) 表示在数据中找出符合的「记录(行)」,可为逻辑判断式或正则表达式 (Regex),当省略 pattern 时,表示匹配所有记录(行)。
{action} 是在匹配记录时所运行的动作,由大括号 {} 中的语句 (statement) 组成,{action} 也可省略,省略时缺省动作是打印该行的全部内容 (print $0)。
语句 (statement) 可以解读为「指令」。

一个动作 (action) 有多个指令 (语句) 时,要写在下一行或以分号 (;) 分开,如下:
awk -F: '
{
  print $1
  print $6
}' /etc/passwd
awk -F: '{print $1; print $6}' /etc/passwd
另外在「控制语句」 (如 if) 中,如果有多个指令也需要由大括号 {} 组成复合语句,如下:
awk -F: '
{
  if ($3 >= 1000) {
    print $1
    print $3
  }
}' /etc/passwd

awk 模式 (pattern)

模式 (pattern) 判断式

逻辑判断语法

x == y

相等

x != y

不相等

x > y

大于

x >= y

大于或等于

x < y

小于

x <= y

小于或等于

正则表达式语法

/Regex/

匹配 Regex 表达式,表达式必须以定界字符 / 包围。

!/Regex/

否定匹配 Regex 表达式。

x ~ y

x 匹配 y 正则表达式。

x !~ y

x 不匹配 y 正则表达式。

AND OR 语法,适用逻辑判断式及正则表达式。

&&

AND 条件

||

OR 条件

以逻辑判断式,列出 UID 大于或等于 1000 的帐户
awk -F: '($3 >= 1000) {print $1, $6}' /etc/passwd

列出用户名及家目录为第 1 个字段及第 6 个字段 {print $1, $6},字段变量之间的逗号 (,) 是打印字段分隔字符 OFS(Output field separator) 缺省为空白字符。如果是 {print $1 $6} 并不会把字段分隔,可采用 " " 字符串,在字符串中加入空格,如 {print $1 " " $6}

以控制语句,列出 UID 大于或等于 1000 的帐户
awk -F: '{if ($3 >= 1000) print $1 " " $6}' /etc/passwd
以正则表达式 (Regex) 匹配模式 (pattern),过滤具有家目录的帐户
awk -F: '/\/home/ {print $1, $6}' /etc/passwd

将记录中整行的内容匹配模式 /\/home/,正则表达式必须以定界字符 / 包围,而字符 / 必须以转义字符 \ 转译成 \/,内容匹配 /home 才会打印。

匹配 (~) 两个运算,$6 ~ /\/home/ 为字段 6 匹配 /home,本例的结果跟上面是一样,只是上面是不区分字段。
awk -F: '$6 ~ /\/home/ {print $1, $6}' /etc/passwd
以模式指定记录范围
pattern1, pattern2

采用逗号 (,) 区分两个位置;位置范围由第一个位置 pattern1 至 (,) 第二个位置 pattern2, 第二个开始匹配是在第一个位置或之后,记录范围始终跨越至少一个记录 (行)。

打印记录内容 2 至 3 的记录
seq 5 | awk '$0==2, $0==3'
seq 5 | awk '/2/, /3/'
可混合「逻辑判断式」及「正则表达式」
seq 5 | awk '$0==2, /3/'
打印记录内容 3 至 2,由于第二个位置不合乎条件,将打印第一个位置记录 (3) 至最后记录 (5)
seq 5 | awk '/3/, /2/'
打印记录内容 2 或 3 或 Yes 的记录
echo Yes | awk '/2/ || /3/ || /Yes/'
在范围模式中,逗号 (,) 是所有操作符中最低的优先权 (即;最后判断)。因此,范围模式与另一个运算结合起来:
echo Yes | awk '/2/,/3/ || /Yes/'
程序的意图是 (/1/,/2/) || /Yes/,但 awk 将其解读为 /1/, (/2/ || /Yes/),这无法改变或解决;范围模式不能与其他模式结合:
$echo Yes | awk '(/1/,/2/) || /Yes/'

awk: cmd. line:1: (/1/,/2/) || /Yes/
awk: cmd. line:1:           ^ syntax error
采用「控制语句」来处理:
echo Yes | awk '{if ( ($0>=2 && $0<=3) || ($0=="Yes") ) print}'
多条模式 (pattern)

在程序中可以有多条模式,合乎模式的以不同动作来处理。

awk -F: '
$1=="root" {
  print "The superuser is on line ", NR
}

$1=="www-data" {
  print "The user for the web server is on line ", NR
}
' /etc/passwd
BEGIN 和 END 特殊模式 (pattern)
BEGIN { 尚未读取前先运行的动作 }

BEGINFILE { 文件尚未读取的动作  }

{ 读取时运行的动作 }

ENDFILE { 文件读取后运行的动作 }

END { 读取完成后运行的动作 }

当程序 (program) 只有 BEGIN 时,并不需要读取文件或管道输入。
在运行 BEGIN 时并未打开及读取文件,BEGINFILE 为打开文件,尚未读取,再运行 读取时运行的动作,文件读取完成后运行 ENDFILE,所有文件处理完毕后最后运行 END

当传入多个文件时,处理文件按顺序 (先打开左边的文件) 把多个文件当成接续的 (一个) 文件,其行号 NR 为接续。

awk '
BEGIN {
  print "BEGIN FILENAME is", FILENAME
}
BEGINFILE {
  print "BEGINFILE", FILENAME, NR
}
{
  if (LastFileName != FILENAME) {
    LastFileName = FILENAME
    print "Read file", FILENAME, NR
  }
}
ENDFILE {
  print "ENDFILE", FILENAME, NR
}
END {
  print "END FILENAME is", FILENAME
}
' /etc/passwd /etc/group
BEGIN FILENAME is (1)
BEGINFILE /etc/passwd 0 (2)
Read file /etc/passwd 1 (3)
ENDFILE /etc/passwd 27 (4)
BEGINFILE /etc/group 27
Read file /etc/group 28
ENDFILE /etc/group 81
END FILENAME is /etc/group (5)
1 BEGIN 未打开及读取文件
2 BEGINFILE 打开文件,尚未读取文件。
3 运行「读取时的动作」读取文件。
4 ENDFILE 文件读取完成。
5 END 所有文件处理完毕。

awk 变量

内置变量

ARGC

命令行输入参数数量

ARGV

命令行输入参数内容 (数组)

NR

(目前) 记录位置 (行数)。

NF

(目前) 记录中字段数。

FILENAME

文件名称。

FS

(输入的) 字段分隔字符 (Field Separator),缺省为「空白字符」或制表字符 (Tab 键)。

RS

(输入的) 记录分隔字符 (Record Separator),缺省为换行字符。

OFS

输出字段分隔字符 (Output Field Separator),默认值为空白字符。

ORS

输出记录分隔字符 (Output Record Separator),默认值为换行字符。

OFMT

输出数字格式 (Format for numeric output),默认值为 %.6g

IGNORECASE

匹配或排序时不区分大小写,默认值为 1。

RSTART

字符串函数 match 专用,回传匹配的 (开始) 位置。

RLENGTH

字符串函数 match 专用,回传匹配的 (内容) 长度。

SUBSEP

(多主键) 数组分隔字符。

FPAT

匹配 FPAT 正规表示法,以匹配内容创建字段,缺省为 [^[:space:]]+ (不为空格的多个字符)。
在程序中同时设置变量 FSFPAT,是以最后设置哪个变量来解析字段,未设置变量缺省是以 FS 来分隔。

CSV 是以逗号 , 来区分字段,但字符串中也会有逗号的情况如 1,"2,b",3,这时只能用 FPAT = "[^,]+|\"[^\"]+\"" 来处理。

上述的正规表示法的意思是
  • 匹配不为逗号 [^,] 多个 (+) 字符

  • |

  • 前后为双引号 \" 中间不为双引号 [^\"] 多个 (+) 字符

打印具有家目录的帐户及行数
$awk -F: '/\/home/ {print $1,NR}' /etc/passwd
syslog 21
ubuntu 24
BEGIN 在读取前先设置 FS (输入) 字段分隔字符为 :OFS 输出字段分隔字符为 ,
awk 'BEGIN {FS=":"; OFS=","} /\/home/ {print $1,NR}' /etc/passwd
以命令行选项 -v 设置内置变量
awk -vFS=: -vOFS=, '/\/home/ {print $1,NR}' /etc/passwd
设置 OFMT 输出数字格式示例
awk 'BEGIN{OFMT="%.0f"; print 2.5, 3.5, 4.5, 5.5}'
2 4 4 6

awk 的浮点进位为银行进位法 (Banker’s Rounding) 可简记为「四舍六入五成双」,五成双,尾数「5」的前一位,若双数则为双数,若为单数则进位。
如格式化字符串为 %.0f,参数输入 2.5, 3.5, 4.5, 5.5 则输出 2, 4, 4, 6

自定变量

awk 自定变量可为任意类型,如数字、浮点、字符串或数组。变量名称不可跟函数名称相同,如 index = 1 (index 刚好为函数),由于变量区分大小写,可以改名为 Index = 1

变量内容指定为 16 进制数值,则内容值前置 0x,如 myVar = 0x10 (10 进制为 16),x 或数值 af 不区分为大小写可混用。
8 进制则为前置 0myVar = 010 (10 进制为 8)。

引用 shell 变量
num=123
awk -v n=$num 'BEGIN {print n}'

awk 控制语句

{action} 控制语句语法
if (conditional)
  then-body-statement
else if (conditional)
  else-if-then-body-statement
else
  else-body-statement

initialization-statement
while (conditional) {
  body-statement
  increment-statement
  break
  continue
}

for (initialization; condition; increment)
  body-statement
  break
  continue

for (variable in array)
  body-statement
  break
  continue

switch (expression) {
case value or regular_expression:
  case-body-statement
  break
default:
  default-body-statement
  break
}

next

exit [return_code]

nextfile
next

处理下一笔记录,若不为 EOF 重启循环,回到「读取时运行的动作」程序起始位置 (不会运行 next 后面的程序)。

awk 函数

print

print 内的逗号 , 是打印 OFS 字段分隔字符,数字输出格式为 OFMT (缺省为 %.6g),运行 print 后会加印 ORS 输出记录分隔字符 (缺省为换行字符)。可设置内置变量 OFSOFMTORS 改变缺省行为。print $1 $2 并不会把字段分隔,可采用 " " 字符串,在字符串中加入空格,如 print $1 " " $2

print 无参数时为 print $0,另外在 awk 程序 (program) 的 pattern {action},当省略 {action} 时缺省动作也是 print $0

printf

printf 语法
printf format [, argument-list]

format (格式) 为字符串,argument-list 是对应于 format 的参数表表。

格式占位字符语法 (format placeholder)
% [parameter] [flags] [width] [.precision] type

parameter (参数)

N$

打印第 N 个参数,如 printf "%2$s %1$s", "world", "hello" 则输出 hello world

flags (修饰字符)

- (减号)

左对齐,不足宽度时在右边填满空格,如 printf "%-5s", "cat" 则输出 cat  

+ (加号)

用于数字;在正数前面增加一个 + 号。如 printf "%+d", 3 则输出 +3,而负数则照样输出成 -3

space

用于数字;在正数前面增加一个空格 (space),主要是用于跟负数对齐。如 printf "% d", 3 则输出  3,而负数则照样输出成 -3

0

用于数字;不足宽度时在左边填满 0,如 printf "%03d", 3 则输出 003,而负数则输出成 -03

' (单引号)

用于数字;加上千位分隔符号;支持千位分隔表示的 locale 才有作用。如 printf "%\x27d", 1234 则输出 1,234

由于 ' (单引号) 需要转译,以 16 进制 \x27 避开。以下列示处理 ' (单引号) 的方式:
awk 'BEGIN{printf "%\x27d\n", 1234}' &&\
awk 'BEGIN{printf "%\047d\n", 1234}' &&\
awk 'BEGIN{printf("%'"'"'d\n", 1234)}' &&\
awk "BEGIN{printf \"%'d\n\",1234}"

#

类型 (type) 为 oxX 时,如 %#o 前置 0%#x 前置 0x%#X 前置 0X
类型为 gG 时,如 %#g %#G,补上尾端的 0
当参数的数值为 0 时,只会显示成 0,并不会「前置」或「补上尾端」。

width (宽度)

「宽度」指定输出的最少字符数,通常用于固定宽度的输出。
宽度不足时左边填满空格 (右对齐),超出宽度则全部打印,一般应用于数字,如 printf "%5d", 123 则输出   123
应用于字符串如 printf "%5s", "cat" 则输出   cat

「宽度」可以省略,或者当作为另一个参数传递时由星号 * 指示的动态值。 如 printf "%*d", 5, 123 将打印总宽度为 5 个字符的   123

precision (精度)

「精度」通常指定输出的最多字符数,在不同 type (类型) 其义意不同:
  • 整数类型 (d,i,o,u,x,X) 的位数。

  • 浮点类型 (e,E,f,F) 限制小数点位数。

  • 通用浮点型 (g,G) 为有效位数 (整数加小数字数的总位数)。

  • 字符串类型 (s) 限制输出的最大长度,超过的字符串会被截断。
    printf "%.2s","here" 则输出 he

「精度」可以省略,或者当作为另一个参数传递时由星号 * 指示的动态值。 如 printf "%.*s", 2, "here" 则输出 he

type (类型)

c

字符,参数为数字如 65 则输出 A,若为字符串则输出第一个字符。

d, i

整数,无条件舍去法。

e, E

以科学记号显示浮点数,小写 e 科学记号显示为小写,大写 E 则为大写,银行进位法。

f

浮点数值格式化成定点 (fixed-point)。

g, G

通用浮点型,自动显示成「浮点 (定点) 类型」或「科学记号类型」,整数字数已经超过由「精度」指定的有效位数,则会显示成科学记号。小写 g 科学记号显示为小写 e,大写 G 则为 E,银行进位法。

o

8 进制。

s

字符串。

x, X

16 进制,%x 输出小写 af%X 则输出大写。

%

输出一个 % 字符 (第一个格式占位字符是 %,含占位字符为两个 %%)。

字符转译

\a

Alert (警报) 哔声 (beep), Ctrl+g,ASCII code 7 (BEL).

\b

倒退键 Ctrl+h,ASCII code 8 (BS).

\t

制表字符,TabCtrl+i,ASCII code 9 (HT).

\n

换行字符,Ctrl+j,ASCII code 10 (LF).

\v

垂直制表字符,Ctrl+k,ASCII code 11 (VT).

\f

换页字符,Ctrl+l,ASCII code 12 (FF).

\r

确认键,Ctrl+m,ASCII code 13 (CR).

\nnn

8 进制值 nnn,其中 nnn 为 07 之间的 1 到 3 位数字。

\xhh…​

16 进制数值 hh(0-9A-Fa-f)。

\\

输出一个 \ 字符。

\"

在字符串中输出 " 双引号,如 print "Say \"Hello!\" to the world"

数学函数

数学函数

x % y

x 除以 y 的余数。

x ^ y

x 的 y 次方

sin(x)

正弦

cos(x)

余弦

atan2(y, x)

反正切

exp(x)

指数

log(x)

对数

sqrt(x)

开根号

int(x)

整数值 (无条件舍去小数)

srand([x])

初始化乱数 rand()x 为乱数种子 (seed),若省略,则以运行时的日期和时间为种子。将种子设置为相同的值,则会产生相同的乱数。

rand()

回传乱数 n,0 <= n < 1。

比特操作函数

and(v1, v2 [, …])

回传比特运算 and

compl(val)

回传 val 比特运算的补数 (complement),将 01 互换。

lshift(val, count)

回传 val 以比特 (bit) 左移 count 位,(左边) 高位抛弃,(右边) 低位补 0。

or(v1, v2 [, …])

回传比特运算 or

rshift(val, count)

回传 val 以比特 (bit) 右移 count 位,(左边) 高位补 0,(右边) 低位抛弃。

xor(v1, v2 [, …])

回传比特运算 xor

字符串函数

asort(source [, dest [, how ] ])

source 的「内容值」排序。排序后的数组主键由整数 1 开始,数组内容为「内容值」。如果有指定 dest 则排序结果会保存在 dest (source 不变)。内置变量 IGNORECASE 会影响排序

asorti(source [, dest [, how ] ])

source 的「主键值」排序,排序后的数组主键由整数 1 开始,数组内容为「主键值」。如果有指定 dest 则排序结果会保存在 dest (source 不变)。内置变量 IGNORECASE 会影响排序

gensub(regexp, replacement, how [, source])

将输入的 source 匹配 regexp 替换为 replacement 回传替换后的文本。 未指定 source 参数其来源为字段 $0,how 设置为数字 n 表示第 n 次开始替换,若为 gG 则为全部替换。regexp 可以是 "字符串"/正规表示法/

index(in, find)

回传字符串 findin 的位置 (不支持正规表示法)。

length [(string)]

回传字符串长度,如果没有 string 参数则回传 $0 的长度。

match(string, regexp [, array])

取得 string 匹配 regexp 的位置和长度,将起始位置填入内置变量 RSTART,长度填入内置变量 RLENGTHregexp 可以是 "字符串"/正规表示法/

array 为正规表示法中分组功能的回调
echo 'This is an apple and that is an orange.' | awk '{match($0, /an ([^ ]+).+ an ([^.]+)/, arr); print arr[1], arr[2]}'

patsplit(string, array [, fieldpat [, seps ] ])

字符串 string 匹配 fieldpat 模式,将合乎模式匹配的文本创建数组 array,如省略 fieldpat 缺省为内置变量 FPATfieldsep 可以是 /正规表示法/"字符串",当匹配的文本相同时则数组主键会重复。

split(string, array [, fieldsep [, seps ] ])

字符串 string 分割后 (的前后) 创建数组 array,如省略 fieldsep 缺省为内置变量 FS 字段分隔字符,fieldsep 可以是 "字符串"/正规表示法/ 或内部变量 SUBSEP (数组分隔字符),当分割后的字符串相同时则数组主键会重复。

sprintf(format [, argument-list])

回传(不打印)printf (使用相同参数) 输出的字符串。

strtonum(str)

str 转成数字 (含小数),若 str 前置 0x (x 可为大写) 则由 16 进制转成 10 进制。str 若前置部份数字 (含小数) 则会回传部份数字,无法转换时回传 0。
awk 4.1.4 已不支持前置 0 时转换成 8 进制。

sub(regexp, replacement [, target])

target 匹配 regexp 替换一次成为 replacement,替换后的文本取代 target。没有指定 target 参数时缺省为字段变量 $0regexp 可以是 "字符串"/正规表示法/

gsub(regexp, replacement [, target])

gsub 跟 sub 类似,sub 只替换一次,gsub 为全部替换。

substr(string, start [, length ])

回传字符串 string 起始位置 start 长度 length,没有指定 length 则至 string 的结尾。

tolower(string)

转成小写

toupper(string)

转成大写

输入输出函数

close(target [, how ])

输入的文件或管道,在经过读取后,其读取的位置会增加,如果不关闭输入,则后续的读取是接续上次的位置。
关闭文件 (target) 或者关闭 shell 命令 (target) 的管道,从头读取。

在双向管道 (two-way pipe) 或称为 coprocess (协助) 可以指定 how 参数,from 为关闭管道的输入,to 为关闭管道的输出 。

closetarget 不存在或其他错误,并不会出错只会回传非零值。发生错误时会设置 ERRNO 变量来描述错误发生的原因。

fflush([target])

将输出文件或管道的缓冲区排清 (flush) 确定已写入。 flush 出错时回传非零值,发生错误时会设置 ERRNO 变量来描述错误发生的原因。
一般的命令会有缓冲区用于处理数据,当命令已接收到足够的数据时才会处理。但有时可能需要强制命令处理缓冲区的资枓,以 fflush 函数排清命令的缓冲区。

system(command)

运行操作系统的命令 command 回传命令的状态码。

[command | ] getline [target] [ < file]

读取下一笔记录保存至 target 并回传读取状态,没有指定 target 时将保存至 $0 并解析字段。

运行 getline 错误时并不会出错,而是回传状态:
  • 1 正确读取记录。

  • 0 文件结束 (EOF)。
    注:如果是管道输入,表示管道没有正确发送「标准输出」,可能是文件不存在,「标准输出」发送 EOF

  • -1 错误,文件无法打开。

  • -2 错误,如果是设置重试 PROCINFO[command, "RETRY"] = 1,当发生逾时 (可能) 回传 -2。

  • 发生错误时会设置 ERRNO 变量来描述错误发生的原因。

采用 getline < file,由文件 file 读取数据,file字符串型态
使用 command | getline,将 command (命令) 的输出管道传输到 getlinecommand字符串型态

为什么要强调是字符串型态?

因为很容易写成 getline < file,awk 会将 file 解读成变量,正确应该为 getline < "file"。当 file 是以变量来组合时,例如 getline < dir "/" file 并不明确,应使用括号包围如 getline < (dir "/" file)

取得 IP 位置的名称
getent hosts 8.8.8.8
8.8.8.8         dns.google
getline 取得 IP 位置的名称
echo -e '8.8.8.8\n8.8.4.4' | awk '
{
  "getent hosts " $0 | getline Hosts;
  split(Hosts, Hosts_Arr, " ");
  print $0, Hosts_Arr[2]
}'
8.8.8.8 dns.google
8.8.4.4 dns.google
列出用户帐户、UID 及 GID
awk  -F: '
{
  User = $1
  UID = $3
  while ( (getline < "/etc/group") > 0 )
  {
    if (User ==  $1) {
      print $1, UID, $3
      break
    }
  }
  close("/etc/group") (1)
}' /etc/passwd
1 关闭文件后,第二次才能从头开始读取。

注:本例是为了说明 close,并未优化,在 BEGIN{} 时先将 /etc/group 全部读取至数组再以数组查表,比较理想。

print printf 重定向输出
printprintf 的管道输出字符其运作跟管道一样。
print > "filename"
print >> "filename"
print | "command"
print |& "command_coprocess"
将输出文件错误视为非严重错误

PROCINFO["NONFATAL"] 或者 PROCINFO["filename", "NONFATAL"] 存在时 (不管项目设置值,设为 0 也一样),则 print 重定向至文件的 I/O 错误将变为非严重错误 (注:awk 5.0 才会作用)。

awk '
BEGIN {
  PROCINFO["/no/such/file", "NONFATAL"] = 0 (1)
  ERRNO = 0
  print "hi" > "/no/such/file"
  if (ERRNO) {
    print("Output failed:", ERRNO) > "/dev/stderr"
    exit 1
  }
}'
1 设置为 0 是在解释该项目存在时即有作用,实际上不需要设置;或者设置为 1。
Output failed: No such file or directory
双向管道

将管道的输入及输出;由 print 发送给命令的「管道输入」;由 getline 读取命令的「管道输出」。

|& 创建双向管道
print |& command
command |& getline x

||& 可能混淆以为行为一样,| command 是由命令直接输出,而 |& command 其命令是回传给 awk,命令并不会输出。

依据命令的特性,如命令是以逐行来处理,则每发送一笔记录后运行 close(command, "to") 在管道中写入 EOF,避免要求读取所有数据后才会处理的「命令」发生阻塞 (永久等待)。
发送后运行关闭管道的输出 close(command, "to"),在运行 getline 之后也需要运行 close(command),这两个 close 是成对的。

{
  print |& command
  close(command, "to") (1)

  if ( (command |& getline x) > 0 )
     print x

  close(command) (2)
}
1 close(command, "to") 在管道中写入 EOF
2 在处理后需要运行 close(command),这两个 close 是成对的。

如果命令是读取所有的输入后才处理 (或者是读取全部后才会正确),那么则分段;先发送所有记录后,再发送 EOF,再以 getline (全部) 读取。

{
  print |& command (1)
}

END {
  close(command, "to") (2)
  while ( (command |& getline x) > 0) {
    print x
  }
  close(command) (3)
}
1 先发送所有记录。
2 发送所有记录后,再发送 EOF
3 实际上并不需要该指令,但加入该指定比较合乎 close 的用法。
将用户帐户转成大写 (逐行处理)
awk  -F: '
BEGIN {
  command="awk \x27{ print toupper($0) }\x27" (1)
}
{
  print $1 |& command
  close(command, "to")

  if ( (command |& getline x) > 0 )
     print x
  close(command)
}
' /etc/passwd
1 command 即为 awk '{ print toupper($0) }',将 ' (单引号) 以 16 进制 \x27 避开转译。
将用户帐户经过 cat -n 加上行号,读取后再打印 (全部数据分段处理)。
awk  -F: '
BEGIN {
  command="cat -n"
}

{
  print $1 |& command
}

END {
  close(command, "to")
  while ( (command |& getline x) > 0 )
    print x
  close(command)
}
' /etc/passwd
逾时测试
awk '
BEGIN {
  command = "echo hi"
  print "getline return:" (command | getline)
  print "close return:" close(command)

  print "----------"
  command = "sleep 1 && echo hi"
  PROCINFO[command, "RETRY"] = 1
  PROCINFO[command, "READ_TIMEOUT"] = 200
  print "getline return:" (command | getline), "ERRNO:" ERRNO
  print "close return:" close(command), "ERRNO:" ERRNO
  print "close return:" close(command), "ERRNO:" ERRNO
}'
getline return:1
close return:0
----------
getline return:-2 ERRNO:Connection timed out (1)
close return:269 ERRNO: (2)
close return:-1 ERRNO:close of redirection that was never opened
1 不一定是 -2,awk 4.1.4 为 -1。
2 不一定是 269 可能是其他非 0 的数字。

awk 关联数组

数组主键值为唯一,数组主键 (及内容值) 可为文本或数字,收集或统计字段的情况。

数组类型
  • array[x,y] 多主键数组
    水果有多种颜色,水果及颜色为同一组数据时,可采用多主键数组。

  • array[x][y] 多维数组
    水果有多种颜色,如要各别列出水果、颜色,处理不同的维度时,应采用多维数组。

多主键数组是保存在一维数组,其主键是以 SUBSEP (数组分隔字符) 来分隔,该字符即为 Ctrl+\
awk '
BEGIN {
  a[1,1] = 11;
  a[1,2] = 12;
  a[2,2] = 22;
  for (i in a)
    print i
}
' | cat -v
2^\2
1^\1
1^\2
多维数组,在程序中可取得一个维度,其数量为该维度的个数。
awk '
BEGIN {
  a[1][1] = 11;
  a[1][2] = 12;
  a[2][2] = 22;
  for (x in a) {
    print x
    for (y in a[x])
      print "\t" y
  }
}
'
1
        1
        2
2
        2
创建销售范例数据
cat > sales.txt << EOF
sales,fruit,color,qty,amount
john,apple,red,1,100
john,banana,yellow,2,200
john,kiwi,green,3,300
kevin,lemon,yellow,1,100
marry,banana,yellow,2,200
marry,apple,green,3,300
EOF
统计水果及业务员销售金额
awk -F, '
{
  if (NR < 2) next
  fruitAmount[$2] += $5
  salesAmount[$1] += $5
}
END {
  print "---------- Fruit amount"
  for (fruit in fruitAmount) print fruit, fruitAmount[fruit]

  print "---------- Sales amount"
  for (sales in salesAmount) print sales, salesAmount[sales]
}
' sales.txt
程序说明:
  1. if (NR < 2) next 目前行号 (NR) 小于 2 时,运行下一行 next

  2. fruitAmount[$2] += $5 水果销售数组 fruitAmount 其主键为水果字段 $2,内容为累加销售金额 $5
    写成 fruitAmount[$2] = fruitAmount[$2] + $5 也相同。

  3. for (fruit in fruitAmount)for 取得 fruitAmount 的每个主键 fruit (水果字段)。

  4. fruitAmount[fruit] 以主键 fruit 取得 fruitAmount 销售金额内容值。

---------- Fruit amount
apple 400
banana 400
lemon 100
kiwi 300
---------- Sales amount
kevin 100
marry 500
john 600
预先定义的数组排序
数组排序 @[ind|val]_[num|type|str]_[asc|desc] 选项
  • @unsorted 无排序。

  • ind 按主键(索引)排序,val 按内容值排序。

  • num 尝试以数字排序 (1a 会以 1 排序),无前置数字在前。
    str 按字符串 (文本) 顺序排序。
    type 按类型排序,数字类型或类数字 (内容全部为数字) 在前,字符串 (文本) 类型在后 (1a 为字符串类型)。

  • asc 为升幂,desc 为降幂。

数组排序组合列表

@ind_num_asc @ind_num_desc @ind_str_asc @ind_str_desc @val_num_asc @val_num_desc @val_str_asc @val_str_desc @val_type_asc @val_type_desc
注:没有 ind_type 的组合。

使用方式
PROCINFO["sorted_in"] = "@ind_str_asc"
for (i in array) print i, array[i]
str num type 排序示例
awk '
function sortdemo(format, arr)
{
  PROCINFO["sorted_in"] = format (4)
  print format
  for (i in arr) {
    print typeof(arr[i]), arr[i] (5)
  }
  print "-------------"
}
BEGIN {
  split("31 5 red 1a", a) (1)
  a[100] = 400 (2)
  sortdemo("@val_str_asc", a) (3)
  sortdemo("@val_num_asc", a)
  sortdemo("@val_type_asc", a)
}
'
1 创建数组 a 内容值为 315red1a
2 数组 a[100] 设置为数字 400。
3 将排序方式 @val_str_asc 传递给 sortdemoformat
4 sortdemo 设置排序方式 format
5 打印每个数组的数据类型及内容 (awk 4.2 才有支持 typeof 函数)。
@val_str_asc
string 1a
strnum 31
number 400
strnum 5
string red
-------------
@val_num_asc
string red
string 1a
strnum 5
strnum 31
number 400
-------------
@val_type_asc
strnum 5
strnum 31
number 400
string 1a
string red
-------------
统计水果销售明细 (多主键数组)
awk -F, '
{
  if (NR < 2) next
  fruitQty[$2,$3] += $4
  fruitAmount[$2,$3] += $5
}
END {
  PROCINFO["sorted_in"] = "@ind_str_asc"
  for (combKey in fruitQty) {
    split(combKey, sepKey, SUBSEP)
    print sepKey[1], sepKey[2], fruitQty[sepKey[1], sepKey[2]], fruitAmount[sepKey[1], sepKey[2]]
  }
}
' sales.txt
程序说明:
  1. for (combKey in fruitQty)for 取得 fruitQty 的每个主键 combKey

  2. split(combKey, sepKey, SUBSEP)
    由于 (原始的) fruitQty 为多主键,combKey 也是多主键,运行 splitSUBSEP (数组分隔字符) 分割出每个主键保存至数组 sepKey 中的 sepKey[1]sepKey[2]

  3. fruitQty[sepKey[1], sepKey[2]] 以主键 sepKey[1]sepKey[2] 取得 fruitQty 的内容值,fruitAmount 的主键跟 fruitQty 相同,以相同的主键 fruitAmount[sepKey[1], sepKey[2]] 取得 fruitAmount 的内容值。

水果销售明细结果
apple green 3 300
apple red 1 100
banana yellow 4 400
kiwi green 3 300
lemon yellow 1 100
统计水果销售明细 (asorti 示例)
awk -F, '
{
  if (NR < 2) next
  fruitQty[$2,$3] += $4
  fruitAmount[$2,$3] += $5
}
END {
  asorti(fruitQty, sortKey)
  for (i=1; i in sortKey; i++) {
    split(sortKey[i], sepKey, SUBSEP)
    print sepKey[1], sepKey[2], fruitQty[sepKey[1], sepKey[2]], fruitAmount[sepKey[1], sepKey[2]]
  }
}
' sales.txt

运行结果跟前述的「水果销售明细结果」相同。

程序说明:
  1. asorti(fruitQty, sortKey)fruitQty 的主键排序后保存至 sortKey 其主键值由整数 1 开始。

    sortKey 数组如下:
    1 apple^\green
    2 apple^\red
    3 banana^\yellow
    4 kiwi^\green
    5 lemon^\yellow
  2. for (i=1; i in sortKey; i++)for 取得每个整数主键 iforconditioni in sortKey 改为 i <= length(sortKey) 也相同。

    for (i=1; i <= length(sortKey); i++)
  3. split(sortKey[i], sepKey, SUBSEP)
    sortKey[i] 为 (已排序的) 多主键的内容,运行 splitSUBSEP (数组分隔字符) 分割出每个主键保存至数组 sepKey 中的 sepKey[1]sepKey[2]

  4. fruitQty[sepKey[1], sepKey[2]] 以主键 sepKey[1]sepKey[2] 取得 fruitQty 的内容值,fruitAmount 的主键跟 fruitQty 相同,以相同的主键 fruitAmount[sepKey[1], sepKey[2]] 取得 fruitAmount 的内容值。

水果销售要统计出小计,以多维数组来实作比较简单
awk -F, '
{
  if (NR < 2) next
  fruitSales[$2][$3]["qty"] += $4
  fruitSales[$2][$3]["amt"] += $5
}
END {
  PROCINFO["sorted_in"] = "@ind_str_asc"
  for (fruit in fruitSales) {
    subqty = 0; subamt = 0;
    for (color in fruitSales[fruit]) {
      qty = fruitSales[fruit][color]["qty"]
      amt = fruitSales[fruit][color]["amt"]
      print fruit, color, qty, amt
      subqty += qty; subamt += amt;
    }
    print "--------------------"
    print fruit, "subtotal", subqty, subamt
    print ""
  }
}
' sales.txt
apple green 3 300
apple red 1 100
--------------------
apple subtotal 4 400

banana yellow 4 400
--------------------
banana subtotal 4 400

kiwi green 3 300
--------------------
kiwi subtotal 3 300

lemon yellow 1 100
--------------------
lemon subtotal 1 100
将水果按颜色分类 (多维数组)
(
cat << EOF
fruit,color-list
apple,green,red
lemon,green,yellow
banana,yellow
kiwi,green,gold
grape,black,red,purple
EOF
) |
awk -F, '
{
  if (NR < 2) next
  for(i=2; i<=NF; i++)
    array[$i][$1]
}
END {
  PROCINFO["sorted_in"] = "@ind_str_asc"
  for (color in array) {
    print color, length(array[color])

    for (fruit in array[color]) {
      print "\t" fruit
    }
  }
}
'
程序说明:
  1. for(i=2; i<=NF; i++)for 取得 i,该值跳过第 1 字段的水果,由 2 开始至该行的字段数 NF,i 值为颜色字段。

  2. array[$i][$1] 收集多维数组 array 第 1 维度的主键为颜色字段 $i,第 2 维度的主键为水果 $1
    注:只需收集主键值,不需要内容值。

black 1
        grape
gold 1
        kiwi
green 3
        apple
        kiwi
        lemon
purple 1
        grape
red 2
        apple
        grape
yellow 2
        banana
        lemon