Unix单线程交换/转置多个文本文件中的两行?

我希望使用UNIX工具(如sedawk)根据行号(例如,切换第10行和第15行的位置)在多个文本文件中交换或转换行对。

例如,我相信这个sed命令应该在一个文件中交换第14行和第26行:

sed -n '14p' infile_name > outfile_name sed -n '26p' infile_name >> outfile_name 

这怎么能扩展到多个文件? 任何一线解决scheme的欢迎。

这可能适用于你(GNU sed):

 sed -ri '10,15!b;10h;10!H;15!d;x;s/^([^\n]*)(.*\n)(.*)/\3\2\1/' f1 f2 fn 

这将在存储空间中存储一系列行,然后在完成范围之后交换第一行和最后一行。

i标志编辑每个文件( f1f2fn )。

如果你想编辑一个文件,你可以使用ed ,标准的编辑器。 你的任务在ed相当简单:

 printf '%s\n' 14m26 26-m14- wq | ed -s file 

它是如何工作的?

  • 14m26告诉ed线#14,并在#26线之后移动
  • 26-m14-告诉我们在第26行(这是你原来的第26行)前面的一行,并把它移到第14行的前面(这是你的第14行原来的位置)
  • w告诉编写这个文件
  • q告诉ed退出。

如果你的数字在一个变量中,你可以这样做:

 linea=14 lineb=26 { printf '%dm%d\n' "$linea" "$lineb" printf '%dm%d-\n' "$lineb" "$linea" printf '%s\n' wq } | ed -s file 

或类似的东西。 确保linea<lineb

  • 如果您想要对输入文件进行稳健的就地更新 ,请使用gniourf_gniourf出色的基于ed的答案

  • 如果你有GNU sed并且希望一次使用多个文件进行就地更新 ,请使用
    @ potong基于GNU sed的出色答案 (请参阅下面的便携式替代方法,底部为解释)

注意: ed真的更新了现有的文件sed-i选项在后台创建了一个临时文件,然后替换原来的文件 – 通常不是问题,这可能会产生不希望的副作用 ,最值得注意的是用一个普通的文件(相反,文件权限被正确保存)。

下面是兼容POSIX的shell函数 ,它们包含了两个答案


stdin / stdout处理 ,基于@potong的优秀答案 :

  • POSIX sed不支持-i进行就地更新。
  • 它也不支持在字符类中使用\n ,所以[^\n]必须被一个繁琐的解决方法所取代,该解决方法肯定地定义了除\n之外的所有字符,这可以在一行中发生 – 这是通过字符类实现的将可打印字符与除文本之外的所有(ASCII)控制字符组合在一起(通过使用printf命令替换)。
  • 还要注意,需要将sed脚本分成两个-e选项,因为POSIX sed要求分支命令( b ,在这种情况下)以单独的-e选项中的实际换行符或继续来终止。
 # SYNOPSIS # swapLines lineNum1 lineNum2 swapLines() { [ "$1" -ge 1 ] || { printf "ARGUMENT ERROR: Line numbers must be decimal integers >= 1.\n" >&2; return 2; } [ "$1" -le "$2" ] || { printf "ARGUMENT ERROR: The first line number ($1) must be <= the second ($2).\n" >&2; return 2; } sed -e "$1"','"$2"'!b' -e ''"$1"'h;'"$1"'!H;'"$2"'!d;x;s/^\([[:print:]'"$(printf '\001\002\003\004\005\006\007\010\011\013\014\015\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037\177')"']*\)\(.*\n\)\(.*\)/\3\2\1/' } 

例:

 $ printf 'line 1\nline 2\nline 3\n' | swapLines 1 3 line 3 line 2 line 1 

就地更新 ,基于gniourf_gniourf的出色答案 :

小警告:

  • 尽管ed是一个POSIX实用程序 ,但并不是预装在所有平台上,特别是在Debian和Windows下的Cygwin和MSYS Unix仿真环境上。
  • ed总是将输入文件作为一个整体读入内存。
 # SYNOPSIS # swapFileLines lineNum1 lineNum2 file swapFileLines() { [ "$1" -ge 1 ] || { printf "ARGUMENT ERROR: Line numbers must be decimal integers >= 1.\n" >&2; return 2; } [ "$1" -le "$2" ] || { printf "ARGUMENT ERROR: The first line number ($1) must be <= the second ($2).\n" >&2; return 2; } ed -s "$3" <<EOF H $1m$2 $2-m$1- w EOF } 

例:

 $ printf 'line 1\nline 2\nline 3\n' > file $ swapFileLines 1 3 file $ cat file line 3 line 2 line 1 

@ potong的基于GNU sed的答案的解释

