awk one-liner 仅替换第一个匹配的正则表达式出现的文本

Posted

技术标签:

【中文标题】awk one-liner 仅替换第一个匹配的正则表达式出现的文本【英文标题】:Awk one-liner to replace text of first matching regex occurence only 【发布时间】:2015-09-06 03:23:15 【问题描述】:

我需要这个 awk 命令将文本中第一个 XML 标记中的 ss:Width="252" 替换为 ss:Width="140" 并保留其余标记:

cat <<- EOF > text
    <ss:Column ss:AutoFitWidth="1" ss:Width="252"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="252"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="189"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="189"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="252"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="252"/>
EOF

awk 'c=++count[$0] c==1 sub(/ss:Width=\"[0-9]1,4\"/,"ss:Width=\"140\"") print' text > newf

cat newf

相反,它替换了三个唯一匹配项中每一个的第一个实例中的表达式(总共三个替换,而我只想要一个。)

<ss:Column ss:AutoFitWidth="1" ss:Width="140"/>
<ss:Column ss:AutoFitWidth="1" ss:Width="140"/>
<ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
<ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
<ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
<ss:Column ss:AutoFitWidth="1" ss:Width="252"/>
<ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
<ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
<ss:Column ss:AutoFitWidth="1" ss:Width="140"/>
<ss:Column ss:AutoFitWidth="1" ss:Width="189"/>
<ss:Column ss:AutoFitWidth="1" ss:Width="252"/>
<ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
<ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
<ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
<ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
<ss:Column ss:AutoFitWidth="1" ss:Width="252"/>

为什么会这样?增量器在我的 awk 命令中的行为如何?我希望它在 /ss:Width=\".*\"/ 的第一个合格匹配之后增加,但它似乎在找到所有 unique 匹配之前不会增加,然后忽略后续的非唯一只匹配。那正确吗?我试图强制计数器在 c == 1 块的末尾递增,如下所示:

awk 'c=++count[$0] c==1 sub(/ss:Width=\".*\"/,"ss:Width=\"140\"");c++ print' text > newf

但我得到相同的输出。我在 sed 中尝试这个任务没有任何运气,而且我宁愿在 awk 中完成它。我对理解这种 awk 语法特别感兴趣。

编辑:我通过将一个宽度属性更改为另一个随机数来测试这个理论。它也确实将那个替换为 140。因此,它仅限于所有匹配表达式的第一个实例,而不是第一个匹配表达式本身。

编辑:正如 Cody 指出的那样,我的正则表达式是贪婪的。我将 .* 更改为 [0-9]1,4 但是行为是相同的 - 它仍然只替换每个唯一匹配的第一个实例。我还将 XML 标记的宽度属性之一更改为第三个唯一编号,并更新了输出以说明我正在尝试修复的行为。

这是 AIX/ksh。

【问题讨论】:

【参考方案1】:
awk 'found == 0  found = sub(/ss:Width=\"[0-9]1,4\"/,"ss:Width=\"140\"") //' text > newf

你也许可以缩短一点。

您的旧方法是保留由输入行索引的计数器数组。这就是为什么它表现出你没有预料到的行为。

其他一些答案假设所有行都将匹配 /ss:Width/ 正则表达式和/或总是在行尾找到宽度属性。在您的情况下可能是正确的,但值得注意。我决定不在上面的脚本中假设这些事情。

【讨论】:

谢谢 - 这个解释是我怀疑的,但这个解决方案对我不起作用,我明白了,但还不能解决它。尝试在各个地方添加一个结束括号,但没有运气。 “错误上下文是 // line=$0 found == 0 >>> line= 我正在尝试分解您的语法并理解它,我认为这将有很大帮助。另外,我需要它匹配 [0-9]1,4,而不仅仅是 252(在第一个 XML 标记中可能并不总是 252。) 只能在 NR == 1 时进行替换吗?我想我不太确定你想影响哪些线路。 我编辑了答案以匹配宽度属性中的任何数值。我认为这可能是问题所在。 Shawn,你说得对,并非所有行都包含 ss:Width ,我相信这就是 NR == 1 不起作用的原因。这只是一个较大的 XML 文档的摘录,我需要在其中识别这个文本块并转换这一系列标签中的第一个。我得到了要执行的命令,但现在 - 我不知道为什么 - 它只在第一个匹配项上打印“1”而不是完整的 XML 标记。 1 等我试过发现 = $1 但我不要得到它,它没有工作。谢谢,这让我更接近了!【参考方案2】:

看起来你的正则表达式很贪心。

sub(正则表达式,替换 [,目标]) 子函数改变目标的值。它在这个被视为字符串的值中搜索与正则表达式 regexp 匹配的最长子字符串。

【讨论】:

有趣,我没有考虑过。但是...我认为因为 awk 是面向行的,所以它不会评估其他行的内容并吃掉整个文本块。但你是对的,我不应该使用 .* - 我用 [0-9]1,4 替换它,但不幸的是我仍然得到相同的行为。不过谢谢【参考方案3】:

试试这个:

awk '($0 ~ /ss:Width/) if (once != 1) sub("[0-9]+\"/>","140\"/>"); once=1; print' text

它查找包含ss:Width 的第一行,然后将结束标记前的最后一个数字替换为140

【讨论】:

【参考方案4】:

使用自定义字段分隔符实际上非常简单:

awk -F ' ss:Width="252"' -v r=' ss:Width="140"' '!p && NF>1p=1; $1 = $1 r 1' text
    <ss:Column ss:AutoFitWidth="1" ss:Width="140"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="252"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="189"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="189"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="252"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="126"/>
    <ss:Column ss:AutoFitWidth="1" ss:Width="252"/>

-F ' ss:Width="252"' 将字段分隔符设置为ss:Width="252"

!p &amp;&amp; NF&gt;1 将替换值 r 用于搜索文本的第一个实例。

【讨论】:

以上是关于awk one-liner 仅替换第一个匹配的正则表达式出现的文本的主要内容,如果未能解决你的问题,请参考以下文章

Perl单行(Perl One-Liners)命令

sed / awk 匹配文件中第二次出现的正则表达式,并替换整行

awk / sed:如果任何字段与模式匹配,则替换所有字段

如何使用正则表达式匹配或替换仅包含数值的密码

awk 基本函数用法

awk 正则 去掉回车换行