【问题标题】:Unix one-liner to swap/transpose two lines in multiple text files?Unix单行在多个文本文件中交换/转置两行?
【发布时间】:2015-04-24 17:21:45
【问题描述】:

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

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

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

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

【问题讨论】:

  • 要在一个步骤中就地执行此操作,需要存储第一行的内容并存储每个后续行的内容,直到目标行,然后打印目标行,中间行然后是原始行,然后只打印文件的其余内容。 (另外,不,这两个 sed 命令不会交换这两行。它们只会将第一行和第二行打印到outfile_name,并且对infile_nameoutfile_name 不执行任何操作,而outfile_name 将只包含这两行。 ) awk 可以为所欲为;使用sed 会更难。

标签: bash unix awk sed text-files


【解决方案1】:

如果要编辑文件,可以使用标准编辑器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

【讨论】:

  • 好东西; ed 没有得到足够的爱。 ed is a POSIX utility 并且似乎出现在 大多数 平台上,但我注意到它没有预装在 Debian 上。同样,运行 Cygwin 或 MSYS 的 Windows 用户也不走运。有人知道其他未预装的类 Unix 平台吗?
  • 嗨 mklement0。我刚刚安装了一个新的 Debian 8.0,事实上,ed 没有默认安装...
  • 感谢您的检查。这很奇怪也很不幸,因为ed 通常非常方便(正如您在此处演示的那样),但尤其是在您真正想要编辑 existing 文件的情况下(而不是用新创建的同名文件替换它,这会破坏符号链接)。我在 Windows 上对 Cygwin 和 MSYS 所说的后续内容:ed is 提供,但仅作为 option (编辑器包),而 MSYS 没有提供它。我还听说在某些平台上(不知道是哪个)ed 是另一个编辑器的符号链接,例如vim
  • 漂亮的答案,我新出来的应该是这样的实用程序。
【解决方案2】:

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

以下是符合 POSIX 的 shell 函数,它们包装两个答案


标准输入/标准输出处理,基于@potong's excellent answer

  • POSIX sed 不支持 -i 就地更新。
  • 它也不支持在字符类中使用\n,因此必须将[^\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's excellent answer

小提示:

  • 虽然ed is a POSIX utility,但它并未预装在所有 平台上,尤其是在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's GNU sed-based answer的解释

他的命令交换了第 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)被更新就地无需备份 ,因为 no 备份文件的可选后缀与 -i 选项相邻。

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

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

如您所见,只有跨越两行要交换的 范围 行保存在内存中,而所有其他行都是单独传递的,这使得这种方法更节省内存。 p>

