AWK是一种处理文本文件的语言。它将文件作为记录序列处理。在一般情况下,文件内容的每行都是一个记录。每行内容都会被分割成一系列的域,因此,我们可以认为一行的第一个词为第一个域,第二个词为第二个,以此类推。AWK程序是由一些处理特定模式的语句块构成的。AWK一次可以读取一个输入行。对每个输入行,AWK解释器会判断它是否符合程序中出现的各个模式,并执行符合的模式所对应的动作。

—— 阿尔佛雷德·艾侯The A-Z of Programming Languages: AWK

1、AWK概述

Awk 是一个维护和处理文本数据文件的强大语言。在文本数据有一定的格式,即每行数据包 含多个以分界符分隔的字段时,显得尤其有用。它非常强大,专为文本处理而设计。 它的名字来源于其作者的姓氏Alfred Aho, Peter Weinberger, and Brian Kernighan。

AWK有下面几个变种:

  • AWK是最原始的AWK, 来自 AT&T 实验室的原始AWK
  • NAWK - 来自AT&T实验室的更新和改进的AWK版本
  • GAWK是GNU AWK。 所有GNU/Linux发行版都默认提供GAWK, 它与AWK和NAWK完全兼容

AWK 可以用来处理很多任务,如文字处理、格式化的文本报告等。本文中AWK如果没有特别说明,那么指的就是GAWK

awk

2、AWK语法

Awk的基础语法:

1
awk options '/pattern/{action}' input-file

1
awk options '{action}' input-file
  • options 是可选参数,主要有-F-f-v:
    • -F fs 或 --field-separator fs:字段分界符,如不指定,默认是使用空格作为分界符
    • -f scripfile or --file scriptfile:从文件中读取awk命令,文件可以使用任意扩展名(也可不用),使用.awk扩展名便于维护
    • -v var=value or --asign var=value:赋值一个用户定义变量
  • /pattern/{action} 需要用单引号包围起来
  • /pattern 是可选的,如不指定,awk将处理input-file中所有的记录,如果指定了,那么只会对处理匹配模式的记录
  • {action} 是 awk 命令, 可以是单个命令,也可以是多个命令,所有的命令必须放在{}中间
  • input-file 是指要处理的文件
1
2
$ awk '/screen/{print $1}' /etc/passwd
_screensaver:*:203:203:Screensaver:/var/empty:/usr/bin/false

3、AWK执行流程

3.1、程序结构

在典型的awk程序中包含三个区域(BEGIN区域、body区域、END区域):

1
awk 'BEGIN{awk commands} /pattern/{action} END{awk commands}'

BEGIN区域:

该区域的语法:

1
BEGIN {awk commands}

BEGIN区域的命令在读取文件之前、在执行body区域命令之前执行,而且仅执行一次。 这里块区域适合初始化变量、打印报告头部信息等工作。此外需要注意的是BEGIN是关键字且必须是大写的,同时BEGIN区域是可选的 BEGIN区域的命令包含在{}中,可以是一个也可以是多个命令

body区域: 该区域的语法:

1
/pattern/{action}

body区域的命令会在文件读取一行就执行一次, /pattern/是可选的。

END区域 该区域的语法:

1
END{awk commands}

END区域会在执行完所有操作后再执行,且只执行一次,这里适合执行一些清理操作,或打印报文结尾信息等。END是关键字需要大写,END区域可以有一个或多个命令,需要包含在 {} 中,同时END区域是可选的。

3.2、AWK 执行流程

awk

awk在读取文件并执行body区域命令前,执行BEGIN区域命令一次。然后每读取一次输入行,就会执行一次body区域命令,该区域命令可以由多个/pattern/{action} 组成,会依次执行。最后程序在结束时,会执行一次BEGIN区域命令。

1
2
3
4
5
$ date |  awk 'BEGIN{print "----------Current Time-----------"} \
  {print $4}END{print "----------- END -----------------"}'
----------Current Time-----------
15:51:45
----------- END -----------------

4、AWK内置变量

