【问题标题】:Why does sed not replace overlapping patterns为什么 sed 不替换重叠模式
【发布时间】:2021-01-22 03:37:32
【问题描述】:

我有一个数据库卸载文件,其中的字段用 字符分隔。我正在通过 sed 运行此文件,以用 \N 替换任何出现的 。这样当文件加载到 MySQL 中时,\N 被解释为 NULL。

sed 命令 's/\t\t/\t\N\t/g;'几乎可以工作,只是它只替换第一个实例,例如“......”变为“...\N...”。

如果我使用 's/\t\t/\t\N\t/g;s/\t\t/\t\N\t/g;'它替换了更多实例。

我有一个概念,尽管有 /g 修饰符,但这与一场比赛的结束是另一场比赛的开始有关。

谁能解释正在发生的事情并提出一个可行的 sed 命令或者我是否需要循环。

我知道我可能会切换到 awk、perl、python,但我想知道 sed 中发生了什么。

【问题讨论】:

  • 如果您可以明确回答替换重叠时应该发生的情况,我相信这会引起很多关注。如果输入是 a 并且请求替换 s/a/b/&s/b/a/,您的“并行”sed 会发生什么? (我使用 & 作为命令分隔符来表示两者应该同时发生。)
  • 另一个问题是无限循环:s/something/something or other/sed 需要很长时间才能运行(或者更准确地说,它耗尽了内存)!
  • 感谢@triplee,当您认为重新扫描该行可能会引入循环时,这一点变得显而易见。
  • 需要注意的是,如果\t=a\N=b 然后aaa 在第一个替换将被abaaababa 替换在第二。因此不需要循环来替换所有情况,只需两次应用相同的全局替换。

标签: shell unix sed


【解决方案1】:

我知道你想要 sed,但 sed 根本不喜欢这个,它似乎特别(见here)不会做你想做的事。但是,perl 会这样做(AFAIK):

perl -pe 'while (s#\t\t#\t\n\t#) {}' <filename>

【讨论】:

  • 也许这是为了确保尾随的空值不会被视为空字符串? (注意我用 \\N 替换了 \n) perl -pe 'while (s#\t\t#\t\\Nt#) {}' -pe 's/\t$/\t\\N/ g'
  • Perl 支持正向前瞻,所以perl -pe 's|\t(?=\t)|\t\n|g' 有效
【解决方案2】:

作为一种解决方法,将每个选项卡替换为 tab + \N;然后删除所有没有紧跟制表符的 \N。

sed -e 's/\t/\t\\N/g' -e 's/\\N\([^\t]\)/\1/g'

...前提是您的 sed 在分组括号之前使用了反斜杠(有些 sed 方言不需要反斜杠;如果这不适合您,请尝试不使用反斜杠。)

【讨论】:

    【解决方案3】:

    与 perl 解决方案没有什么不同,这对我使用纯 sed 有效

    随着@Robin A. Meade 的改进

    sed ':repeat;
         s|\t\t|\t\n\t|g;
         t repeat'
    

    说明

    • :repeat是标签,用于分支命令,类似于批处理
    • s|\t\t|\t\n\t|g; - 标准用 tab-newline-tab 替换 2 个选项卡。我仍然使用全局标志,因为如果您有 15 个选项卡,则只需循环两次,而不是 14 次。
    • t repeat 表示如果“s”命令进行了任何替换,则转到标签 repeat,否则转到下一行并重新开始。

    所以事情是这样的。只要有 2 个选项卡的模式匹配,就继续重复(转到 repeat)。

    虽然可以说您可以只执行两个相同的全局替换并称其为好,但同样的技术可以在更复杂的场景中工作。

    正如@thorn-blake 指出的那样,sed 只是不支持前瞻等高级功能,因此您需要执行这样的循环。

    原答案

    sed ':repeat;
         /\t\t/{
           s|\t\t|\t\n\t|g;
           b repeat
         }'
    

    说明

    • :repeat是标签,用于分支命令,类似于批处理
    • /\t\t/ 表示匹配模式 2 个选项卡。如果它匹配的模式,则执行第二个 / 之后的命令。
    • {} - 在这种情况下,匹配命令之后的命令是一个组。因此,如果满足匹配模式,则执行组中的所有命令。
    • s|\t\t|\t\n\t|g; - 标准用 tab-newline-tab 替换 2 个 tab。我仍然使用全局,因为如果你有 15 个标签,你只需要循环两次,而不是 14 次。
    • b repeat 表示总是转到(分支)标签repeat

    短版

    可以简写成

    sed ':r;s|\t\t|\t\n\t|g; t r'
    
    # Original answer
    # sed ':r;/\t\t/{s|\t\t|\t\n\t|g; b r}'
    

    MacOS

    Mac(但仍兼容 Linux/Windows)版本:

    sed $':r\ns|\t\t|\t\\\n\t|g; t r'
    
    # Original answer
    # sed $':r\n/\t\t/{ s|\t\t|\t\\\n\t|g; b r\n}'
    
    • 制表符在 BSD sed 中必须是文字
    • 换行符需要同时是文字和转义,因此单个斜杠(即在 $ 处理之前是 \,使其成为单个文字斜杠)加上成为实际换行符的 \n >
    • 标签名称 (:r) 和分支命令(当不是表达式结尾时为 b r)都必须以换行符结尾。 BSD 中的 label name/branch 命令会使用分号和空格等特殊字符,这使得这一切变得非常混乱。

    【讨论】:

    • 可以通过将branch command 从无条件的b 更改为有条件的t,将您的解决方案简化为sed ':repeat; s|\t\t|\t\n\t|g; t repeat'
    【解决方案4】:

    是的,即使使用/g,sed 也不会再次匹配它替换的文本。因此,它读取&lt;TAB&gt;&lt;TAB&gt; 并输出&lt;TAB&gt;\N&lt;TAB&gt;,然后从输入流中读取下一个内容。见http://www.grymoire.com/Unix/Sed.html#uh-7

    在支持前瞻的正则表达式语言中,您可以通过前瞻来解决这个问题。

    【讨论】:

      【解决方案5】:

      好吧,sed 只是按设计工作。输入行被扫描一次,而不是多次。如果sed 默认使用重新扫描输入行来处理重叠模式,那么看看后果可能会有所帮助:在这种情况下,即使是简单的替换也会完全不同——有些人可能会说违反直觉——例如

      • s/^/ / 在行首插入空格永远不会终止
      • s/$/foo/ 将 foo 附加到每一行 - 同样
      • s/[A-Z][A-Z]*/CENSORED/ 用 CENSORED 替换大写单词 - 同样

      可能还有很多其他情况。当然,这些都可以通过替换修饰符来补救,但在设计 sed 时,选择了当前行为。

      【讨论】: