sed精彩实例解析

来源:互联网 发布:ubuntu rime 编辑:程序博客网 时间:2024/05/19 20:20

 

以下的例子都来自sedinfo文档。近日安装LFS,安装软件包完全是重复劳动,所以想写一个自动安装脚本,趁机好好地学习了一把sed。看info文档中的sed的例子,感觉非常精彩,所以把它们加上注释,记录下来。
例一:把文本行居中显示。脚本代码:
1      #!/usr/bin/sed -f
 2
 3      # Put 80 spaces in the buffer
 4      1 {
 5        x
 6        s/^$/          /
 7        s/^.*$/&&&&&&&&/
 8        x
 9      }
10
11      # del leading and trailing spaces
12      y/tab/ /
13      s/^ *//
14      s/ *$//
15
16      # add a new-line and 80 spaces to end of line
17      G
18
19      # keep first 81 chars (80 + a new-line)
20      s/^/(./{81/}/).*$//1/
21     
22      # /2 matches half of the spaces, which are moved to the beginning
23      s/^/(.*/)/n/(.*/)/2//2/1/
24

第一行是脚本头。
4行的1是地址,表示第一行(文件的行编号从1开始而不是从0开始)。后面用大括号括起来的是组合命令,当我们需要把一组命令应用到一个地址或者地址范围上时,就用大括号把它们括起来。现在详细看这个组合命令做了什么:第5行的x命令表示交换hold bufferpattern buffer中的内容,pattern buffersed当前正在处理的缓冲区,hold buffer是辅助缓冲区,下面用说到主缓冲区时指的是pattern buffer,说到副缓冲区时指的是hold buffer。副缓冲区在脚本开始时被初始化为空。所以现在用x命令交换后,sed的主缓冲区被初始化成空。第6行是一个替换命令,把空行替换成10个空格。那么现在主缓冲区中的内容变成了10个空格,在命令中^表示行首,$表示行尾。第7行把主缓冲区中的内容重复8次,替换命令格式:s/REGEXP/REPLACEMENT/FLAGS,在替换命令中用&符号表示REGEXP匹配到的内容。第8行再交换一次,现在80个空格放到了副缓冲区中。
12行把tab字符替换成空格,其中y/tab/ /中的”tab”应该被删掉,然后按下键盘上的TAB键插入一个TAB字符,否则会出错,但sedinfo文档上是这样写,可能自己方法不对。第1314两行分别把行首和行尾的空格除掉。
17行的G命令表示Appending Get,这条命令在主缓冲区最后加上一个换行,然后把副缓冲区的内容追加到主缓冲区后面。所以,现在的主缓冲区内容就是第一行输入首尾的空白字符被砍掉然后再后缀80个空格。
20行,s/^/(./{81/}/).*$//1//1表示/(./{81/}/),即表示81个字符,这条命令保留主缓冲区中前81个字符,后面的全部抛掉。
23行,这精彩的一行代码完成了文本居中显示的功能,仔细看看它:s/^/(.*/)/n/(.*/)/2//2/1/回顾下替换命令的格式s/REGEXP/REPLACEMENT/FLAGS,在这行代码中,REGEXP部分是:^/(.*/)/n/(.*/)/2,从第17行的G命令知道,/n前面的是第一行输入文本,后面的全是空格,而后面的全部空格由正则表达式/(.*/)/2来匹配,/2是回溯引用,引用的正是/(.*/)部分,全部空格就这样被巧妙地平均分成了等长的两半!
 
例二:给输入的整数做加1运算,代码:
 1      #!/usr/bin/sed -f
 2
 3      /[^0-9]/ d
 4
 5      # replace all leading 9s by _ (any other character except digits, could
 6      # be used)
 7      :d
 8      s/9/(_*/)$/_/1/
 9      td