他的指挥掉线10和15:

 sed -ri '10,15!b;10h;10!H;15!d;x;s/^([^\n]*)(.*\n)(.*)/\3\2\1/' f1 f2 fn 
  • -r激活对扩展正则表达式的支持; 在这里,值得注意的是,它允许使用未转义的括号形成捕获组
  • -i指定指定为操作数( f1f2fn )的文件 没有备份的情况下 就地更新,因为没有用于备份文件的可选后缀连接到-i选项。

  • 10,15!b意味着所有不属于! )的行落入第10行至第15行的范围内,应该将( b )隐式地分支到脚本末尾 (假设在b没有目标标签名称),这意味着以下命令将跳过这些行。 实际上,它们是按原样打印的

  • 10h复制( h )行号10 (范围的开始)到所谓的保持空间 ,这是一个辅助缓冲器。
  • 10!H 不是10行的每一行(在这种情况下表示第11行到第15行) 附加H )到保持空间。
  • d删除 不是15行(这里是第10行到第14行)的每一行,并跳转到脚本的末尾(跳过剩下的命令)。 通过删除这些行,它们不被打印。
  • x仅在第15行(范围的末尾)执行,用所保存的空间的内容替换所谓的模式空间 ,此时保存范围( 1015 )中的所有行; 模式空间是sed命令操作的缓冲区,其内容默认打印(除非指定了-n )。
  • 然后使用捕获组(形成传递给函数s的第一个参数的正则表达式的带括号的子表达式)将模式空间的内容分割为第一行( ^([^\n]*) ),中间行( (.*\n) )和最后一行( (.*) ),然后,在替换字符串(第二个参数传递给函数s )中,使用反向引用将最后一行( \3 )放在中间行( \2 )之前,接着是第一行( \1 ),有效地交换第一行和最后一行在范围内的线。 最后,打印修改的图案空间。

正如你所看到的,只有跨越交换的两行的行的范围被保存在内存中,而所有其他的行被单独地传递,这使得这种方法具有内存效率。

用GNU awk:

 awk ' FNR==NR {if(FNR==14) x=$0;if(FNR==26) y=$0;next} FNR==14 {$0=y} FNR==26 {$0=x} {print} ' file file > file_with_swap 

使用以下帮助程序脚本可以使用find ... -exec ./script '{}' l1 l2 \; 找到目标文件并在每个文件中交换行l1l2 。 (它要求文件中没有相同的重复行,属于搜索范围)脚本使用sed将每个文件的两个交换行读入索引数组,并将行传递给sed以通过匹配完成交换。 sed调用使用其“匹配的第一个地址”状态来将第二个表达式交换限制在第一个匹配项。 在下面的帮助程序脚本中用来交换所有匹配文件中第5行和第15行的示例如下:

 find . -maxdepth 1 -type f -name "lnum*" -exec ../swaplines.sh '{}' 5 15 \; 

例如,上面的find调用在当前目录中发现了文件lnumorig.txtlnumfile.txt ,最初包含:

 $ head -n20 lnumfile.txt.bak 1 A simple line of test in a text file. 2 A simple line of test in a text file. 3 A simple line of test in a text file. 4 A simple line of test in a text file. 5 A simple line of test in a text file. 6 A simple line of test in a text file. <snip> 14 A simple line of test in a text file. 15 A simple line of test in a text file. 16 A simple line of test in a text file. 17 A simple line of test in a text file. 18 A simple line of test in a text file. 19 A simple line of test in a text file. 20 A simple line of test in a text file. 

并按照预期交换了515行:

 $ head -n20 lnumfile.txt 1 A simple line of test in a text file. 2 A simple line of test in a text file. 3 A simple line of test in a text file. 4 A simple line of test in a text file. 15 A simple line of test in a text file. 6 A simple line of test in a text file. <snip> 14 A simple line of test in a text file. 5 A simple line of test in a text file. 16 A simple line of test in a text file. 17 A simple line of test in a text file. 18 A simple line of test in a text file. 19 A simple line of test in a text file. 20 A simple line of test in a text file. 

帮手脚本本身是:

 #!/bin/bash [ -z $1 ] && { # validate requierd input (defaults set below) printf "error: insufficient input calling '%s'. usage: file [line1 line2]\n" "${0//*\//}" 1>&2 exit 1 } l1=${2:-10} # default/initialize line numbers to swap l2=${3:-15} while IFS=$'\n' read -r line; do # read lines to swap into indexed array a+=( "$line" ); done <<<"$(sed -n $((l1))p "$1" && sed -n $((l2))p "$1")" ((${#a[@]} < 2)) && { # validate 2 lines read printf "error: requested lines '%d & %d' not found in file '%s'\n" $l1 $l2 "$1" exit 1 } # swap lines in place with sed (remove .bak for no backups) sed -i.bak -e "s/${a[1]}/${a[0]}/" -e "0,/${a[0]}/s/${a[0]}/${a[1]}/" "$1" exit 0 

即使我没有设法将这一切完成在一个班轮,我决定值得张贴,以防万一你可以使用它或从中采取的想法。 注意:如果您确实使用了它,请在您的系统上进行松动之前测试您的满意度。 该脚本目前使用sed -i.bak ...来创建为了测试而更改的文件的备份。 当您满意时,您可以删除.bak ,以满足您的需求。

如果你没有用helper脚本来设置默认行来交换,那么我会把第一个验证检查改成[ -z $1 -o -z $2 -o $3 ]以确保当脚本是调用。

