Unix 一行到 swap/transpose 多个文本文件中的两行?

Unix one-liner to swap/transpose two lines in multiple text files?

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

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

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

如何扩展它以处理多个文件?欢迎任何单线解决方案。

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

#!/bin/sh
sed -n "1,$((-1))p" ""
sed -n "p" ""
sed -n "$((+1)),$((-1))p" ""
sed -n "p" ""
sed -n "$((+1)),$p" ""

下一个

sh swap.sh infile_name 14 26 > outfile_name

如果要交换的行号是固定的,那么您可能想在以下示例中尝试类似 sed 命令的操作,以便在多个文件中就地交换行:

#!/bin/bash

# prep test files
for f in a b c ; 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 的评论。)

这可能适合您 (GNU sed):

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

这会在 hold space 中存储一系列行,然后在范围完成后交换第一行和最后一行。

i 标志编辑每个文件 (f1,f2 ... fn)。

使用以下帮助脚本允许使用 find ... -exec ./script '{}' l1 l2 \; 的强大功能来定位目标文件并在每个文件中交换 l1l2 行。 (它要求文件中没有 相同的重复行 行落在搜索范围内)该脚本使用 sed 从每个文件中读取两个交换行到一个索引array 并将行传递给 sed 以通过匹配完成交换。 sed 调用使用其 "matched first address" 状态 将第二个表达式交换限制为第一次出现。使用下面的帮助脚本在所有匹配文件中交换行 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  ] && {              # 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 "" && sed -n $((l2))p "")"

((${#a[@]} < 2)) && {       # validate 2 lines read
    printf "error: requested lines '%d & %d' not found in file '%s'\n" $l1 $l2 ""
    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]}/" ""

exit 0

尽管我没能在 单行本 中完成所有工作,但我认为它值得发布,以防您可以使用它或采纳想法从中。 注意: 如果您确实要使用它,请先测试您是否满意,然后再将其用于您的系统。该脚本目前使用 sed -i.bak ... 为测试目的更改的文件创建备份。当您满意它满足您的需要时,您可以删除 .bak

如果您在帮助程序脚本本身中设置默认行交换没有用,那么我会将第一个验证检查更改为 [ -z -o -z -o ] 以确保 all 需要调用脚本时给出参数。

虽然它通过数字识别要交换的行,但它依赖于每一行的直接匹配来完成交换.这意味着任何 相同的副本 直到交换范围末尾的行都将导致意外匹配并且无法交换预期的行。这是由于不将每一行都存储在注释中讨论的要交换的行范围内而施加的限制的一部分。这是一个权衡。有很多很多方法可以解决这个问题,所有方法都有其优点和缺点。如果您有任何问题,请告诉我。


蛮力法

根据您的评论,我修改了帮助程序脚本以使用粗暴的 copy/swap 方法来消除搜索范围内任何重复行的问题。这个助手通过 sed 获取行,就像在原来的那样,但是然后读取从 filetmpfile 的所有行,在遇到时交换适当编号的行。 tmpfile填充后复制到原来的file,去掉tmpfile

#!/bin/bash

[ -z  ] && {              # 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 "" && sed -n $((l2))p "")"

((${#a[@]} < 2)) && {       # validate 2 lines read
    printf "error: requested lines '%d & %d' not found in file '%s'\n" $l1 $l2 ""
    exit 1
}

                            # create tmpfile, set trap, truncate
fn=""
rmtemp () { cp "$tmpfn" "$fn"; rm -f "$tmpfn"; }
trap rmtemp SIGTERM SIGINT EXIT

declare -i n=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

如果要编辑文件,可以使用标准编辑器eded:

你的任务相当简单
printf '%s\n' 14m26 26-m14- w q | ed -s file

它是如何工作的?

  • 14m26 告诉 ed 将第 14 行移到第 26 行之后
  • 26-m14- 告诉 ed 取第 26 行之前的行(这是你原来的第 26 行)并将它移到第 14 行之前的行之后(这是你的第 14 行原来是)
  • w 告诉 ed 写入文件
  • q 告诉 ed 退出。

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

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

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

如果你想交换两行,你可以发送两次,如果你真的想要的话,你可以让它在一个 sed 脚本中循环,但这行得通:

例如

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

然后交换 69 行:

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 行建立了保留 space。 在第 9 行,它打印第 9 行,然后打印 hold space(第 6 到 8 行),这完成了 9 到 6 的第一次移动。注意:6h; 6!H 避免在模式顶部换行space。

第二步发生在第二个 sed 脚本中,它将第 7 行保存到保留 space,然后将其删除并在第 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}'

其中 AB 是您要交换的行,在本例中是第 3 行和第 7 行。

使用 GNU awk:

awk '
FNR==NR {if(FNR==14) x=[=10=];if(FNR==26) y=[=10=];next} 
FNR==14 {[=10=]=y} FNR==26 {[=10=]=x} {print}
' file file > file_with_swap
  • 如果您想要 稳健 就地更新 输入文件,请使用

  • 如果你有 GNU sed 并且想 一次更新多个个文件,使用
    (请参阅下面的便携式替代方案,以及底部的解释)

注意:ed 真正更新 现有 文件 sed' s -i 选项在幕后创建一个临时文件,然后 替换 原来的 - 虽然通常不是问题,但这个 可以有不良副作用,最值得注意的是,用常规文件替换符号链接(相比之下,文件权限得到正确保留)。

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


Stdin/stdout处​​理,基于:

  • POSIX sed 不支持 -i 就地更新。
  • 它也不支持在字符 class 中使用 \n,所以 [^\n] 必须用一个繁琐的解决方法来替换 肯定 定义除 \n 之外的所有字符,它们可以出现在一行中 - 这是通过字符 class 将可打印字符与除 \n 之外的所有(ASCII)控制字符组合在一起作为文字(通过使用 printf).
  • 的命令替换
  • 还要注意需要将 sed 脚本分成两个 -e 选项,因为 POSIX sed 需要一个分支命令 (b,在这种情况下)以实际的换行符或在单独的 -e 选项中继续。
# SYNOPSIS
#   swapLines lineNum1 lineNum2
swapLines() {
  [ "" -ge 1 ] || { printf "ARGUMENT ERROR: Line numbers must be decimal integers >= 1.\n" >&2; return 2; }
  [ "" -le "" ] || { printf "ARGUMENT ERROR: The first line number () must be <= the second ().\n" >&2; return 2; }
  sed -e ""','""'!b' -e ''""'h;'""'!H;'""'!d;x;s/^\([[:print:]'"$(printf '[=10=]1[=10=]2[=10=]3[=10=]4[=10=]5[=10=]6[=10=]7013456701234567012345677')"']*\)\(.*\n\)\(.*\)//'
}

示例:

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

就地更新,基于

小注意事项:

  • 虽然 ed is a POSIX utility,但它并未预装在 所有 平台上,特别是 Debian 和 [=236= 的 Cygwin 和 MSYS Unix 仿真环境中没有].
  • ed 始终将输入文件 作为一个整体 读入内存。
# SYNOPSIS
#   swapFileLines lineNum1 lineNum2 file
swapFileLines() {
  [ "" -ge 1 ] || { printf "ARGUMENT ERROR: Line numbers must be decimal integers >= 1.\n" >&2; return 2; }
  [ "" -le "" ] || { printf "ARGUMENT ERROR: The first line number () must be <= the second ().\n" >&2; return 2; }
  ed -s "" <<EOF
H
m
-m-
w
EOF
}

示例:

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

解释:

他的命令交换了第 10 行和第 15 行:

sed -ri '10,15!b;10h;10!H;15!d;x;s/^([^\n]*)(.*\n)(.*)//' f1 f2 fn
  • -r 激活对 扩展 正则表达式的支持;在这里,值得注意的是,它允许使用 unescaped 括号来形成 capture groups.
  • -i指定指定为操作数的文件(f1f2fn)就地更新 无备份,因为备份文件的可选后缀与-i选项相连。

  • 10,15!b表示所有做!)的行都在10行范围内through 15 应该隐含地分支 (b) 到脚本的 末尾 (假设 b 后面没有目标标签名称),这意味着对于这些行,以下命令被 跳过 。实际上,它们只是 按原样打印

  • 10h 复制 (h) 行号 10 (范围的开始)到所谓的 hold space,这是一个辅助缓冲区。
  • 10!H appends (H) 每行 not line 10 -在这种情况下,这意味着 1115 行 - 到货舱 space.
  • 15!d 删除 (d) 不是 行的每一行 15 (此处,行 1014) 并分支到脚本末尾(跳过其余命令)。通过删除这些行,它们不会打印出来。
  • x,只对行15(范围的末尾)执行,取代了所谓的模式space 与保持 space 的内容一起保存范围内的所有行(1015);模式 space 是 sed 命令运行的缓冲区,默认打印其内容(除非指定 -n)。
  • s/^([^\n]*)(.*\n)(.*)// 然后使用捕获组(构成传递给函数 s 的第一个参数的正则表达式的带括号的子表达式)将模式 space 的内容划分为第一个行 (^([^\n]*))、中间行 ((.*\n)) 和最后一行 ((.*)),然后在替换字符串中(传递给函数的第二个参数 s), 使用 backreferences 将最后一行 (</code>) 放在中间行 (<code>) 之前,然后是第一行 ( ), 有效地交换范围内的第一行和最后一行。最后,打印修改后的模式space。

如您所见,只有跨越要交换的两行的 range 行保留在内存中,而所有其他行都是单独传递的,这使得这种方法内存-高效。