【讨论】:

    【解决方案3】:

    这可能对你有用(GNU sed):

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

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

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

    【讨论】:

    • 几乎完美——但如果我想交换第 1 行和第 2 行怎么办?在那种情况下它似乎不起作用。
    • 糟糕,错误地输入了换行符。编辑了解决方案,因此适用于连续行。
    • @mklement0 默认情况下,i 标志还调用s 标志,它将每个新文件视为一个单独的文件,并且每个文件的任何地址都是唯一的。所以这确实适用于多个文件。
    • 干得好,我昨天投了赞成票,但我没有时间发表评论 - 你唯一可以使用更多的是解释。听你多提一点逻辑会很有用。
    • 例如首先只对转置空间进行操作10,15!b 表示如果它不是第 10-15 行,则在其余逻辑上进行分支,在转置行范围内建立一个保持空间模式窗口,转置范围之间的行被删除而不是打印,当到达转置范围的最后一行时,打印该行(因为它将是新的第一行),然后将保持空间交换x 到模式空间,然后替换 s 交换第一行以及使用换行符的正则表达式保存的转置空间的最后一行。
    【解决方案4】:

    使用 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
    

    【讨论】:

    • 很好——比@potong 的sed 命令更容易理解,但代价是必须两次读取文件;您可以通过将行号作为变量传递来轻松概括这一点(例如,-v l1=14 -v l2=26)。您实际上没有使用任何特定于 GNU awk 的功能,因此这应该适用于 any POSIX 兼容的 awk 实现。
    • @mklement0 谢谢,我不确定是否所有常见的 AWK 都能做到。但我实际上更喜欢 gniourf_gniourf 的 ed 解决方案!
    • 同意 - 这是唯一真正的 文件 编辑解决方案,它比公认的答案更有效,并且在概念上更清晰。不过,公认的答案有两个优点:它可以一次处理多个文件,而且内存效率更高。
    【解决方案5】:

    使用以下帮助脚本允许使用find ... -exec ./script '{}' l1 l2 \; 的强大功能来定位目标文件并在每个文件中交换行l1l2。 (它要求文件中没有相同的重复行在搜索范围内)该脚本使用sed 将每个文件中的两个交换行读取到索引数组中并传递这些行到sed通过匹配完成交换。 sed 调用使用其“匹配的第一个地址”状态 将第二个表达式交换限制为第一次出现。下面是使用帮助程序脚本在所有匹配文件中交换行 515 的示例:

    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

    如果您不需要设置默认行来交换帮助程序脚本本身,那么我会将第一个验证检查更改为 [ -z $1 -o -z $2 -o $3 ] 以确保在脚本运行时提供所有所需的参数调用。

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


    蛮力法

    根据您的评论,我修改了帮助脚本以使用蛮力复制/交换方法,该方法将消除搜索范围内任何重复行的问题。该帮助程序通过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 -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
    

    【讨论】:

    • 感谢您的贡献,但我的文件通常包含相同的重复行,因此模式匹配并不是一个真正的选择。
    • 赢得一些,你失去一些。我正在对源代码进行测试,所以它运行得非常好,但很明显,任何相同的重复项都会导致立即失败。我想到的另一种方法是简单的“蛮力”方法。读取两行以与sed 交换(如上),然后逐行读取/复制文件到tmpfile 交换line1-&gt;2line2-&gt;1,因为它们在读取过程中遇到(然后cp tmpfile origfile &amp;&amp; rm tmpfile )。这也适用于相同的重复项。
    • @Roger 我重写了帮助脚本以使用 brute force 方法而不是替换为 sed 匹配。它将仅根据行号进行交换。试一试。
    【解决方案6】:

    如果要交换的行号是固定的,那么您可能想尝试以下示例中的 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 的评论。)

    【讨论】:

      【解决方案7】:

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

      例如

      test.txt:for a in {1..10}; do echo "this is line $a"; done &gt;&gt; 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 行建立了保持空间。 在第 9 行,它打印第 9 行,然后打印保持空间(第 6 到第 8 行),这完成了从 9 到放置 6 的第一步。注意:6h; 6!H 避免在模式空间顶部换行。

      第二个移动发生在第二个 sed 脚本中,它将第 7 行保存到保留空间,然后将其删除并在第 9 行之后打印。

      要使其成为准泛型,您可以使用如下变量: A=3 &amp;&amp; B=7 &amp;&amp; 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 行。

      【讨论】:

        【解决方案8】:

        如果,你想交换两行,创建脚本“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
        

        【讨论】:

        • 这是否适用于多个文件?比如说*.txt?
        • @Roger 我不明白你的意思.....for f in $(ls *.txt) ; do sh swap.sh $f 14 26 &gt; ${f}.swap; done .....这是你需要的吗?
        • 将功能打包为脚本是值得称赞的(尽管多次调用 sed 效率低下),但您的脚本需要条件来处理边缘情况,例如第一个行号为1(需要在第1条命令前加[ "$2" -gt 1 ] &amp;&amp; ),行号为相邻数字,如@9​​87654326@和15(需要在第3条命令前加[ "$((${2}+1 &lt;= ${3}-1))" -eq 1 ] &amp;&amp; ) .
        • @mklement0 非常感谢您的 cmets,我一直在研究学习的答案.....我修复了最相关的.....但是条件不添加它们,因为有更好的比我的答案,并认为将来删除.....再次感谢
        • @mklement0 :-o 我不知道....我总是用另一种方式....再次感谢感谢您的反馈
        猜你喜欢
        • 1970-01-01
        • 2019-10-07
        • 2014-12-07
        • 2020-01-11
        • 1970-01-01
        • 1970-01-01
        • 2019-04-02
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多