我正在努力处理文件。通常我应该为每个主机定义一行。但有时,有人会将某些字段拆分为不同的行。这是一个例子:
"host1","host1","linux
server",""
"host2","host2","linux server",""
现在我想找到一种方法(在 bash 中更好)来解决这个问题:
"host1","host1","linux server",""
"host2","host2","linux server",""
每个字段都应该用双引号引起来;如果不是这种情况,则意味着\n
插入了 a,然后我想将其删除,以便每行始终有 4 个字段。
请注意,我可能会将描述分为几行,例如:
"host1","host1","linux
server
centos",""
"host2","host2","linux server",""
我尝试了几种awk
方法,例如
awk 'BEGIN {ORS=""; RS="\"\n\""; FS="\",\""; OFS="\",\""} {if (NF == 3) print "\"" $1 "\"," $2 "\"," $3 "\"\n"; else printf "%s", $0} END {print ""}' /tmp/ngr4
但我没有成功,我开始用这个强大的工具达到我的极限。
答案1
使用米勒 ( mlr
),一个支持 CSV 的多用途处理实用程序,适用于各种结构化文档格式,用于清理所有字段的空白:
$ cat file
"host1","host1","linux
server",""
"host2","host2","linux server",""
$ mlr --csv -N clean-whitespace file
host1,host1,linux server,
host2,host2,linux server,
这会将数据读取file
为无标头 CSV 记录并应用操作clean-whitespace
每一个。该clean-whitespace
操作会修剪每个字段值中的侧翼空格,并将连续的空格字符组合成单个空格。
改为只用空格替换换行符,您可以使用简短的语句迭代字段put
表达:
$ mlr --csv -N put 'for (k,v in $*) { $[k] = gssub(v, "\n", " ") }' file
host1,host1,linux server,
host2,host2,linux server,
功能gssub()
其行为类似于gsub()
Awk,但不会将其查询参数视为正则表达式(Miller 也有gsub()
)。
如果您觉得需要对字段进行引号,即使它不是严格需要的(如果字段的值需要,Miller 会自动添加引号),然后mlr
与其--quote-all
选项一起使用:
$ mlr --csv -N --quote-all clean-whitespace file
"host1","host1","linux server",""
"host2","host2","linux server",""
$ mlr --csv -N --quote-all put 'for (k,v in $*) { $[k] = gssub(v, "\n", " ") }' file
"host1","host1","linux server",""
"host2","host2","linux server",""
答案2
您最不想做的就是尝试在 bash 中执行此操作。看为什么使用 shell 循环处理文本被认为是不好的做法?。
现在,如果您想要的可以表达为“删除任何换行符,除非它们紧接在字符之后"
”,您可以执行以下操作:
perl -pe 's/(?<!")\n/ /g' file
匹配(?<!")\n
任何前面没有 的换行符"
。因此给出一个像这样的输入示例:
$ cat file
"host0","host0","linux
server",""
"host1","host1","linux
server
centos",""
"host2","host2","linux server",""
上面的命令给出:
$ perl -pe 's/(?<!")\n/ /g' file
"host0","host0","linux server",""
"host1","host1","linux server centos",""
"host2","host2","linux server",""
但实际上,mlr
这是最好的方法。
答案3
使用乐(以前称为 Perl_6)
受到 @terdon 出色的 Perl 答案的启发:
~$ raku -ne '/ <!after \" > $/ ?? print "$_ " !! put $_;' file
以下是用 Raku(又名 Perl6)编写的答案。 Raku 有一个新的 Unicode 感知正则表达式引擎,它试图清理一些众所周知的习语。因此(例如),“Y not-after X”负回顾习语<!after X > Y
在 Raku 中变为,其中<?after … >
表示正回顾,<!after … >
表示负回顾。
\n
由于 Raku 规范了行终止符处理(现在默认自动截断换行符),因此我们只需使用 Raku 的三元运算符即可检测模式测试 ??
真的 !!
错误的,然后使用print
(不添加\n
换行符)或put
(\n
在文本末尾添加换行符)输出。
输入示例:
"host0","host0","linux
server",""
"host1","host1","linux
server
centos",""
"host2","host2","linux server",""
示例输出:
"host0","host0","linux server",""
"host1","host1","linux server centos",""
"host2","host2","linux server",""
其他 Raku 解决方案
使用 Raku 的Text::CSV
模块:
使用 Raku 生态系统中适当的 CSV 解析器(模块)相当容易(请参阅:https://raku.land/?q=CSV)。这些检查是否符合 RFC 4180 合规性,并可以为您提供标准化的 CSV 输出以及大量自定义内容。
下面,Raku 的Text::CSV
模块很好地解析了 OP 的输入,并且在删除\n
换行符后,默认情况下将仅输出那些带有内部空白的列作为双引号列(第一个答案)。第二个答案按行读取并产生与第一个答案相同的答案:
将整个文件读入内存,默认csv()
输出:
~$ raku -MText::CSV -e 'my @a = csv(in => "/path/to/file", sep => ",");
@a = @a>>.map( *.trans: "\n" => " ");
csv(in => @a, out => $*OUT, sep => ",");'
host0,host0,"linux server",
host1,host1,"linux server centos",
host2,host2,"linux server",
逐行读取,然后“手动”引用输出:
~$ raku -MText::CSV -e 'my $fh = "/path/to/file"; my $io = open $fh, :r, :!chomp;
my $csv = Text::CSV.new; my @data;
while $csv.getline($io) -> $row {
@data.push: $row.map: *.trans: "\n" => " "; };
put $_.join(",") for @data>>.map({ / \s / ?? (q["] ~ $_ ~ q["]) !! $_ });'
host0,host0,"linux server",
host1,host1,"linux server centos",
host2,host2,"linux server",
https://docs.raku.org/language/operators#infix_??_!!
https://github.com/Tux/CSV/blob/master/doc/Text-CSV.md#embedded-newlines
https://raku.org
答案4
我们假设尽管有许多不需要的换行符,但所有双引号都匹配并且所有字段分隔符都存在。在这种情况下你可以使用这个命令:
$ sed '/^"/! s/^/ /' infile | tr -d '\n' | sed '-e s/"/"\n/'{8..1000..8}
"host1","host1","linux server centos",""
"host2","host2","linux server",""
在哪里:
$ cat infile
"host1","host1","linux
server
centos",""
"host2","host2","linux server",""
无论哪里可能发生不需要的换行符,这都应该有效。
大括号内的数字 1000 是任意大的数字,并且必须大于输入文件中的字符总数。
如果您怀疑输入行可能包含前导/尾随空格,请先将其删除。例如使用这个命令awk 'NF{$1=$1}1' infile