10
11      # incr last digit only.  The first line adds a most-significant
12      # digit of 1 if we have to add a digit.
13      #
14      # The `tn' commands are not necessary, but make the thing
15      # faster
16
17      s/^/(_*/)$/1/1/; tn
18      s/8/(_*/)$/9/1/; tn
19      s/7/(_*/)$/8/1/; tn
20      s/6/(_*/)$/7/1/; tn
21      s/5/(_*/)$/6/1/; tn
22      s/4/(_*/)$/5/1/; tn
23      s/3/(_*/)$/4/1/; tn
24      s/2/(_*/)$/3/1/; tn
25      s/1/(_*/)$/2/1/; tn
26      s/0/(_*/)$/1/1/; tn
27
28      :n
29      y/_/0/
30

 
3行把输入文件中所有包含非数字字符的行删掉。
7:d是一个LABEL,和C语言中的goto指定LABEL的含义差不多。跳转到一个LABLE有两种指令方式,一种是b LABEL,无条件跳转,另一种是t LABLE,表示需要有一次s命令的成功方可跳转。
8行,注释上是说把所有的leading 9s换成_,其实应该是把所有的trailing 9s换成_,且看替换中的REGEXP部分:9/(_*/)$,就是说9后面后缀一串(或者没有)_字符的,把9替换成_,比如899,第一次循环把末尾的9换成_,替换后是89_,第二次循环把9_换成__,替换后变成8__,第三次循环替换失败,于是继续处理下一行,再比如898,第一次替换即失败,直接处理下一行。
从第17行到第26行,都是很简单的s命令,其中的tn如上所说它表示当s命令成功后就会生效,否则不生效,从第17行到第26行相当于一个switch-case结构,其中的tn相当于switch case中的break。到第29行,把所有的_替换成0。整个过程代码很清晰简单,算法非常好。
 
例三:文件名大小写转换,代码在下面这个地址也能找到:
http://www.gnu.org/software/sed/manual/html_node/Rename-files-to-lower-case.html
 1      #! /bin/sh
 2      # rename files to lower/upper case...
 3      #
 4      # usage:
 5      #    move-to-lower *
 6      #    move-to-upper *
 7      # or
 8      #    move-to-lower -R .
 9      #    move-to-upper -R .
