如何使用sed一次性基于模式交换文本?

338

假设我有一个字符串'abbc'并且我想要进行如下替换:

  • ab -> bc
  • bc -> ab

如果我尝试进行两次替换,结果并不是我想要的:

echo 'abbc' | sed 's/ab/bc/g;s/bc/ab/g'
abab

那么,我可以使用什么sed命令来替换如下内容?

echo abbc | sed SED_COMMAND
bcab

编辑: 事实上,文本可能有两种以上的模式,而我不知道需要多少替换。由于有一个答案说 sed 是流编辑器,其替换是贪婪的,所以我认为我需要使用一些脚本语言来完成这个任务。


你需要在同一行上进行多个替换吗?如果不需要,请从这两个s///命令中删除g标志,那么它就可以工作了。 - Etan Reisner
你没有理解我的问题。我的意思是说,你需要在同一行上对每个替换进行多次操作吗?原始输入中有多个匹配项,如ab bc吗? - Etan Reisner
抱歉@EtanReisner,我误解了,答案是肯定的。文本可以有多个替换。 - DaniloNC
13个回答

498
也许像这样的东西:
sed 's/ab/~~/g; s/bc/ab/g; s/~~/bc/g'

用一个你确定不会出现在字符串中的字符来代替~


11
GNU sed 可以处理空字符,因此您可以使用 \x0 代替 ~~ - jthill
5
“g”是否必要,它的作用是什么? - wsdzbm
22
@g是全局替换标记,它可以在每行中替换所有符合模式的实例,而不仅仅是第一个(这是默认行为)。 - naught101
2
请查看我的答案https://dev59.com/wV8d5IYBdhLWcg3wpzr4#41273117,其中包含ooga答案的变体,可以同时替换多个组合。 - Zack Morris
8
在生产代码中,不要对输入做任何假设。在测试中,尽管测试无法真正证明正确性,但一个好的测试想法是:将脚本本身用作输入。请注意,原文中的 "do not never" 是一种双重否定的错误用法,应该简化为 "never". - hagello

81

我总是使用多条带有“-e”参数的语句。

$ sed -e 's:AND:\n&:g' -e 's:GROUP BY:\n&:g' -e 's:UNION:\n&:g' -e 's:FROM:\n&:g' file > readable.sql

这将在所有AND、GROUP BY、UNION和FROM之前附加一个'\n',其中'&'表示匹配的字符串,'\n&'表示您想要在'matched'之前用'\n'替换匹配的字符串。


2
它返回 sed: -e: 没有那个文件或目录 - alper
2
如果我在使用 sed -i -e 命令会怎样呢? - alper
1
这并没有解决操作顺序的主要问题。每个命令都是在前一个命令运行后才对整个文件运行的。因此,运行以下命令:echo 'abbc' | sed -e 's:ab:bc:g' -e 's:bc:ab:g' 仍然会得到 abab 而不是问题所要求的 bcab - ADJenks
是的,ADJenks,你是对的! :)也许你可以用以下方式来欺骗它: echo 'abbc' | sed -e 's:ab:xx:g' -e 's:bc:ab:g' -e 's:xx:bc:g' - Paulo Henrique Lellis Gonalves
@alper,它可以工作。也许只有一个-e被指定。在这种情况下,-e选项应该在每个语句前面加上前缀。 - Artfaith

23

sed是一种流编辑器。它通过贪婪地搜索和替换来实现功能。要实现您所要求的操作,唯一的方法是使用一个中间替换模式,并在最后将其改回来。

echo 'abcd' | sed -e 's/ab/xy/;s/cd/ab/;s/xy/cd/'

使用sed进行流编辑,将输入字符串'abcd'中的“ab”替换为“xy”,然后将“cd”替换为“ab”,最后将“xy”替换为“cd”。输出结果为"cdab"

19
这是一个在ooga的答案上进行变化的版本,适用于多个搜索和替换对,而无需检查值如何被重用:
Debian/Ubuntu GNU sed:
sed -i '
s/\bAB\b/________BC________/g
s/\bBC\b/________CD________/g
s/________//g
' path_to_your_files/*.txt

macOS FreeBSD sed (-i '' and 单词边界的工作方式不同):

sed -i '' '
s/[[:<:]]AB[[:>:]]/________BC________/g
s/[[:<:]]BC[[:>:]]/________CD________/g
s/________//g
' path_to_your_files/*.txt

你也可以使用find来包含/排除文件/目录(对于你的操作系统可能是-i''):

find path_to_your_files -type f \( -name '*.js' -o -name '*.jsx' \) \
-not \( -path './node_modules/*' -o -path './vendor/*' \) -exec \
sed -i '
s/\bAB\b/________BC________/g
s/\bBC\b/________CD________/g
s/________//g
' {} \;

这是一个例子:

之前:

some text AB some more text "BC" and more text.

之后:

some text BC some more text "CD" and more text.

请注意,\b[[:<:]]/[[:>:]] 单词边界可以防止 ________ 干扰搜索。如果您没有使用单词边界搜索,则此技术可能无法正常工作。
还要注意,这与删除 s/________//g 并将 && sed -i 's/________//g' path_to_your_files/*.txt 添加到命令的末尾会产生相同的结果,但不需要两次指定路径。
一般变化是在您知道文件中没有空值的情况下,使用 \x0_\x0_ 替换 ________,如 jthill 建议

