使用 awk sed 和 grep 将多行文本转换为 csv

transform multiline text into csv with awk sed and grep

我运行一个shell命令returns像这样的重复值列表(注意缩进):

Name:               vm346
  cpu                1 (12%)      6150m (76%)
  memory             1130Mi (7%)  1130Mi (7%)
Name:               vm847
  cpu                6 (75%)        30150m (376%)
  memory             12980Mi (87%)  12980Mi (87%)
Name:               vm848
  cpu                3500m (43%)   17150m (214%)
  memory             6216Mi (41%)  6216Mi (41%)

我正在尝试像这样转换数据(在 csv 中):

vm346,1,(12%),6150m,(76%),1130Mi,(7%),1130Mi,(7%)
vm847,6,(75%),30150m,(376%),12980Mi,(87%),12980Mi,(87%)
vm848,3500m,(43%),17150m,(214%),6216Mi,(41%),6216Mi,(41%)

问题是任何给定的数据集,如上面的数据集,总是在不止一行上。

当我将其通过管道输入 awk 时,它让我发疯,因为即使我使用:

BEGIN{ FS="\n" }

尝试将数据拼接成一行,但行不通。无论我做什么,awk 都会将 name 值作为分隔线保留在其他所有内容之上。

很抱歉,我没有太多代码可以分享,但我已经用这个转了几个小时了,我 运行 没有想法...

我可以用 Perl 解决这个问题:

perl -ane 'print join ",", @F[1 .. $#F]; print $F[0] eq "memory" ? "\n" : ","'

如果你需要的话,把它翻译成 awk 应该很容易。

它是如何工作的?

  • -a 将空白处的每一行拆分为@F 数组
  • -n逐行读取输入并为每一行运行-e之后指定的代码
  • 我们打印所有元素,但第一个元素用逗号分隔(参见 join
  • 然后我们看第一列,如果是内存,我们在块的最后一行,所以我们打印一个换行符,否则我们打印一个逗号

这里有一个 ruby 可以做到这一点:

ruby -e '
s=$<.read
s.scan(/^([^ \t]+:)([\s\S]+?)(?=^|\z)/m).      # parse blocks
    map(&:last).                                 # get data part
    # parse and join the data fields:
    map{|block| block.split(/\n[ \t]+[^ \t]+[ \t]+/)}.
    map{|lines| lines.map(&:strip).join(" ").split().join(",")}.
    each{|l| puts "#{l}"}
' file 
vm346,1,(12%),6150m,(76%),1130Mi,(7%),1130Mi,(7%)
vm847,6,(75%),30150m,(376%),12980Mi,(87%),12980Mi,(87%)
vm848,3500m,(43%),17150m,(214%),6216Mi,(41%),6216Mi,(41%)

优点是不依赖于行数或字段数。它正在解析以下形式的块中的数据:

START:   ([ \t]+[data_with_no_space])*\n
   l1    ([ \t]+[data_with_no_space])*\n
   ...
START:
   ...

这样工作:

  1. THIS REGEX解析块;
  2. 保存数据元素的数组;
  3. 加入子数组,然后拆分成数据字段;
  4. Join(',') 生成 csv。

使用 AWK,一个选项是将 RS 设置为“名称:”,并忽略带有 NR > 1 的第一条记录,例如

awk -v RS="Name: " 'BEGIN{OFS=","} NR > 1 {print , , , , , , , , }' file
#> vm346,1,(12%),6150m,(76%),1130Mi,(7%),1130Mi,(7%)
#> vm847,6,(75%),30150m,(376%),12980Mi,(87%),12980Mi,(87%)
#> vm848,3500m,(43%),17150m,(214%),6216Mi,(41%),6216Mi,(41%)
awk '{=""}1' | paste -sd'  \n' - | awk '{=}1' OFS=,

删除第一列。每三行加入一次。与 sed 相同的想法:

sed 's/^ *[^ ]* *//' | paste -sd'  \n' - | sed 's/  */,/g'

其他:

awk '
=="Name:" {
  sep=ors
  ors=ORS
} {
  for (i=2;i<=NF;++i) {
    printf "%s%s",sep,$i
    sep=OFS
  }
} END {printf "%s",ors}'

或者,如果您想根据第一个字段为“内存”来打印 ORS(请注意,此程序可能会在不打印终止 ORS 的情况下结束):

awk '{for (i=2;i<=NF;++i) printf "%s%s",$i,(i==NF && =="memory" ? ORS : OFS)}'

其他:

awk -v OFS=, '
index([=14=],)==1 {
  OFS=ors
  ors=ORS
} {
  =""
  printf "%s",[=14=]
  OFS=ofs
} END {printf "%s",ors} BEGIN {ofs=OFS}'

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

sed -nE '/^ +\S+ +/{s///;H;$!d};x;/./s/\s+/,/gp;x;s/^\S+ +//;h' file

总的来说,sed 程序处理缩进行、已收集的行(当前行是文件第一行的情况除外)和非缩进行。

关闭隐式打印并启用扩展正则表达式。 (-nE).

如果当前行缩进,删除缩进,第一个字段和任何后续 spaces,将结果附加到保留 space,如果不是最后一行,删除它。

否则,检查保留 space 是否有聚集行,如果找到,用逗号替换一个或多个白色 space 并打印结果。然后通过删除第一个字段和任何后续 space 来准备当前行,并用结果替换保持 space。

该解决方案在逻辑上似乎是从后到前的,但是以这种方式编程可以避免多次检查文件末尾以及调用标签和 goto。

N.B。此解决方案适用于任意数量的缩进行。