Awk提供了很多内置变量。如awk默认分隔符是空格,可以使用-F选项来指定它,如:

1
awk -F':' '{print $1}' /etc/passwd

这样可以通过内置变量 FS 来完成,如:

1
awk 'BEGIN{FS=":"}{print $1}' /etc/passwd

awk还提供了其他的内置变量,如:

变量 说明
ARGC 保存着传递给awk脚本的所有参数的个数
ARGV ARGV 是一个数组,保存着传递给awk脚本的所有参数,其索引范围从0到ARGC
ARGIND ARGINDARGV 的一个索引, ARGV[ARGIND] 会返回当前正在处理的文 件名
FILENAME 当前处理的文件名(awk处理多个输入文件时很有用)
FS 输入字段分隔符
OFS 输出字段分隔符
RS 记录分隔符
ORS 输出记录分隔符
NF “number of fields in the current record”, 当前记录的字段个数
NR “ordinal number of the current record”,当前记录在所有记录中的序号
FNR 当前处理的记录号,在处理一个新的文件时FNR会被重置为1,而NR不会被重置
ENVIRON ENVIRON 是一个包含所有 shell 环境变 量的数组,其索引就是环境变量的名称。
IGNORECASE IGNORECASE 的默认值是0,所有awk区分大小写。值设置为 1 时,则不区分大小写
ERRNO 当执行 I/O 操作(比如 getline)出错时,变量 ERRNO 会保存错误信息
FIELDWIDTHS 按固定宽度来解析字段,如文件中有三列,每列分别含有4、5、6个字符,那么可以设置成BEGIN {FIELDWIDTHS="4 5 6"}
RSTART match() 函数匹配的字符串的起始位置,如果没有匹配则为0(匹配时从1开始)
RLENGTH match() 函数匹配的字符串的长度
SUBSEP 数组中下标分隔符
1
2
$ date | awk 'BEGIN{OFS="-"} {print $2, $3, $6}'
May-23-2022

5、AWK变量的操作符

和其他程序设计语言一样,awk允许在程序中设置变量。变量以字母开头,后续字符可以是数字、字符、下划线,但关键字 不能作为awk变量。变量可以直接使用而不需要事先声明,而且没有数据类型的概念,一个awk变量是number还是string 取决于变量所处的上下文。如果要初始化变量,最好在 BEGIN 区域中操作(因为 BEGIN 区域只会执行一次)

也可以使用 -v 选项对用户定义的变量进行赋值,该变量在 BEGIN 区域也是可用的,如:

1
$ awk -v hello=$date '{print hello}'

awk 支持多种运算,这些运算与 C 语言基本相同。

5.1、一元操作符

操作符 描述
+ 取正(返回数字本身)
- 取反
++ 自增
自减

5.2、算术操作符

操作符 描述
+
-
*
/
% 取余

5.3、字符串操作符

操作符 描述
空格 空格是连字符的操作符,如str3=str2 str1 str3为str2 连接str1后的内容

5.4、赋值操作符

操作符 描述
= 赋值
+= 加法赋值
-= 减法赋值
*= 乘法赋值
/= 触发赋值
%/ 取余赋值

5.5、比较操作符

操作符 描述
> 大于
>= 大于等于
< 小于
<= 小于等于
== 等于
!= 不等于
&&
||

5.6、正则表达式操作符

操作符 描述
~ 匹配
!~ 不匹配

6、AWK分支和循环

awk提供完备的流程控制语句类似于 C 语言:if, if-else, while, do-while, for, break, continue

7、AWK关联数组

Awk 的数组,都是关联数组,即一个数组包含多个”索引/值”的元素。索引没必要是一系列 连续的数字,实际上,它可以使字符串或者数字,并且不需要指定数组长度。 语法:

1
2
3
4
arrayname[string]=value
- arrayname是数组名称
- string是数组索引
- value是为数组元素赋的值

可以使用arrayname[index]访问数组中的某个特定元素:

 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
