使用 sed 可以将类似于 vi 编辑器中手动的操作过程提取出来,并转换成一个非手动的过程,即通过执行一个脚本来实现。大多数不熟悉 sed 的人都觉得,编写执行一系列编辑动作的脚本,比手动做一些改动更冒险。这种担心的原因是自动化任务会发生一些不可逆的事情。学习 sed 的目标就是要理解它从而可以预测执行结果。
这就要求采用可控制的方式来使用 sed。在编写脚本时,应遵循以下这些步骤:
- 在着手做之前要弄清楚想做什么。
- 明确地描述处理过程。
- 在提交最终的改变之前反复测试这个过程。
检测脚本是否中工作的最好方式,是使用不同的输入样本进行测试并观察结果。sed 工作的三个基本原理为:
- 脚本中的所有编辑命令都将依次应用于每个输入行。
- 命令应用于所有的行(全局的),除非行寻址限制了受编辑命令影响的行。
- 原始的输入文件未被改变,编辑命令修改原始行的备份并将修改后的备份发送到标准输出。
一、在脚本中应用命令
一次一行的设计的一个优点是 sed 比交互式屏幕编辑程序更适合处理大文件。后者必须将整个文件(或大部分)读入内存,这可能产生内存溢出或处理大文件时速度非常慢。sed 首先将整个编辑脚本应用于第一个输入行,然后再读取第二个输入行并对其应用整个脚本。因为 sed 总是处理原始行的最新形式,所以生成的任何编辑工作都会改变后续命令的应用的行。sed 不会保留最初的行,这意味着与原始输入行匹配的模式可能不再与经过编辑操作之后的行匹配。
sed 维护一种模式空间,即一个工作区或临时缓冲区,当应用编辑命令时将在那里存储单个输入行。当应用了所有的指令后,当前行被输出并且输入的下一行被读入模式空间。然后脚本中的所有命令应用于新读入的行。
结果是,任何 sed 命令都可以为应用下一个命令改变模式空间的内容。模式空间的内容是动态的,而且并不总是匹配最初的输入行。看个例子,假设输入为 pig cow,希望的输出为 cow horse。下面的 sed 命令输出中包含两个 horse:
代码语言:javascript复制s/pig/cow/
s/cow/horse/
第一个命令将“pig”换成“cow”,第二个命令在同一行上将“cow”换成“horse”时,它还改变了由“pig”换成的“cow”。这个错误只是脚本命令中的顺序问题,技巧在于反转命令的顺序:
代码语言:javascript复制s/cow/horse/
s/pig/cow/
一些 sed 命令会改变整个脚本的流程,例如 N 命令将另一行读入模式空间但不删除当前行,所以可以用来测试跨越多行的模式。其它一些命令告诉 sed,在到达脚本底部之前退出或者转到带标记的命令。sed 还维护了称为保持空间(hold space)的令一个临时缓冲区。可以将模式空间的内容复制到保持空间并在以后检索它们。
二、寻址上的全局透明
sed 是隐式全局的,即缺省将命令应用于每个输入行。行地址用于提供操作(或限制)的上下文环境。sed 命令可以指定零个、一个或两个地址。每个地址都是一个描述模式、行号或者行寻址符号的正则表达式。
- 如果没有指定地址,那么命令将应用于每一行。
- 如果只有一个地址,那么命令应用于与这个地址匹配的任意行。
- 如果指定了由逗号分隔的两个地址,那么命令应用于匹配第一个地址的第一行和它后面的行,直到匹配第二个地址的行(包括此行)。
- 如果地址后面跟有感叹号(!),那么命令就应用于不匹配该地址的所有的行。
删除所有行:
代码语言:javascript复制d
只删除第一行:
代码语言:javascript复制1d
行号指由 sed 维护的内部行号,该计数器不会因为多个输入文件而重置。因此不管指定多少个输入文件,在输入流中也只有一行 1。同样输入流也只有一个最后的行,可以使用寻址符号 $ 指定。
删除输入的最后一行:
代码语言:javascript复制$d
当正则表达式作为地址提供时,命令只影响于这个模式匹配的行。正则表达式必须封闭在斜杠(/)中。
删除空行:
代码语言:javascript复制/^$/d
如果提供两个地址,那么就指定了命令执行的行范围。
删除 .TS 开头的行,一直删到(包含).TE 开头的行:
代码语言:javascript复制/^.TS/,/^.TE/d
删除从行 50 到最后一行的所有行:
代码语言:javascript复制50,$d
可以混合使用行地址和模式地址。
删除从第一行直到第一个空行的所有行:
代码语言:javascript复制1,/^$/d
可以把第一个地址看做是启动动作,并把第二个地址看做是禁用动作。sed 没办法先行决定第二个地址是否会匹配。一旦匹配了第一个地址,这个动作就将应用于这些行,于是命令应用于所有随后的行直到第二个地址被匹配。上例中如果没有空行,那么将删除所有行。
跟在地址后面的感叹号会反转匹配的意义。
删除除了 .TS 到 .TE(实际上是提取 .TS 到 .TE) 开头的行:
代码语言:javascript复制/^.TS/,/^.TE/!d
sed 使用大括号({})将一个地址嵌套在另一个地址中,或者在相同的地址上应用多个命令。如果想指定行的范围,然后在这个范围内指定另一个地址,则可以嵌套地址。
删除 .TS 开头到(包含).TE 开头的行中的空行:
代码语言:javascript复制/^.TS/,/^.TE/{
/^$/d
}
左大括号必须在行末,而且右大括号必须独占一行。要确保在大括号后没有空格。可以使用大括号将编辑命令括起来以对某个范围的行应用多个命令。
不仅删除 .TS/.TE 块中的空行,还在块中执行两个替换:
代码语言:javascript复制/^.TS/,/^.TE/{
/^$/d
s/^.ps 10/.ps 8/
s/^.vs 12/.vs 10/
}
三、测试并保存输出
缺省 sed 将所有行送往标准输出(一般是屏幕),包括被修改的行和没有被修改的行,可以用重定向将这些输出保存到一个新文件。不要将输出重定向到输入文件,否则会改写输入文件,甚至可能在 sed 处理这个文件之前发生,并破坏数据。
代码语言:javascript复制# 将 sed 输出重定向到新文件
$ sed -f sedscr testfile > newfile
# 比较文件,验证结果
$ diff testfile newfile
1. 用于测试 sed 的 shell 脚本 testsed
代码语言:javascript复制$ cat testsed
#!/bin/bash
for x
do
sed -f sedscr $x > tmp.$x
diff $x tmp.$x
done
2. sed 永久性改动的 shell 脚本 runsed
代码语言:javascript复制$ cat runsed
#! /bin/bash
for x
do
echo "editing $x: c"
if test "$x" = sedscr; then
echo "not editing sedscript!"
elif test -s $x; then
sed -f sedscr $x > /tmp/$x$$
if test -s /tmp/$x$$
then
if cmp -s $x /tmp/$x$$
then
echo "file not changed: c"
else
mv $x $x.bak # save original, just in case
cp /tmp/$x$$ $x
fi
echo "done"
else
echo "Sed produced an empty filec"
echo " - check your sedscript."
fi
rm -f /tmp/$x$$
else
echo "original file is empty."
fi
done
echo "all done"
四、sed 脚本的四种典型应用
1. 对同一文件的多重编辑
推荐逐步编写的技术,因为将每个命令隔离开可以容易看出哪些功能实现了,哪些还没有。如果同时尝试几个命令,则在问题出现时需要按和创建命令相反的过程来结束,即一个一个地删除命令直到找到问题为止。来看下面的例子。
需求:
- 用 .LP 取代所有空行。
- 删除每行所有的前导空格。
- 删除打印机下划线的行,即以“ ”开始的行。
- 删除添加在两个单词之间的多个空格。
用部分文本逐个测试 sed 命令脚本:
代码语言:javascript复制$ cat sedscr
s/^ *$/.LP/
/^ */d
s/^ *//
s/ */ /g
s/. */. /g
$ sed -f sedscr horsefeathers
下一阶段使用 testsed 在完整的文件上测试脚本并彻底地检查结果,当对这个结果满意时,可以使用 runsed 生成永久性的改变:
代码语言:javascript复制$ runsed hf.product.bulletin
all done
2. 改变一组文件
sed 最常见的用法是对一组文件进行一系列搜索和替换的编辑操作。这样的脚本不需要有趣,只要它们有用并能节省手工工作就行。通常这些脚本只是将单词或短语变成另一种形式的替换命令列表,例如:
代码语言:javascript复制s/ON switch/START switch/g
s/ON button/START switch/g
s/STANDBY switch/STOP switch/g
s/STANDBY button/STOP switch/g
s/STANDBY/STOP/g
s/[cC]abinet [Ll]ight/control panel light/g
s/core system diskettes/core system tape/g
s/TERM=542[05] /TERM=PT200 /g
s/Teletype 542[05]/BigOne PT200/g
s/542[05] terminal/PT200 terminal/g
s/Documentation Road Map/Documentation Directory/g
s/Owner/Operator Guide/Installation and Operation Guide/g
s/AT&T 3B20 [cC]omputer/BigOne XL Computer/g
s/AT&T 3B2 [cC]omputer/BigOne XL Computer/g
s/3B2 [cC]omputer/BigOne XL Computer/g
s/3B2/BigOne XL Computer/g
一旦这个脚本通过测试,就可以使用 runsed 一次性处理多个文件。文本之间有很大的不同,不能认为一种特殊情况为真,所有情况就都为真。测试每个文件是不切实际的,因此选择有代表性且包含异常的测试文件非常重要。使用 grep 检查大量输入很有帮助。
在某些方面,编写脚本就像为给定事实的某种集合设计一个假设。通过增加测试数据来试着验证假设的合法性。如果打算在多个文件上运行该脚本,使用 testsed 首先在较小的示例上测试它,然后在许多文件上运行这个脚本。接着比较临时文件和原始文件来确认假设是否正确,有问题时修改脚本。花费在测试上的时间越多,那么在解决由拙劣脚本导致的问题上花费的时间就越少。
3. 提取文件内容
sed 应用程序的一种典型的用法是从文件中提取相关的材料,这一功能类似于 grep,而且它具有在输出之前修改输入的又一优点。
(1)提取宏定义脚本 getmac
脚本内容:
代码语言:javascript复制#! /bin/bash
# getmac -- print mm macro definition for $1
sed -n "/^.de$1/,/^..$/p" /usr/lib/macros/mmt
执行方式:
代码语言:javascript复制$ getmac BL
.deBL
.if\n(.$<1 .)L \n(Pin 0 1n 0 \*(BU
.if\n(.$=1 .LB 0\$1 0 1 0 \*(BU
.if\n(.$>1 {.ie !w^G\$1^G .)L \n(Pin 0 1n 0 \*(BU 0 1
.el.LB 0\$1 0 1 0 \*(BU 0 1 }
..
下面的 getmac 版本允许用户将宏包的名字指定为第二个命令行参数。
代码语言:javascript复制#! /bin/bash
# getmac - read macro definition for $1 from package $2
file=/usr/lib/macros/mmt
mac="$1"
case $2 in
-ms) file="/work/macros/current/tmac.s";;
-mm) file="/usr/lib/macros/mmt";;
-man) file="/usr/lib/macros/an";;
esac
sed -n "/^.de *$mac/,/^..$/p" $file
(2)生成提纲的脚本 do.outline
脚本内容:
代码语言:javascript复制sed -n '
s/"//g
s/^.Se /CHAPTER /p
s/^.Ah / A. /p
s/^.Bh / B. /p' $*
执行方式:
代码语言:javascript复制$ do.outline ch13/sect1
CHAPTER 13 Let the Computer Do the Dirty Work
A. Shell Programming
B. Stored Commands
B. Passing Arguments to Shell Scripts
B. Conditional Execution
B. Discarding Used Arguments
B. Repetitive Execution
B. Setting Default Values
B. What We've Accomplished
do.outline 对在命令行上指定的所有文件($*)起作用。可以修改这个脚本以搜索任意种类的编码格式。例如:
代码语言:javascript复制sed -n '
s/[{}]//g
s/\section/ A. /p
s/\subsection/ B. /p' $*
4. 编辑工作转移
在管道中进行编辑操作是 sed 作为真正的流编辑器的一个应用,这些编辑操作不会被写回到文件中。下面的例子是用脚本 format 将输入转换为 troff 能够处理的文本,具体是用 sed 处理输入,将一对连字符(--)替换为 troff 的 “(em”。
脚本 format 内容如下:
代码语言:javascript复制#! /bin/bash
eqn= pic= col=
files= options= roff="ditroff -Tps"
sed="| sed '/---/!s/--/\(em/g'"
while [ $# -gt 0 ]
do
case $1 in
-E) eqn="| eqn";;
-P) pic="| pic";;
-N) roff="nroff" col="| col" sed= ;;
-*) options="$options $1";;
*) if [ -f $1 ]
then files="$files $1"
else echo "format: $1: file not found"; exit 1
fi;;
esac
shift
done
eval "cat $files $sed | tbl $eqn $pic | $roff $options $col | lp"
当对一个文档排版时,将连字符换成长破折号不是唯一要做的“美化”工作,因为有许多涉及到标点符号、空格和制表符等各种情况。脚本可能看上去如下所示:
代码语言:javascript复制s/^"/``/
s/"$/''/
s/"? /''? /g
s/"?$/''?/g
s/ "/ ``/g
s/" /'' /g
s/ "/ ``/g
s/" /'' /g
s/")/'')/g
s/"]/'']/g
s/("/(``/g
s/["/[``/g
s/";/'';/g
s/":/'':/g
s/,"/,''/g
s/",/'',/g
s/."/.\&''/g
s/"./''.\&/g
s/\(em\^"/\(em``/g
s/"\(em/''\(em/g
s/\(em"/\(em``/g
s/@DQ@/"/g
五、几个使用 sed 脚本的提示
- 设计脚本前使用 grep 仔细检查输入文件,充分了解输入。
- 从测试文件中的小示例开始。在示例上运行脚本并且确信脚本能正常工作。记住,确保脚本在不想让它工作的地方不能工作同样重要。然后增加示例的规模,试着增加输入的复杂性。
- 仔细测试脚本中的每个命令,比较输入和输出文件看看发生了什么变化,亲自证明脚本是完整的。确认在输入文件正确的前提下,脚本可以正确地工作,而不仅仅是认为可以。
- 尝试用 sed 脚本完成工作,但不必100%。遇到困难时检查它们发生的频繁程度,有时手动来完成剩下的几个编辑工作比较好。