10      #
11     
12      help()
13      {
14         cat << eof
15      Usage: $0 [-n] [-r] [-h] files...
16     
17      -n      do nothing, only see what would be done
18      -R      recursive (use find)
19      -h      this message
20      files   files to remap to lower case
21     
22      Examples:
23             $0 -n *        (see if everything is ok, then...)
24             $0 *
25     
26             $0 -R .
27     
28      eof
29      }
30     
31      apply_cmd='sh'
32      finder='echo "$@" | tr " " "/n"'
33      files_only=
34     
35      while :
36      do
37          case "$1" in
38              -n) apply_cmd='cat' ;;
39              -R) finder='find "$@" -type f';;
40              -h) help ; exit 1 ;;
41              *) break ;;
42          esac
43          shift
44      done
45     
46      if [ -z "$1" ]; then
47              echo Usage: $0 [-h] [-n] [-r] files...
48              exit 1
49      fi
50     
51      LOWER='abcdefghijklmnopqrstuvwxyz'
52      UPPER='ABCDEFGHIJKLMNOPQRSTUVWXYZ'
53     
54      case `basename $0` in
55              *upper*) TO=$UPPER; FROM=$LOWER ;;
56              *)       FROM=$UPPER; TO=$LOWER ;;
57      esac
58     
59      eval $finder | sed -n '
60     
61      # remove all trailing slashes
62      s///*$//
63     
64      # add ./ if there is no path, only a filename
65      ////! s/^/.///
66     
67      # save path+filename
68      h
69     
70      # remove path
71      s/.*////
72     
73      # do conversion only on filename
74      y/'$FROM'/'$TO'/
75     
76      # now line contains original path+file, while
77      # hold space contains the new filename
78      x
79     
80      # add converted file name to line, which now contains
81      # path/file-name/nconverted-file-name
82      G
83     
84      # check if converted file name is equal to original file name,
85      # if it is, do not print nothing
86      /^.*///(.*/)/n/1/b
87     
88      # now, transform path/fromfile/n, into
89      # mv path/fromfile path/tofile and print it
90      s/^/(.*///)/(.*/)/n/(.*/)$/mv "/1/2" "/1/3"/p
91     
92      ' | $apply_cmd
93

 
注意从网站上复制下来的代码每行前面都有些空白,要把这些空白去掉,否则会语法错误:
[root@test root]# sed 's/^[ /t]*/(.*/)$//1/' remap.sh > movetoupper.sh
[root@test root]# sed 's/^[ /t]*/(.*/)$//1/' remap.sh > movetolower.sh
help函数显示该脚本的用法,其中用到了here document语法,其中cat << eofeof没有用引号引起来,这表示在here doc中将会启用变量扩展。
3133行设置一些变量的默认值。@bash脚本中表示所有的positional parameters$@用来取变量@的值。在bash手册中的special parameters一节中有详细介绍。第33行的files_only变量只在这一个地方出现,没有什么作用。
3544行的while循环根据用户传进来的命令选项重新设定第3133行定义的参数的值,共有三个可能的命令选项:-h, -n ,-R。如果用户没有提供任何命令选项,直接以./movetoupper file1 file2 file3这样的形式来使用,在这种情况下,第32行的tr命令起了作用,它把以空格分隔的各个文件名变成以换行来分隔,以使得作为参数的文件名可以一行一行地交给sed去处理。第35while后面那个冒号是bashbuiltin函数,它的返回值永远是0,所以这是一个死循环。
4649行做一个判断,如果用户没有提供任何命令选项和参数,或者在需要参数的命令选项后面没有提供参数,就输出帮助信息然后退出。
下面以./.eshell/lastdir为例来说明sed命令部分:
62行和65行的作用注释已经写的很清楚,经过第6265行后,主缓冲区的内容是:./.eshell/lastdir。第65行相当于sed ////!s/^/.///’。
68行的h命令的作用是把主缓冲区的内容放到副缓冲区去,覆盖副缓冲区原有的内容,现在主缓冲区内容是./.eshell/lastdir
71行的作用是移除所有的上级路径部分,只留下文件名,正则表达式匹配具有贪婪性,这里正是利用了这一点。执行后,主缓冲区的内容是:lastdir,副缓冲区的内容是:./.eshell/lastdir。查看主缓冲区和副缓冲区可以用p; x; p; x命令序列来实现。
74行用一个y命令实现大小写转换,执行后,主缓冲区内容是LASTDIR,副缓冲区内容是:./.eshell/lastdir
78x命令交换主缓冲区和副缓冲区的内容。
82行的G命令是往主缓冲区中添加一个换行,然后把副缓冲区的内容后缀到主缓冲区中。执行后,主缓冲区的内容是:./.eshell/lastdir/nLASTDIR,副缓冲区的内容是:LASTDIR
这个脚本所实现的功能可以用tr命令很容易地实现。
86行值得注意的是那个b命令,它表示无条件分支跳转,当跳转的目标被忽略时,则放弃当前处理,直接开始处理下一条输入。它的info文档的原文讲解是:
`b LABEL'
     Unconditionally branch to LABEL.  The LABEL may be omitted, in
     which case the next cycle is started.
86行正则表达式部分没有特殊的,对照82行执行后的主缓冲区内容看得更清楚些。如果正则表达式匹配成功,就执行b命令,否则,自然就不执行b命令而直接执行90的命令。
90行是一个s命令,REGEXP部分是/^/(.*///)/(.*/)/n/(.*/)$/REPLACEMENT部分是mv "/1/2" "/1/3",对照82行执行后的主缓冲区的内容可以理解。90行的s命令还带了一个p命令,s命令中的p命令的意思是如果替换成功完成则输出新的替换后的主缓冲区内容。
90行的输出通过管道传递给$apply_cmd执行,如果用户指定了-n选项,apply_cmd变量的值就是cat,它只是执行一个简单的输出,如果用户没有指定-n选项,这个变量的值就是默认的sh,它将执行90行输出的mv命令。
语言并不复杂,其背后的算法思想非常漂亮。
 
例四:打印bash环境变量,代码:
 1      #!/bin/sh
 2
 3      set | sed -n '
 4      :x
 5
 6      # if no occurrence of "=()" print and load next line
 7      /=()/! { p; b; }
 8      / () $/! { p; b; }
 9
10      # possible start of functions section
11      # save the line in case this is a var like FOO="() "
12      h
13
14      # if the next line has a brace, we quit because
15      # nothing comes after functions
16      n
17      /^{/ q
18
19      # print the old line
20      x; p
21
22      # work on the new line now
23      x; bx
24      '
25
26

这个脚本比较短,也很清晰。
用到的命令都可以在sedinfo文档中查到。
 
例四的功能是反转一个字符串,这个脚本稍后再说。截止现在也看到了,时刻了解sed脚本执行时主缓冲区和副缓冲区中的内容是学习和理解sed的好方法。那么,有没有一种工具来自动监视sed执行时它的缓冲区内容呢?幸运的是,网上有很多sed调试器,它们可以实时显示sed执行时的命令和缓冲区,在http://sedsed.sourceforge.net/就可以下载到一款,这是一个用python写成的sed调试器,作者是Aurelio Jargas
现在已经下载到这个调试器,它放在~/sedDebug目录下,sed的脚本都放在~/sedExample下。我们试着用一用这个调试器,以下这个sed info文档中的例子脚本完成tac命令的功能:
     #!/usr/bin/sed -nf
 
     # reverse all lines of input, i.e. first line became last, ...
 
     # from the second line, the buffer (which contains all previous lines)
     # is *appended* to current line, so, the order will be reversed
     1! G
 
     # on the last line we're done -- print everything
     $ p
 
     # store everything on the buffer again
     h
 
这个脚本只有三行命令,我们执行它:
[root@test sedExample]# cat > test << "eof"
> first line
> 2line
> here, this is the third line
> ok, last
> eof
[root@test sedExample]# cat test | ../sedDebugger/sedsed-1.0 -d -f tac.sed
PATT:first line$
HOLD:$
COMM:1 !G
PATT:first line$
HOLD:$
COMM:$ p
PATT:first line$
HOLD:$
COMM:h
PATT:first line$
HOLD:first line$
first line
PATT:2line$
HOLD:first line$
COMM:1 !G
PATT:2line/nfirst line$
HOLD:first line$
COMM:$ p
PATT:2line/nfirst line$
HOLD:first line$
COMM:h
PATT:2line/nfirst line$
HOLD:2line/nfirst line$
2line
first line
PATT:here, this is the third line$
HOLD:2line/nfirst line$
COMM:1 !G
PATT:here, this is the third line/n2line/nfirst line$
HOLD:2line/nfirst line$
COMM:$ p
PATT:here, this is the third line/n2line/nfirst line$
HOLD:2line/nfirst line$
COMM:h
PATT:here, this is the third line/n2line/nfirst line$
HOLD:here, this is the third line/n2line/nfirst line$
here, this is the third line
2line
first line
PATT:ok, last$
HOLD:here, this is the third line/n2line/nfirst line$
COMM:1 !G
PATT:ok, last/nhere, this is the third line/n2line/nfirst line$
HOLD:here, this is the third line/n2line/nfirst line$
COMM:$ p
ok, last
here, this is the third line
2line
first line
PATT:ok, last/nhere, this is the third line/n2line/nfirst line$
HOLD:here, this is the third line/n2line/nfirst line$
COMM:h
PATT:ok, last/nhere, this is the third line/n2line/nfirst line$
HOLD:ok, last/nhere, this is the third line/n2line/nfirst line$
ok, last
here, this is the third line
2line
first line
以上是sed调试器的输出结果。PATT后面是pattern space即主缓冲区的内容,HOLD后面是hold space即副缓冲区的内容。COMMAND后面是当前正在执行的命令。其它的都是sed脚本执行的输出结果。
 
有了这个sed调试器后,理解学习sed就非常快捷非常方便了。然而这并不代表自此就成为了sed高手。sed只是定义了一些表达方式,学习sed,了解它的表达方式,然后就可以用这些表达方式创作出美丽的篇章。
现在复习一下上面的例子,总结一下它的表达方式。两块缓冲区,一主一从,这是物质基础,除了s命令外,所有操作都是针对缓冲区全部内容的,比如对整个缓冲区的delete, append, flush等等,只有s命令可以对缓冲区中的部分内容进行操作,因此s命令也是sed中最灵活的命令了。
下面是反转字符串的sed脚本,它展示了使用s命令的一种思路:
#!/usr/bin/sed -f
 
/../! b
 
# Reverse a line.  Begin embedding the line between two new-lines
s/^.*$//
&/
/
 
# Move first character at the end.  The regexp matches until
# there are zero or one characters between the markers
tx
:x
s//(/n./)/(.*/)/(./n/)//3/2/1/
tx
 
# Remove the new-line markers
s//n//g
可以用调试器仔细看它的工作流程。
 
sedinfo文档包含了很多实用的示例,上面是其中的五个。前两天在网上查找使用sed删除文件中的所有换行符,当前看不明白,现在明白了,把那个代码贴在这里:
sed –e :a –e $!N;s//n//;tadatafile
 
原创粉丝点击