sed流编辑器
简介
sed被称为流编辑器(stream editor),是Linux和Unix系统中一个强大的文本处理工具。它被设计用于大批量的文本处理,可以逐行读入文本,并根据命令进行字符串的查找、筛选、移动、替换、添加和删除等操作。sed非常适合对大量文本进行批处理,也适合在脚本中进行自动化文本处理。
与交互式编辑器(如vim)不同,vim等编辑器适合用来进行交互式编辑和细粒度的文本修改,而流编辑器适合用来进行大批量的非交互式文本处理,适合执行自动化文本处理任务,如进行批量文件修改、数据清洗、日志文件分析等。
sed的工作流程
sed强调逐行处理文本,默认情况下,sed编辑器会基于换行符的位置将文本流分成行,以一行文本作为一个处理单位,sed编辑器对文本的处理会在一个名为模式空间(pattern space)的临时缓冲区中进行,sed编辑器在工作时会依次执行以下流程:
- 读取:sed首先会从文件、管道、标准输入等输入流中读取一行内容并存储到模式空间
- 执行:根据用户提供的命令在行中匹配数据,并按照命令增删改查,默认情况下,所有sed操作命令会在模式空间中按顺序执行
- 打印:字符串行在模式空间中被处理完后,默认会将结果输出到屏幕,然后清空模式空间,然后读取下一行文本
具体执行过程为:sed首先会从输入流中(可以是来自文件、管道、标准输入设备的文本流)读取一行内容并存储到模式空间,然后按照命令顺序对满足要求的文本进行处理,不满足要求的文本也会被读入模式空间,但sed不做任何处理,随后这些文本会被发送到输出流显示给用户,sed编辑器会随之清空模式空间中的内容,然后读取下一行文本重复执行上述操作,直到文本流中的所有行被处理完毕。默认情况下,sed命令会对所有行都生效,但可以通过指定行号、模式匹配等方式指定命令只对某些行生效,sed编辑器默认会将所有文本行输出到屏幕,即便编辑器没有修改任何原文本的内容(如:指定某个命令只对第2行文本生效,但其他行的内容依旧会被sed编辑器打印到屏幕上),且sed命令的文本处理操作都是在缓冲区中进行的,默认不会对原文件内容进行修改。sed编辑器还支持将操作命令存储于一个单独的文件(通常拓展名为.sed)中,然后从文件中批量加载命令。
sed命令
sed [选项] ‘文本处理命令’ [处理的文件]
- -e 命令:显式指定命令,该选项可以重复使用来分开指定多个命令
- -f 文件名:从该文件中载入处理命令
- -n 不打印命令的默认输出
- -i 直接修改原文件,不输出到终端,具体参考通过-i选项修改文件,因为-i需要一个参数,所以它后面不应该再跟其他短选项
- -r 支持拓展的正则表达式,某些版本中为-E,具体可以man sed查看支持哪一个选项
- 如果不指定处理文件,或者输入文件名为-,sed会从标准输入STDIN读取数据
- 文本处理命令如果直接在命令行中指定,则需要加上一对单引号‘ ‘,这样shell就不会将其解析为shell的特殊字符或命令,并且shell会将其视为sed命令的单个参数,允许文本处理命令的长度超过一行,如果命令写于文件中不需要使用单引号。如果sed文本处理命令中需要调用外部变量,可以使用双引号“”(类似于在字符串中引用变量)
- 可以指定多个需要处理的文件,此时,sed会将多个输入文件视为一个合并的长文件流。如:sed -n ‘1p;$p’ one.txt two.txt three.txt将打印one.txt文件的第一行和three.txt文件的最后一行
- -s 默认情况下,sed会将指定的多个输入文件视为单个连续的长流,该选项允许sed将这些文件视为单独的文件
文本处理命令
文本处理命令是sed命令的关键部分,告诉sed编辑器要处理哪些文本,以及如何处理文本。sed可以输入单个命令,也支持一次性指定多个命令,它会依次执行这些命令。
单行模式下,多个命令之间使用分号;分隔即可
e.g.文件test.txt包含以下内容,使用字符串"周杰伦"替换"Jay Chou",使用"陈奕迅"替换"Eason Chan"
These are my favorite singers Jay Chou and Eason Chan
These are my favorite singers Jay Chou and Eason Chan
These are my favorite singers Jay Chou and Eason Chan
单行模式下指定多个命令,命令之间使用分号分隔
sed 's/Jay Chou/周杰伦/; s/Eason Chan/陈奕迅/' test.txt
也可以使用-e选项,分开指定命令,使命令更具可读性和清晰度
sed -e 's/Jay Chou/周杰伦/' -e 's/Eason Chan/陈奕迅/' test.txt
多行模式下,命令之间的分号可以省略
多行模式下指定多个命令,可以不使用分号,但单引号必须保留
sed '
>s/Jay Chou/周杰伦/
>s/Eason Chan/陈奕迅/' test.txt
从文件调用:如果处理命令较多或需要反复调用,可以将这些命令写于一个命令文件中,每条命令写一行,sed编辑器会将每行都识别为一个单独的命令。存放sed命令的文件通常使用.sed作为文件扩展名,然后通过-f选项输入sed编辑器,命令之间的分号可以省略,且命令不使用单引号包裹
sed -f script.sed test.txt
script.sed文件内容:
s/Jay Chou/周杰伦/
s/Eason Chan/陈奕迅/
多个命令的分隔
一般情况下,在sed中指定多个命令时,多个命令之间直接使用分号;分隔即可,但部分命令本身需要接收一个额外参数,如果在参数后面指定了其他命令,sed可能会将这些命令错误解析为参数的一部分,因此,这类命令在使用时,应该以换行符结束,或者将命令放在脚本的末尾。以换行符结束是指,如果命令后面还有其他命令(包括命令分组时用的}符号),则需要使用换行符来分隔命令,或者使用-e选项来指定命令,直接使用分号;进行分隔可能导致sed错误解析命令,这类命令包括:
- 插入文本:i\文本内容
- 追加文本:a\文本内容
- 替换整行:c\替换内容
- 读取文件:r 文件名
- 写入文件:w 文件名
- 退出命令:q 退出码
- 执行外部命令:e 命令
e.g.在第二行后插入一行文本,然后删除原来的第二行(该功能和c命令一样,仅做测试)
如果直接执行
sed '2{i\new line;d}' data.txt
则会报错unmatched `{',使用换行符分隔命令将正确执行
sed '2{i\new line
> d }' data.txt
调换命令顺序,
sed '2{d;i\new line}' data.txt
同样报错unmatched `{',i命令需要以换行符结尾
而命令
sed '2{d;i\new line
> }' data.txt
会执行,但执行结果是错误的,这是由于先执行d命令删除了第二行文本,而i命令是需要将文本插入到第二行文本前,此时sed的模式空间中内容为空,i命令将不再执行
另一种命令的正确指定方法为:
sed -e "2i\new line" -e "2d" data.txt
文本输入方式
不指定处理文件时,sed会从标准输入STDIN读取数据,在输入完sed命令后按下回车,sed编辑器会开始等待文本输入,每输入一行文本sed编辑器就处理一行,并实时打印处理结果,直到使用Ctrl+D结束输入,或使用Ctrl+C终止任务。
e.g.将所输入字符串中的A替换为a
sed 's/A/a/' (回车)
Ajax #用户输入
ajax #sed编辑器输出结果
APACHE #用户输入
aPACHE #sed编辑器输出结果
(Ctrl+D结束输入,退出sed编辑器)
也可以通过管道符|将文本输入sed编辑器
如果直接在sed命令行输入文本,sed编辑器会将其识别为文件名,然后返回错误:No such file or directory
因此,通过以下语句输入文本到sed编辑器中的方法都是错误的:
sed 's/A/a/' Ajax
sed 's/A/a/' "Ajax"
如果需要直接用sed编辑器处理单行或多行文本,可以考虑使用管道符:
e.g.使用s命令将输出中的old替换为not old,s命令的功能是使用斜线后的第二个字符串替换第一个字符串
echo "This is old song" | sed 's/old/not old/'
当需要处理大量文本时,可以将文本写入文件中,sed命令会以每行作为一个单位,依次读取文件内容并处理,但注意:默认情况下,Sed编辑器只会将模式空间中处理完毕的数据发送到屏幕STDOUT,而不会修改原文件中的数据
e.g.从文件test.txt读取内容并进行文本操作
sed 's/A/a/' test.txt
字符转义
由于在sed命令中,斜线/被用作命令字符串的分隔符,因此如果在命令中包含了诸如文件路径等需要用到斜线的参数,需要使用转义字符\对其进行转义。此外,sed编辑器也支持使用其他字符作为替换命令中的字符串分隔符,如使用?、!等
e.g.用C shell替换/etc/passwd文件中的bash shell
sed -n 's?/bin/bash?/bin/csh?p' /etc/passwd
或
sed -n 's!/bin/bash!/bin/csh!p' /etc/passwd
也可以使用转义字符
sed -n 's/\/bin\/bash/\/bin\/csh/p' /etc/passwd
在sed中调用外部变量
如果在使用sed时需要调用环境变量,或者在shell脚本中,sed要调用脚本中定义的变量,需要注意以下:
- 由于单引号中无法调用变量,因此sed的命令部分需要使用双引号“”来包裹,这样才能使用$变量名来调用变量
- 如果变量中包含/等字符,需要将sed命令的分隔符修改为其他字符
e.g.需要在sed命令中调用PATH变量
由于PATH环境变量的值包含/,如:
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin"
如果直接执行命令:
sed "s/aaa/${PATH}/" config.json
sed会将PATH值中的/解析为命令字符串分隔符,从而出错,需要将sed命令修改为:
sed "s|aaa|${PATH}|" config.json
通过-i选项修改文件
相较于awk,sed编辑器提供了-i(in-place)选项可以直接修改原文件内容,完整的语法格式为:
sed -i[备份文件名] ‘文本处理命令’ 原文件
- 指定-i选项后,所有sed处理完的文本流将被重定向到文件中,而不再打印到屏幕。此时即便再添加p等打印命令,也不会输出任何内容到屏幕,反而会多输出一行相同内容到文件,因此修改文件时不要使用p等打印命令,除非明确需要打印行号等信息到文件
- -i选项可以接收一个字符串参数,此时sed会将原文件以原文件名备份文件名作为文件名,进行一次备份,然后再修改原文件
e.g.将原文件data.txt备份一份为data.txt.bak,然后修改data.txt文件中的aaa为bbb,
sed -i.bak 's/aaa/bbb/g' data.txt
e.g.修改配置文件中的端口号
config.json文件:
{
"ip": "88.77.99.88"
"port": 2100,
}
modifyPort.sh文件:
#!/bin/bash
configFile="/usr/local/myServer/config.json"
newPort=2200
if [ -f "${configFile}" ];then
sed -i[.bak] "/port/s/\([^0-9]*\)[0-9]*\(.*\)/\1${newPort}\2/" ${configFile}
fi
行寻址
默认情况下,sed编辑器的命令会作用于文本数据的所有行。如果只想将命令作用于某些特定行,则可以用行寻址,行寻址的方式有两种:
两种方式的语法都相同,直接将行地址加于命令前即可:
sed ‘行地址命令‘ [文件名]
如果需要为某个地址指定多个命令,可以使用大括号进行分组:
sed ‘行地址{命令1;命令2;…..}’ [文件名]
使用数字指定行号
sed编辑器会将文本的第一行的行号设定为1,随后依次递增,可以直接使用行号指定命令在哪些行生效。可以直接使用单个行号,也可以通过起始行,终止行的方式选定一个区间,如果不清楚某个文件到底有多少行,可以使用起始行,$方式选定从某行开始的所有行。
e.g.替换data.txt文件中第二行的指定字符串
sed '2s/AAA/替换内容/' data.txt
替换2到4行的匹配字符串
sed '2,4s/AAAA/替换内容/' data.txt
替换第2开始的所有行中的匹配的字符串
sed '2,$s/AAAA/替换内容/' data.txt
使用正则表达式匹配行
使用行号只能选取特定位置的行,如果需要根据内容筛选出对应行,如查找包含指定内容的行,并对该行进行处理,则需要使用正则表达式筛选出指定行,其语法为:
sed ‘/正则表达式/命令‘
也可以指定一个区间:
sed ‘/正则表达式1/,/正则表达式2/命令‘
命令会在正则表达式1匹配到内容时,从该行开始执行(开启命令执行),直到正则表达式2匹配到内容的行为止(关闭命令执行),如果只匹配到正则表达式1(开启),而正则表达式2无匹配的行,那么文件该行以后的所有行都将执行该命令(无关闭)
e.g.选取data.txt文件中包含2222字符串的行,并替换该行中AAAA字符串为替换内容
sed -n '/2222/s/AAAA/替换内容/p' data.txt
e.g.文件data.txt含以下内容
1111AA
2222AA
3333AA
4444AA
5555AA
6666AA
替换含字符串22行开始,到含字符串44行为止,中间所有行中的AA字符串为aa
sed '/22/,/44/s/AA/aa/' data.txt
输出为:
1111AA
2222aa
3333aa
4444aa
5555AA
6666AA
如果将开始行和末尾行替换,命令将从第4行开始,一直执行到文件末
sed '/44/,/22/s/AA/aa/' data.txt
输出为:
1111AA
2222AA
3333AA
4444aa
5555aa
6666aa
命令分组:如果为指定的行要执行多个命令,则可以使用大括号{ }将命令分组
e.g.替换data.txt文件中的多个不同内容
sed '2,${
> s/AAAA/替换内容/
> s/BBBB/OvO/
}' data.txt
打印命令p、=、l
sed提供了3个打印命令用于打印不同的信息:
- p:打印文本行
- =:打印行号
- l:(小写L)打印文本和非打印字符(如制表符等)
打印文本
打印命令p用于打印匹配到的字符串行。默认情况下,sed命令也会将模式空间中的内容打印到屏幕,因此通常将p命令与sed命令的-n选项搭配使用。先使用-n选项禁止sed命令的默认输出,再执行p命令打印匹配的字符串,这样终端上就只会显示我们需要的信息。
e.g.打印包含指定字符串的行
data.txt文件内容:
1111AAA111
2222AAA222
3333333333
如果直接执行命令
sed '/AAA/p' data.txt
则会输出显示以下内容:
1111AAA111 #p命令打印
1111AAA111 #sed编辑器默认打印
2222AAA222 #p命令打印
2222AAA222 #sed编辑器默认打印
3333333333 #sed编辑器默认打印
内容会被重复打印,即便文件第3行不包含字符串AAA,不满足命令执行要求,该行内容依旧会被sed编辑器读入模式空间,然后被默认打印出来,而满足执行要求的第一第二行文本则会打印两遍(p命令打印+sed默认打印),因此,p命令通常会和-n选项搭配使用,使用-n取消sed编辑器的默认打印行为,然后使用p命令打印满足要求的字符串行:
sed -n '/AAA/p' data.txt
这样,sed编辑器输出值就是满足命令处理要求被筛选过的字符串行:
1111AAA111
2222AAA222
e.g.打印文件的某几行
sed -n '2,3p' data.txt
打印文件中包含某字符串的行
sed -n '/AAA/p' data.txt
通过p命令可以先打印字符串行被修改前的样子,再打印一次被修改后的养样子,方便比较
sed -n '/AAA/{
>p
>s/AAA/aaa/p
>}' data.txt
打印行号
等号命令=会打印行在数据流中的当前行号。行号由数据流中的换行符决定。每次数据流中出现一个换行符,sed编辑器会认为一行文本结束了,行号默认从1开始。
e.g.打印包含某字符串的行号,及其该行的完整内容
sed -n '/AAA/{
>=
>p
}' data.txt
打印非打印字符
列出命令l(list)会打印数据流中的所有文本和不可打印的ASCII字符,这些非打印字符会以C语言转义字符的形式显示(如:\t水平制表符),或显示其八进制ASCII码值,并会在前面添加反斜线。
e.g.打印文件data.txt,文件内容包含\
data.txt文件内容:
1111 1111
2222 \ 2222
命令l会将制表符、\等需要转义的字符在文本中输出为转义字符形式,并使用$表示末尾换行
sed 'l' data.txt
输出形式为:
1111\t1111$
2222\\2222$
字符串替换命令s
替换命令s可以将文本行中的指定字符串替换为其他字符串,语法格式为:
sed ‘[行地址]s/替换前的字符串或正则表达式/替换后的字符串/标记符’
匹配数量
默认情况下,s命令只会替换每行中出现的第一个匹配字符串。要想让编辑器处理当前行中其他位置的字符串,需要使用标记符:
s/匹配字符串/替换字符串/标记符
标记符可以为以下字符:
- 数字:表示替换该行中的第几处匹配的字符串
- g:表示替换该行中所有字符串
- p:打印替换过的内容到屏幕
- w 文件名:将修改完的字符串行另存到文件中,只会保存匹配到的行
- e:将替换字符串视为shell命令执行,并使用命令执行结果作为最终的替换字符串(GNU拓展)
e.g.假设文件data.txt中有以下内容
data.txt
111AAA111BBB111AAA
222AAA222BBB222AAA
333AAA333BBB333AAA
so live a life you will remember
1. 替换文件中每行第2个字符串AAA为替换内容
sed 's/AAA/替换内容/2' data.txt
2. 默认情况下,上述命令会将替换完的完整文件内容输出到屏幕,包括第4行没有AAA内容的文本,如果只想输出匹配到行,则需要搭配-n选项和p标记符
sed -n 's/AAA/替换内容/2p' data.txt
p标记符用于输出匹配到的字符串行,如果没有-n选项,由于sed命令本身默认就会输出完整文件内容,加上p选项输出的匹配内容,因此会输出完整文件内容+匹配内容,匹配到的部分会输出两遍,因此需要使用-n取消掉sed命令的默认输出,只输出内容被修改的部分
3. 如果要先输出一行原文件内容,再输出一行被替换的内容,二者一上一下以作比较,可以使用以下命令
sed -n -e 's/AAA/&/p' -e 's/AAA/替换内容/p' data.txt
4. 将替换的内容另存到另外一个文件
sed 's/AAA/替换内容/w result.txt' data.txt
上述命令依旧会输出默认的完整文件到屏幕,但w标记符只会将替换过到的第1,2,3行保存到文件,没有AAA字符串的第4行将不被另存
5. 将匹配到的字符串替换为指定命令的执行结果
sed 's/Hello/date/e' file.txt
将file.txt中的Hello替换为date命令的执行结果(当前日期和时间)
大小写转换
s命令允许匹配的字符串通过L、l、U、u、E标志在大小写之间进行转换,以下命令中的&为模式引用(参考模式引用部分)
- s/pattern/\L&/g:将匹配的 pattern 转换为小写
- s/pattern/\l&/g:将匹配的 pattern 的第一个字符转换为小写
- s/pattern/\U&/g:将匹配的 pattern 转换为大写
- s/pattern/\u&/g:将匹配的 pattern 的第一个字符转换为大写
- s/pattern/\E&/g:将匹配的 pattern 的首字母转换为大写
字符替换命令y
sed编辑器中,字符替换命令y(yank v.急速抽取,猛拉)是唯一一个可以处理单个字符的命令,其语法格式为:
[地址]y/替换前的字符集set1/替换后的字符集set2/
在进行字符替换时,目标文本行中只需要包含字符集set1中的任意一个字符,该字符就会被替换,替换字符时,两个字符集一一对应,如set1中的第一个字符会被转换为set2中的第一个字符,依此类推。如果两个字符集长度不同,sed编辑器会抛出一条错误消息。
e.g.文件data.txt包含以下内容
111ABC111
ABC222ABC
333AAA333
444BBB444
555DDD555
字符替换命令会单独解析每个字符,文本中只要包含字符集中的任意字符就会被替换,且同一行有多个匹配的字符也会被转换,而不会像s命令一样只替换第一个
sed 'y/ABC/abc/' data.txt
111abc111
abc222abc
333aaa333
444bbb444
555DDD555
也可以用地址限定只在某行生效,以下命令只修改第一行中的ABC
sed '1y/ABC/abc/' data.txt
也支持使用正则表达式定位某行
sed '/22/y/ABC/abc/' data.txt
删除行d
sed编辑器中的d(delete)命令用于删除特定的行,它会根据匹配到的信息删除对应行,并且支持寻址。注意,如果d命令没有指定任何匹配内容,则默认会删除所有行。同样,d命令不会修改原文件,只是将经过删除处理后的文本内容打印到屏幕。d命令的语法为:
sed ‘地址d’ [文件名]
e.g.文件data.txt中包含以下内容
Line 1 AAAA
Line 2 BBBB
Line 3 CCCC
line 4 AAAA
line 5 DDDD
more line
1. 可以通过行号删除指定行
sed '2,4d' data.txt
2. 也可以根据正则表达式匹配内容删除指定行
sed '/AAAA/d' data.txt
3. 也可以通过两个正则表达式来指定一个区间,然后删除区间之间的行。注意,使用该语法时,指定的第一个模式会“打开”行删除功能,第二个模式会“关闭”行删除功能
sed '/Line 1/,/Line 3/d' data.txt #删除第1,2,3行
如果d命令再次匹配到满足要求的行,删除功能会再次打开,如果没有匹配到关闭删除的字符串,d命令有可能会删除后续整个文件内容!
sed '/AAAA/,/CCCC/d' data.txt
# sed会首先删除匹配到的包含AAAA到CCCC之间的行,即第1,2,3行,而后第4行再次匹配到了AAAA,于是删除功能再次打开,由于后续再无匹配到CCCC,删除功能一直继续,直到将文件内容删除完
插入和附加行i&&a
sed编辑器也支持加入新的行,两个操作分别为:
对诸如echo语句等单行命令前插入内容时,可以不指定地址。而在文件中插入新行,需要使用行号或正则表达式为其指定一个插入位置,但不支持地址区间,即只能将新增的行插入和附加到单个行前或行后,而不支持插入某个区间前后
e.g.如果在单行命令中插入文本,可以不指定地址
echo "这是一段文本" | sed 'i\插入的新行'
在文件第一行前插入内容
sed '1i\新增行内容' data.txt
在文件最后一行后附加内容
sed '$a\新增行内容' data.txt
也可以使用正则表达式匹配包含指定内容的行,这会将内容插入所有满足要求行的前后
sed '/AAAA/i\新增的行' data.txt
也可以同时插入多行内容
sed '3a\
>第一行\
>第二行\
>第三行' data.txt
修改行c
命令c(change)命令用来修改整行文本的内容,其语法和插入、附加命令类似:
sed '地址c\
> 修改后的第一行内容\
> 修改后的第二行内容' [文件名]
c命令支持使用地址区间,将该区间的行修改为指定的内容,但它只会将所有行修改为一个(单行或多行)指定内容,而不会每行修改一次,地址可以使用行号,也可以使用正则表达式匹配
e.g.根据行号修改指定行内容
sed '1,2c\
>修改的内容1\
>修改的内容2' data.txt
根据正则表达式匹配行并修改内容
sed '/AAAA/c\修改的内容' data.txt
写入文件w
w命令会将匹配的行写入文件(另存),如果文件不存在,则会自行创建,如果文件存在且文件中已有内容,则该内容会被覆盖,w命令语法格式为:
[地址]w 文件名
文件名可以包含文件的绝对路径或相对路径,但执行该命令的用户需要拥有对该文件的写权限
e.g.将data文件的第1,2行写入test文件
sed '1,2w test.txt' data.txt
将文件包含字符串AAAA的行写入文件
sed '/AAAA/w test.txt' data.txt
读取文件r
r(read)读取命令会从指定文件读取所有文本内容,并插入到指定的地址,该命令的语法格式为:
[地址]r 文件名
文件名可以包含文件的绝对路径或相对路径,指定地址时,只能指定一个单独的行号,而不能是一个区间,也可以是通过正则表达式匹配的行
e.g.将文件test中的所有内容插入到文件data末
sed '$r test.txt' data.txt
该功能常用于一些模板文件,这些模板文件(如信件模板)会在文件中用一个占位符预留空间,然后从另外一份文件导入字符串替换占位符,这样模板文件可以重复使用。如:文件customer写有顾客名单,文件Invitation为顾客姓名预留了位置
customer.txt文件内容:
丹尼斯里奇, 肯汤普森
詹姆斯高斯林, 林纳斯
Invitation.letter文件内容:
邀请的嘉宾:
NAME
....#邀请说明内容....
可以将占位符NAME删除,并在该位置填入邀请名单
sed '/NAME/{
>r customer.txt
>d
>}' Invitation.letter
多行文本的处理
默认情况下,sed编辑器会基于换行符的位置将数据分成行,以一行文本作为一个处理单位,如果处理的数据分布在两行或多行上,则sed编辑器可能返回错误的结果。
sed编辑器为了解决该问题,提供了三个命令用于处理多行文本:
- n(next):读取数据流中的下一行内容并覆盖当前模式空间中的内容
- N(next):将数据流中的下一行追加到当前模式空间中
- D:删除多行组中的一行
- p:打印多行组中的一行
next命令
next命令分为两个,命令n会读入数据流中的下一行内容并覆盖当前模式空间中的内容,而命令N会将下一行内容追加到当前模式空间中一起处理,二者有不同的用途。
命令n可用来通过字符串定位某行后,基于此行处理之后的每一行的内容。这些字符串在文件中出现的位置(行号)可能是随机的,因此只能通过字符串来定位,然后通过命令n读入下一行的内容并做处理。
e.g.找出文件data中的uuid
data文件内容
AAAAAA
BBBBBB
uuid:
a3d187a8-3b92-11ef-89d3-610452174e68
CCCCCC
uuid:
a3d187a9-3b92-11ef-89d3-610452445a10
DDDDDD
输出uuid
sed -n '/uuid/{n;p}' data
e.g.删除文件data.txt中第一行后的空白行
data.txt文件内容:
this is first line.
this is second line.
this is last line.
如果使用命令 sed '/^$/d' data.txt,文件中的所有空白行都会被删除,如果只想删除第一行后的空白行,则可以先定位到第一行,然后通过 n 命令读入下一行并覆盖第一行内容,然后通过删除命令删除该行即可,命令为:
sed '/first/{n; d}' data.txt
#通过first字符串定位第一行,然后通过命令n读入第一行后的空白行,再通过命令d删除该空白行
命令N会将数据流中的两个文本行合并到同一个模式空间中,文本行仍然用换行符分隔,但sed编辑器现在会将两行文本当成一行来处理。当文本信息(如一个短语)被分隔到了不同的行,需要统一处理时,则可以使用该命令。
如果对一个文件执行N命令,N命令的工作方式为:
先读入第一行文本,执行N命令,sed编辑器会立即读入第二行文本,并将二者作为一行文本处理,文本中的换行符\n会保留
随后读入第三行文本,继续执行N命令,读入第四行文本进行合并,以此循环
因此如果文件是偶数行,则所有文本都会两两作为一行进行处理
如果文件是奇数行,则执行到最后一行文本时,N命令会被sed编辑器叫停,最后一行文本单独作处理
e.g.文件data.txt中有以下内容:
AA AAAA
BB BBBB
执行命令sed 'N;l' data.txt,命令会打印出:
AA AAAA\nBB BBBB$
即模式空间中,两行内容会被当作一行保留了\n字符的单行字符串进行处理,因此,对被分割到了两行的字符串进行处理时,可以使用以下方法:
e.g.替换文件data.txt中的"周杰伦"为"陈奕迅"
data.txt文件内容
1111 11 11周
杰伦 22 22周
杰伦 33 周杰
伦44 44 4444
5555 55 周杰伦
考虑到字符串有不同的换行可能,需要考虑所有换行情况,但如果直接执行:
sed 'N
>s/周杰伦/陈奕迅/
>s/周\n杰伦/陈\n奕迅/
>s/周杰\n伦/陈奕\n迅/
>' data.txt
将产生以下问题:
1.N命令会将每两行合并为一行进行处理,一二行处理一次,三四行处理一次,这会导致如果字符串分散在二三行将无法匹配到
2. 文件为奇数行时,最后一行执行到N命令后将会被sed叫停,因为它没有下一行可以读入,这会导致sed退出,后续命令不再执行,这将导致最后一行中的字符串无法被匹配和替换
正确的命令应该为:
sed '
>:loop
>N
>$!bloop
>s/周杰伦/陈奕迅/g
>s/周\n杰伦/陈\n奕迅/g
>s/周杰\n伦/陈奕\n迅/g
>' data.txt
先将所有行合并到一行上(针对小文件),然后进行全局替换
\$!bloop表示未执行到最后一行前,均跳转回loop标签
N命令可以多次调用,来实现三行、四行等多行的合并处理,但需要注意最后一行是否会被合并到之前的行中,如果最后一行被孤立,其中的内容可能会无法被正确处理
data.txt文件包含以下内容
AA AA AAAA
BB BB BBBB
CC CC CCCC
DD DD DDDD
只调用一次N命令时:
sed -n 'N;l' data.txt
返回值为
AA AA AAA\nBB BB BBB$
CC CC CCC\nDD DD DDD$
调用两次N命令时:
sed -n 'N;N;l' data.txt
sed编辑器读入第一行后,执行第一个N命令读入第二行,执行第二个N命令读入第三行,执行l命令输出,然后清空模式空间。并读入第四行,开始重新执行指定的命令,但执行到第一个N命令后,由于文件已经读取到最后一行,于是sed编辑器会叫停命令执行,于是第四行由于没有执行到l命令,所以输出中将缺少第四行:
AA AA AAA\nBB BB BBB\nCC CC CCC$
类似的,调用3次N命令,将使整个文件作为同一个行进行处理,输出值为:
AA AA AAA\nBB BB BBB\nCC CC CCC\nDD DD DDD$
多行文本的删除D
在使用N命令时,数据流中的两行或多行文本会被当作同一行来处理,因此如果在N命令后使用d命令,将会同时删除多行文本。如果只需要删除第一行,可以使用D命令。它只会删除模式空间中的第一行,删除内容包括从第一行开头到换行符(含换行符)为止的所有字符,这个命令常用来删除目标数据字符串所在行的前一文本行。
e.g.需要搜索的关键词被分割于两行上,需要使用N命令合并搜索,但只删除其中的第一行内容
data.txt文件内容:
1111 11 11周
杰伦 22 2222
3333 33 周杰伦
如果使用d命令,则包含字符串的两行都会被删除
sed 'N;/周\n杰伦/d' data.txt
输出值为:
3333 33 周杰伦
只删除第一行,需要使用D命令
sed 'N;/周\n杰伦/D' data.txt
输出值为:
杰伦 22 2222
3333 33 周杰伦
多行打印P
与多行删除类似,如果将打印命令p(小写)与N命令一起使用,则p命令会同时输出所有匹配到的多行的内容。如果只想输出多行中的第一行内容,则需要使用P(大写)命令。同样,该命令只会输出模式空间中第一行开始到换行符之间的内容
e.g.打印上个例子中文件data.txt的指定字符串
小写的命令p会输出包含该字符串的多个行
sed -n 'N;/周\n杰伦/p' data.txt
输出:
1111 11 11周
杰伦 22 2222
大写的命令P只输出模式空间中的第一行
sed -n 'N;/周\n杰伦/P' data.txt
输出:
1111 11 11周
排除命令!
在命令前加上行地址可以指定命令只在这些行生效,而如果想命令在这些行中不生效,直接在行地址后加上!即可。排除命令!可以用来指定命令在某些行中不生效,而对文本剩下的其他行均生效。
e.g.文件data.txt含以下内容
1111 AA
2222 AA
3333 AA
4444 AA
输出文本内容,包含字符串2222的行除外
sed -n '/2222/!p' data.txt
输出值为:
1111 AA
3333 AA
4444 AA
将文本行中的AA替换为BB,2,3行除外
sed '2,3!s/AA/BB/' data.txt
输出值为:
1111 BB
2222 AA
3333 AA
4444 BB
命令跳转b
排除命令可以用来指定某个命令对哪些行不生效,当有大量的命令都要求对这些行不生效时,如果需要为每个命令都加上!过于繁琐。为此,sed提供了分支命令b(branch),用来帮助这些匹配的行跳过一整组命令,branch命令的格式为:
[行地址]b [跳转位置]
在文本处理命令中可以使用: 位置标识符(位置标识符最长可以为7个字符)来指定一个branch命令跳转的位置。注意:这里的跳转位置是指在命令行中的跳转位置,而不是跳转到文本流的哪个位置。行地址参数决定了branch命令触发的条件以及branch在哪些行生效,当在匹配的行执行到branch命令时,sed编辑器会跳转到位置标识符所在命令位置,并略过branch命令与位置标识符之间的所有命令。如果没有为branch命令指定跳转位置,则branch命令会跳过所有在它之后的命令,直接结束本行文本的处理。
命令的常见用法:
处理某文本流时,对于包含内容AA的行只执行命令3,命令4,其他行命令1,2,3,4均执行,就可以使用branch命令
sed '/AA/b jump;命令1;命令2; :jump; 命令3;命令4' data.txt
e.g.文件data.txt含以下内容
1111 AA BB CC
2222 AA BB CC
3333 AA BB CC
4444 AA BB CC
1. 不指定跳转位置,文本处理命令会跳转到命令末尾
sed '2,3b;s/AA/aa/;s/BB/bb/;s/CC/cc/' data.txt
由于没有指定命令跳转位置,对于第二,第三行,b命令会跳过后面的两个s命令,但对于其他行,b命令不生效,所有命令不跳过,输出值为:
1111 aa bb cc
2222 AA BB CC
3333 AA BB CC
4444 aa bb cc
2. 指定跳转位置,文本处理命令会跳转到位置标识符开始的地方
sed '2,3b jump1
>s/AA/aa/
>s/BB/bb/
> :jump1
>s/CC/cc/' data.txt
定义了一个位置跳转符 jump1,文本执行到第二,第三行时,s/AA/aa;s/BB/bb/命令会被跳过,跳转到jump1开始执行,因而s/CC/cc会正常执行
1111 aa bb cc
2222 AA BB cc
3333 AA BB cc
4444 aa bb cc
该命令等价于:
sed '2,3!{s/AA/aa/;s/BB/bb/};s/CC/cc/' data.txt
注意,命令在进行跳转时有可能形成循环,如果branch命令触发条件没写好,可能形成死循环
e.g.删除文本中的所有感叹号
echo "乌!蒙!山!外!连!着!山!"| sed ':jump;s/!//1p;/!/b jump';
sed会依次删除感叹号,直到所有感叹号删除完毕退出循环:
乌蒙!山!外!连!着!山!
乌蒙山!外!连!着!山!
乌蒙山外!连!着!山!
乌蒙山外连!着!山!
乌蒙山外连着!山!
乌蒙山外连着山!
乌蒙山外连着山
乌蒙山外连着山
测试命令t
测试命令t(test)也可以实现命令的跳转,其跳转的条件是t命令前的替换命令如果成功匹配并替换了一个字符串,测试命令就会跳转到指定的位置标识符,如果替换命令没有匹配到指定的模式,测试命令就不会跳转。注意,t 命令只能基于替换命令的成功与否触发跳转,而不能直接基于其他命令的结果。测试命令的语法格式与分支命令相同:
[行地址]t [跳转位置]
与分支命令类似,如果跳转条件满足而没有为t命令指定跳转位置,则sed会跳到命令结束的位置。
测试命令的工作模式类似于if-then语句
e.g.如果匹配到AA就替换为aa,此时由于没有指定跳转位置,t命令会跳转到命令末尾,第二个替换命令将不会执行。相反,如果第一个替换命令没有执行,则t命令不跳转,会尝试执行第二个替换命令
sed 's/AA/aa/;t;s/BB/bb/' data.txt
e.g.去除文本中的所有感叹号
echo "乌!蒙!山!外!连!着!山!"| sed '
>:jump
>s/!//1p
>t jump '
退出命令q
退出命令q或Q可以用来退出sed编辑器,注意,执行该命令后sed编辑器将直接退出而不再读取后续文本行,当然也不再执行后续命令,该命令也常用来退出由b命令和t命令形成的循环
此外,退出命令允许指定一个退出状态码,语法为q[状态码],用户可以自定义状态码,也可以使用默认的状态码,sed有以下默认的退出状态码:
退出状态码 |
说明 |
0 |
成功执行 |
1 |
无效的命令、语法或正则表达式 |
2 |
无法打开指定的文本文件(可能未找到或权限不足等) |
4 |
I/O错误或运行错误,sed终止 |
e.g.文件data.txt包含以下内容
1111AA1111
2222AA2222
3333AA3333
4444AA4444
5555AA55BB
6666AA6666
替换文本行中的AA,直到第四行
sed '4q;s/AA/aa/' data.txt
执行到第四行后,命令停止执行,后续文本也不再读入
1111aa1111
2222aa2222
3333aa3333
4444AA4444
e.g.一直读入文本行,直到遇到BB字符串,并将包括带有BB字符串的行及其之前的文本行合并为一行,q命令用于终止循环
sed ':start;/BB/q;N;s/\n/ /;b start' data.txt
返回值为
1111AA1111 2222AA2222 3333AA3333 4444AA4444 55AA55BB55
保持空间
sed对文本的操作都是在名为模式空间的缓冲区进行的,除了模式空间外,sed还有一个名为保持空间(hold space)的缓冲区,用来辅助模式空间工作。当模式空间中在处理文本行时,保持空间可用来临时存储一些行,有5条命令可以用来操作保持空间:
命令 |
命令来源 |
说明 |
h |
hold space |
将模式空间中的内容复制到保持空间 |
H |
hold space |
将模式空间中的内容追加到保持空间 |
g |
get from get space |
将保持空间中的内容复制到模式空间 |
G |
get from get space |
将保持空间中的内容追加到模式空间 |
x |
exchange |
交换模式空间和保持空间的中内容 |
保持空间的运用
巧妙使用保持空间可以实现一些有趣的功能,如实现文件的反序处理。默认情况下,sed编辑器会从文本流的第一行开始读取,第一行处理完毕才会载入第二行文本并作处理,但通过保持空间,我们可以将文本第一行暂存到保持空间,让sed编辑器先处理第二行,然后再从保持空间读出第一行文本进行处理,实现文本的反序处理,修改sed编辑器的默认处理流程。
e.g.实现文件的反序输出,即最先输出最后一行,然后依次从后到前输出文件内容
实现思路为:
1. 等sed编辑器读入第一行内容到模式空间后,使用h命令将内容复制到保持空间
2. 等sed编辑器读入第二行内容后,使用G命令将保持空间中的内容追加到模式空间中,此时第一行文本将排于第二行文本之后,然后再使用h命令将此时模式空间中的内容都存储到保持空间
3. sed编辑器读入第三行,继续使用G命令将保持空间中的内容追加到模式空间,此时文本行顺序为:第三行、第二行、第一行。再次使用h命令将其保持到保持空间
4. 重复上述步骤,直到文本行都被读取完毕,此时所有行都会反序存储于保持空间中
5. 读取并打印文本行
文件data.txt内容:
1111
2222
3333
4444
sed编辑器会在每次执行完一次命令,都会自行读入下一行,因此只需要使用G命令将保持空间中的内容追加到模式空间即可。但刚开始模式空间读入文本流第一行时,保持空间为空,因此对于第一行,不需要将保持空间追加到其之后,可以用排除命令!解决:
1!G
将文本行反序后,需要将此时模式空间的内容存储到保持空间:
h
等到sed编辑器读取到文本最后一行,并执行完G命令,说明文本流的反序已经完成,此时打印模式空间中的内容即可:
$p
完整的命令为:
sed -n '1!G;h;$p' data.txt
模式引用&
当sed匹配到了字符串时,可以在后续的命令中使用&引用匹配到的字符串
e.g.查找文本中以at结尾的单词,并在该单词外加上双引号""
echo "The cat sleeps in his hat." | sed 's/*at/"&"/g'
文本会替换为:
The "cat" sleeps in his "hat".
sed替换命令中,替换后的字符串不能使用*等通配符,因此上述语句改为以下命令将不会正常完成替换工作:
echo "The cat sleeps in his hat." | sed 's/*at/"*at"/g'
文本会被替换为:
The "*at" sleeps in his "*at".
部分引用
&符号会引用匹配到的整个字符串,如果只想引用字符串的一部分,需要使用一些特殊的方法:
sed编辑器可以使用圆括号( )在替换模式中定义子模式,然后在替换后的字符串中使用 \数字 的形式来引用每个子模式,数字表示子模式的位置。sed编辑器会将第一个子模式分配为\1,第二个子模式分配为\2,依此类推。注意!在使用圆括号定义子模式时,需要使用转义字符将圆括号标识为分组字符,否则sed编辑器会将其识别为普通的圆括号。
在使用部分引用时,将字符串中需要保留的部分使用\(字符串\)的形式定义为子模式,然后在替换后的字符串中使用\数字的形式引用需要保留的部分
e.g.文件data.txt含以下内容:
最常用的是Java语言和SpringBoot框架
替换Java但保留后缀"语言",替换SpringBoot但保留后缀"框架"
sed -n 's/Java\(语言\)和SpringBoot\(框架\)/JavaScript\1和Vue\2/p'
输出值为:
最常用的是JavaScript语言和Vue框架
在使用替换命令时,由于在指定替换后的字符串时不能使用通配符,而又需要在替换后的字符串中引用部分匹配到的字符串时,就可以很方便地以子模式的形式引用
e.g.文件data.txt含以下内容
furry cat
furry hat
furry dog
在替换后的字符串中不能使用通配符时,可以用该方法引用所匹配到字符串中的一部分,相当于替换后的字符串中也使用了通配符
sed 's/furry \(.at\)/pretty \1/'
输出值:
pretty cat
pretty hat
furry dog
在shell脚本中使用sed
默认情况下,sed编辑器会将命令结果打印到屏幕上,因此在脚本中使用sed命令时,需要对其输出结果进行重定向,常用的方法是使用$()将其结果重定向到一个变量中,方便后续处理。以外是一些可能用到的sed脚本示例:
格式化数值
e.g.输入一个值计算其阶乘,然后将计算结果以1,000,000格式输出
计算部分很简单,重点在于使用sed处理计算结果时,需要从个位开始每3位添加一个逗号,处理命令为:
sed '
>:start
>s/\(.*[0-9]\)\([0-9]\{3\}\)/\1,\2/
>t start'
关键在于正则表达式部分,它定义了两个子模式:
.*[0-9]
[0-9]{3}
第一个子模式中的 .* 组合会匹配任意长度的除回车和换行符外的任意字符,[0-9]代表该子模式匹配的字符串必须以数字结尾,二者结合,该子模式会匹配任意长度以数字结尾的字符串
第二个子模式会匹配三位数字
此外,该sed命令每一次只会在字符串中插入一个逗号,因此使用了跳转命令t形成了一个循环,它会一直循环直到t命令前的s命令语句无法插入逗号为止
当为该sed命令输入字符串12345678时
第一次循环:
正则表达式.*[0-9][0-9]{3}中的.*[0-9]需要为[0-9]{3}预留3位数字,然后*[0-9]本身匹配任意多数字结尾的字符串,所以:
.*[0-9]匹配字符串:12345
[0-9]{3}匹配字符串:678
然后s命令会在两个子模式间插入逗号,因此第一次循环执行结果为:
12345,678
第二次循环:
输入值为:12345,678
如果[0-9]{3}匹配了"678",.*[0-9]会匹配到"12345,",不满足以数字结尾的要求。因此在字符串"12345,678"只有"12345"部分满足正则表达式要求。后面的",678"在s替换命令中作为不匹配的字符串原样保留,替换作用对其不生效
因此,第二次循环,"12345"被替换为了"12,345",再加上原字符串保留的",678",最后的输出结果为12,345,678,这样数值会以方便阅读值的格式输出,整个脚本完整的代码如下:
#!/bin/bash
result=1
counter=$1
while [ $counter -gt 1 ]
do
result=$[ $result * counter ]
counter=$[ $counter - 1]
done
outputValue=$(echo $result | sed '
:start
s/\(.*[0-9]\)\([0-9]\{3\}\)/\1,\2/
t start')
echo "计算结果为:$outputValue"
将上述代码保存于test.sh文件,然后执行
bash test.sh 20
计算20的阶乘,最后输出结果为:
计算结果为:2,432,902,008,176,640,000
加倍行距
我们可以向文本文件的行间插入空白行,使得文本行看起来没那么密集
This is the header line.
This is the first data line.
This is the second data line.
This is the last line.
插入空白行可以从以下几点出发考虑:
1.如果文件本身已经有一些空白行,如果再加入空白行每两行文本之间可能会有多个空白行,因此应该先删除原文件的空白行(除非文本本身需要刻意保留这些空白行)
2.最后一行文本之后不需要空白行
3.默认情况下,sed编辑器保持空间就是一个空白行,可以使用G命令将其追加到模式空间内容之后
由此,加倍文件行距的命令为:
sed '/^$/d;$!G' data.txt
通过^$匹配文件原有空白行然后删除,通过$!G命令给除最后一行以外的行添加空白行
给文本添加行号
通过 = 命令可以打印行号,但行号会打印在文本内容上方,因此需要使用N命令合并,然后删除其中的换行符
sed '=' data.txt | sed 'N;s/\n/ /'
预览效果为:
1 This is the header line.
2 This is the first data line.
3 This is the second data line.
4 This is the last line.
查看文件末尾10行
用sed实现一个类似于tail命令的效果,读取文件最后10行,由此脚本可以任意魔改实现查看文件末尾n行
由于sed会从第一行开始读取文本,而我们需要文件最后的10行,因此在使用N命令读入下一行的同时,需要不断删除模式空间最顶部的那一行,直到模式空间中保留文件的末尾10行
sed '{
>:start
>$q
>N
>11,$D
b start}' data.txt
命令中,$q只会在读取文件最后一行时执行,q命令会退出sed编辑器,这里用来退出循环,而11,$D命令只会在读取文件第11行到最后一行之间的行时执行
执行流程为:
sed读入第一行,不执行$q,读入第二行,不执行11,$D
读入第二行后依旧不执行$q和11,$D
直到读入第11行,此时sed保持空间中已经有了11行内容,由于读入第11行内容,11,$D命令生效删除保持空间的第一行(也是文件第一行),让保持空间中只保留10行文本
之后的行类似,新加入保持空间中的行会替换掉保持空间顶部的行,保障保持空间中保留的是位于文件底部的行
直到读入文件最后一行,$q生效退出sed编辑器,退出前sed会默认打印此时保持空间中的内容,即文件的最后10行
可以在此基础上修改命令,任意指定查看行数,如:查看文件末尾3行:
sed ':start;$q;N;4,$D;n start' data.txt
删除空白行
1.删除文件开头的空白行
删除文件开头的空白行
sed '/./,$!d' 文件名
地址区间 /./,$ 表示从有任意字符的行开始到文件末,而/./,$!d表示地址区间的行不执行删除命令,即删除文件开头无字符的空白行
2.删除文件末尾的空白行
删除文件末尾的空白行
sed ':start ; /^\n*$/{$d ; N ; b start}' data.txt
与打印文件末的文本行类似,删除文件末的空白行会有点麻烦,需要使用循环来判断空白行位于文本之间还是文件末。正则表达式^\n*$会匹配空行和只包含换行符的行,然后执行大括号中的命令组,$d表示如果该行位于文件最后一行则删除,此时由于文件处于最后一行,N命令不执行。如果不是最后一行,则$d不执行,N命令会读入下一行附加到模式空间中,然后命令跳转回开头重新执行。
假设文件data.txt包含以下内容(为方便分析,文件末尾有两个空行):
1111
2222
4444
7777
根据命令,sed编辑器首先读入第一行,不满足/^\n*$/,编辑器不做任何操作,输出第一行
然后sed载入第二行,同样不满足/^\n*$/,第二行原样输出
sed载入第三行,该行为空行,满足正则表达式/^\n*$/中*为0的情况,执行{ }中的命令组,由于该行不为文件最后一行,$d不执行,N命令会读入下一行,此时保持空间中的内容为"\n4444",然后命令跳转回开头,此时保持空间中的字符串不再满足/^\n*$/,sed输出该内容然后读取下一行
sed读取的第五行也为空行,满足/^\n*$/,执行命令时同样不执行$d命令,但执行N命令,读入的第六行也为空行,此时模式空间内容为"\n\n",命令跳转到回开头重新执行,此时模式空间中的内容依旧满足/^\n*$/,再次执行命令N,读入第七行,此时模式空间内容为"\n\n7777",跳转命令后,不满足/^\n*$/,sed将输出该字符串
sed读入第八行,该行为空白行,sed执行命令组,随后sed通过N命令读入第九行,也为空白行,此时保持空间中的字符串"\n",然后命令进行跳转,再次进行正则匹配,由于"\n"满足/^\n*$/,开始执行命令组,此时sed已经读取到文件末,所以$d执行,删除最后一行(且由于此时两个空白行被N命令合并为了一行,两个空白行会被一起删除),执行到N命令时由于无下一行,sed会退出,此时文件末尾的所有空白行被删除完毕
执行命令sed -n ':start;/^\n*$/{$d;N;l;b start}' data.txt,可以清晰看到保持空间中合并过的内容:
\n4444$
\n\n7777$
\n$
该命令只会删除文件末的空白行,不会去删除文本之间的空白行,只删除文件末的空白行时可以使用此命令
在多行文本中,文本行末尾虽然隐式地在带有\n,但是是无法用正则表达式/\n/匹配的,但如果sed中使用了命令N来读入下一行文本,则当前文本和下一行文本之间会显式地写入\n,此时则可以使用/\n/来匹配。空白行同理,多个空白行虽然隐式地在末尾带有\n,但无法用正则表达式/^\n$/匹配空白行,而要使用/^$/来匹配,当使用命令N读入两个空白行时,sed编辑器保持空间中的内容将变成"\n",此时可以使用/\n/匹配到。该例子中,/^\n*$/刚好能完美匹配空白行未被读入时/^$/的形式,也能匹配保持空间中存在多个空白行时/\n*/的形式。
e.g.文件data.txt包含以下内容
1111
2222
3333
5555
8888
9999
未使用N命令时,无法匹配到\n
sed -n '/\n/l' data.txt
当使用了N命令时,\n会被显式写入文本中,因此可以被正则表达式匹配到
sed -n 'N;/\n/p' data.txt
会返回文件带\n的行(但由于文件是奇数行,最后一行无法执行N而导致sed退出,也就不会执行后续的p命令,无输出)
1111
2222
3333
5555
8888
通过输出命令l更能清晰看出文本行在保持空间中的具体存储方式
1111\n2222$
3333\n$
5555\n$
\n8888$
3.删除文件中连续的空白行
删除文件中连续的空白行
sed '/./,/^$/!d' 文件名
文件中数据行之间的多个空白行会被删除,只保留一个空白行。该命令的关键在于,创建包含一个非空白行和一个空白行的地址区间,如果sed编辑器匹配到了这个区间,它不会删除行。但对于不匹配这个区间的行(两个或更多的空白行),它会删除这些行。
删除HTML标签
如果需要删除html文件或xml文件中的标签,只留下标签之间的内容,通过sed也可以轻松实现
如果使用命令:
sed 's/<.*>//g' index.html
/<.*>/首先会匹配该行中最开始遇到的<,此后由于.*会进行贪婪匹配,*会匹配尽可能多的字符,>将匹配闭合标签中最末尾的>,这会导致标签中的文本也被替换掉,如:
<div> test </div>
该正则表达式会匹配高亮的两个尖括号:
<div> test </div >
这会导致整个标签,包括标签中的内容也会被替换掉,因此,正确的删除标签的命令是:
sed 's/<[^>]*>//g' 文件名
正则表达式<[^>]*>表示匹配以 '<' 开头,后面跟任意数量的非 '>' 字符,并以 '>' 结束的字符串,这正是一个HTML标签的形式。这样它会单独匹配双标签中的前半部分,由于使用了全局匹配g,标签的后半部分也会被匹配到,且对单标签也生效
<div> test </div>
这样,标签会被依次替换删除,只留下标签中的字符串部分
awk
简介
awk 是一种强大的文本处理工具,广泛用于 Linux 和 Unix 系统中。它的名字来源于其三位创建者 Alfred V. Aho、Peter J. Weinberger 和 Brian W. Kernighan的首字母。相较与sed,awk提供的是一种类编程语言而不仅仅只是编辑器命令,它支持算术运算、数组运算、自定义变量、自定义函数、结构化编程语句(如:循环,判断)、模式匹配等,且awk本身提供了大量的内部函数和内部变量用于进行字符串处理、数值计算、时间处理等。与sed编辑器一样,awk会为每一行文本都执行一次指定的awk程序,不同的是,sed编辑器更注重以行为处理单位,处理列数据的能力较弱,而awk支持将行中的数据提取为字段,并提供了许多内建变量和函数用于调用和处理这些字段,处理列数据的能力远要比sed强,因此,awk被广泛用于格式化文本、运算和过滤数据,快速生成报告等领域。
Linux中主要使用的是awk的GNU版本——gawk,它是 awk 的扩展实现,由 GNU 项目开发,提供了许多扩展功能和增强特性,同时保持了与传统 awk 的兼容性。在大多数 Linux 发行版中,预装的都是gawk,awk命令通常是gawk的一个符号链接,通过awk命令和gawk命令启动的都是gawk编辑器。
awk的工作流程
awk会依次执行以下流程:
- awk首先会执行BEGIN代码块中的内容,因此该代码块通常用来初始化变量、设置环境或打印初始信息
- 随后awk会逐行读取数据,并根据普通代码块中的模式-动作对进行匹配和处理,如果代码块中没有指定任何命令,awk会默认执行{ print $0 },即打印当前文本行
- 在处理完所有数据后,awk会执行END代码块中的命令,该代码块可用于打印最终结果、清理资源
命令格式
awk程序的基本格式如下:
awk 选项 '{程序体}' 文件1 文件2 ...
- -F 字符:将字段分隔符修改为指定字符
- -f 文件名:从指定的文件中读取程序
- -v 变量名=值:定义一个awk变量并赋值,一个-v选项只能定义一个变量,但可以多次使用-v选项,通过-v选项定义的变量可以在BEGIN程序块中读取到
- -mf 值:指定要处理的数据文件中的最大字段数
- -mr 值:指定数据文件中的最大数据行数
- -w 关键字:指定awk的兼容模式或警告等级
- awk默认支持拓展正则表达式,不需要像sed一样使用额外选项
- awk的程序体用一对大括号{ }定义,并且当程序是在命令行中指定时需要放到单引号‘ ‘中,这样shell会将其视为字符串,而不会去尝试解析awk程序,如果从文件中调用awk程序,则不需要单引号
通常情况下,awk程序包含模式和命令两个部分,命令部分使用大括号包裹,模式或命令部分可以省略其中一个,但不能两个都省略。如果省略模式,则会对所有输入的行都执行命令。如果省略命令(包括大括号在内的命令部分),则会默认打印所有与模式匹配的行。
e.g.打印data.txt文件中所有包含"li"字符串的行
awk '/li/{print $0}' data.txt
如果省略命令,则根据awk的默认行为,同样会打印匹配的行
awk '/li/' data.txt
如果保留大括号,但不写命令,则awk会进行空操作,不执行任何动作(也不打印任何内容)
awk '/li/{}' data.txt
程序定义方式
awk程序通常分为多个语句块,语句块使用大括号{ }包裹,同一个语句块允许定义多条命令,单行模式下,命令之间使用分号;分隔即可
echo "java c python go" | awk '{print $1;print $2}'
输出值为:
java
c
多行模式下,可以不使用分号
echo "java c python go" | awk '{
> print $1
> print $2 }'
从文件中调用:awk也支持将程序存储于文件中,然后在命令行中通过-f选项调用,跟sed编辑器一样,awk程序会对文件中的每行文本都执行脚本
script.awk
{print $1; print $2}
在命令行中使用-f选项调用
echo "java c python go" | awk -f script.awk
awk可执行程序:awk程序写于文件中也可以像shell脚本一样通过’#!’来指定文件的执行器,在赋予相应执行权限后直接通过文件路径直接执行。注意,与shell不同的是在指定awk解释器的时候,需要将选项-f作为参数传入awk
script.awk
#!/bin/awk -f
BEGIN {print "Hello World"}
然后赋给文件权限:
chmod +x script.awk
之后通过文件路径可以直接执行:
./script.awk
或
/myAwk/script.awk
文本输入方式
与sed编辑器相同,当命令行未指定输入的文本文件时,awk默认会从STDIN读取输入,每输入一行,awk会处理一行并即时打印处理结果,直到使用Ctrl+D结束输入,或使用Ctrl+C终止程序
awk '{print "输入为:"$0}'
10 #用户输入
输入为:10 #awk输出
GNU/Linux #用户输入
输入为:GNU/Linux #awk输出
也可以使用管道符|将文本输入awk程序
echo "java c python go" | awk '{print $1;print $2}'
文本也可以从文件中大量读取
文本较多时,可以写于文件中,awk可以从文件读取文本
awk '{print $1;print $2}' data.txt
在处理数据前/后执行脚本BEGIN与END
awk允许数据处理前,或数据处理后执行某些特定的功能。默认情况下,awk会先从输入读入一行文本,然后对该文本执行对应脚本。如果需要在数据处理前执行某个功能(如:脚本执行前打印提示),则可以使用BEGIN关键字来定义脚本,同样,awk还提供了END关键字用于在数据处理完毕后执行脚本,BEGIN和END是用来在读取数据流之前或之后执行命令的特殊模式。
awk 'BEGIN { print "程序启动" }
> {print "执行程序体"}
> END { print "程序执行完毕"}' data.txt
使用该功能可以生成漂亮且清晰的信息报告
e.g.文件script.awk中的程序
BEGIN {
print "当前Linux的用户列表和默认登录shell"
print "用户名\t\t默认登录shell"
print "--------------------"
FS=":"
}
{
print $1 "\t\t" $7
}
END {
print "----------------"
print "信息来自/etc/passwd文件"
}
执行命令
awk -f script.awk /etc/passwd
生成的信息报告(只展示一部分用户信息):
当前Linux的用户列表和默认登录shell
用户名 默认登录shell
------------------------
root /bin/bash
bin /sbin/nologin
daemon /sbin/nologin
adm /sbin/nologin
lp /sbin/nologin
-----------------------
信息来自/etc/passwd文件
使用模式
与sed相似,awk支持使用模式来过滤数据,使awk程序只作用于某些满足要求的行,BEGIN与END是两个awk预定义的特殊模式
使用正则表达式
awk支持基础正则表达式(BRE)和扩展正则表达式(ERE),通过正则表达式可以筛选出满足匹配的行,正则表达式之后语句块中的awk程序将只对这部分行生效
e.g.找出包含字符串"AA"的行,并打印这些行的第一个字段
awk '/AA/{print $1}' data.txt
匹配操作符
正则表达式会在每一行的所有内容中进行匹配,如果需要限定正则表达式只能在某个字段内进行匹配,可以使用匹配操作符~,语法为:
字段~正则表达式{ 程序体 }
e.g.文件data.txt包含以下内容
11AA 11AA
22BB 22AA
33CC 33AA
44DD 44AA
列出字段1中包含"AA"的行
awk '$1~/^AA/{print $0}' data.txt
输出值:
11AA 11AA
排除操作符
正则表达式的匹配结果可以使用!来进行排除,该排除操作符可以扩展为!~来根据字段匹配来进行排除,awk会选定除满足正则表达式外的其他所有行
e.g.打印不包含"22"的行
awk '!/22/{print $0}' data.txt
输出值为:
11AA 11AA
33CC 33AA
44DD 44AA
打印第一个字段不包含"AA"的行
awk '$1!~/AA/{print $0}' data.txt
输出值为:
22BB 22AA
33CC 33AA
44DD 44AA
使用数学表达式
在进行模式匹配时支持使用数学表达式进行比较,比较类型可以是数值,也可以是字符串,awk支持以下常用的比较符:
表达式 |
示例 |
== |
$1==”root” |
!= |
$3!=0 |
< |
$1<10 |
<= |
$1<=10 |
> |
$1>=10 |
>= |
$1>=10 |
e.g.列出所有超级管理员(UID为0)的用户名
awk -F: '$3==0{print $1}' /etc/passwd
内建变量
字段相关内建变量
变量 |
说明 |
$n |
$0代表整行文本,$n代表第n个字段 |
$NF |
表示最后一个字段(可以在不知道总共有几个字段,无法使用$n表示时使用) |
FS |
输入字段分隔符 |
OFS |
输出字段分隔符 |
RS |
输入记录分隔符 |
ORS |
输出记录分隔符 |
FIELDWIDTHS |
由空格分隔的一列数字,定义了每个数据字段确切宽度 |
字段数据变量
默认情况下,awk会将每一行文本使用字段分隔符划分为多个数据字段,并自动给每个字段分配一个变量,awk默认使用空白字符(如:空格、制表符)作为字段分隔符,数据字段可以使用相应的变量来引用,也可以通过修改变量来修改字段:
- $0代表整个文本行
- $n代表第n个数据字段
- $NF代表最后一个字段,其中NF表示最后一个字段的索引值(也代表了该记录一共有几个字段)
1.可以通过字段数据变量引用和修改字段
echo "java c python go " | awk '{$2="c++" ; print $0}'
修改后的文本变为:
java c++ python go
2. 有时无法确切得知该行一共有多少个字段,$NF可以在不知道字段数量的情况下获取最后一个字段的数据,同时NF也表示该行一共有多少个字段
echo "AA BB CC DD EE" | awk '{print NF,$NF}'
输出为:
5 EE
输入字段分隔符FS与输出字段分隔符OFS
变量FS和OFS决定了awk如何处理数据流中的数据字段。其中变量FS决定了awk根据什么字符来分割输入的数据流,默认情况下,FS的值是空白字符,因此awk默认会根据空格、制表符等字符的位置来分割数据流。而某些文件有它独特的字段分割符,如:/etc/passwd 文件默认使用:为字段分割符,在处理这些文件时,我们可以在将awk程序中FS的值修改为:,方便awk程序正确分割passwd文件的字段信息。
变量OFS决定了输出字段时,这些字段间的分隔符,在输出字段值时,print命令会自动将OFS变量的值放置在输出中的每个字段间。
e.g.文件data含以下内容
10,20,30,40
AA,BB,CC,DD
java,c,python,go
文件内容由,隔开,因此FS值为,,将字段使用----分隔后输出,注意print语句中字段变量之间的逗号只是用来指定多个变量,并不会输出到字段之间
awk 'BEGIN{FS=",";OFS="----"}{print $1,$2,$3,$4}' data
输出信息:
10----20----30----40
AA----BB----CC----DD
java----c----python----go
根据数据宽度分隔字段FIELDWIDTHS
有时文件中的数据并没有很规律地使用某个字符分隔,甚至部分信息是粘合在一起的,此时可以使用FIELDWIDTHS变量来定义一串值,以决定每个字段的字符长度,awk程序会根据这串值将文本分割为字段。FIELDWIDTHS变量能更精确地处理每个字段的宽度,很适合用来处理那些已经严格按照固定宽度格式化的数据。FIELDWIDTHS定义字段宽度的语法为:FIELDWIDTHS=”值1 值2 值3….”
e.g.命令ls -lh列出的文件信息,文件所有者、所属组、其他用户权限是合并在一起写的,可以按字符长度将其分割出来
(1)ls -lh命令输出信息为:
total 24K
-rw-r--r-- 1 root root 258 Jul 19 12:58 data.txt
-rw-r--r-- 1 root root 185 Jul 19 13:40 script.awk
drwxr-xr-x 2 root root 4.0K Jul 16 05:40 test
即首行会输出目录的总大小,可以通过sed命令'1d'删除第一行,然后将剩下信息用awk来处理,这里只分析文件的权限部分
ls -lh | sed '1d' | awk '
> BEGIN {
> print "文件类型\t用户权限\t用户组权限\t其他用户权限"
> print"---------\t--------\t----------\t------------"
> FIELDWIDTHS="1 3 3 3"
> }
> {
> print $1"\t\t"$2"\t\t"$3"\t\t"$4
> }'
输出值为:
文件类型 |
用户权限 |
用户组权限 |
其他用户权限 |
---- |
---- |
----- |
------- |
- |
rw- |
r– |
r– |
- |
rw- |
r– |
r– |
d |
rwx |
r– |
r-x |
记录分隔符RS与ORS
默认情况下,awk以一行为一个处理单位,awk会对所有行都执行一次程序,因此awk会将一行视为一个独立的数据记录。但有时,一个对象的不同数据可能会分布于多行,如一个用户的用户名,账号,Email地址等数据各占一行,这些多行数据共同构成一个用户的数据记录。我们不希望awk为每一行都执行一次程序,而是需要将这些多行视为一个处理单位(一个记录),此时就需要用RS和ORS变量来指定awk程序的记录分隔符,以便字段变量能正确分配到数据值,其中,RS为输入记录分割符,ORS为输出记录分割符,默认情况下,它们的值都是换行符
e.g.文件data.txt含以下内容
Riley Mullen
123 Main Street
Chicago, IL 60601
(312)555-1234
Frank Williams
456 Oak Street
Indianapolis, IN 46201
(317)555-9876
Haley Snell
4231 Elm Street
Detroit, MI 48201
(313)555-4938
数据中的每一组包含了一个用户的用户名,地址,电话信息,如果使用默认的FS和RS变量,awk会通过空格来分割字段,通过换行符来分割记录,导致数据被错误解析,对于该文件,一行是一个字段,数据记录通过空白行分割,因此FS变量应当是换行符,RS变量为空字符串:
awk 'BEGIN{FS="\n";RS=""}{print $1,$4}' data.txt
输出值为:
Riley Mullen (312)555-1234
Frank Williams (317)555-9876
Haley Snell (313)555-4938
通过ORS可以修改输出记录分隔符
awk 'BEGIN{FS="\n";RS="";ORS="======"}{print $1,$4}' data.txt
输出值:
Riley Mullen (312)555-1234 ======Frank Williams (317)555-9876 ======Haley Snell (313)555-4938======
记录计数FNR和NR
awk命令允许一次输入多个文件进行处理,在处理多文件时,FNR变量表示当前数据文件中已处理过的记录数,NR变量则含有已处理过的记录总数,FNR变量每处理完一个文件就会重置一次,而NR变量则会基于上一个文件的统计值继续增加。
e.g.处理两个文件,统计处理的记录数
awk '
> BEGIN {FS=","}
> {"FNR="FNR,"NR="NR}' data1.txt data2.txt
FNR=1 NR=1 #开始处理data1.txt
FNR=2 NR=2
FNR=3 NR=3
FNR=1 NR=4 #开始处理data2.txt
FNR=2 NR=5
FNR=3 NR=6
FNR=4 NR=7
其他内建变量
awk还提供了一些用于提取shell环境变量,命令执行参数等信息的内建变量
变量名 |
说明 |
ARGC |
命令行参数个数(Argument Count的缩写) |
ARGV |
命令行参数的值,以数组形式存储(Argument Vector的缩写) |
ARGIND |
awk处理的当前文件在ARGV中的位置 |
CONVFMT |
数字的转换格式(参见printf语句),默认值为%.6 g |
ENVIRON |
当前shell环境变量及其值组成的关联数组 |
ERRNO |
当读取或关闭输入文件发生错误时的系统错误号 |
FILENAME |
用作gawk输入数据的数据文件的文件名 |
FNR |
当前数据文件中的数据行数 |
IGNORECASE |
设成非零值时,忽略gawk命令中出现的字符串的字符大小写 |
NF |
数据文件中的字段总数 |
NR |
已处理的输入记录数 |
OFMT |
数字的输出格式,默认值为%.6 g |
RLENGTH |
由match函数所匹配的子字符串的长度 |
RSTART |
由match函数所匹配的子字符串的起始位置 |
提取命令行参数
ARGC和ARGV变量用来从shell中获得命令行参数的个数以及参数的值,其中,ARGV是一个数组,存储了命令行中所有参数的值,其索引从0开始。注意,awk不会将awk程序当作命令行参数的一部分。awk命令也可以指定多个输入文件,awk会依次处理这些文件中的内容,而ARGIND变量就用来获取当前awk程序处理的文件在ARGV数组中的索引
1.awk不会将程序部分视为命令行的参数
awk 'BEGIN{print ARGC,ARGV[0]}' data.txt config.json
该命令行有3个参数,ARGV数组会存储这些参数,它们的值分别是:awk,data.txt,config.json。因此该命令输出值为:
3 awk
2.提取的参数只包含awk命令部分,如果shell命令包含其他部分,这些变量不会获取它们,如:
ls -lh | sed '1d' | awk 'BEGIN{print ARGC,ARGV[0]}'
ARGC和ARGV只会提取awk命令部分的参数个数和参数值
1 awk
3.ARGIND变量用来获取awk程序当前处理的文件在ARGV数组中的索引
awk 'BEGIN{print ARGC,ARGV[2]}{print ARGIND}' data.txt config.json
每读取一行文件内容,awk程序会执行一遍(BEGIN和END指定的除外),因此文件data.txt和config.json有多少行,print ARGIND语句就会执行多少次,输出值为:
3 config.json
1
1
2
2
2
获取shell环境变量
awk程序中也可以获取shell的环境变量,awk提供了一个关联数组ENVIRON用于存储shell环境变量,我们可以通过ENVIRON[“环境变量名”]的方式来提取环境变量的值
e.g.在awk程序中获取环境变量HOME和PATH的值
awk 'BEGIN{
>print ENVIRON["HOME"]
>print ENVIRON["PATH"]}'
输出为:
/root
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
自定义变量
awk允许像shell一样自定义变量,且声明变量的方法与shell类似,直接声明变量名=值即可,不需要var,int等关键字,变量名可以由字母、数字、下划线组成,且不能以数字开头,严格区别大小写。在awk中调用变量值时,不需要$
在程序中声明变量
awk在程序中声明变量时,直接像shell变量一样声明使用即可
1.变量值可以是字符串,调用变量时不需要$符号
awk 'BEGIN{ name="Linux"; print name }'
2.变量值也可以是数值,并且支持包括加减乘除、取余(%)、冥运算(^或**)在内的数学运算
awk 'BEGIN{ x=10; x=x*10+20; print x }
变量的作用域
awk程序是按顺序执行的,因此变量也将按语句生效生效,且awk程序中的大括号语句块并不会限制变量的作用域,如果在变量生效前调用该变量不会有任何输出值。
1.由于BEGIN语句块是最先执行的,因此剩下的语句块都可以调用BEGIN语句块中定义的变量
awk 'awk 'BEGIN{ x=2 }{ print "普通语句块:"x }END{ print "END语句块:"x }' data.txt
输出值为:
普通语句块:2 #data文件有多少行,普通语句块就执行多少次
普通语句块:2
普通语句块:2
END语句块:2
2.END语句块是最后执行的,因此END语句块中定义的变量无法在其他语句块中调用
awk 'BEGIN{print "BEGIN语句块:"x}{print "普通语句块:"x}END{x=2}' data.txt
输出值为:
BEGIN语句块:
普通语句块:
普通语句块:
普通语句块:
3. 普通语句块中定义的变量,可以在END和位于该语句块之后的普通语句块中调用
awk 'BEGIN{print "BEGIN语句块:"x}{x=2}{print "第二个普通语句块:"x}END{print "END语句块:"x}' data.txt
输出值为:
BEGIN语句块:
第二个普通语句块:2
第二个普通语句块:2
第二个普通语句块:2
END语句库:2
4.如果后面的普通语句块中定义了变量,前面的普通语句块在第一次执行程序时无法调用变量,但从第二次开始,由于该变量在第一次执行时会被存入,因此此后该变量就可以在前面的语句块中也读取到
awk 'BEGIN{print "BEGIN语句块:"x}{print "第一个普通语句块:"x}{x=2}END{print "END语 句块:"x}' data.txt
输出值为:
BEGIN语句块:
第一个普通语句块: #第一次执行x并不存在
第一个普通语句块:2 #awk读入文件第二行,程序再次执行,此时x存在
第一个普通语句块:2
第一个普通语句块:2
END语句块:2
在命令行中声明变量
变量也可以在命令行中赋值,尤其是当awk程序被写于文件中时,程序变量在文件中不方便修改,此时可以在命令行中定义或修改变量,能十分方便地在每次执行命令时指定不同的值。awk使用第一个非选项参数作为awk的程序,因此定义变量时,变量要写于awk程序之后,否则将报错
1.awk变量可以直接在命令行上定义,下述语句会输出data文件每行的第一个字段
awk '{print $x}' x=1 data.txt
2. 但通过该方式定义的变量无法在BEGIN代码块中访问到
awk 'BEGIN{print "begin代码块:"x};{print "普通代码块:"x}' x=10 data.txt
输出值为
begin代码块: #输出的x值为空
普通代码块:10
普通代码块:10
3. 通过-v选项定义的变量,则可以在BEGIN代码块访问到
awk -v x=10 'BEGIN{print "begin代码块:"x};{print "普通代码块:"x}' data.txt
输出值为:
begin代码块:10 #此时x的值可以访问到
普通代码块:10
普通代码块:10
4. 不使用选项定义变量时,变量不能写于awk程序前
awk x=10 'BEGIN{print x}' data.txt
该语句将报错
数组
awk中使用的数组是关联数组,在定义和使用时需要加上数组的索引值
定义数组
数组变量赋值语法格式为:
数组名[索引值]=变量值
数组索引可以为数值,也可以是字符串
awk 'BEGIN{
>myArray[0]=10
>myArray[1]=20
>myArray["name"]="awk"
>}{
>print myArray[0]+myArray[1]
>print myArray["name"]
>}' data.txt
输出值为:
30
awk
遍历数组
由于用户无法提前得知数组的索引值是什么,因此awk提供了一个for语句可以用来获取awk的索引,用户可以在for语句中定义一个变量,for语句会将数组的索引赋值给该变量,然后就可以使用该变量作为索引获取到数组元素值,for语句语法格式为:
for (变量名 in 数组名)
{
循环体
}
awk 'BEGIN {
>array["name"]="awk";
>array["age"]=18;
>array["system"]="GNU/Linux";
>for(something in array){
>print something #数组索引
>print array[something] #数组值}
>}'
删除数组元素
删除数组元素的命令为:
delete 数组名[索引值]
数组元素删除后无法再调用
delete array["name"]
结构化命令
awk中的条件和循环语句与shell语法差别很多,更像是C语言的语法,循环语句支持使用break和continue语句跳出或跳过循环
if-else语句
awk中的if-else语句不需要then:
if(条件语句){
#程序体
}else{
#程序体
}
e.g.文件data包含以下内容
11 11AA 11AA
22 22BB 22AA
33 33CC 33AA
44 44DD 44AA
如果字段1大于30,则输出字段2,否则输出字段3
awk '{if($1>30){ print "大于30";print $2}else {print "小于30"; print $3}}' data.txt
使用多行模式代码更为清晰
awk '{
> if($1>30)
> {
> print "大于30"
> print $2
> }else
> {
> print "小于30"
> print $3
> }}' data.txt
while语句
while(条件语句)
{
# 循环体
}
awk 'BEGIN{
>i=1
>total=0
>while(i<5)
>{
>total+=$i #求字段1,2,3,4的和
>i++
>}
>print total}'
do-while语句
do
{
# 循环体
}while(条件语句)
for语句
awk支持的for语句有两种,一种是C语言风格的for循环:
for(变量定义; 循环条件; 迭代语句)
{
# 循环体
}
一种是用于数组迭代的for-in循环(详见数组部分):
for (变量名 in 数组名)
{
# 循环体
}
e.g.计算0-9的和
> awk 'BEGIN{
> total=0
> for(i=0;i<10;i++)
> {
> total+=i;
> }
> print total }'
格式化打印printf
print语句只能进行基本的打印工作,如果要进行格式化打印需要使用printf语句,awk支持像C语言一样使用格式化打印命令,只是语法有略微不同:
printf "格式控制语句",变量1,变量2...
与C语言一样,printf语句支持以下控制字符:
控制字符 |
说明 |
%c |
输出一个字符,支持将0~255内的整数输出为ASCII码对应字符 |
%s |
输出字符串 |
%d或%i |
输出整数 |
%f |
输出浮点数 |
%e |
用科学计数法(规格化指数形式)输出数值 |
%g |
根据数值大小自动选择使用%f或%e形式,保证输出宽度最小,不输出无意义的0 |
%o |
以八进制形式输出整型 |
%x |
以十六进制(字母小写)形式输出整型 |
%X |
以十六进制(字母大写)形式输出整型 |
另外,printf也支持:
- 指定字宽,如:”%5d”,输出宽度为5的整数
- 指定小数位数,如:”%6.2”,输出包括小数点在内的6位数据,只保留2位小数(如果原数据小数部分超过两位,则四舍五入)
- 使用-来指定数据左对齐(默认情况下,数据会进行右对齐)
- 使用\t(制表符)、\n(换行符)等转义字符
awk中printf语句的用法完全可以参考C语言printf()函数用法,这里只写简单示例
1.整齐输出每个字段的数据
awk '{printf "数据:%s\t%s\t%s\n",$1,$2,$3}' data.txt
输出值:
数据:11 11AA 11AA
数据:22 22BB 22AA
数据:33 33CC 33AA
数据:44 44DD 44AA
2.输出浮点数,保留两位小数
awk 'BEGIN{printf "%6.2f",10.175}
输出6位,因此左边会补两个空格
10.18
内置函数
awk提供了一些内建函数,用于处理字符串、时间等信息,以及进行数学运算
数学函数
在进行数学运算时,值不能太大,awk无法处理一些过大的数据
函数 |
说明 |
sin(x) |
正弦函数,x为弧度值 |
cos(x) |
余弦函数,x为弧度值 |
atan2(x,y) |
x/y的反正切,x和y为弧度值 |
exp(x) |
ex |
log(x) |
以e为底的对数,即logex或ln x |
sqrt(x) |
x的平方根,即 x1/2 |
int(x) |
取x的整数部分,抛弃小数,不进行四舍五入 |
rand() |
生成0-1(不包括0和1)之间的随机浮点数 |
srand(x) |
以x为种子值,生成随机数 |
e.g.生成0-9之间(包括0和9)的随机整数
x= int (10 * rand())
字符串函数
awk提供了一些用于处理字符串的函数,以下函数方括号中的参数是可选项,如果直接给函数输入字符串时,注意添加双引号
函数 |
说明 |
index(s,t) |
返回字符串t在字符串s中的索引值,如果没找到的话返回0 |
length([s]) |
返回字符串s的长度;如果没有指定的话,返回$0的长度 |
tolower(s) |
将s中的所有大写字符转换成小写 |
toupper(s) |
将s中的所有小写字符转换成大写 |
asort(s [,d]) |
将数组s按元素值排序,索引值会被替换成从1开始的连续数字,原索引被覆盖。如果指定数组d,则排序后的数组会被存储在数组d中,原数组s保持不变 |
asorti(s [,d]) |
将数组s按索引值排序,并使用索引值作为元素值,原数组元素值被覆盖,而数组的索引值会使用从1开始的连续数字替换。如果指定d,则排序后的数字存储于数组d中,原数组s保持不变 |
gsub(r, s [,t]) |
从$0或目标字符串t(如果提供了的话)来匹配正则表达式r。如果找到了,就全部替换成字符串s |
gensub(regexp, replacement, how [, target]) |
使用正则表达式regexp匹配字符串,然后使用replacement替换匹配到的文本,replacement中可以使用”\1”,”\2”等形式引用匹配的到的字符串,how用来指定替换方式,如果为”g”表示替换所有匹配项,如果为数字表示匹配第几个,target(可选)用来指定要应用替换的字符串,如果省略,则默认为$0 |
match(s, r [,a]) |
返回字符串s中正则表达式r出现位置的索引。如果指定了数组a,它会存储s中匹配正则表达式的那部分 |
split(s, a [,r]) |
将s用FS字符或正则表达式r(如果指定了的话)分开放到数组a中,返回字段的总数 |
sprintf(format, variables) |
用提供的format和variables返回一个类似于printf输出的字符串 |
sub(r, s [,t]) |
在变量$0或目标字符串t中查找正则表达式r的匹配。如果找到了,就用字符串s替换掉第一处匹配 |
substr(s, i [,n]) |
返回s中从索引值i开始的n个字符组成的子字符串。如果未提供n,则返回s剩下的部分 |
1. index(s,t)函数返回字符串t从另一字符串s的哪个位置开始,它会将字符串s的每个字符(包括空格)都视为一个占位的字符,然后返回的是子字符串t是从字符串s的第几个字符位置开始
awk 'BEGIN{print index("aa bb cc dd","cc")}'
输出值为:7
2. length([s])函数如果不指定字符串s,则会返回$0的长度
如:输出data文件每行内容的文本长度(包含字段间的空格)
awk '{print length()}' data.txt
3. asort(s [,d])函数会按照数组元素在ASCII中的顺序对数组元素进行排序,大致排序类型为:数值(从小到大)->首字母大写的数组元素->首字母小写的数组元素,使用该函数进行排序时注意两点:
(1)如果数组元素有数值,也有字符串,数值会按其大小正常排序,而不会将其转换为字符串进行处理
(2)排序完的数组使用for-in遍历会乱序输出,无法正确看到被排序后的数组结果
(3)关联数组的索引会被替换为从1开始的连续数字,索引从1开始而非0
如:将下述代码写于文件script.awk中
#!/bin/awk -f
BEGIN {
arr["shell"] = "bash"
arr["number1"] = 2
arr["number2"]=10
arr["name1"] = "Awk"
arr["name2"] = "awk"
arr["char"] = "2"
for(some in arr){
print some"-------"arr[some]
}
asort(arr);
print "---------------\n"
#使用for-in遍历关联数组输出顺序是随机的,想要数组按顺序输出则想要从索引1开始遍历,而不能用for-in
for(i=1;i>6;i++){
print i"-------"arr[i]
}}
给与script.awk文件执行权限后,执行./script.awk命令后输出值为:
char-------2 #for-in语句输出顺序随机
name1-------Awk
shell-------bash
name2-------awk
number1-------2
number2-------10
---------------
1-------2 #数值按其大小正常排序,而不会将其作为字符按其ASCII码排序(否则10的首字母为1应该排于2前)
2-------10
3-------2 #字符串类型的2排序于数值后
4-------Awk #首字母大写排于小写前
5-------awk
位运算
awk支持以下位运算
函数 |
说明 |
and(x,y) |
将x与y按位进行与运算 |
or(x,y) |
将x与y按位进行或运算 |
xor(x,y) |
将x与y按位进行异或运算 |
compl(x) |
对x进行补运算(按位取反) |
lshift(x,count) |
将x左移count位 |
rshift(x,count) |
将x右移count位 |
时间函数
awk包含一些用来处理时间的函数
函数 |
说明 |
systime( ) |
返回当前时间的UNIX时间戳 |
mktime(“YYYY MM DD HH MM SS”) |
将一个按YYYY MM DD HH MM SS格式指定的日期转换成UNIX时间戳,如果输入的日期时间无效,则返回 -1 |
strftime(“时间格式” [,time]) |
将时间time(如果未指定time则使用系统当前时间)以指定格式输出(格式需要为系统函数date允许的格式) |
1.systime( )会输出系统当前时间的UNIX时间戳,即返回系统当前时间到1970年1月1日以来的秒数,
awk 'BEGIN { print systime() }'
输出值:1580788800
2.mktime()函数会将指定的时间转换为UNIX 时间戳,该函数常用来计算时间间隔
awk '
>BEGIN {
> t1 = mktime("2000 01 01 12 00 00")
> t2 = mktime("2020 07 31 12 00 00")
> diff = t2 - t1
> print "秒数差:", diff
>}'
3.strftime()函数用来格式化时间,常用来将时间输出为指定格式
awk 'BEGIN { print strftime("%Y-%m-%d %H:%M:%S") }'
输出格式为:2020-11-02 16:30:05
也可以指定一个时间戳
awk 'BEGIN{time=systime();print strftime("%Y/%m/%d %H:%M:%S",time)}'
输出值:2020/07/31/24 20:33:20
自定义函数
函数定义与调用
在awk中定义、调用函数与C语言类似,但注意,在awk中需要将函数定义于代码块外,且必须出现在所有代码块之前(包括BEGIN代码块),且awk中的自定义函数也支持传参与返回值:
function 函数名(形参)
{
#函数体
return 值
}
e.g.对文件data.txt中的字段1和字段2求和
awk '
>function add(num1,num2){
>return num1+num2
>}
>BEGIN{ print "程序启动" }
>{
> result=add($1,$2)
> print result
>}' data.txt
函数库的创建与使用
可以将多个函数写于一个库文件中,然后在awk命令中使用-f选项调用,如果awk程序也要从文件中加载,可以使用多个-f选项
e.g.库文件funclib定义了多个函数
function myfunc1()
{
....
}
function myfunc2()
{
....
}
使用时在加载程序之前加载库文件即可:
awk -f funclib -f script.awk data.txt
调用外部变量
在shell脚本中使用awk时,可能需要在awk中调用变量,有以下两种方法
通过-v选项定义一个awk变量,然后将shell变量值赋值给该awk变量,如:
newPort=8080
awk -v port="$newPort" '{gsub(8000, port); print}' config.json
通过双引号与单引号的巧妙使用来获取变量
awk 'BEGIN{print "'"$变量名"'"}'
如:
awk 'BEGIN{print "'"$LANG"'"}'
输出值:en_US.UTF-8
解析:
awk程序由单引号来包裹,因此需要通过一对单引号来关闭awk程序,即初始的命令形式为:
awk 'BEGIN{print ' $LANG '}'
该命令中,'BEGIN{print '为一部分,'}'为另一部分,$LANG左侧的'号关闭了awk程序,将控制权重新交给shell,shell解析$LANG获取到变量值en_US.UTF-8,然后$LANG右侧的'号重新开启了awk程序,
此时命令相当于awk 'BEGIN{print en_US.UTF-8 }',awk会将en_US.UTF-8视为变量,从而报错
因此需要在外层加上一对双引号,将shell程序解析结果作为字符串交给awk程序,即命令修改为:
awk 'BEGIN{print "' $LANG '"}'
为什么不加在里面写出:awk 'BEGIN{print '"$LANG"'}'
这是因为''中的为shell需要解析的部分,对于shell来说,$LANG和"$LANG"是相同的,都会获取变量,这就相当于执行echo $LANG和echo "$LANG"都一样。如果写出这种形式,shell处理完变量,awk解析的命令依旧为:awk 'BEGIN{print en_US.UTF-8 }',依旧会由于变量en_US.UTF-8不存在而报错
而双引号加在外面相当于awk执行:awk 'BEGIN{print "en_US.UTF-8"}',它会直接输出字符串
通常情况下,awk 'BEGIN{print "' $LANG '"}'也可以获取到变量值
但如果变量值本身包含空格或引号,这依旧可能造成awk工作错误,因此通常会在$LANG两边再加上一对双引号,将LANG变量的值完全作为一个整体值获取,命令优化为:
awk 'BEGIN{print "'"$LANG"'"}'
"'"$LANG"'"中最外层的双引号用来保证awk会将值作为字符串处理,最内层的双引号保证变量值会被作为整体处理
修改文件
awk 没有直接提供类似sed的 -i 的选项来修改原文件内容,通常是先备份原文件,然后将awk的处理结果重定向到临时文件或原文件的方式来实现这一功能
e.g.修改文件data.txt中的字符串old_text为
new_text,然后将awk处理结果重定向到临时文件temp中,处理完毕后修改其文件名为data.txt进行覆盖(在此之前应该先备份好data.txt)
awk '{gsub(/old_text/, "new_text"); print}' data.txt > temp && mv temp data.txt