$ cat array-assign.awk
BEGIN {
  item[101]="HD Camcorder";
  item[102]="Refrigerator";
  item[103]="MP3 Player";
  item[104]="Tennis Racket";
  item[105]="Laser Printer";
  item[1001]="Tennis Ball";
  item[55]="Laptop";
  item["na"]="Not Available";
  print item["101"];
  print item[102];
  print item["103"];
  print item[104];
  print item["105"];
  print item[1001];
  print item["na"];
}
$ awk -f array-assign.awk
HD Camcorder
Refrigerator
MP3 Player
Tennis Racket
Laser Printer
Tennis Ball
Not Available

7.1、判断数组元素是否存在

如果视图访问一个不存在的数组元素,awk 会自动以访问时指定的索引建立该元素,并赋予 null 值。为了避免这种情况,在使用前最后检测元素是否存在。

可以使用if语句检查元素是否存在,返回true,则表示元素存在数组中,语法:

1
if(index in array-name)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ cat array-refer.awk
BEGIN {
 x = item[55];
 if (55 in item)
   print "Array index 55 contains",item[55];
 item[101]="HD Camcorder";
 if (101 in item)
   print "Array index 101 contains",item[101];
 if (1010 in item)
   print "Array index 1010 contains",item[1010];
}

$ awk -f array-refer.awk
Array index 55 contains
Array index 101 contains HD Camcorder

7.2、遍历数组元素

可以使用for来遍历数组,语法:

1
2
for (var in array-name) 
  actions

如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ cat array-for-loop.awk
BEGIN {
 item[101]="HD Camcorder";
 item[102]="Refrigerator";
 item[1001]="Tennis Ball";
 item[55]="Laptop";
 item["no"]="Not Available";
 for(x in item)
  print item[x]
}
$ awk -f array-for-loop.awk
Not Available
Laptop
HD Camcorder
Refrigerator
Tennis Ball

7.3、删除数组元素

可以使用 delete 语句删除数组元素,语法:

1
delete arrayname[index]

如:

 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
BEGIN {
  item[101]="HD Camcorder";
  item[102]="Refrigerator";
  item[1001]="Tennis Ball";
  item[55]="Laptop";
  item["no"]="Not Available";
  for(x in item){
    print item[x]
  }
  print "------- After deleted --------"
  delete item[101]
  delete item[102]
  delete item["no"]
  for(x in item){
    print item[x]
  }
}
$ awk -f array-for-loop.awk
Not Available
Laptop
HD Camcorder
Refrigerator
Tennis Ball
------- After deleted --------
Laptop
Tennis Ball

8、AWK函数

awk定义并支持一系列的内置函数,这使得awk提供的功能更为完善、强大。

8.1、数值函数

函数 描述
int(n) 返回给定参数的整数部分值
log(n) 返回给定参数的自然对数,参数 n 必须是正数,否则会抛出错误
sqrt(n) 返回指定整数的正平方根,该函数参数也必须是整数,如果传递负数将会报错
exp(n) 返回e的n次幂
sin(n) 返回 n 的正弦值,n 是弧度值
cos(n) 返回 n 的余弦值,n 是弧度值
atan2(m, n) 该函数返回 m/n 的反正切值,m 和 n 是弧度值。

8.2、字符串函数

函数 描述
index 用来获取给定字符串在输入字符串中的索引(位置)
length 返回字符串的长度
split(input-string,output-array,separator) split 函数把字符串分割成单个数组元素
substr(input-string,location,length) substr 函数从字符串中取指定的部分(子串)
sub(original-string,replacement-string,string-variable) string-variable中用replacement-string替换第一次出现的original-string
gsub(original-string,replacement-string,string-variable) gsub 和 sub 类似,只是gsub 会把所有的 original-string 替换成 replacement-string
match(input-string, search-string) 函数从输入字符串中检索给定的字符串(或正则表达式),当检索到字符串时,返回一个正数值
tolower(input-string) 把给定的字符串转换为小写
toupper(input-string) 把给定的字符串转换为大写
printf “print format”, variable1,variable2,etc. 格式化输出结果

