Linux三剑客


达达尼昂和三个火枪手

Linux三剑客

Linux运维想做好,三剑客少不了。所谓Linux三剑客,指的是三个Linux程序:grepsedawk。这三个程序都是处理文本时所用的。因为三个命令具有互补性,经常一起使用,因此称为Linux三剑客。

具体来说,三剑客的功能分别是:

  • grep:擅长单纯的查找、匹配文本
  • sed:擅长编辑、处理匹配到的文本
  • awk:擅长格式化文本,对文本进行复杂的处理

下面我们将详细介绍Linux三剑客。

文本过滤/搜索器:grep

grepGlobal search Regular Expression(RE) and Print out the line),直白的翻译就是全面搜索正则表达式并把行打印出来grep其实是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来。

通常,我们使用grep过滤、搜索的特定字符。正是因为grep能够使用正则表达式,因此grep + 正则表达式 + 多种命令,就可以非常灵活的使用。

1. 基础用法:查找单个文件

grep的基础语法是

grep [option] [pattern] file

其中:

  • option是命令行选项
  • partter是要查找的内容或者正则表达式,统称为模式
  • file是将被查找的文件

当然,一般来说更常见的用法是下面这种:

grep [pattern] file [option]

因为这种用法更加灵活,我们先写好需要查询的模式,而后再根据需要写好选项。

无参数查找文本

使用grep的基础语法,会待查找到内容输出到屏幕上

cat /etc/passwd > test.txt
grep "root" test.txt

查找文本结果

也可以使用正则表达式来匹配

cat /etc/passwd > test.txt
grep "^#" test.txt

正则表达式查找结果

查找多个文件

事实上,grep支持从多个文件中查询文本。只需要给出多个文件即可,即

grep [option] [pattern] file1 file2 file3 ...

例如

cat /etc/passwd > test1.txt
cat /etc/ssh/sshd_config >> test2.txt
grep "root" test1.txt test2.txt

多个文件查找结果

事实上,我们可以使用Bash的花括号扩展来直接多次查询

cat /etc/passwd > test1.txt
cat /etc/ssh/sshd_config >> test2.txt
grep "root" test{1,2}.txt

Bash花括号扩展多个文件查找结果

-E 使用扩展正则表达式

如果要使用扩展正则表达式的话,需要使用-E参数

cat /etc/passwd > test.txt
grep -e "(root)+" -e "(.:)" test.txt  -i -n -E

扩展正则表达式查找结果

-i 忽略大小写

使用-i参数忽略大小写

cat /etc/passwd > test.txt
grep -i "root" test.txt

忽略大小写查找文本结果

-n 显示行数

使用-n参数显示匹配的行的行号

cat /etc/passwd > test.txt
grep -n "root" test.txt

显示行号查找文本结果

-c 统计行数

使用-c参数输出匹配到的行数

cat /etc/passwd > test.txt
grep -c "root" test.txt

统计行数查找文本结果

-v 反向匹配

使用-v来反向匹配,即排除给定的关键字

cat /etc/passwd > test.txt
grep -v "root" test.txt

反向匹配查找文本结果

-e 多个模式匹配

使用-e参数匹配多个模式

cat /etc/passwd > test.txt
grep -e "root" -e "install" test.txt -i -n

多模式匹配查找文本结果

-o 输出匹配内容

使用-o参数输出匹配到的内容,一般和-n联合使用

cat /etc/passwd > test.txt
grep -o "install" test.txt -i -n

输出匹配内容查找文本结果

-q 条件测试

一般grep的结果都会在屏幕上输出,使用-q将会强制不输出。

一般来说使用-q参数都是为了在shell脚本中使用,用于条件测试。例如:

cat /etc/passwd > test.txt
grep "root" test.txt -q
echo $?

使用-q参数则不会输出任何信息,如果命令运行成功返回0,失败则返回非0值。换而言之,如果在文件中找到了要查找的内容,则返回0,否则返回其他非0值。一般用于条件测试。

运行结果为:

条件测试运行结果

2. 进阶用法:递归查找

有时候,我们需要对一个目录中的所有文件全部进行查找。可能有人会说,我们直接写出多个文件不就可以了么?

但是更多的时候,我们需要对一个目录树进行递归查找,例如:

tree ./ -I build

tree结果

当前文件夹下既有文件,也有文件夹,而文件夹下又有文件,单纯的罗列出当前文件夹下所有的文件就很蠢笨了。

所以,正式我们可能需要对一个文件夹进行递归查找,而非多文件查找,grep提供了特殊的功能。

-r 递归查找

使用-r参数进行递归查找,同时grep命令的语法格式为

grep [option] [pattern] folder -r

-r参数放在option中还是单独放在后面都没问题,但是一般更常用的是下面这种写法

grep [pattern] folder -r [option]

例如查找tools文件夹下所有有echo的内容

grep "echo" ./tools -r -n

递归查找结果

3. 管道符结合其他命令

事实上,grep如果只是查找文件的话,那么可能还不是那么强大,grep真正强大的地方就再无,我们使用管道符可以把前一个命令输出到屏幕上的内容作为输入,而后进行查询。

此时,grep的语法为

command | grep [option] [pattern]

其中:

  • option是命令行选项
  • partter是要查找的内容或者正则表达式,统称为模式

当然,一般来说更常见的写法依旧是

command | grep [pattren] [option]

原因么还是一样的,先确定好pattern,然后再根据我们的需要给出option

多次筛选/查找文件

grep命令本身会将查找的结果输出在屏幕上,因此command当然可以是grep命令。

单独使用一次grep只能够对文本进行一次筛选,但是结合管道符|就可以进行多次筛选。

例如下面我们先排除空行,然后再排除echo的行

wget https://raw.githubusercontent.com/jackwang0108/CS537-Project3/main/tools/autobuild.sh -O test.txt
cat test.txt

下载测试文件

grep -v "^$" test.txt | grep -v "^echo" -n

管道符多次筛选结果

搜索文件

grepls结合,就可以实现查找符合指定模式的文件。而我们找到所有的文件名后,可以在用xargs进一步去用一个新的程序处理这些文件,或者使用awk去生成报告。

例如我们下面找出/etc中所有的配置文件。配置文件一般的名字就是run config,所以一般都是以rc为名。

ls /etc/ | grep "rc"

搜索配置文件结果

再举一个例子,我们找出所有QQ下载的图片

ls ~/Pictures | grep "QQ" -i

搜索QQ下载图片的例子

流编辑器:sed

sed的全称是Stream Editor,即流编辑器。它是操作、过滤和转换文本内容的强大工具。一般来说,我们通过grepls命令获取到文件的名称之后,接下来就可以把文件名传给sed,而后由sed来处理文件。

