bash:将五行输入组合成每行输出

3

我有一个输入文件,内容如下:

MB1 00134141 
MB1 12415085 
MB1 13253590
MB1 10598105
MB1 01141484
...
...
MB1 10598105

我希望将5行合并为一行。 我希望我的bash脚本能够处理bash脚本,生成以下输出 -
MB1 00134141 MB1 12415085 MB1 13253590 MB1 10598105 MB1 01141484
...
...
...                                                 

我已经写了下面的脚本,它能运行,但对于23051行的文件来说速度较慢。 我能否编写更好的代码以使其更快?

#!/bin/bash
file=timing.csv
x=0
while [ $x -lt $(cat $file | wc -l) ]
do
   line=`head -n $x $file | tail -n 1`
   echo -n $line " "
   let "remainder = $x % 5"
   if [ "$remainder" -eq 0 ] 
   then
        echo ""
   fi
   let x=x+1
done
exit 0

我尝试执行以下命令,但它弄乱了一些数字。
cat timing_deleted.csv | pr -at5

在循环中每次调用cat $file | wc -l可能会导致速度变慢,因此最好在循环之前保存该值。 - chepner
我在想为什么 pr -at5 timing_deleted.csv 没有起作用... 当我尝试时,它运行得非常好,而且速度很快... 是文件具有 DOS 样式的行结尾或其他原因吗? - Sundeep
6个回答

8
使用 tr 标签:
cat input_file | tr "\n" " "

2
无用的cat使用:这个命令最好改写为tr '\n' ' ' <input_file,直接将文件句柄传递给tr命令,而不是通过管道传递给另一个读取输入文件的进程。 - Charles Duffy
此外,这并不满足问题的要求:它将所有输入组合成一行,而不是将五行输入组合成每行输出。 - Charles Duffy
这不符合问题的要求。 - FatherMathew

5
使用粘贴命令:
 paste -d ' ' - - - - - < tmp.txt

paste更好,但我不想删掉之前基于mapfile的解决方案。

[更新:在使用-n选项时,在mapfile版本4.2.35之前,mapfile会读取过多行。]

#!/bin/bash
file=timing.csv
while true; do
    mapfile -t -n 5 arr
    (( ${#arr} > 0 )) || break
    echo "${arr[*]}"
done < "$file"
exit 0

我们不能使用 while mapfile ...; do 这种写法,因为即使没有读取任何输入,mapfile 的状态也会返回为 0。

是的,这个很好用!只是由于每个数字长度的变化,输出结果可能会出现不规则的空格或制表符。但它的功能是正确的! :) 谢谢! - Raj
粘贴应该可以正常工作;基于地图文件的解决方案存在一个小错误,我会修复它。 - chepner
很好 -- 我感到有些尴尬,竟然没想到自己使用 paste。 - Charles Duffy

3

在纯bash中,没有外部进程(为了速度):

while true; do
  out=()
  for (( i=0; i<5; i++ )); do
    read && out+=( "$REPLY" )
  done
  if (( ${#out[@]} > 0 )); then
    printf '%s ' "${out[@]}"
    echo
  fi
  if (( ${#out[@]} < 5 )); then break; fi
done <input-file >output-file

这个版本能正确处理文件行数不是5的倍数的情况。


下面的答案好多了:cat input_file | tr "\n" " " - Charney Kaye
@charneykaye,原帖提到他们想要合并每五行成一批,而不是合并所有行。请仔细阅读问题。 - Charles Duffy
这是个好观点,Charles-- 在我看来,“bash脚本输出多行到单行”这个问题标题应该被编辑以准确反映这一点。否则,下面的答案对于那些(像我自己一样)提出简单问题的人最有用。 - Charney Kaye

3

使用sed,但是这个命令不会处理最后几行,因为它们的数量不是5的倍数:

 sed 'N;N;N;N;s/\n/ /g;' input_file
N 命令读取下一行并将其附加到当前行,保留换行符。该脚本为每个读取的行读取四行额外的内容,将5行块累积到缓冲区中。对于每个这样的块,它将所有换行符替换为一个空格。

3

如果您的输入每行包含相同数量的空格,您可以使用xargs

cat timing_deleted.csv | xargs -n 10

这将从cat timing_deleted.csv中获取输入,并在10(-n 10)个空格字符上组合输入。每列中的空格,如MB1 00134141,都计为一个空格字符 - 同样是每行末尾的换行符。所以,对于5行,你需要使用10个空格字符。 编辑
正如Charles所评论的那样,您可以跳过使用cat并直接将数据推送到xargs中:
xargs -n 10 < timing_deleted.csv

我没有注意到使用非常大的文件时有性能提升,但它不需要多个命令。


这个可以用,但是我发现它比SED慢。可能是因为它运行了两个进程并将输出进行了管道处理。 感谢您的解决方案! - Raj
1
@RajTendulkar 是的,那是一个有价值的观点;然而,我不是在否定另一个解决方案 - 那也是个好办法,但如果最后几行不足5行,这种方法就不会漏掉它们(当然,如果你也传入“-x”选项,那就另当别论了)。 - newfurniturey
3
这里不需要使用"cat": "xargs -n 10 <timing_deleted.csv"。 - Charles Duffy

1
一个 awk 脚本可以做到这一点。我猜也可以用 sed 替换。我不太了解 sed,所以给你提供了 awk 脚本。
NF{ 
    if(i>=5){
        line = line "\n";
        i=0;
    }else{
        line = line " " $0;
        i++;
    }
}

END{
    print line;
}

称之为 merge.awk,以下是如何调用它:

    awk -f merge.awk filetomerge.txt

或者

cat filetomerge.txt | awk -f merge.awk

速度应该相当快。


1
或者如果您喜欢混淆的awk:awk ORS=NR%5\?FS:RS :) - geirha
我那样做是因为在终端上只打印一次速度更快,但不确定现在还有多大意义。对我来说,一些awk机制仍然很难理解... 值得知道。 - someone
嗯,缓冲整个输入并没有太多好处。此外,如果输入大于可用内存,您的awk将失败。 - geirha

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接