8.3、字符串格式化

printf 可以非常灵活、简单的进行格式化输出结果,printf中可以使用的特殊字符:

特殊字符 描述
\n 换行
\t 制表符
\v 垂直制表符
\b 退格
\r 回车符
\f 换页

printf 格式化字符

格式化字符 描述
s 字符串
c 单个字符
d 数值
e 指数
f 浮点数
g 根据值决定使用e 或 f 中较短的输出
o 八进制
x 十六进制
% 百分号

printf 支持复杂的格式化控制输出,比如: - 指定字符串宽度时,在%和格式化字符之间加上一个-,表示左对齐, 如:

1
2
$ awk 'BEGIN{printf("|%10s|%-10s|\n", "Hello", "Hello")}'
|     Hello|Hello     |
  • 在字符串长度不够时可以进行补0,%05d%.5d效果相同,如:
1
2
$ awk 'BEGIN{ printf "|%5d|%05d|%.5d|\n", 10, 10, 10}'
|   10|00010|00010|

更多的情况如下:

Format Variable Results
%c 100 “d”
%10c 100 ” d”
%010c 100 ” 000000000d”
%d 10 “10”
%10d 10 ” 10”
%10.4d 10.123456789 ” 0010”
%10.8d 10.123456789 ” 00000010”
%.8d 10.123456789 ” 00000010”
%010d 10.123456789 “0000000010”
%e 987.1234567890 “9.871235e+02”
%10.4e 987.1234567890 “9.8712e+02”
%10.8e 987.1234567890 “9.87123457e+02”
%f 987.1234567890 “987.123457”
%10.4f 987.1234567890 ” 987.1235”
%010.4f 987.1234567890 “00987.1235”
%10.8f 987.1234567890 “987.12345679”
%g 987.1234567890 “987.123”
%10g 987.1234567890 ” 987.123”
%10.4g 987.1234567890 ” 987.1”
%010.4g 987.1234567890 “00000987.1”
%.8g 987.1234567890 “987.12346”
%o 987.1234567890 “1733”
%10o 987.1234567890 ” 1733”
%010o 987.1234567890 “0000001733”
%.8o 987.1234567890 “00001733”
%s 987.123 “987.123”
%10s 987.123 ” 987.123”
%10.4s 987.123 ” 987.”
%010.8s 987.123 “000987.123”
%x 987.1234567890 “3db”
%10x 987.1234567890 ” 3db”
%010x 987.1234567890 “00000003db”
%.8x 987.1234567890 “000003db”
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
$ cat format.awk
#!/usr/bin/env awk -f
BEGIN{
  printf "%s\n", 987.123
  printf "%10s\n", 987.123
  printf "%10.4s\n", 987.123
  printf "%x\n", 987.1234567890
  printf "%10x\n", 987.1234567890
  printf "%010x\n", 987.1234567890
  printf "%.8x\n", 987.1234567890
}
$ awk -f format.awk
987.123
   987.123
      987.
3db
       3db
00000003db
000003db

8.3、自定义函数

需要编写大量代码同时又要多次重复执行其中某些片段时,可以使用自定义函数。语法:

1
2
3
4
function fn-name(parameters)
{
    #.....
}
  • fn-name: 函数名,名称规则和变量名一样,以字母开头,后续可以是字母、数值、下划线,关键字不能作为函数名
  • parameters: 多个参数使用逗号进行分隔,也可以没有参数

8.4、位操作

和 C 语言类似,awk 也可以进行位操作。

操作符 描述
and 按位与
or 按位或
xor 按位异或
compl 取反, 如:15 = 01111, 15 compl = 10000
lshift 左移,函数把操作数向左位移,可以指定位移多少次,位移后右边补 0
rshift 右移,该函数把操作数向右位移,可以指定位移多少次,位移后左边补 0

