如何使用 awk 交换行,只有一次通过和有限的内存使用?

how swap lines with awk with only a single pass and limited memory use?

在之前的post中,显示了这个答案:,虽然很漂亮,但问题是你应该读取输入文件两次。

我希望制作一个 GNU awk 脚本来只读取一次输入。

cat swap_line.awk

你得到

BEGIN {
  if(init > end){
    exit 1;
  }
  flag = 1;
  memory_init = "";
  memory = ""
}
{
  if (NR != init && NR != end){
    if(flag==1){
      print [=11=];
    }else{
      memory = memory""[=11=]"\n";
    }
  }else if(end == init){
    print [=11=];
  }else if(NR == init){
    flag = 0;
    memory_init = [=11=];
  }else{
    #NR == end
    print [=11=];
    printf("%s",memory);
    print memory_init;
    flag = 1;
  }
}
END {
  #if end is greater than the number of lines of the file
  if(flag == 0){
    printf("%s",memory);
    print memory_init;
  }
}

脚本运行良好

cat input
1
2
3
4
5

awk -v init=2 -v end=4 -f swap_line.awk input
1
4
3
2
5

awk -v init=2 -v end=2 -f swap_line.awk input
1
2
3
4
5

awk -v init=2 -v end=8 -f swap_line.awk input 
1
3
4
5
2

问题

我怎样才能更好地制作脚本?因为,我不喜欢使用 memory 变量,因为对于大文件可能会有问题,例如,如果输入文件是 1000 万行并且想要在第 1 行和第 1000 万行之间进行交换,我存储memory 变量

中的 9,999,998 行

@JoseRicardoBustosM。如果不保存从 init 到内存中结束行之前的行,就不可能在 awk 中一次完成。试想一下不可能在您已经阅读的内容之前获得一行 N 行以奇迹般地出现在当前行的位置。最好的解决方案绝对是一种简单的 2 遍方法,即在第 1 遍中保存线条并在第 2 遍中使用它们。我包括所有涉及提前 grep 或在“2”遍方法桶中使用 getline 循环的解决方案。

FWIW 这是我真正会做的方式(这是一个 2-pass 方法):

$ cat swap_line.awk
BEGIN     { ARGV[ARGC]=ARGV[ARGC-1]; ARGC++ }
NR==FNR   { if (NR==end) tl=[=10=]; next }
FNR==init { hd=[=10=]; [=10=]=tl; nr=NR-FNR; if (nr<end) next }
FNR==end  { [=10=]=hd }
FNR==nr   { if (nr<end) [=10=] = [=10=] ORS hd }
{ print }

.

$ awk -v init=2 -v end=4 -f swap_line.awk input
1
4
3
2
5

$ awk -v init=2 -v end=2 -f swap_line.awk input
1
2
3
4
5

$ awk -v init=2 -v end=8 -f swap_line.awk input
1
3
4
5
2

请注意,如果您对如何处理超过文件末尾的 "end" 没有非常具体的要求,那么解决方案就是:

$ cat swap_line.awk
BEGIN     { ARGV[ARGC]=ARGV[ARGC-1]; ARGC++ }
NR==FNR   { if (NR==end) tl=[=12=]; next }
FNR==init { hd=[=12=]; [=12=]=tl }
FNR==end  { [=12=]=hd }
{ print }

如果你真的想考虑一些事情(同样,只是为了晴天的情况):

$ cat swap_line.awk
NR==init { hd=[=13=]; while ((getline<FILENAME)>0 && ++c<end); }
NR==end  { [=13=]=hd }
{ print }

$ awk -v init=2 -v end=4 -f swap_line.awk input
1
4
3
2
5

我仍然认为最后一个是“2”遍方法,如果我没有完全理解 http://awk.info/?tip/getline 中列出的所有注意事项,我就不会这样做。

我觉得你太辛苦了。这不会尝试处理极端情况(例如,如果 end 大于行数,则不会打印初始行,但这可以很容易地在 END 块中处理),因为我认为处理边缘情况会模糊不清这个想法。即,打印直到到达要换出的行,然后将数据存储在文件中,然后打印要交换的行、存储的数据和初始行,然后打印文件的其余部分:

$ cat swap.sh
#!/bin/sh


trap 'rm -f $T1' 0
T1=$(mktemp)

awk '
        NR<init { print; next; }
        NR==init { f = [=10=]; next; }
        NR<end { print > t1; next; }
        NR==end { print; system("cat "t1); print f; next; }
        1
' init=${1?} end=${2?} t1=$T1
$ yes | sed 10q | nl -ba | ./swap.sh 4 8
     1  y
     2  y
     3  y
     8  y
     5  y
     6  y
     7  y
     4  y
     9  y
    10  y

我同意需要 2 次通过。第一步可以使用专为该任务设计的工具完成:

# $init and $end have been defined

endline=$( tail -n "+$end" file | head -n 1 )
awk -v init="$init" -v end="$end" -v endline="$endline" '
    NR == init {saved = [=10=]; [=10=] = endline} 
    NR == end {[=10=] = saved} 
    {print}
' file

在函数中隐藏细节:

swap_lines () { 
    awk -v init="" \
        -v end="" \
        -v endline="$(tail -n "+" "" | head -n 1)" \
    '
        NR == init {saved = [=11=]; [=11=] = endline}
        NR == end {[=11=] = saved}
        1
    ' ""
}
seq 5 > file
swap_lines 2 4 file
1
4
3
2
5