详细的说,sed 是一种流编辑器,它是文本处理中非常重要的工具,能够完美的配合正则表达式使用,功能不同凡响。处理时,把当前处理的行存储在临时缓冲区中,称为“模式空间”(pattern space),接着用sed命令处理缓冲区中的内容,处理完成后,把缓冲区的内容送往屏幕。接着处理下一行,这样不断重复,直到文件末尾。

sed处理文件时,文件的内容并没有改变,除非使用重定向存储输出。sed主要用来自动编辑一个或多个文件;简化对文件的反复操作;编写转换程序等。

简单的来说,sed功能结合正则表达式对文件试试快速增删改查。其中查询功能中最常用的两大功能是:

  • 过滤:过滤指定字符串
  • 取行:取出指定的行

sed的工作模式如下图所示:

  • 第一步:sed逐行读取输入的文件(也可以管道传入的上一个文件的输出)
  • 第二步:对读入的行进行模式匹配
  • 第三步:若匹配成功则按照指定的方式进行编辑
  • 第四步:若匹配失败则输出到屏幕(可以取消)

sed工作模式

举一个形象的例子:

sed举例

1. sed介绍

sed的基础语法为:

sed [option] [command] [file]

其中:

  • option是命令行选项
  • commandsed命令,指定了sed处理文本的方式,例如删除、增加等等
  • file是将源文件

一个简单的例子为:

echo "First Line.\nThis is my book.\nLook at my book.\nBefore last line.\nLast Line." > test.txt
cat test.txt
sed '1d;$d' test.txt

其中:

  • 1d$dsed命令,表示删除第一行和第二行

sed的例子

2. sed参数

sed中常用的参数如下(先写这么多,用到了再增加):

参数 作用
-n sed默认会将未匹配成功的行输出,使用-n参数则将不会输出匹配失败的行。
-i sed默认是对文本的副本进行处理的,所以源文件不会受到sed的影响。如果我们想要修改源文件,那么就需要加上-i参数。
-e 类似于grep命令使用-e指定多个模式,sed命令也可以使用-e选项来制定多个编辑命令。
-r grep中使用-E来支持扩展正则表达式,而sed使用-r来支持扩展正则表达式

3. sed命令

一个sed命令由两部分组成:范围命令+操作命令,例如:2p表示将第二行的内容打印出来。

多个sed命令可以使用;连接。

操作命令

sed中的操作命令具体有(目前先写这么多,后面用到了再加):

命令 作用
a 在当前行下面插入文本,即append
i 在当前行上面插入文本,即insert
c 把选定的行改为新的文本,即change
d 删除选定的行,即delete,一般不和-n参数一起使用
p 打印匹配的行的内容,即print,通常与-n参数一起使用
s/正则/替换内容/g 将匹配到的正则内容替换为替换内容,g表示所有匹配到的都进行替换,否则仅替换一个
此外,也可以是s#正则#替换内容#g,或者是s@正则@替换内容@g

范围命令

sed中的范围命令有(先写这么多,用到了再加):

命令 作用
空范围 全文
num num
$ 最后一行
/pattern/ 模式匹配成功的行
start,end start行到end
start,+offset start行到start+offset
start~leap start行开始以leap为间隔的航

4. sed案例

下面我们将给出一些sed的使用案例。

注意:

  • macOSecho会自动转移\n,但是在Linux中需要加上-e参数
  • 此外,macOS上原生的sedGNUgnn-sed不同,如果要在macOS上使用GNUsed,使用brew install gnu-sed然后使用gsed即可

输出文件第二、三行内容

echo "My name is Jack Wang.\nI like Linux.\nI like writing computer programs.\nMy email is 123456789@gmail.com.\nMy website is http://jackwang.cafe ." > test.txt
sed -n "2p;3p" test.txt

运行结果

由于sed默认是:

  • 对于匹配成功的行,处理后打印
  • 对于匹配失败的行,直接打印

所以如果我们没有指定-n参数,那么结果如下:

echo "My name is Jack Wang.\nI like Linux.\nI like writing computer programs.\nMy email is 123456789@gmail.com.\nMy website is http://jackwang.cafe ." > test.txt
sed "2p;3p" test.txt

无-n参数运行结果

发现第二、第三行输出了两次,这其实就是sed的两条路径的处理方式

输出含有指定单词的行

sed中可以利用正则表达式来指定行,因此我们可以使用sed输出含有指定单词的行。

例如:

echo "My name is Jack Wang.\nI like Linux.\nI like writing computer programs.\nMy email is 123456789@gmail.com.\nMy website is http://jackwang.cafe ." > test.txt
sed -n '/like/p' test.txt

结果如下:

输出含有特定单词的行

删除含有特定单词的行

查找特定单词简单,使用正则表达式即可,删除则使用d操作命令即可。

例如删除所有含有like的行:

echo "My name is Jack Wang.\nI like Linux.\nI like writing computer programs.\nMy email is 123456789@gmail.com.\nMy website is http://jackwang.cafe ." > test.txt
sed '/like/d' test.txt

结果如下:

删除含有特定次的行

输出第三到最后一行

在输出范围中使用$来表示最后一行

echo "My name is Jack Wang.\nI like Linux.\nI like writing computer programs.\nMy email is 123456789@gmail.com.\nMy website is http://jackwang.cafe ." > test.txt
sed -n '3,$p' test.txt

运行结果如下:

输出第三到最后一行

替换某个单词

使用s操作命令来进行替换

echo "My name is Jack Wang.\nI like Linux.\nI like writing computer programs.\nMy email is 123456789@gmail.com.\nMy website is http://jackwang.cafe ." > test.txt
cat test.txt
sed -n 's/My/His/g' test.txt

运行结果如下:

替换某个单词的结果

我们可以使用-e来指定多个sed命令

echo "My name is Jack Wang.\nI like Linux.\nI like writing computer programs.\nMy email is 123456789@gmail.com.\nMy website is http://jackwang.cafe ." > test.txt
cat test.txt
sed -n 's/My/His/g' test.txt

-e指定多个sed命令

增加(Append)一行

使用a操作命令来在某行后面添加一行。

例如在第二行后添加My Linux skill is good.

echo "My name is Jack Wang.\nI like Linux.\nI like writing computer programs.\nMy email is 123456789@gmail.com.\nMy website is http://jackwang.cafe ." > test.txt
sed '2a My Linux skill is good.' test.txt

运行结果如下:

增加一行运行结果

再举一个例子,在每行后增加----------作为分隔符。

因为要匹配每行,直接使用空地址即可

echo "My name is Jack Wang.\nI like Linux.\nI like writing computer programs.\nMy email is 123456789@gmail.com.\nMy website is http://jackwang.cafe ." > test.txt
gsed "a ----------------" test.txt

每行增加分隔符结果

但是注意,macOS原生的seda之后必须要加\ + <Enter>换行来输入需要插入的内容

例如:

echo "My name is Jack Wang.\nI like Linux.\nI like writing computer programs.\nMy email is 123456789@gmail.com.\nMy website is http://jackwang.cafe ." > test.txt
sed '2a\<回车键
quote> My Linux skill is good' test.txt

运行结果如下:

屏幕录制2022-11-11-上午1.50.08

插入(Insert)一行

我们接下来使用insert插入一行

echo "My name is Jack Wang.\nI like Linux.\nI like writing computer programs.\nMy email is 123456789@gmail.com.\nMy website is http://jackwang.cafe ." > test.txt
gsed "4i My phone number is 00000000" test.txt

image-20221111024342796

类似于ai的表现在LinuxmacOS上也不同,macOS上的sedi表现如下:

echo "My name is Jack Wang.\nI like Linux.\nI like writing computer programs.\nMy email is 123456789@gmail.com.\nMy website is http://jackwang.cafe ." > test.txt
sed '4i\<回车键
quote> My phone number is 00000000' test.txt

运行结果为:

屏幕录制2022-11-11-上午2.05.56

5. sed进阶案例

我们上面都只讲解了sed的基础用法,下面我们将讲解sed的进阶用法。

获取行内内容:掐头去尾

我们前面介绍的sed的用法,都是针对行进行操作的。但是很多时候,我们往往只需要获取一行中的某个内容。

例如我们使用ifconfig查看计算机在当前网络下的IP地址

ifconfig

ifconfig查看IP地址

但是我们在Shell脚本中往往只需要本机的IP即可,而后在后面的脚本中使用IP地址,例如:

IP=魔法命令 #获取IP
echo $IP

所以我们接下来将讲解sed如何获得一个单词。

sed获取单词的基本思路就是:

  • 第一步:筛选出行
  • 第二步:掐头去尾,用空替换所有不要的字符

例如我们获取IP地址:

  1. 第一步:筛选出行

    ifconfig en0 | sed -n '5p'
    

    筛选出行

  2. 第二步:掐头去尾,用空替换掉inet之前的内容以及netmask之后的内容

    # 首先去掉inet之前的东西
    ifconfig en0 | sed -n '5p' | sed 's/^.*inet //'
    

    先掐头

    # 然后去掉netmask之后的内容
    ifconfig en0 | sed -n '5p' | sed 's/^.*inet //' | sed 's/netmask.*$//'
    

    image-20221111031436912

这样,我们就成功通过sed获取到了行内的内容。另外,我们其实可以用grep和匹配IP地址的正则表达式带代替掐头去尾。

相比于管道符多次替换,我们其实可以用-e参数来使用多个匹配,但是在多个匹配的时候,最后一个命令必须要加上p来输出

  1. 第一步:掐头

    ifconfig en0 | gsed -e '5s/^.*inet//p' -n
    

    第一步:掐头

  2. 第二步:去尾。注意,因为两个gsed command的输入都是ifconfig的输出,所以命令都是5s

    ifconfig en0 | gsed -e '5s/^.*inet//' -n -e "5s/net.*$//p" -n
    

    第二步:去尾

文本格式化工具:awk

awk是用于对文本和数据进行处理的工具。类似于python解释器可以解释、执行python语言编写的脚本,awk也可以解释、执行awk语言编写的脚本。

因此awk根据上下文的不同,有时候指代awk工具本身,有时候指代的是awk这个编程语言。

awk 编程语言用于在Linux/Unix下对文本和数据进行处理。数据可以来自标准输入(stdin)、一个或多个文件,或其它命令的输出。它支持用户自定义函数和动态正则表达式等先进功能,是Linux/Unix下的一个强大编程工具。它在命令行中使用,但更多是作为脚本来使用。awk语言内还有很多内建的功能,比如数组、函数等,这是它和C语言的相同之处,灵活性是awk最大的优势。

1. awk基础

学习akw的时候需要学习:

  • awk程序的用法
  • awk语言的语法

因此,我们下面将先介绍awk的基础知识,而后再慢慢讲解awk的进阶知识

A. awk的工作原理

sed逐行处理相似,awk也是从文件file中读取数据,而后针对每一行按照指定的命令序列(称为脚本script)处理该行数据

对于每一行,awk中将其称为一个记录(Record)。每一行中使用分隔符分割出来不同的内容,称为字段(field)。例如针对test.txt文件:

// test.txt
Jack1 Jack2 Jack3 Jack4 Jack5
Jack6 Jack7 Jack8 Jack9 Jack10
Jack11 Jack12 Jack13 Jack14 Jack15
Jack16 Jack17 Jack18 Jack19 Jack20

test.txt中包含了4个记录(Record),每个记录有5个字段(Field)。

举一个概念上的例子来介绍awk的工作原理:

awk 'BEGIN{ commands } pattern{ commands } END{ commands }' test.txt
  • 第一步:读取test.txt
  • 第二步:执行awk程序,这里的awk程序为BEGIN{ commands } pattern{ commands } END{ commands }

这里的awk程序的解释如下:

  1. 执行BEGIN{ commands }语句块中的语句;
  2. 逐行扫描文件,然后执行pattern{ commands }语句块,即首先查看该行是否和指定的模式pattern符合,若符合则对改行执行commands指令,若不符合则调到下一行。从第一行到最后一行重复这个过程,直到处理完文件所有行。
  3. 当处理完最后一行后,执行END{ commands }语句块。

关于BEGINEND这些awk程序具体得解释将在下面介绍awk程序的部分进行介绍

下面举一个真实的例子(中间的pattern为空表示匹配所有行):

echo Jack{1..5} > test.txt && echo Jack{6..10} >> test.txt && echo Jack{11..15} >> test.txt && echo Jack{16..20} >> test.txt
cat test.txt
awk 'BEGIN{print "Start"} {print $1,$3,$5} END{print "End"}' test.txt

运行结果如下:

真是的例子的运行结果

B. awk调用方式

awk的调用方式有两种:

  • 字符串命令方式
  • 脚本文件方式
字符串命令方式

字符串命令方式的调用方式如下

awk [options] 'command' var=value file(s)

其中:

  • options指定了命令行参数

  • 'command'中指定了需要执行的命令序列,注意命令序列必须要用单引号包裹

  • var赋值一个用户定义变量,将外部变量传递给awk

  • file(s)指定了要读取的文件

举一个例子,下面的例子是输出每行中的第二个字段的值

echo Jack{1..5} > test.txt && echo Jack{6..10} >> test.txt && echo Jack{11..15} >> test.txt && echo Jack{16..20} >> test.txt
cat test.txt
awk '{print $2}' test.txt

运行结果如下:

字符串命令调用方式运行结果

注意,上面的例子中我们没有使用指定命令行参数,也没有赋值用户定义变量。

脚本文件方式
awk [options] -f scriptfile var=value file(s)

其中:

  • options指定了命令行参数

  • scriptfile中指定了需要读取的脚本文件。需要执行的命令序列存储在脚本文件中

  • var赋值一个用户定义变量,将外部变量传递给awk

  • file(s)指定了要读取的文件