简单的位操作示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ cat bits.awk      
BEGIN{
  num1=15
  num2=25
  print "AND: " and(num1,num2);
  print "OR: " or(num1,num2);
  print "XOR: " xor(num1,num2);
  print "LSHIFT: " lshift(num1,2);
  print "RSHIFT: " rshift(num1,2);
}
$ awk -f bits.awk
AND: 9
OR: 31
XOR: 22
LSHIFT: 60
RSHIFT: 3

8.5、时间函数

systime()函数返回系统的 POSIX 时间,即自1970 年 1 月 1 日起至今经过的 秒数。

1
2
 $ awk 'BEGIN { print systime() }'
 1365585325

可以使用 strftime(string)strftime(string, timestamp) 函数对时间进行格式化。strftime 支持的格式标识符如下:

格式标识符 描述
%Y 年份的完整格式,如 2011
%y 两位数字的年份,如 2011 显示为 11
%m 两位数字月份,一月显示为 01
%d 两位数字日期,4 号显示为 04
%H 24 小时格式, 1 p.m 显示为 13
%I 12 小时格式, 1 p.m 显示为 01
%M 两位数字分钟,9 分显示为 09
%S 两位数字秒,5 秒显示为 05
%c 显示本地时间的完整格式,如:Fri May 20 21:24:25 2022
%D 简单日期格式,和%m/%d/%y 相同
%F 简单日期格式,和%Y-%m-%d 相同
%T 简单时间格式,和%H:%M:%S 相同
%x 基于本地设置的时间格式
%X 基于本地设置的时间格式
%r 简单时间格式,和%I:%M%:%S %p相同
%R 简单时间格式,和%H:%M相同
%B 月份完整单词,一月显示为 January
%b 月份缩写,一月显示为 Jan
%p 显示 AM 或 PM,和%l 搭配使用
%a 三位字符星期,周一显示为 Mon
%A 完整的日期,周一显示为 Monday
%Z 时区,太平洋地区时区显示为 PST
%n 换行符
%t 制表符
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ cat time.awk
BEGIN{
  print strftime("%Y-%m-%d %H:%M:%S")
  print strftime("%Y-%m-%d %H:%M:%S", systime())
  print strftime("%F %T")
}
$ awk -f time.awk
2022-05-20 16:36:43
2022-05-20 16:36:43
2022-05-20 16:36:43

9、AWK脚本程序

awk 和 shell 一样是一个解释型语言,也可以用来写可以执行的脚本程序。

和shell脚本类似,awk脚本以下面一行开头:

1
#!/path/to/awk/utility -f

如在我的系统中,awk安装在/usr/local/bin/awk,所以我的脚本开头第一行是:

1
#!/usr/local/bin/awk -f
  • #!,指明使用那个解释器来执行脚本中的命令
  • /usr/local/bin/awk,解释器
  • -f,解释器选项,用来指定读取程序文件

需要注意的是,直接指定解释器位置,有可能导致一个问题,换到其他机器,同样脚本却无法执行,因为脚本解释器可能安装在不同的目录中。更好的解决办法是用#!/usr/bin/env awk, env 会在系统PATH目录中查找awk。

编辑保存好脚本bits.awk,如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#! /usr/bin/env awk -f
BEGIN{
  num1=15
  num2=25
  print "AND: " and(num1,num2);
  print "OR: " or(num1,num2);
  print "XOR: " xor(num1,num2);
  print "LSHIFT: " lshift(num1,2);
  print "RSHIFT: " rshift(num1,2);
}

然后,给脚本添加可自行权限:

1
$ chmod +x bits.awk

此时就可以执行它了:

1
2
3
4
5
6
$ ./bits.awk              
AND: 9
OR: 31
XOR: 22
LSHIFT: 60
RSHIFT: 3

10、有趣的使用案例

10.1、只打印特殊行号的行

可以使用内置变量NR(表示当前记录在所有记录中的行号)进行处理。

myheart文件内容如下:

1
2
3
1 Every night in my dreams
2 I see you, I feel you
3 That is how I know you go on

输出文件myheart的奇数行:

1
2
3
$ awk "NR % 2 == 1" myheart
1 Every night in my dreams
3 That is how I know you go on

10.2、显示一行最后一个字段值

