给定样例的文本如下:
123456 ...
1. asdfasdf
2. asdfasdf
3. asdfasdf
654321
4. asdfasdf
|
其中以123456 和 以654321 开头的行是两个特殊行, 只在文件中各出现一次, 但前后次序不确定, 中间也可能杂以其它的文本行. 要求将以654321开头的行移到123456开头的行下面. 其它文本行不变.
方案1: ed
echo $'g/^654321/m/^12456/\nwq' | ed tmp.txt |
或者是
ed tmp.txt <<<$'g/^654321/m/^123456/\nwq' |
或者是
ed tmp.txt < <(echo $'g/^654321/m/^123456/\nwq') |
这几个命令的实体是一样的, 只是构造命令行的方式不一样. 都用到了bash的$'...' 表示法, 出现在其中的字符串跟C语言中双引号中的escape序列的解释是一样的. 我个人觉得这个是非常方便的, 因为你可以以一种可读的形式构造几乎任意的数据了, 说几乎是因为在某些情况下, ASCII为0 的是个特殊数据, 你看不见.比如:
echo -n $'a\000b' |xxd -g1 |
就只能看到前面的a, 这是因为shell无法把0和它后面的数据通过命令行参数传递给外部命令.
不过这里不受影响.
值得一提的还有 <<< 这种形式, 这是一种就地提供标准输入的特殊语法. 不需要管道, 不需要把字串先存入文件.
< <(...) 当然也不是笔误, 第一个<和后面的<(之间至少要有一个空白字符, <(...) 是执行()中的命令, 并把命令的输出另存为一个临时文件名, 临时文件的名字会被bash传递给被调程序, 而命令结束后bash会把该文件删除. 这个方法的弊病就在于我在cygwin下面没办法作为一种通用的规则来用, 因为它生成的文件名是形如 /d/tmp/filexxx 这样的unix风格的, cygwin一家子的程序自然没问题, 但WINDOWS的原住民程序就不能用fopen打开这种形式的文件名了.
方案2: grep + sed
{ sed '/^654321/d;/^123456/q' tmp.txt ; grep '^654321' tmp.txt; sed '0,/^123456/d; /^654321/d' tmp.txt; } > result.txt
|
当然这是三个命令了, 但只有把它们写在同一行上才显得象是一种对该问题的解决方案, 因为这样比较方便重定向输出结果.
第一个sed 输出123456及它前面的行, 如果在此之前就碰到了654321也要删除它.
第二个grep当然是做好本职工作, 只输出以654321开头的那一行.
第三个sed 输出123456开头的行之后的内容, 当然以654321开头的行除外.
方案3: grep + cat + sed
{ grep '^654321' tmp.txt; cat tmp.txt; } | sed '1h;/^123456/{x;H;g;p;d;};/^654321/d' |
这里又show了一下另一个bash的技巧, 用{ } 在当前bash的实例中分组打包多个命令, 好处是它们的输出可以作为一个整体通过管道或重定向送到流水线的下一个环节上. 开个小差, 这有点类似于C语言中用()实现的优先级:
在C语言中, 3 + 2 * 4 结果是2与4相乘, 结果与3相加, 但(3+2)*4 可以让3和2都被4乘到.
echo yes; echo no > ss.txt |
这样你会在屏幕上看到一个yes, 在ss.txt中得到一个 no
而
{ echo yes; echo no; } > ss.txt |
则让yes和no尽收ss.txt囊中. 用{}分组命令注意两个事项: 首先是{之后的空格不可少, 而}之前的空格则不需要, 但是疑问加上的话可以提供清晰性. 2 最后一个命令后面仍然需要一个分号结束, 也就是}之前的那个位置.
回到正题, 这个方案的想法是总是把654321开头的行调到第一行, 然后合并原来文件的内容一起送给管道线后面的sed 命令来处理, 此时文件的内容是
654321 123456 ...
1. asdfasdf
2. asdfasdf
3. asdfasdf
654321
4. asdfasdf
|
sed 的命令1h 是把第一行放入内部的一个交换空间, 放到交换空间之后又因为它符合第三个sed操作的条件, 被删除掉, 所以该行不输出. 分号用来分隔多个命令, 正如bash在命令行上的分号一样, 这使得你不必在命令行上用多个-e exp1 -e exp2, 因为sed规定多于一个表达式作为参数时必需使用-e.
接下来看第二个sed 操作/123456/{x;H;g;p;d;} 它是碰到123456开头的行时, 执行{}里面的子操作, 这些子操作都是在匹配/^123456/时要依次执行的动作, x是把原来保存到sed内部交换空间中的654321开头的行与当前匹配到的模式^123456互相交换, 即让以654321开头的行变成当前匹配模式空间中的内容, 让123456开头的行变成sed内部交换空间的内容, 然后下一个H子操作是把当前匹配模式空间中的内容即654321开头的行追加(注意小写的h是直接放入, 原来的内容就被覆盖掉了)到123456开头的行之后, 看不见的变幻如下:
/^123456/匹配之后==>
当前匹配到的模式空间
123456 ...
| 内部交换空间 654321
|
x子操作之后==>
当前匹配到的模式空间
654321
| 内部交换空间 123456 ...
|
H子操作之后==>
当前匹配到的模式空间
654321
| 内部交换空间 123456 ...
654321
|
g子操作之后==>
当前匹配到的模式空间
123456 ...
654321
| 内部交换空间 123456 ...
654321
|
p子操作, 将123456...和 654321两行的内容都输出, 它之后还有一个d命令, 用处是把它从匹配模式空间中删除, 否则它还会被再输出一次, 因为sed在没有-n参数时的规则是每行都会输出一次, 通过p操作输出的只是额外的输出, 并不会因此抵制对每行的例行公事的输出.
然后是下一个sed 子命令/^654321/d, 其作用是把tmp.txt里面原来的以654321开头的行删掉, 不要输出它.
其它行照样输出, 最后, 结果就是:
654321
123456 ...
1. asdfasdf
2. asdfasdf
3. asdfasdf
4. asdfasdf
|
方案4: grep + bash
line="$(grep '^654321' tmp.txt)" while read i do [ "${i:0:6}" = "654321" ] && continue ; echo $i; [ "${i:0:6}" = "123456" ] && echo $line done < tmp.txt
|
办法是先把 654321开头的行保存起来, 然后逐行读入, 碰到以123456开头的行时附带着输出该行, 而碰到该行本身时则不输出. 用到的是bash的substring 方法: ${i:start_index:length}. 虽然看上去乱了点, 其实想法很简单. 同样有一些注意事项: [之后和]之前要有空格, []中的变量测试要用双引号括起来.
结论: bash 和Linux下的工具是强大和灵活的, 不止一种办法来完成你的任务, 但缺点也是, 强大和灵活的代价, 你要花时间熟悉这些工具, 它们并不总是优雅的, 有些地方甚至是暗礁丛生无比险恶. 没有好的办法, 只能去了解游戏规则, 积累你的技巧, 进而, 把这些过程记录下来, 利人利己, 象我这样. 第一篇终于写完了, 好累.