上面输出每行中的第二个字段的值的脚本文件的方式如下

echo Jack{1..5} > test.txt && echo Jack{6..10} >> test.txt && echo Jack{11..15} >> test.txt && echo Jack{16..20} >> test.txt
echo '{print $2}' > cmd.awk
awk -f cmd.awk test.txt

脚本文件调用方式运行结果

C. awk模式和操作

sed命令类似,awk命令也是由模式和操作组成的,操作必须要放在{}中,故awk命令:模式{操作}

awk中的模式和操作具体有:

  1. 模式可以是以下任意一个:

    • /正则表达式/:使用通配符的扩展集
    • 关系表达式:使用运算符进行操作,可以是字符串或数字的比较测试
    • 模式匹配表达式:用运算符~(匹配)和!~(不匹配)
    • BEGIN语句块、pattern语句块、END语句块:稍后会进行详细的介绍
  2. 操作由一个或多个命令、函数、表达式组成,之间由换行符或分号隔开,并位于大括号内,主要部分是:

    • 变量或数组赋值
    • 输出命令
    • 内置函数
    • 控制流语句

例如:

echo Jack{1..5} > test.txt && echo Jack{6..10} >> test.txt && echo Jack{11..15} >> test.txt && echo Jack{16..20} >> test.txt
awk 'NR==2{print $1,$3,$5}' test.txt

其中:

  • NR==2是模式,这里是表示第二行
  • {print $1,$3,$5}是操作。

运行结果如下:

awk模式和操作示例

D. awk常用参数

下面我们将列举awk中常用的参数

参数 说明
-F 指定分割字符段
-v 定义或修改一个awk内部的变量
-f 从脚本文件中读取awk命令

2. awk 程序及语法

上面我们介绍了awk的基础知识,我们接下来讲介绍awk程序.

下面这一块,随用随看。没必要一次性看完,因为语法基本上和C是一样的。

A. awk程序基本结构

awk程序的基本结构如下:

BEGIN{ print "start" } pattern{ commands } END{ print "end" }

一个awk程序通常由3部分组成:

  • BEGIN语句块:在awk开始从输入流中读取行 之前 被执行,这是一个可选的语句块,比如变量初始化、打印输出表格的表头等语句通常可以写在BEGIN语句块中
  • 能够使用模式匹配的通用语句块:在读取完每一行后,如果该行符合模式匹配,则按照指令处理改行。即上面的pattern { commands }
  • END语句块:在awk从输入流中读取完所有的行 之后 即被执行,比如打印所有行的分析结果这类信息汇总都是在END语句块中完成,它也是一个可选语句块

这三个部分是可选的。任意一个部分都可以不出现在脚本中,脚本通常是被 单引号 中,例如:

echo Jack{1..5} > test.txt && echo Jack{6..10} >> test.txt && echo Jack{11..15} >> test.txt && echo Jack{16..20} >> test.txt
cat test.txt
awk 'BEGIN{print "Start"} {print $1,$3,$5} END{print "End"}' test.txt

运行结果如下:

运行结果

B. 内置变量

类似于Python中有__name__C中有__LINE__等内置变量(不严格的说),awk语言也有内置的变量。

这里只列出一些常用的变量,更多的变量使用man手册去查看即可。

下表中,[A][N][P][G]表示第一个支持变量的awk实现,[A]=awk[N]=nawk[P]=POSIXawk[G]=gawk。不过一般来说这些变量现在所有的实现都支持了

工具 变量 说明
全部 $n 当前记录的第n个字段(field),比如n为1表示第一个字段,n为2表示第二个字段
全部 $0 这个变量包含执行过程中当前行的文本内容
[N] argc 命令行参数的数目
[N] argv 包含命令行参数的数组
[N] ERRNO 系统错误号
[N] RSTART 由match函数所匹配的字符串的第一个位置
[N] RLENGTH 由match函数所匹配的字符串的长度
[N] SUBSEP 数组下标分隔符(默认值是34)
[G] ARGIND 命令行中当前文件的位置(从0开始算)
[G] CONVFMT 数字转换格式(默认值为%.6g)
[G] FIELDWIDTHS 字段宽度列表(用空格键分隔)
[G] IGNORECASE 如果为真,则进行忽略大小写的匹配
[P] ENVIRON 环境变量关联数组
[P] FNR 同NR,但相对于当前文件
[A] FILENAME 当前输入文件的名
[A] FS 字段分隔符(默认是任何空格)
[A] NF 当前记录分割后的字段数
[A] NR 表示记录数,在执行过程中对应于当前的行号
[A] OFMT 数字的输出格式(默认值是%.6g)
[A] OFS 输出字段分隔符(默认值是一个空格)
[A] ORS 输出记录分隔符(默认值是一个换行符)
[A] RS 记录分隔符(默认是一个换行符)

C. 转移序列

为了能够输出控制字符,awk也支持转义序列。awk中的转义字符如下表所示:

转义字符 说明
\\ \自身
\$ 转义$
\t 制表符
\n 换行符
\r 回车符
\b 退格符
\c 取消换行

D. 运算符

算术运算符

awk中的算术运算符有:

算术运算符 描述
+ - 加,减
* / & 乘,除与求余
+ - 一元加,减
^ *** 求幂
++ -- 增加或减少,作为前缀或后缀

注意,使用算术运算符的时候,操作数自动转为数值,所有非数值都变为0

赋值运算符

awk中的赋值运算符有:

运算符 描述
= += -= *= /= %= ^= **= 赋值语句
逻辑运算符

awk中的逻辑运算符有:

运算符 描述
` ` 逻辑或
&& 逻辑与
! 逻辑非
正则运算符
运算符 描述
~ 匹配正则表达式
!~ 不匹配正则表达式

awk中支持基础正则表达式:

^ 行首
$ 行尾
. 除了换行符以外的任意单个字符
* 前导字符的零个或多个
.* 所有字符
[] 字符组内的任一字符
[^]对字符组内的每个字符取反(不匹配字符组内的每个字符)
^[^] 非字符组内的字符开头的行
[a-z] 小写字母
[A-Z] 大写字母
[a-Z] 小写和大写字母
[0-9] 数字
\< 单词头单词一般以空格或特殊字符做分隔,连续的字符串被当做单词
\> 单词尾

如果要使用正则表达式的话,需要用/正则/的形式使用。

关系运算符
运算符 描述
< <= > >= != == 关系运算符
其它运算符
运算符 描述
$ 字段引用
空格 字符串连接符
?: C条件表达式
in 数组中是否存在某键值

E. 流程控制语句

awkwhiledo-whilefor语句中允许使用breakcontinue语句来控制流程走向,也允许使用exit这样的语句来退出。break中断当前正在执行的循环并跳到循环外执行下一条语句。if是流程选择用法。