内置变量 NF 可以获取到一行的字段数量,使用$NF就可以获取到一行的最后一个字段值,如:

1
2
3
4
5
6
7
8
$ cat myheart           
1 Every night in my dreams
2 I see you, I feel you
3 That is how I know you go on
$ awk '{print $1, $NF}' myheart              
1 dreams
2 you
3 on

10.3、在Makefile中输出命令帮助信息

下面是来自项目kratos-layoutMakefile:

 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
GOPATH:=$(shell go env GOPATH)
VERSION=$(shell git describe --tags --always)
INTERNAL_PROTO_FILES=$(shell find internal -name *.proto)
API_PROTO_FILES=$(shell find api -name *.proto)

.PHONY: init
# init env
init:
	go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
	go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
	go install github.com/go-kratos/kratos/cmd/kratos/v2@latest
	go install github.com/go-kratos/kratos/cmd/protoc-gen-go-http/v2@latest
	go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest

.PHONY: config
# generate internal proto
config:
	protoc --proto_path=./internal \
	       --proto_path=./third_party \
 	       --go_out=paths=source_relative:./internal \
	       $(INTERNAL_PROTO_FILES)

.PHONY: api
# generate api proto
api:
	protoc --proto_path=./api \
	       --proto_path=./third_party \
 	       --go_out=paths=source_relative:./api \
 	       --go-http_out=paths=source_relative:./api \
 	       --go-grpc_out=paths=source_relative:./api \
 	       --openapi_out==paths=source_relative:. \
	       $(API_PROTO_FILES)

.PHONY: build
# build
build:
	mkdir -p bin/ && go build -ldflags "-X main.Version=$(VERSION)" -o ./bin/ ./...

.PHONY: generate
# generate
generate:
	go mod tidy
	go get github.com/google/wire/cmd/wire@latest
	go generate ./...

.PHONY: all
# generate all
all:
	make api;
	make config;
	make generate;

# show help
help:
	@echo ''
	@echo 'Usage:'
	@echo ' make [target]'
	@echo ''
	@echo 'Targets:'
	@awk '/^[a-zA-Z\-\_0-9]+:/ { \
	helpMessage = match(lastLine, /^# (.*)/); \
		if (helpMessage) { \
			helpCommand = substr($$1, 0, index($$1, ":")-1); \
			helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
			printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \
		} \
	} \
	{ lastLine = $$0 }' $(MAKEFILE_LIST)

.DEFAULT_GOAL := help

其中help命令输出帮助信息是用awk从Makefile文件中收集注释信息生成的,提取awk命令如下:

1
2
3
4
5
6
7
8
9
awk '/^[a-zA-Z\-\_0-9]+:/ { \
helpMessage = match(lastLine, /^# (.*)/); \
	if (helpMessage) { \
	  helpCommand = substr($1, 0, index($1, ":")-1); \
		helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
		printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \
	} \
} \
{ lastLine = $0 }' $(MAKEFILE_LIST)

这段awk程序中没有BEGINEND区域命令,只有body部分,且是由两个body区域命令构成。 第一个由awk '/^[a-zA-Z\-\_0-9]+:/ { \开始的这段body区域,由一段模式匹配开始,只有匹配的行才会交给 其后的{}中命令进行执行,具体也就是第一个body区只处理Makefile中的命令行,也就是init:config:help:这些行。

第二个body区域中只有一行命令:lastLine = $0 , 把读取到的行保存到变量lastLine中,此时awk读取到下一行进行处理时,lastLine保存的就是它上一行的内容。

$(MAKEFILE_LIST) 指定是当前的Makefile文件

此时再回头看第一个body区域命令,就很清晰了。该区域命令,在遇到当前行是Markfile中的命令,且上一行是以#开始的注释行时,使用substrindex函数进行截取出Markfile指令(不包含:), 并使用substr截取上一行的注释内容(去掉开头的#),再使用printf 函数进行格式化输出指令和它的说明信息,%-22s以左对齐、最小22个字符宽度格式化指令进行输出,如:

1
2
3
4
5
6
7
init                   init env
config                 generate internal proto
api                    generate api proto
build                  build
generate               generate
all                    generate all
help                   show help

10.4、解析处理CSV文件

逗号分隔值(Comma-Separated Values,CSV,有时也称为字符分隔值,因为分隔字符也可以不是逗号),以纯文本形式存储表格数据(数字和文本)。 典型情况下每行一条记录,用分隔符来分隔字段。CSV格式的标准定义参见RFC 4180

awk 内置变量FS, 指定分隔符,也就是出现两个字段之间的部分。FS 定义了字段不是什么,而不是直接去定义字段是什么。

在对于仅仅使用分隔符(如逗号)分割数据,字段中没有嵌入的分隔符时,如文件imgs.csv内容:

1
2
3
sj_mino1001.jpg,715282,4FB55FE8
sj_mino1002.jpg,471289,93203C5C
sj_mino1003.jpg,451929,C4E80467

对于这种情况,用FS 就可以正确解析出每个字段内容, 如:

1
2
3
4
$ awk -v FS="," '{print $1, $2, $3}' demo.csv
sj_mino1001.jpg 715282 4FB55FE8
sj_mino1002.jpg 471289 93203C5C
sj_mino1003.jpg 451929 C4E80467

但对于字段中嵌套了分隔符的CSV,如在双引号(double quotes)中嵌入逗号作为字段的情况,如下面的数据:

1
"SELECT `id`, `name`, `phone` FROM `customer` ORDER BY `id` DESC LIMIT ?, ?",saas,"17",3.146,"3.251","0","0","7.53","7530192","3.53","5"

变量FPAT为这种情况提供了解决方案,变量 FPAT 值是一个正则表达式字符串,描述了每个字段的内容。上面的数据中,字段有用双引号包围并且其中 嵌套了逗号、没有逗号分隔符、用双引号包围没有逗号分隔符。这种情况下,可以用正则表达式 /([^,]+)|("[^"]+")/来匹配,也就是匹配不还有"的一个或多个字符,或者匹配 用双引号包围,但是包围的部分是非引号的一个或多个字符。赋值给FPAT时需要将这个正则表达式转换为字符串,并对其中的双引号进行转义,也就是:

1
FPAT = "([^,]+)|(\"[^\"]+\")"

用这个来解析上面的csv数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
$ cat sql.awk 
BEGIN{
  FPAT = "([^,]+)|(\"[^\"]+\")"
}
{
  for(i=1;i<=NF;i++){
    printf("$%d = %s\n", i, $i)
  }
}
$ awk -f sql.awk sql.csv
$1 = "SELECT `id`, `name`, `phone` FROM `customer` ORDER BY `id` DESC LIMIT ?, ?"
$2 = saas
$3 = "17"
$4 = 3.146
$5 = "3.251"
$6 = "0"
$7 = "0"
$8 = "7.53"
$9 = "7530192"
$10 = "3.53"
$11 = "5"

上面的数据中,字段都是非空的,如果允许字段为空,那么可以把正则表达式中的+改为*来处理这种情况:

1
FPAT = "([^,]*)|(\"[^\"]*\")"

10.5、查看服务器当前80端口网络连接数

netstat 是查看网络相关数据的常用命令,可以显示路由表、实际的网络连接以及每一个网络接口设备的状态信息。ss 直接从Linux内核中获取TCP和连接状态信息, 效率比 netstat 好:

1
2
3
4
$ netstat -tan | awk '$4~/:80$/{++state[$NF]} END {for(key in state) print key, "\t", state[key]}'
LISTEN 	1
$ ss -tan | awk '$4~/:80$/{++state[$1]} END {for(key in state) print key, "\t", state[key]}'
LISTEN 	1

10.6、最常用命令列表

1
$ history | awk '{++a[$2]}END{for(i in a){print a[i] " " i}}' | sort -rn | head

1
$ history | awk '{print $2}' | sort | uniq -c | sort -rn | head

11、参考资源