unix one-liner在多个文本文件中交换/转置两行?

时间:2015-04-24 17:21:46

标签: bash unix awk sed text-files

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

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

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

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

8 个答案:

答案 0 :(得分:7)

如果要编辑文件,可以使用标准编辑器ed。您在ed

中的任务非常简单
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

答案 1 :(得分:4)

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

以下符合POSIX标准的shell函数 包装两个答案

Stdin / stdout处理,基于@potong's excellent answer

  • POSIX sed不支持-i进行就地更新。
  • 它也不支持在字符类中使用\n,因此[^\n]必须替换为繁琐的解决方法,肯定定义除{{之外的所有字符1}}可以出现在一行上 - 这是一个字符类,它将可打印字符与除\n以外的所有(ASCII)控制字符组合在一起作为文字(通过使用\n的命令替换)
  • 还要注意需要将printf脚本拆分为两个sed选项,因为POSIX -e要求终止分支命令(在这种情况下为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/'
}

就地更新,基于gniourf_gniourf's excellent answer

小警告:

  • 虽然ed is a POSIX utility,它并没有预先安装在所有平台上,特别是在Debian和用于Windows的Cygwin和MSYS Unix仿真环境中。
  • $ printf 'line 1\nline 2\nline 3\n' | swapLines 1 3 line 3 line 2 line 1 始终将输入文件作为整体读入内存。
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
}

@potong's GNU sed-based answer解释:

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

$ printf 'line 1\nline 2\nline 3\n' > file
$ swapFileLines 1 3 file
$ cat file
line 3
line 2
line 1
  • sed -ri '10,15!b;10h;10!H;15!d;x;s/^([^\n]*)(.*\n)(.*)/\3\2\1/' f1 f2 fn 激活对扩展正则表达式的支持;在这里,值得注意的是,它允许使用未转义的括号来形成捕获组
  • -r指定指定为操作数的文件(-if1f2)在就地更新,< em>无备份,因为没有备份文件的可选后缀与fn选项相邻。

  • -i表示10,15!b)的所有行都属于!10行的范围分支(15)隐式地分配给脚本的(假设没有目标标签名称跟在b之后),这意味着以下命令被跳过< / em>这些行。实际上,它们只是打印

  • b将(10h)行号h(范围的开头)复制到所谓的保留空间,这是一个辅助缓冲区
  • 10 追加10!HH的每一行 - 在这种情况下意味着行{{ 1}}通过10 - 到保留空间。
  • 11 删除1515!d的每一行(此处为行d通过15)并分支到脚本的末尾(跳过剩余的命令)。通过删除这些行,它们打印。
  • 10,仅对行14执行(范围的结尾),用保留空间的内容替换所谓的模式空间,在那一点上包含范围内的所有行(x15);模式空间是10命令运行的缓冲区,默认情况下打印其内容(除非指定了15)。
  • sed然后使用捕获组(形成传递给函数-n的第一个参数的正则表达式的带括号的子表达式)将模式空间的内容划分为第1行({{1} }),中间行(s/^([^\n]*)(.*\n)(.*)/\3\2\1/)和最后一行(s),然后,在替换字符串(传递给函数^([^\n]*)的第二个参数)中,使用反向引用将最后一行((.*\n))放在中间行((.*))之前,然后是第一行(s),有效地交换第一行和最后一行范围中。最后,打印修改后的图案空间。

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

答案 2 :(得分:2)

这可能适合你(GNU sed):

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

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

i标记会对每个文件(f1f2 ... fn)进行编辑。

答案 3 :(得分:2)

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

答案 4 :(得分:1)

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

交换了5&amp;行。按预期15

$ 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 ],以确保在所有所需的参数时脚本被调用。

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

蛮力方法

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

答案 5 :(得分:0)

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

答案 6 :(得分:0)

如果要交换的行号是固定的,那么您可能想尝试类似以下示例中的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 :(得分:0)

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

e.g。

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行构建保留空间。 在第9行,它打印第9行然后打印保留空间(第6行到第8行),这样就完成了第9步到第6步。注意: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}'

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