awk中的流程控制语句、语法结构与C语言很类似。而正是因为有了这些语句,很多Shell程序都可以交给awk,而且性能是非常快的。下面是各个语句用法。

条件判断语句
if(表达式)
  语句1
else
  语句2

格式中语句1可以是多个语句,为了方便判断和阅读,最好将多个语句用{}括起来。awk分枝结构允许嵌套,其格式为:

if(表达式)
  {语句1}
else if(表达式)
  {语句2}
else
  {语句3}

示例:

awk 'BEGIN{
test=100;
if(test>90){
  print "very good";
  }
  else if(test>60){
    print "good";
  }
  else{
    print "no pass";
  }
}'

very good

每条命令语句后面可以用; 分号 结尾。

循环语句
while语句
while(表达式)
  {语句}

示例:

awk 'BEGIN{
test=100;
total=0;
while(i<=test){
  total+=i;
  i++;
}
print total;
}'
5050
for循环

for循环有两种格式:

格式1:

for(变量 in 数组)
  {语句}

示例:

awk 'BEGIN{
for(k in ENVIRON){
  print k"="ENVIRON[k];
}

}'
TERM=linux
G_BROKEN_FILENAMES=1
SHLVL=1
pwd=/root/text
...
logname=root
HOME=/root
SSH_CLIENT=192.168.1.21 53087 22

注:ENVIRON是awk常量,是子典型数组。

格式2:

for(变量;条件;表达式)
  {语句}

示例:

awk 'BEGIN{
total=0;
for(i=0;i<=100;i++){
  total+=i;
}
print total;
}'
5050
do循环
do
{语句} while(条件)

例子:

awk 'BEGIN{ 
total=0;
i=0;
do {total+=i;i++;} while(i<=100)
  print total;
}'
5050
其他语句
  • break 当 break 语句用于 while 或 for 语句时,导致退出程序循环。
  • continue 当 continue 语句用于 while 或 for 语句时,使程序循环移动到下一个迭代。
  • next 能能够导致读入下一个输入行,并返回到脚本的顶部。这可以避免对当前输入行执行其他的操作过程。
  • exit 语句使主输入循环退出并将控制转移到END,如果END存在的话。如果没有定义END规则,或在END中应用exit语句,则终止脚本的执行。

F. 数组应用

数组是awk的灵魂,处理文本中最不能少的就是它的数组处理。因为数组索引(下标)可以是数字和字符串在awk中数组叫做关联数组(associative arrays)。awk 中的数组不必提前声明,也不必声明大小。数组元素用0或空字符串来初始化,这根据上下文而定。

数组的定义

数字做数组索引(下标):

Array[1]="sun"
Array[2]="kai"

字符串做数组索引(下标):

Array["first"]="www"
Array"[last"]="name"
Array["birth"]="1987"

使用中print Array[1]会打印出sun;使用print Array[2]会打印出kai;使用print["birth"]会得到1987。

读取数组的值

{ for(item in array) {print array[item]}; }       #输出的顺序是随机的
{ for(i=1;i<=len;i++) {print array[i]}; }         #Len是数组的长度
数组相关函数

得到数组长度:

awk 'BEGIN{info="it is a test";lens=split(info,tA," ");print length(tA),lens;}'
4 4

length返回字符串以及数组长度,split进行分割字符串为数组,也会返回分割得到数组长度。

awk 'BEGIN{info="it is a test";split(info,tA," ");print asort(tA);}'
4

asort对数组进行排序,返回数组长度。

输出数组内容(无序,有序输出):

awk 'BEGIN{info="it is a test";split(info,tA," ");for(k in tA){print k,tA[k];}}'
4 test
1 it
2 is
3 a

for…in输出,因为数组是关联数组,默认是无序的。所以通过for…in得到是无序的数组。如果需要得到有序数组,需要通过下标获得。

awk 'BEGIN{info="it is a test";tlen=split(info,tA," ");for(k=1;k<=tlen;k++){print k,tA[k];}}'
1 it
2 is
3 a
4 test

注意:数组下标是从1开始,与C数组不一样。

判断键值存在以及删除键值:

# 错误的判断方法:
awk 'BEGIN{tB["a"]="a1";tB["b"]="b1";if(tB["c"]!="1"){print "no found";};for(k in tB){print k,tB[k];}}' 
no found
a a1
b b1
c

以上出现奇怪问题,tB[“c”]没有定义,但是循环时候,发现已经存在该键值,它的值为空,这里需要注意,awk数组是关联数组,只要通过数组引用它的key,就会自动创建改序列。

# 正确判断方法:
awk 'BEGIN{tB["a"]="a1";tB["b"]="b1";if( "c" in tB){print "ok";};for(k in tB){print k,tB[k];}}'  
a a1
b b1

if(key in array)通过这种方法判断数组中是否包含key键值。

#删除键值:
awk 'BEGIN{tB["a"]="a1";tB["b"]="b1";delete tB["a"];for(k in tB){print k,tB[k];}}'                     
b b1

delete array[key]可以删除,对应数组key的,序列值。

二维、多维数组使用

awk的多维数组在本质上是一维数组,更确切一点,awk在存储上并不支持多维数组。awk提供了逻辑上模拟二维数组的访问方式。例如,array[2,4]=1这样的访问是允许的。awk使用一个特殊的字符串SUBSEP(�34)作为分割字段,在上面的例子中,关联数组array存储的键值实际上是2�344。

类似一维数组的成员测试,多维数组可以使用if ( (i,j) in array)这样的语法,但是下标必须放置在圆括号中。类似一维数组的循环访问,多维数组使用for ( item in array )这样的语法遍历数组。与一维数组不同的是,多维数组必须使用split()函数来访问单独的下标分量。

awk 'BEGIN{
for(i=1;i<=9;i++){
  for(j=1;j<=9;j++){
    tarr[i,j]=i*j; print i,"*",j,"=",tarr[i,j];
  }
}
}'
1 * 1 = 1
1 * 2 = 2
1 * 3 = 3
1 * 4 = 4
1 * 5 = 5
1 * 6 = 6 
...
9 * 6 = 54
9 * 7 = 63
9 * 8 = 72
9 * 9 = 81

可以通过array[k,k2]引用获得数组内容。

另一种方法:

awk 'BEGIN{
for(i=1;i<=9;i++){
  for(j=1;j<=9;j++){
    tarr[i,j]=i*j;
  }
}
for(m in tarr){
  split(m,tarr2,SUBSEP); print tarr2[1],"*",tarr2[2],"=",tarr[m];
}
}'

G. 内置函数

awk内置函数,主要分以下3种类似:算数函数、字符串函数、其它一般函数、时间函数。

