在
shell版混了3年多了,文本编辑方面小有心得,写出来与大家分享,黑哥等一笑而过。
读本文之前,需要对
sed有一定了解,最好看过sed1line:
本文所有用例的测试环境采用unix-center的ubuntu
服务器
具体版本如下:
- ly5066113@ubuntu:~$ uname -a
- Linux ubuntu 2.6.24-22-generic #1 SMP Mon Nov 24 19:35:06 UTC 2008 x86_64 GNU/Linux
- ly5066113@ubuntu:~$ bash --version
- GNU bash, version 3.2.39(1)-release (x86_64-pc-linux-gnu)
- Copyright (C) 2007 Free Software Foundation, Inc.
- ly5066113@ubuntu:~$ sed --version
- GNU sed version 4.1.5
- Copyright (C) 2003 Free Software Foundation, Inc.
- This is free software; see the source for copying conditions. There is NO
- warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE,
- to the extent permitted by law.
复制代码下面结合实例介绍sed
应用的几个方面。
一、标签
b label ,无条件跳转到标签label,如果label没有指定,跳转到命令的结尾
t label ,如果最后一次输入的最后一个 s/// 子命令执行成功,跳转到标签label,如果label没有指定,跳转到命令的结尾
例1: 用标签完成 是AA就加上YES,不是AA就加NO
使用t命令:
- ly5066113@ubuntu:~$ cat urfile
- AA
- BC
- AA
- CB
- CC
- AA
- ly5066113@ubuntu:~$ sed '/^AA/s/$/ YES/;t;s/$/ NO/' urfile
- AA YES
- BC NO
- AA YES
- CB NO
- CC NO
- AA YES
复制代码如果是AA,执行s/$/ YES/,s命令执行成功,执行t命令,没有标签,跳转到命令的结尾,这样将会跳过后面的s/$/ NO/
如果不是AA,s/$/ YES/不执行,则t命令也不执行,只执行后面的s/$/ NO/
使用b命令:
- ly5066113@ubuntu:~$ sed '/^AA/ba;s/$/ NO/;b;:a;s/$/ YES/' urfile
- AA YES
- BC NO
- AA YES
- CB NO
- CC NO
- AA YES
复制代码如果是AA,执行ba,跳转到标签a处,这样会跳过中间的s/$/ NO/;b,只执行后面的s/$/ YES/
如果不是AA,ba不执行,执行s/$/ NO/,执行b,没有标签,跳转到命令的结尾,这样将会跳过后面的s/$/ YES/
例2: 合并行:
- ly5066113@ubuntu:~$ cat urfile
- 114.113.144.2:
- 19ms
- 19ms
- 19ms
- 36ms
- 22ms
- 19ms
- 18ms
- 218.61.204.73:
- 0ms
- 0ms
- 0ms
- 0ms
- 0ms
- 0ms
- 0ms
- ly5066113@ubuntu:~$ sed ':a;$!N;/ms$/s/\n/ /;ta;P;D' urfile
- 114.113.144.2: 19ms 19ms 19ms 36ms 22ms 19ms 18ms
- 218.61.204.73: 0ms 0ms 0ms 0ms 0ms 0ms 0ms
复制代码实现思路:
1、读入下一行
数据
2、判断是否以ms结尾
3、如果是,替换\n为空格,跳转到1
4、如果不是,打印本行数据,删除本行数据,跳转到1
代码实现:
:a #定义标签a
$!N #不是最后一行,执行N命令
/ms$/s/\n/ / #如果以ms结尾,将\n替换为空格
ta #如果s///命令执行成功,跳转到标签a处
P #打印pattern space的第一行
D #删除pattern space的第一行,循环
此代码是使用sed进行合并行操作的典型代码,对于不同的情况,只需要将/ms$/替换成需要的正则表达式即可,思路上是通用的。
sed的标签类似于C语言中的goto,cjaizss兄说过,写代码的时候要有状态机的思想,代码只不过是一种实现。
下面是他用sed写的加法,里面应用了大量的标签,有兴趣可以研究下:
二、记数
sed不同于awk,内部没有数学运算,是否可以实现记数器,答案是肯定的,因为sed是图灵完备的。
例3:如何替换文中第2次和第4次出现的指定字符串
本例中采用的是这种
方案:
- sed ':a ; N ; $!ba ;s/root/mmmm/4'
复制代码但这个方案有2个弊端:
1、需要将文本一次性读入到pattern space,如果
文件很大,是不行的
2、如果指定字符串在同一行内出现多次,那么这个方法也是不行的
那么是否有其他的替代方案呢?让我们先从简单的开始。
将文本中第1次出现a的行替换为b
- ly5066113@ubuntu:~$ cat urfile
- a
- a
- a
- a
- a
- a
- ly5066113@ubuntu:~$ sed '0,/a/{s//b/}' urfile
- b
- a
- a
- a
- a
- a
复制代码我们可以利用0,/a/这样的地址区间来定位第一次出现a的行。
好,现在提升难度,将文本中第2次出现a的行替换为b
- ly5066113@ubuntu:~$ sed '0,/a/b;s/a/b/;ta;b;:a;n;ba' urfile
- a
- b
- a
- a
- a
- a
复制代码继续,将文本中第3次出现a的行替换为b
- ly5066113@ubuntu:~$ sed '0,/a/b;/a/ba;b;:a;n;s/a/b/;tb;ba;:b;n;bb' urfile
- a
- a
- b
- a
- a
- a
复制代码利用0,/a/和标签,也实现了,但同时我们发现,随着次数的增加,代码将会变的越来越烦琐
是时候另辟蹊径了,下面介绍一下我的方法,我为其命名为“打点记数法”
主要的思路是这样的:
利用sed的hold space,当遇到匹配行时,向hold space里面“打一个.”,使用 . 的个数来记录匹配的次数
如果 . 的个数达到了要求,则执行相应的操作
- ly5066113@ubuntu:~$ sed '/a/{x;s/^/./;/^.\{3\}$/{x;s/a/b/;b};x}' urfile
- a
- a
- b
- a
- a
- a
- ly5066113@ubuntu:~$ sed '/a/{x;s/^/./;/^.\{4\}$/{x;s/a/b/;b};x}' urfile
- a
- a
- a
- b
- a
- a
- ly5066113@ubuntu:~$ sed '/a/{x;s/^/./;/^.\{5\}$/{x;s/a/b/;b};x}' urfile
- a
- a
- a
- a
- b
- a
复制代码我们可以看到,对于次数的增加,我们只需要调整需要匹配的数值即可。
/a/{ #匹配时,开始记数
x #交换pattern space与hold space
s/^/./ #向hold space打一个 .
/^.\{3\}$/{ #判断 . 的个数是否达到要求
x #如果达到要求,交换hold space与pattern space
s/a/b/ #进行替换
b #跳转到代码结束
} #
x #交换hold space与pattern space
} #
接下来我们看看“打点记数法”的另一种应用:
例4:提取函数
- ly5066113@ubuntu:~$ cat urfile
- void
- foo(int a, int b) {
- //impl. skipped
- }
- void bigfunction() {
- int x1, x2, y1, y2, z1, z2;
- // after many lines
- bar(); foo (x1, x2);
- ly5066113@ubuntu:~$ sed -n 'N
- > /foo/{
- > :a
- > /{/{
- > x
- > s/^/./
- > x
- > }
- > /}/{
- > x
- > s/.//
- > /^$/{
- > x
- > p
- > q
- > }
- > x
- > }
- > p
- > n
- > ba
- > }
- > D' urfile
- void
- foo(int a, int b) {
- //impl. skipped
- }
复制代码此代码的主要思路,就是我在原贴中的回复:
当匹配 { 时,x;s/^/./;x ,向hold space里面放一个 .
当匹配 } 时,x;s/.//;x ,将hold space里的 . 去掉一个
就这样,当hold space再次为空时,就表示所有的 { 和 } 都配对了
三、lookup table
在讲lookup table
技术之前,先讲一下“回溯引用”
回溯引用对于大家来说并不陌生,会经常用于 s/REGEXP/REPLACEMENT/ 中REPLACEMENT部分
但回溯引用并不是只能用于REPLACEMENT部分,在REGEXP中也是可以使用的。
例5:找出100以内,个位与十位相同的2位数
- ly5066113@ubuntu:~$ seq 100 | sed -n '/^\(.\)\1$/p'
- 11
- 22
- 33
- 44
- 55
- 66
- 77
- 88
- 99
复制代码我们可以看到,这个问题利用回溯引用很容易解决。
\1表示前面\(.\)中内容,换种说法就是\1与前面\(.\)是相同的,那么就是个位与十位相同了
在理解了REGEXP中的回溯引用之后,我们来看看lookup table技术
lookup table就是回溯引用的方式进行前后定位,然后取出我们需要内容
例6:
AIX下怎么用DATE取上月的月份
UNIX下一般都没有GNU date,即便有GNU date,在某些时间点(如3月31号)上进行取上月操作(-1 month)的时候也有问题
- ly5066113@ubuntu:~$ date +%m
- 08
- ly5066113@ubuntu:~$ date +%m | sed 's/$/b12a01a02a03a04a05a06a07a08a09a10a11a12/;s/^\(..\)b.*\(..\)a\1.*/\2/'
- 07
复制代码我们来看看这段代码是如何工作的:
1、构造一个列表,字母a左边的2位数字是右边2位数字的上一个月
2、利用lookup table取出上一个月
pattern space初始为:
08
第一个s命令处理后pattern space变为:
08b12a01a02a03a04a05a06a07a08a09a10a11a12
下面我们重点来看看第二个s命令是怎么工作的:
s/^\(..\)b.*\(..\)a\1.*/\2/
将pattern space里面的内容按照上面的正则表达式进行分解
^\(..\) 08
b.* b12a01a02a03a04a05a06a
\(..\) 07
a\1 a08
.* a09a10a11a12
整个过程就是通过第一个括号里面的内容 08 ,定位到后面的 a08 ,从而取出它前面的2位数字 07 ,也就是第二个括号里的内容 \2
这种方法就称之为 lookup table
例7:文本处理
- ly5066113@ubuntu:~$ cat urfile
- 172.27.38.0&1=99&2=100
- 192.168.9.2&1=100&3=111
- 202.96.64.68&1=99&2=1&3=111
- 202.96.69.38&1=99&3=111&4=110
- 202.77.88.99&1=99&2=111&3=66&4=100&5=44
- ly5066113@ubuntu:~$ sed -r 's/&/\n1\n2\n3\n4\n5&/;:a;s/\n(.)(.*)&\1=([^&]+)/\t\3\2/;ta;s/\n./\t0/g' urfile
- 172.27.38.0 99 100 0 0 0
- 192.168.9.2 100 0 111 0 0
- 202.96.64.68 99 1 111 0 0
- 202.96.69.38 99 0 111 110 0
- 202.77.88.99 99 111 66 100 44
复制代码整体思路,用原贴中dream3401的描述:
1、产生1,2,3,4,5的"坐标"
2、对每天"有坐标的赋值"中的值代入坐标
3、对没有"赋值的坐标"代入0
我们以第一行数据为例,看看这段代码是怎么工作的:
pattern space初始为:
172.27.38.0&1=99&2=100
s/&/\n1\n2\n3\n4\n5&/后:
172.27.38.0\n1\n2\n3\n4\n5&1=99&2=100
s/\n(.)(.*)&\1=([^&]+)/\t\3\2/后:
172.27.38.0 99\n2\n3\n4\n5&2=100
s命令执行成功,t命令执行,跳转到标签a处,再次执行s/\n(.)(.*)&\1=([^&]+)/\t\3\2/:
172.27.38.0 99 100\n3\n4\n5
s命令执行成功,t命令执行,跳转到标签a处,再次执行s/\n(.)(.*)&\1=([^&]+)/\t\3\2/,s命令执行失败,无替换
t命令不执行,执行s/\n./\t0/g:
172.27.38.0 99 100 0 0 0
对于以上步骤,第一个s命令和最后一个s命令都不难理解,关键是中间的这句:
s/\n(.)(.*)&\1=([^&]+)/\t\3\2/
那我们以第一次的执行为例,将pattern space里面的内容按照上面的正则表达式进行分解
\n(.) \n1
(.*) \n2\n3\n4\n5
&\1= &1=
([^&]+) 99
利用第一个括号的数字1,定位到后面&1=中的数字1,从而取出=号后面的数字99
172.27.38.0 \n1\n2\n3\n4\n5&1=99 &2=100
172.27.38.0 99\n2\n3\n4\n5 &2=100
此正则表达式在工作的过程中,开头的 172.27.38.0 和结尾的 &2=100 都是没有处理的,处理的只是中间的一部分
例7和例6虽然都是用的lookup table技术,但思路梢有不同。
例6是构造一个列表,然后从列表中lookup出想要的值
例7也是构造一个列表,但是将外面的值填充到列表中
无论是那种方式,lookup table技术的基本做法都是先构造一个列表,然后用回溯引用定位,从而得到我们需要的值
四、GNU拓展
最后,介绍几个GNU sed的拓展功能,个人认为比较实用:
1、\U \L \u \l
大小写转换
- ly5066113@ubuntu:~$ echo 'abc' | sed 's/.*/\U&/'
- ABC
- ly5066113@ubuntu:~$ echo 'abc' | sed 's/.*/\u&/'
- Abc
- ly5066113@ubuntu:~$ echo 'ABC' | sed 's/.*/\L&/'
- abc
- ly5066113@ubuntu:~$ echo 'ABC' | sed 's/.*/\l&/'
- aBC
复制代码2、/REGEXP/I
正则匹配忽略大小写
- ly5066113@ubuntu:~$ echo 'AbC' | sed -n '/abc/Ip'
- AbC
复制代码3、FIRST~STEP
取出奇数行或者偶数行
- ly5066113@ubuntu:~$ seq 10 | sed '0~2d'
- 1
- 3
- 5
- 7
- 9
- ly5066113@ubuntu:~$ seq 10 | sed '1~2d'
- 2
- 4
- 6
- 8
复制代码每5行合并成1行,类似 xargs -n5
- ly5066113@ubuntu:~$ seq 20 | xargs -n5
- 1 2 3 4 5
- 6 7 8 9 10
- 11 12 13 14 15
- 16 17 18 19 20
- ly5066113@ubuntu:~$ seq 20 | sed ':a;N;s/\n/ /;0~5b;ba'
- 1 2 3 4 5
- 6 7 8 9 10
- 11 12 13 14 15
- 16 17 18 19 20
复制代码4、\%REGEXP%
进行路径匹配时,使用此方法,可以剩去很多转义符\
- 16 17 18 19 20
- ly5066113@ubuntu:~$ echo 'a/b/c/d/e/f' | sed -n '/a\/b\/c\/d/p'
- a/b/c/d/e/f
- ly5066113@ubuntu:~$ echo 'a/b/c/d/e/f' | sed -n '\%a/b/c/d%p'
- a/b/c/d/e/f
复制代码5、ADDR1,+N
地址 +行数,可以实现类似 grep -A N 的功能
- ly5066113@ubuntu:~$ seq 10 | grep -A2 '5'
- 5
- 6
- 7
- ly5066113@ubuntu:~$ seq 10 | sed -n '/5/,+2p'
- 5
- 6
- 7
复制代码文笔有限,如果有描述不清的地方请大家见谅。希望本文会对大家学习sed有所帮助。
关于sed记数,补充2个例子,均来自info sed中的examples(略有改动)
例8:实现 awk '{print NR,$0}' 的功能
- ly5066113@ubuntu:~$ head sed.info | awk '{print NR,$0}'
- 1 File: sed.info, Node: Top, Next: Introduction, Up: (dir)
- 2
- 3 sed, a stream editor
- 4 ********************
- 5
- 6 This file documents version 4.1.5 of GNU `sed', a stream editor.
- 7
- 8 Copyright (C) 1998, 1999, 2001, 2002, 2003, 2004 Free Software
- 9 Foundation, Inc.
- 10
- ly5066113@ubuntu:~$ head sed.info | sed -nf test.sed
- 1 File: sed.info, Node: Top, Next: Introduction, Up: (dir)
- 2
- 3 sed, a stream editor
- 4 ********************
- 5
- 6 This file documents version 4.1.5 of GNU `sed', a stream editor.
- 7
- 8 Copyright (C) 1998, 1999, 2001, 2002, 2003, 2004 Free Software
- 9 Foundation, Inc.
- 10
- ly5066113@ubuntu:~$ cat test.sed
- #! /usr/bin/sed -f
- x
- 1s/^/1/
- G
- s/\n/ /p
- s/ .*//
- /^9*$/s/^/0/
- s/.9*$/x&/
- h
- s/.*x//
- y/0123456789/1234567890/
- x
- s/x.*//
- G
- s/\n//
- h
复制代码x #交换pattern space与hold space,保存读入的内容
1s/^/1/ #如果是第一行,初始化行号1
G #将保存的内容追加回pattern space
s/\n/ /p #将换行替换为空格,并打印
s/ .*// #去处空格以后的所有内容,pattern space只剩下行号
/^9*$/s/^/0/ #如果行号都为9,在前面补0
s/.9*$/x&/ #用x分隔不需要改变和需要改变的数字
h #将pattern space中的内容保存到hold space
s/.*x// #删除不需要改变的数字
y/0123456789/1234567890/ #对数字进行 +1 的操作
x #交换pattern space与hold space
s/x.*// #删除需要改变的数字
G #将改变后的数字追加回pattern space
s/\n// #删除换行,得到新的行号
h #保存新行号到hold space
整体的思路:
每读入一行记录,将保存在hold space中的行号(如果是第一行,需要初始化),和本行记录合并输出
然后将行号 +1 ,保存至hold space
代码的核心部分就是实现“行号 +1”,具体做法:
将行号分为2个部分,一部分保持不变(此部分可能没有),一部分进行改变,然后将需要改变的部分进行处理
将处理后的结果与前面不变的部分拼接在一起,形成新的行号,但这里有个情况需要考虑,就是进位
我们已实际的例子来看看
如果行号是 123
那么首先将其分成2个部分变为 12x3
x左边的12不需要改变,x右边的3需要变成4
那么就对3进行y/0123456789/1234567890/的操作,将其变成4
然后拼上前面的12变为 124
如果行号是129(需要进位)
那么首先将其分成2个部分变为 1x29
x左边的1不需要改变,x右边的29需要变成30
那么就对29进行y/0123456789/1234567890/的操作,将其变成30
然后拼上前面的1变为 130
这里面有个特殊情况需要考虑,就是如果行号都为9
9, 99, 999 之类的情况,那么我们需要在前面加数字0用做进位用
例9:实现 wc -c 的功能
- ly5066113@ubuntu:~$ wc -c urfile
- 254 urfile
- ly5066113@ubuntu:~$ sed -nf test.sed urfile
- 254
- ly5066113@ubuntu:~$ cat test.sed
- #! /usr/bin/sed -f
- s/./a/g
- H
- x
- s/\n/a/
- : a; s/aaaaaaaaaa/b/g; t b; b done
- : b; s/bbbbbbbbbb/c/g; t c; b done
- : c; s/cccccccccc/d/g; t d; b done
- : d; s/dddddddddd/e/g; t e; b done
- : e; s/eeeeeeeeee/f/g; t f; b done
- : f; s/ffffffffff/g/g; t g; b done
- : g; s/gggggggggg/h/g; t h; b done
- : h; s/hhhhhhhhhh//g
- : done
- $! {
- h
- b
- }
- : loop
- /a/! s/[b-h]*/&0/
- s/aaaaaaaaa/9/
- s/aaaaaaaa/8/
- s/aaaaaaa/7/
- s/aaaaaa/6/
- s/aaaaa/5/
- s/aaaa/4/
- s/aaa/3/
- s/aa/2/
- s/a/1/
- y/bcdefgh/abcdefg/
- /[a-h]/ b loop
- p
复制代码此段代码虽然看起来烦琐,但思路却比上一例好理解。
每读一行数据,将里面所有的字符都替换成字母a,因为sed读数据时会将换行符(\n)去掉
所以我们利用H命令产生的\n将其补充回来,也替换成字母a,统一做字符统计
为了节省
内存开销,提高效率,这里做了进位的处理,就是将10个a替换成1个b,10个b替换成1个c 。。。
这样到最后,字母a的个数就代表个位数字,字母b的个数就代表十位数字,字母c的个数代表百位数字。。。
如果最后剩下是这样一串字符:
ccbbbbbaaaa
那么就表示总共的字符数为:254
本段代码的统计是有上限的,如果字符数量超过1亿,将无法得到正确结果
可以通过增加替换的次数来增加统计上限,如 s/hhhhhhhhhh/i/g , s/iiiiiiiiii/j/g 。。。