1
我同意hagello上面的评论,不要对输入内容进行假设。因此,除了将seds叠加在一起 (sed 's/ab/xy/' | sed 's/cd/ab/' .....)之外,我个人认为这是最可靠的解决方案。 - leetbacoon

8

以下是SED手册的摘录:

-e script

--expression=script

将脚本中的命令添加到处理输入时要运行的命令集合中。

在每个替换前加上-e选项,并将它们收集在一起。以下是适用于我的示例:

sed < ../.env-turret.dist \
  -e "s/{{ name }}/turret$TURRETS_COUNT_INIT/g" \
  -e "s/{{ account }}/$CFW_ACCOUNT_ID/g" > ./.env.dist

这个例子还展示了如何在替换中使用环境变量。

5
这可能适用于您(GNU sed):
sed -r '1{x;s/^/:abbc:bcab/;x};G;s/^/\n/;:a;/\n\n/{P;d};s/\n(ab|bc)(.*\n.*:(\1)([^:]*))/\4\n\2/;ta;s/\n(.)/\1\n/;ta' file

这里使用了一个查找表,该表已经准备好并保存在保留空间(HS)中,然后附加到每一行。一个唯一的标记(在本例中为\n)被添加到行的开头,并用作将搜索沿着整个行进行推进的方法。一旦标记到达行的末尾,该过程就完成了,并打印出查找表,同时丢弃标记。

注意:查找表在开始时预处理,选择第二个唯一标记(在这种情况下为:),以避免与替换字符串发生冲突。

附带一些注释:

sed -r '
  # initialize hold with :abbc:bcab
  1 {
    x
    s/^/:abbc:bcab/
    x
  }

  G        # append hold to patt (after a \n)

  s/^/\n/  # prepend a \n

  :a

  /\n\n/ {
    P      # print patt up to first \n
    d      # delete patt & start next cycle
  }

  s/\n(ab|bc)(.*\n.*:(\1)([^:]*))/\4\n\2/
  ta       # goto a if sub occurred

  s/\n(.)/\1\n/  # move one char past the first \n
  ta       # goto a if sub occurred
'

表格的工作方式如下:
   **   **   replacement
:abbc:bcab
 **   **     pattern

4
Tcl中有一个内置的功能可以实现此操作。请参考Tcl文档
$ tclsh
% string map {ab bc bc ab} abbc
bcab

这可以通过逐个字符遍历字符串并从当前位置开始进行字符串比较来实现。
在perl中:
perl -E '
    sub string_map {
        my ($str, %map) = @_;
        my $i = 0;
        while ($i < length $str) {
          KEYS:
            for my $key (keys %map) {
                if (substr($str, $i, length $key) eq $key) {
                    substr($str, $i, length $key) = $map{$key};
                    $i += length($map{$key}) - 1;
                    last KEYS;
                }
            }
            $i++;
        }
        return $str;
    }
    say string_map("abbc", "ab"=>"bc", "bc"=>"ab");
'

bcab

4

如果只需要处理单个模式的情况,您可以尝试以下更简单的方法:

echo 'abbc' | sed 's/ab/bc/;s/bc/ab/2'

我的输出结果:

 ~# echo 'abbc' | sed 's/ab/bc/;s/bc/ab/2'
 bcab

对于模式的多个出现:

sed 's/\(ab\)\(bc\)/\2\1/g'

示例

~# cat try.txt
abbc abbc abbc
bcab abbc bcab
abbc abbc bcab

~# sed 's/\(ab\)\(bc\)/\2\1/g' try.txt
bcab bcab bcab
bcab bcab bcab
bcab bcab bcab

希望这能有所帮助!!

2

如果用变量替换字符串,解决方案将不起作用。sed命令需要使用双引号而不是单引号。

#sed -e "s/#replacevarServiceName#/$varServiceName/g" -e "s/#replacevarImageTag#/$varImageTag/g" deployment.yaml

2

echo "C:\Users\San.Tan\My Folder\project1" | sed -e 's/C:\\/mnt\/c\//;s/\\/\//g'

将Windows路径替换为Windows Subsystem for Linux(WSL)路径,使用上述命令,可以将

C:\Users\San.Tan\My Folder\project1

替换为

mnt/c/Users/San.Tan/My Folder/project1

请注意保留HTML标签。


这与发布的问题无关。 - Rajib
是的,不是直接的。这就是为什么我用“万一”的方式来限定它。如果人们和我一样,每次在 Stack Overflow 上搜索时并不总是能找到一个特定的问题的答案。但是针对你的观点,我已经在其他地方提供了这个答案,那里的问题是如何使用 sed 将 Windows 路径更改为 Linux 路径。 谢谢。 - Sandeepraj Singh
1
你知道你可以发布自己的问题并回答它。如果有那个特定的问题“如何将Windows路径更改为Linux”,那么如果人们真的在寻找这个问题,它会很有帮助。真正需要这个答案的人不太可能在这里找到它。 - Rajib

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