算术函数
函数名 描述
atan2( y, x ) 返回 y/x 的反正切。
cos( x ) 返回 x 的余弦;x 是弧度。
sin( x ) 返回 x 的正弦;x 是弧度。
exp( x ) 返回 x 幂函数。
log( x ) 返回 x 的自然对数。
sqrt( x ) 返回 x 平方根。
int( x ) 返回 x 的截断至整数的值。
rand( ) 返回任意数字 n,其中 0 <= n < 1。
srand( [expr] ) 将 rand 函数的种子值设置为 Expr 参数的值,或如果省略 Expr 参数则使用某天的时间。返回先前的种子值。

举例说明:

awk 'BEGIN{OFMT="%.3f";fs=sin(1);fe=exp(10);fl=log(10);fi=int(3.1415);print fs,fe,fl,fi;}'
0.841 22026.466 2.303 3

OFMT 设置输出数据格式是保留3位小数。

获得随机数:

awk 'BEGIN{srand();fr=int(100*rand());print fr;}'
78
awk 'BEGIN{srand();fr=int(100*rand());print fr;}'
31
awk 'BEGIN{srand();fr=int(100*rand());print fr;}'
41
字符串函数
函数名 描述
gsub( Ere, Repl, [ In ] ) 除了正则表达式所有具体值被替代这点,它和 sub 函数完全一样地执行。
sub( Ere, Repl, [ In ] ) 用 Repl 参数指定的字符串替换 In 参数指定的字符串中的由 Ere 参数指定的扩展正则表达式的第一个具体值。sub 函数返回替换的数量。出现在 Repl 参数指定的字符串中的 &(和符号)由 In 参数指定的与 Ere 参数的指定的扩展正则表达式匹配的字符串替换。如果未指定 In 参数,缺省值是整个记录($0 记录变量)。
index( String1, String2 ) 在由 String1 参数指定的字符串(其中有出现 String2 指定的参数)中,返回位置,从 1 开始编号。如果 String2 参数不在 String1 参数中出现,则返回 0(零)。
length [(String)] 返回 String 参数指定的字符串的长度(字符形式)。如果未给出 String 参数,则返回整个记录的长度($0 记录变量)。
blength [(String)] 返回 String 参数指定的字符串的长度(以字节为单位)。如果未给出 String 参数,则返回整个记录的长度($0 记录变量)。
substr( String, M, [ N ] ) 返回具有 N 参数指定的字符数量子串。子串从 String 参数指定的字符串取得,其字符以 M 参数指定的位置开始。M 参数指定为将 String 参数中的第一个字符作为编号 1。如果未指定 N 参数,则子串的长度将是 M 参数指定的位置到 String 参数的末尾 的长度。
match( String, Ere ) 在 String 参数指定的字符串(Ere 参数指定的扩展正则表达式出现在其中)中返回位置(字符形式),从 1 开始编号,或如果 Ere 参数不出现,则返回 0(零)。RSTART 特殊变量设置为返回值。RLENGTH 特殊变量设置为匹配的字符串的长度,或如果未找到任何匹配,则设置为 -1(负一)。
split( String, A, [Ere] ) 将 String 参数指定的参数分割为数组元素 A[1], A[2], . . ., A[n],并返回 n 变量的值。此分隔可以通过 Ere 参数指定的扩展正则表达式进行,或用当前字段分隔符(FS 特殊变量)来进行(如果没有给出 Ere 参数)。除非上下文指明特定的元素还应具有一个数字值,否则 A 数组中的元素用字符串值来创建。
tolower( String ) 返回 String 参数指定的字符串,字符串中每个大写字符将更改为小写。大写和小写的映射由当前语言环境的 LC_CTYPE 范畴定义。
toupper( String ) 返回 String 参数指定的字符串,字符串中每个小写字符将更改为大写。大写和小写的映射由当前语言环境的 LC_CTYPE 范畴定义。
sprintf(Format, Expr, Expr, . . . ) 根据 Format 参数指定的 printf 子例程格式字符串来格式化 Expr 参数指定的表达式并返回最后生成的字符串。

注:Ere都可以是正则表达式。

gsub,sub使用

awk 'BEGIN{info="this is a test2010test!";gsub(/[0-9]+/,"!",info);print info}'
this is a test!test!

在 info中查找满足正则表达式,/[0-9]+/””替换,并且替换后的值,赋值给info 未给info值,默认是$0

查找字符串(index使用)

awk 'BEGIN{info="this is a test2010test!";print index(info,"test")?"ok":"no found";}'
ok

未找到,返回0

正则表达式匹配查找(match使用)

awk 'BEGIN{info="this is a test2010test!";print match(info,/[0-9]+/)?"ok":"no found";}'
ok

截取字符串(substr使用)

[wangsl@centos5 ~]$ awk 'BEGIN{info="this is a test2010test!";print substr(info,4,10);}'
s is a tes

从第 4个 字符开始,截取10个长度字符串

字符串分割(split使用)

awk 'BEGIN{info="this is a test";split(info,tA," ");print length(tA);for(k in tA){print k,tA[k];}}'
4
4 test
1 this
2 is
3 a

分割info,动态创建数组tA,这里比较有意思,awk for …in循环,是一个无序的循环。 并不是从数组下标1…n ,因此使用时候需要注意。

格式化字符串输出(sprintf使用)

格式化字符串格式:

其中格式化字符串包括两部分内容:一部分是正常字符,这些字符将按原样输出; 另一部分是格式化规定字符,以"%"开始,后跟一个或几个规定字符,用来确定输出内容格式。

格式 描述 格式 描述
%d 十进制有符号整数 %u 十进制无符号整数
%f 浮点数 %s 字符串
%c 单个字符 %p 指针的值
%e 指数形式的浮点数 %x %X 无符号以十六进制表示的整数
%o 无符号以八进制表示的整数 %g 自动选择合适的表示法
awk 'BEGIN{n1=124.113;n2=-1.224;n3=1.2345; printf("%.2f,%.2u,%.2g,%X,%on",n1,n2,n3,n1,n1);}'
124.11,18446744073709551615,1.2,7C,174
一般函数
格式 描述
close( Expression ) 用同一个带字符串值的 Expression 参数来关闭由 print 或 printf 语句打开的或调用 getline 函数打开的文件或管道。如果文件或管道成功关闭,则返回 0;其它情况下返回非零值。如果打算写一个文件,并稍后在同一个程序中读取文件,则 close 语句是必需的。
system(command ) 执行 Command 参数指定的命令,并返回退出状态。等同于 system 子例程。
Expression getline [ Variable ] 从来自 Expression 参数指定的命令的输出中通过管道传送的流中读取一个输入记录,并将该记录的值指定给 Variable 参数指定的变量。如果当前未打开将 Expression 参数的值作为其命令名称的流,则创建流。创建的流等同于调用 popen 子例程,此时 Command 参数取 Expression 参数的值且 Mode 参数设置为一个是 r 的值。只要流保留打开且 Expression 参数求得同一个字符串,则对 getline 函数的每次后续调用读取另一个记录。如果未指定 Variable 参数,则 $0 记录变量和 NF 特殊变量设置为从流读取的记录。
getline [ Variable ] < Expression 从 Expression 参数指定的文件读取输入的下一个记录,并将 Variable 参数指定的变量设置为该记录的值。只要流保留打开且 Expression 参数对同一个字符串求值,则对 getline 函数的每次后续调用读取另一个记录。如果未指定 Variable 参数,则 $0 记录变量和 NF 特殊变量设置为从流读取的记录。
getline [ Variable ] 将 Variable 参数指定的变量设置为从当前输入文件读取的下一个输入记录。如果未指定 Variable 参数,则 $0 记录变量设置为该记录的值,还将设置 NF、NR 和 FNR 特殊变量。

