![Sed——替换文件中单词的前 k 个实例](https://linux22.com/image/55834/Sed%E2%80%94%E2%80%94%E6%9B%BF%E6%8D%A2%E6%96%87%E4%BB%B6%E4%B8%AD%E5%8D%95%E8%AF%8D%E7%9A%84%E5%89%8D%20k%20%E4%B8%AA%E5%AE%9E%E4%BE%8B.png)
我只想替换k
单词的第一个实例。
我怎样才能做到这一点?
例如。假设文件foo.txt
包含 100 次出现单词 'linux' 。
我只需要替换前 50 个出现的位置。
答案1
下面的第一部分描述了如何sed
更改一行中的前 k 次出现。第二部分扩展了这种方法,只更改文件中的前 k 次出现,无论它们出现在哪一行。
面向线路的解决方案
在标准 sed 中,有一个命令可以替换一行中第 k 次出现的单词。如果k
是3,例如:
sed 's/old/new/3'
或者,可以将所有出现的情况替换为:
sed 's/old/new/g'
这些都不是你想要的。
GNUsed
提供了一个扩展,它将更改第 k 次出现以及之后的所有情况。如果k为3,例如:
sed 's/old/new/g3'
这些可以组合起来做你想做的事。要更改前 3 个出现的位置:
$ echo old old old old old | sed -E 's/\<old\>/\n/g4; s/\<old\>/new/g; s/\n/old/g'
new new new old old
where\n
在这里很有用,因为我们可以确定它永远不会出现在一行上。
解释:
我们使用三个sed
替换命令:
s/\<old\>/\n/g4
这是 GNU 扩展,用于将第四个和所有后续出现的 替换
old
为\n
。扩展的正则表达式功能
\<
用于匹配单词的开头和\>
单词的结尾。这可确保仅匹配完整的单词。扩展正则表达式需要-E
选项sed
.s/\<old\>/new/g
仅保留前三个出现的
old
,这会将它们全部替换为new
。s/\n/old/g
第四个和所有剩余的出现的
old
被替换为\n
第一步。这将使它们恢复到原来的状态。
非 GNU 解决方案
如果 GNU sed 不可用并且您想要更改前 3 个出现的old
to new
,则使用三个s
命令:
$ echo old old old old old | sed -E -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'
new new new old old
k
当数字较小时,此方法效果很好,但扩展到较大数字时效果不佳k
。
由于某些非 GNU sed 不支持用分号组合命令,因此这里的每个命令都有自己的-e
选项。可能还需要验证您是否sed
支持单词边界符号\<
和\>
。
面向文件的解决方案
我们可以告诉 sed 读入整个文件,然后执行替换。例如,要替换old
使用 BSD 样式 sed 的前三个出现:
sed -E -e 'H;1h;$!d;x' -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'
sed 命令H;1h;$!d;x
读取整个文件。
因为上面没有使用任何 GNU 扩展,所以它应该可以在 BSD (OSX) sed 上运行。请注意,这种方法需要sed
能够处理长线的方法。 GNUsed
应该没问题。那些使用非 GNU 版本的人sed
应该测试它处理长线的能力。
使用 GNU sed,我们可以进一步使用g
上述技巧,但用 ,\n
替换\x00
,以替换前三个出现的位置:
sed -E -e 'H;1h;$!d;x; s/\<old\>/\x00/g4; s/\<old\>/new/g; s/\x00/old/g'
这种方法可以很好地扩展并k
变大。不过,这是假设它\x00
不在您的原始字符串中。由于不可能将字符放入\x00
bash 字符串中,因此这通常是一个安全的假设。
答案2
使用awk
awk 命令可用于将前 N 个出现的单词替换为替换内容。
仅当单词完全匹配时,命令才会进行替换。
在下面的示例中,我将第一次27
出现的替换old
为new
使用子
awk '{for(i=1;i<=NF;i++){if(x<27&&$i=="old"){x++;sub("old","new",$i)}}}1' file
此命令循环遍历每个字段,直到匹配
old
,它检查计数器是否低于 27,递增并替换该行中的第一个匹配项。然后移至下一个字段/行并重复。
手动替换字段
awk '{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file
与之前的命令类似,但由于它已经在其所在字段上有一个标记
($i)
,因此它只是将字段的值从 更改old
为new
。
之前执行检查
awk '/old/&&x<27{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file
检查该行是否包含旧内容且计数器是否低于 27
SHOULD
可提供较小的速度提升,因为当这些行为 false 时,它不会处理这些行。
结果
例如
old bold old old old
old old nold old old
old old old gold old
old gold gold old old
old old old man old old
old old old old dog old
old old old old say old
old old old old blah old
到
new bold new new new
new new nold new new
new new new gold new
new gold gold new new
new new new man new new
new new new new dog new
new new old old say old
old old old old blah old
答案3
假设您只想替换字符串的前三个实例......
seq 11 100 311 |
sed -e 's/1/\
&/g' \ #s/match string/\nmatch string/globally
-e :t \ #define label t
-e '/\n/{ x' \ #newlines must match - exchange hold and pattern spaces
-e '/.\{3\}/!{' \ #if not 3 characters in hold space do
-e 's/$/./' \ #add a new char to hold space
-e x \ #exchange hold/pattern spaces again
-e 's/\n1/2/' \ #replace first occurring '\n1' string w/ '2' string
-e 'b t' \ #branch back to label t
-e '};x' \ #end match function; exchange hold/pattern spaces
-e '};s/\n//g' #end match function; remove all newline characters
注意:上面的内容可能不适用于嵌入式评论
...或者在我的示例中,“1”...
输出:
22
211
211
311
在那里我使用了两种值得注意的技术。首先,1
一行中出现的每个 都被替换为\n1
。这样,当我接下来进行递归替换时,我可以确保不会替换出现两次如果我的替换字符串包含我的替换字符串。例如,如果我替换he
为hey
它仍然可以工作。
我这样做是这样的:
s/1/\
&/g
h
其次,我通过在每次出现的旧空间中添加一个字符来计算替换次数。一旦我达到三个就不再发生。如果您将此应用于您的数据,并将 更改\{3\}
为您想要的替换总数,并将地址更改/\n1/
为您想要替换的任何内容,则您应该仅替换您希望替换的数量。
-e
我只是为了可读性才做了所有的事情。 POSIXly 可以这样写:
nl='
'; sed "s/1/\\$nl&/g;:t${nl}/\n/{x;/.\{3\}/!{${nl}s/$/./;x;s/\n1/2/;bt$nl};x$nl};s/\n//g"
和 GNU sed
:
sed 's/1/\n&/g;:t;/\n/{x;/.\{3\}/!{s/$/./;x;s/\n1/2/;bt};x};s/\n//g'
还要记住,这sed
是面向行的 - 它不会读取整个文件,然后尝试循环返回它,这在其他编辑器中经常发生。sed
简单高效。也就是说,执行以下操作通常很方便:
这是一个小 shell 函数,它将它捆绑成一个简单执行的命令:
firstn() { sed "s/$2/\
&/g;:t
/\n/{x
/.\{$(($1))"',\}/!{
s/$/./; x; s/\n'"$2/$3"'/
b t
};x
};s/\n//g'; }
所以我可以这样做:
seq 11 100 311 | firstn 7 1 5
...并得到...
55
555
255
311
...或者...
seq 10 1 25 | firstn 6 '\(.\)\([1-5]\)' '\15\2'
...要得到...
10
151
152
153
154
155
16
17
18
19
20
251
22
23
24
25
...或者,为了匹配您的示例(较小的数量级):
yes linux | head -n 10 | firstn 5 linux 'linux is an os kernel'
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux
linux
linux
linux
linux
答案4
使用 shell 循环和ex
!
{ for i in {1..50}; do printf %s\\n '0/old/s//new/'; done; echo x;} | ex file.txt
是的,这有点愚蠢。
;)
old
注意:如果文件中的实例少于 50 个,则此操作可能会失败。 (我还没有测试过。)如果是这样,它将保持文件不变。
更好的是,使用 Vim。
vim file.txt
qqgg/old<CR>:s/old/new/<CR>q49@q
:x
解释:
q # Start recording macro
q # Into register q
gg # Go to start of file
/old<CR> # Go to first instance of 'old'
:s/old/new/<CR> # Change it to 'new'
q # Stop recording
49@q # Replay macro 49 times
:x # Save and exit