虽然确定了要交换的行号 ,但它依靠每行的直接匹配来完成交换。 这意味着任何相同的重复行直到交换范围的末尾都会导致意外的匹配和失败交换预期的行。 这是限制的一部分,因为不按照注释中所讨论的将每一行存储在要交换的行的范围内。 这是一个折衷。 有很多方法可以解决这个问题,所有的方法都有其优点和缺点。 如果您有任何问题,请告诉我。


蛮力法

根据你的评论,我修改了助手脚本,使用brute的复制/交换方法,可以消除搜索范围内任何重复行的问题。 这个助手像原来一样通过sed获得行,但是当遇到时读取filetmpfile所有行,交换适当编号的行。 在tmpfile被填充之后,它被复制到原始file并且tmpfile被删除。

 #!/bin/bash [ -z $1 ] && { # validate requierd input (defaults set below) printf "error: insufficient input calling '%s'. usage: file [line1 line2]\n" "${0//*\//}" 1>&2 exit 1 } l1=${2:-10} # default/initialize line numbers to swap l2=${3:-15} while IFS=$'\n' read -r line; do # read lines to swap into indexed array a+=( "$line" ); done <<<"$(sed -n $((l1))p "$1" && sed -n $((l2))p "$1")" ((${#a[@]} < 2)) && { # validate 2 lines read printf "error: requested lines '%d & %d' not found in file '%s'\n" $l1 $l2 "$1" exit 1 } # create tmpfile, set trap, truncate fn="$1" rmtemp () { cp "$tmpfn" "$fn"; rm -f "$tmpfn"; } trap rmtemp SIGTERM SIGINT EXIT declare -in=1 tmpfn="$(mktemp swap_XXX)" :> "$tmpfn" # swap lines in place with a tmpfile while IFS=$'\n' read -r line; do if ((n == l1)); then printf "%s\n" "${a[1]}" >> "$tmpfn" elif ((n == l2)); then printf "%s\n" "${a[0]}" >> "$tmpfn" else printf "%s\n" "$line" >> "$tmpfn" fi ((n++)) done < "$fn" exit 0 

如果要交换的行号是固定的,那么你可能想在下面的例子中尝试类似sed命令,让行在多个文件中就地交换:

 #!/bin/bash # prep test files for f in abc ; do ( for i in {1..30} ; do echo $f$i ; done ) > /tmp/$f done sed -i -s -e '14 {h;d}' -e '15 {N;N;N;N;N;N;N;N;N;N;G;x;d}' -e '26 G' /tmp/{a,b,c} # -i: inplace editing # -s: treat each input file separately # 14 {h;d} # first swap line: hold ; suppress # 15 {N;N;...;G;x;d} # lines between: collect, append held line; hold result; suppress # 26 G # second swap line: append held lines (and output them all) # dump test files cat /tmp/{a,b,c} 

(这是根据Etan Reisner的评论。)

如果你想交换两行,你可以通过两次发送,如果你真的想要,可以在一个sed脚本中循环,但是这个工作:

例如

test.txt: for a in {1..10}; do echo "this is line $a"; done >> test.txt for a in {1..10}; do echo "this is line $a"; done >> test.txt

 this is line 1 this is line 2 this is line 3 this is line 4 this is line 5 this is line 6 this is line 7 this is line 8 this is line 9 this is line 10 

然后交换第6和第9

sed ':a;6,8{6h;6!H;d;ba};9{p;x};' test.txt | sed '7{h;d};9{p;x}'

 this is line 1 this is line 2 this is line 3 this is line 4 this is line 5 this is line 9 this is line 7 this is line 8 this is line 6 this is line 10 

在第一个sed ,使用第6行到第8行构建保留空间。在第9行打印第9行,然后打印保存空间(第6行到第8行),这样就完成了第一次移动9到第六个地方6h; 6!H 6h; 6!H避免了在图案空间顶部的一条新线。

第二步是在第二个sed脚本中将行7保存到保留空间,然后删除它并在第9行之后将其打印出来。

为了使它类似泛型,可以使用如下变量: A=3 && B=7 && sed ':a;'${A}','$((${B}-1))'{'${A}'h;'${A}'!H;d;ba};'${B}'{p;x};' test.txt | sed $(($A+1))'{h;d};'${B}'{p;x}' A=3 && B=7 && sed ':a;'${A}','$((${B}-1))'{'${A}'h;'${A}'!H;d;ba};'${B}'{p;x};' test.txt | sed $(($A+1))'{h;d};'${B}'{p;x}'

AB是你想交换的行,在这种情况下是第3行和第7行。

如果你想交换两行,创建脚本“swap.sh”

 #!/bin/sh sed -n "1,$((${2}-1))p" "$1" sed -n "${3}p" "$1" sed -n "$((${2}+1)),$((${3}-1))p" "$1" sed -n "${2}p" "$1" sed -n "$((${3}+1)),\$p" "$1" 

下一个

 sh swap.sh infile_name 14 26 > outfile_name