打开外部文件(close用法)

awk 'BEGIN{while("cat /etc/passwd"|getline){print $0;};close("/etc/passwd");}'
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin

逐行读取外部文件(getline使用方法)

awk 'BEGIN{while(getline < "/etc/passwd"){print $0;};close("/etc/passwd");}'
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
awk 'BEGIN{print "Enter your name:";getline name;print name;}'
Enter your name:
chengmo
chengmo

调用外部应用程序(system使用方法)

awk 'BEGIN{b=system("ls -al");print b;}'
total 42092
drwxr-xr-x 14 chengmo chengmo     4096 09-30 17:47 .
drwxr-xr-x 95 root   root       4096 10-08 14:01 ..

b返回值,是执行结果。

时间函数
函数名 说明
mktime( YYYY MM dd HH MM ss[ DST]) 生成时间格式
strftime([format [, timestamp]]) 格式化时间输出,将时间戳转为时间字符串具体格式,见下表。
systime() 得到时间戳,返回从1970年1月1日开始到当前时间(不计闰年)的整秒数

建指定时间(mktime使用)

awk 'BEGIN{tstamp=mktime("2001 01 01 12 12 12");print strftime("%c",tstamp);}'
2001年01月01日 星期一 12时12分12秒
awk 'BEGIN{tstamp1=mktime("2001 01 01 12 12 12");tstamp2=mktime("2001 02 01 0 0 0");print tstamp2-tstamp1;}'
2634468

求2个时间段中间时间差,介绍了strftime使用方法

awk 'BEGIN{tstamp1=mktime("2001 01 01 12 12 12");tstamp2=systime();print tstamp2-tstamp1;}' 
308201392

strftime日期和时间格式说明符

格式 描述
%a 星期几的缩写(Sun)
%A 星期几的完整写法(Sunday)
%b 月名的缩写(Oct)
%B 月名的完整写法(October)
%c 本地日期和时间
%d 十进制日期
%D 日期 08/20/99
%e 日期,如果只有一位会补上一个空格
%H 用十进制表示24小时格式的小时
%I 用十进制表示12小时格式的小时
%j 从1月1日起一年中的第几天
%m 十进制表示的月份
%M 十进制表示的分钟
%p 12小时表示法(AM/PM)
%S 十进制表示的秒
%U 十进制表示的一年中的第几个星期(星期天作为一个星期的开始)
%w 十进制表示的星期几(星期天是0)
%W 十进制表示的一年中的第几个星期(星期一作为一个星期的开始)
%x 重新设置本地日期(08/20/99)
%X 重新设置本地时间(12:00:00)
%y 两位数字表示的年(99)
%Y 当前月份
%% 百分号(%)

3. awk基础案例

其实说老实话,上面说的一堆语法,很多都平时都应不大,需要用了现查就行。

因为我们的目的是面向运用的,因此我们下面将结合案例来学习awk

输出文件第二列的信息

因为awk对输入的文件是按行处理的,所以我们针对每行输出第二个字段的值即可,即输出$2的内容即可。

echo Jack{1..5} > test.txt && echo Jack{6..10} >> test.txt && echo Jack{11..15} >> test.txt && echo Jack{16..20} >> test.txt
cat test.txt
awk '{print $2}' test.txt

运行结果如下:

输出文本第二列

注意,默认awk是以空格作为分隔符的,并且多个空格在一起也视为一个空格。

同样,我们也能够输出倒数第二列和最后一列。

echo Jack{1..5} > test.txt && echo Jack{6..10} >> test.txt && echo Jack{11..15} >> test.txt && echo Jack{16..20} >> test.txt
cat test.txt
awk '{print $(NF-1),$NF}' test.txt

输出倒数第二列和最后一列运行结果

输出文件第二行信息

因为我们只需要输出文件第二行,所以只需要在要执行的命令前增加匹配第二行的模式即可,即NR==1NRNumber of Record,即当前处理的行数,从1开始。

此外,因为要输出一整行,所以使用$0变量即可。

echo Jack{1..5} > test.txt && echo Jack{6..10} >> test.txt && echo Jack{11..15} >> test.txt && echo Jack{16..20} >> test.txt
cat test.txt
awk 'NR==2{print $0}' test.txt

运行结果如下:

输出第二行的信息运行结果

自定义输出内容

我们前面都是通过引用变量来输出了文件本身的内容,但是如果我们想要在输出文件本身内容的基础上,输出诸如表头等等信息的时候,这个就需要进行自定义输出内容了。

其实实现的方法也很简单,就是使用字符串,awk将所有被""包围的都视为是字符串。所以我们只需要使用""添加字符串即可。例如

echo Jack{1..5} > test.txt && echo Jack{6..10} >> test.txt && echo Jack{11..15} >> test.txt && echo Jack{16..20} >> test.txt
cat test.txt
awk '{print "第一列:"$1,"第二列:"$2}' test.txt

自定义输出内容

指定输入分隔符

awk中有两种分隔符:

  • 输入分隔符(Field SeparatorFS),它指定了输入的文本的分隔符是什么
  • 输出分隔符(Output Field SeparatorOFS),它指定了输出文本的分隔符是什么

awk默认使用空格来作为输入分隔符。但是很多配制文件中适用的分隔符不是空格,而是诸如:等分隔符。这个时候我们就需要改变分隔符了。

awk中,我们可以使用-F参数来指定输入分隔符。例如我们下面分割/etc/passwd这个文件

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

-F指定输入分隔符运行结果

使用-F来更改输入分隔符只是一种方式。输入分隔符的值其实是由内部变量FS指定的,所以我们也可以使用-v参数来修改FS的值,从而实现修改输入分隔符。

tail /etc/passwd
tail /etc/passwd | awk -v FS=':' '{print $1}'

-v指定FS的值从而修改分隔符

修改输入的分隔符只是最基本的,很多时候我们使用awk都是为了获得更好看的报表,所以我们有很多时候需要修改输出分隔符。

例如查看系统上每个用户当前的登录shell

tail /etc/passwd
tail /etc/passed | awk -F ':' -v OFS='\t\t' '{print $0, $NF}'

运行结果为:

修改输出分隔符

显示文件2-4行

输出2-4行的难点在于模式怎么写,因为前面的NR==2只是指定了输出一行。

sed中使用start,end来指定范围,awk中也可以通过类似的方法来实现范围输出。

echo Jack{1..5} > test.txt && echo Jack{6..10} >> test.txt && echo Jack{11..15} >> test.txt && echo Jack{16..20} >> test.txt
cat test.txt
awk '{print "第一列:"$1,"第二列:"$2}' test.txt

注意,我们这里省略了{}里面的action,在省略action的时候,awk默认输出这一行的内容,即默认的action{print $0}

显示文件2-4行

结合前面的关系运算符,我们其实还可以用下面的方式来指定范围

echo Jack{1..5} > test.txt && echo Jack{6..10} >> test.txt && echo Jack{11..15} >> test.txt && echo Jack{16..20} >> test.txt
cat test.txt
awk '2<=NR,NR<=4' test.txt

image-20221111203631491

在每一行的内容前添加行号

每一行的行号其实就是NR变量,所以我们只需要在这一行的内容,即$0前加上行号即可

echo Jack{1..5} > test.txt && echo Jack{6..10} >> test.txt && echo Jack{11..15} >> test.txt && echo Jack{16..20} >> test.txt
cat test.txt
awk '{print NR":",$0}' test.txt

在每一行的内容前添加行号

注意输出变量的值直接使用即可,而不用像shell中一样需要使用的话,结果就会变成

awk '{print $NR":",$0}' test.txt

这是因为,$NRawk眼中就成了$1$2$3等等,所以就成了提取字段的值

使用$提取变量的值的结果

分行输出一行内容

有的时候我们可能只需要输出一行信息,但是如果只用一行输出的话,可能看起来就会很麻烦,所以这个时候我们就可以指定RS参数,即Record Separator参数。

类似于Field Separator分为输入和输出两种,awk中的Record Separator也分为两种:输入和输出。awk中默认Record Seperator的分隔符都是换行,所以我们其实只需要更改输入的记录分隔符即可。

tail -n 1 /etc/passwd
tail -n 1 /etc/passwd | awk -v RS=':' '{print $0}'

注意,因为我们现在指定记录分隔符为:所以现在每个record其实都只有一个字段

分行输出一行内容

使用自定义变量

awk中的变量有两种,一种是内置变量,一种是自定义变量。

内置变量具有特殊的效果,而自定义变量就是单纯的变量。我们前面示范了如何通过修改内置变量来实现特殊的效果。我们下面将展示如何设置自定义变量,以及如何使用自定义变量。

awk -v myname='Jack Wang' 'BEGIN{print "我的名字是?",myname}'

注意,BEGIN模式表示在读取文件前开始执行,因此就首先输出了我的名字。而由于没有输入的文件,所以后面就没有任何输出了

使用自定义变量

awk程序中有一套变量,而Shell脚本中也有一套变量。所以很多时候我们都需要讲Shell脚本中的变量传给awk程序。下面就是具体的做法

echo $USER
awk -v user=$USER 'BEGIN{print "Who am I?",user}'

4. awk进阶案例

获取IP地址

sed命令的讲解中我们讲到可以使用掐头去尾的方法来获取机器的IP地址。而从awk的角度出发,就有一种完全不同的获取方式。

ifconfig en0

我们可以看到,ifconfig命令的输出其实是使用空格分隔的。所以我们能够用awk来直接获得IP地址

ifconfig命令输出

这里IP地址是第五列的第二个字段,所以使用下述的awk命令

ifconfig
ifconfig en0 | awk 'NR==5{print $2}'

awk获取IP地址

事实上,这也就是awk命令最强大的地方,就是因为英文文本是以空格分割的,所以我们利用空格分割,就可以精准的定位到任何一个我们需要的字段。这也就是为什么awk如此强大的最根本原因。

5. awk格式化输出

我们前面讲了awk的基础用法。可是仅仅只是学会基础用法,我们其实是没有办法做到最前面说的,awk是用于生成专业报表(文本格式化)的。

说到文本格式化,我们第一个想到的就是C语言中的printf。C语言中的printf结合各种格式控制字符,例如%d%.4f等等,就可以实现精准的格式化输出。

事实上,awk也借鉴了C语言中的printf,从而使得我们基本上可以无缝的从C语言的printf切换到awkprintf

基础语法

awkprintf的基础语法为

printf f-staring, var1, var2, ...

其中:

  • 格式字符串f-string中的格式数量要和后面的变量对应
  • printf不会自动打印换行符

格式化字符

awk支持的格式化字符有:

控制字符 说明
%c 输出单个字符
%d 输出十进制数字
%e/%E 科学计数法显示数值
%f 输出显示浮点数
%g/%G 科学计数法显示小数
%o 输出无符号的八进制数
%s 输出字符串
%x 输出无符号的十六进制数
%% 输出一个百分号

此外,还可以增加修饰符:

修饰符 说明
- 右对齐,默认左对齐
+ 显示数值的符号

6. awk格式化输出案例

printf和print的不同

我们上面说过,print处理完每一行后,默认会添加换行符号,而printf不会添加换行符号。

echo Jack{1..5} > test.txt && echo Jack{6..10} >> test.txt && echo Jack{11..15} >> test.txt && echo Jack{16..20} >> test.txt
cat test.txt

awk '{print $1}' test.txt
awk '{printf $1}' test.txt

运行结果如下

运行结果

printf按行输出

printf默认是不会换行的,但是实际上我们可以手动添加换行符

echo Jack{1..5} > test.txt && echo Jack{6..10} >> test.txt && echo Jack{11..15} >> test.txt && echo Jack{16..20} >> test.txt
cat test.txt

awk '{print $0}' test.txt
awk '{printf "%s\n",$0}' test.txt

printf按行输出

printf格式化字符与变量匹配

C语言中,如果f-string中的格式化字符和后面的变量不匹配的话,在编译的时候是会报错的。

awk中,如果不匹配的话是不会报错的,而是不会输出没有匹配的变量

awk 'BEGIN{printf "%d\n",1,2,3}'
awk 'BEGIN{printf "%d\n%d\n%d\n",1,2,3}'

运行结果

格式化输出/etc/passwd

最后,我们用awk格式化输出/etc/passwd中的内容

tail /etc/passwd
tail /etc/passwd | awk -F ':' 'BEGIN{printf "%-22s\t%-10s\t%-4s\t%-4s\t%-35s\t%-20s\t%-15s\n","User Name","Password","UID","GID","User Comment","User Home","Loging Shell"} {printf "%-22s\t%-10s\t%-4s\t%-4s\t%-35s\t%-20s\t%-15s\n",$1,$2,$3,$4,$5,$6,$7}'

格式化输出结果


文章作者: Jack Wang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Jack Wang !
  目录