【问题标题】:bash-delete if string exists between two patterns如果两个模式之间存在字符串,则 bash-delete
【发布时间】:2023-03-13 21:22:01
【问题描述】:

我一直在尝试使用sed 来完成以下任务。假设我有以下文件(注意:我的实际文件比这更复杂):

hello world
foo bar
people people
target
something
done

我想检查两个模式之间是否存在target,在本例中,在foo bar 和done 行之间(包括这两行),如果target 确实存在,则删除整个模式。

我知道如何使用这个sed 命令删除两个模式之间的线:

sed '/people.*/,/done/d' file

但我只想在两个字符串匹配之间存在字符串target 时删除它。

我的逻辑是这样的:

sed -n '/people.*/,/done/p' file | check if target string exists | delete entire pattern found by sed

编辑

我忘了提到在同一行中target 之前和target 之后可以有任意数量的单词。

【问题讨论】:

  • 您的意思是删除people 和done 之间的所有内容,或者只删除target 这一行
  • 删除people和done之间的所有内容,包括people和done

标签: regex bash sed


【解决方案1】:

Sed

如果在其中找到$pattern,这将从$start 删除到$end:

sed ":a;N;\$!ba; s/$start.*$pattern.*$end//g"

这里有两个步骤(语句):

  1. 将整个文件作为单个字符串读取(根据文件大小可能会很糟糕)。如需很好的解释,请参阅https://stackoverflow.com/a/1252191。唯一的区别是 $!ba 之前的额外反引号,以使其与双引号一起使用,这对于在 sed 行内传递 Bash 变量很有用。
  2. 常规的旧搜索/替换。

Perl

要处理不贪婪的匹配,如果允许 Perl,请使用:

perl -0777 -p -e 's/$start.*?$pattern.*?$end//s'

这也会将整个文件作为字符串读取。最后的 /s 告诉它包含换行符作为正则表达式匹配的一部分。使用 .* 代替 .*?回到贪婪搜索。

【讨论】:

  • 完美运行...我知道N 是做什么的,但是您能否对:a 和$!ba 进行一些解释
  • 如果文件后面的某处有另一个done,这会不会贪婪地匹配?
  • 是的。 sed 无法处理不贪婪的匹配。为此,必须转向 PCRE 正则表达式引擎。也许是 Perl 或 Python?
  • 您的答案非常适合我上面提供的示例,但不幸的是它与我尝试过的其他一些文件贪婪地匹配:(
【解决方案2】:

sed 是在单行上进行简单替换的出色工具,但在 1970 年代中期,当 awk 被发明时,它所有用于处理多行的构造都已过时,因此只需使用 awk 来简化、清晰、稳健等。例如使用 GNU awk 进行多字符 RS:

$ awk -v RS='^$' '{sub(/\nfoo bar\n.*target.*\ndone\n/,""); print}' file
hello world

【讨论】:

    【解决方案3】:

    如果文件多次包含done,则无需先将整个文件读入内存并引发贪婪匹配问题的方法是

    sed '/^people/ { :loop; N; /\ndone/ ! b loop; /target/ d }' filename
    

    在 Mac OS X 上,显然需要在右括号之前有一个换行符,因此您可以将代码放入多行字符串文字中:

    sed '/^people/ { :loop; N; /\ndone/ ! b loop; /target/ d 
    }' filename
    

    或者将这个(无论如何更易读)版本的代码放在一个文件中,比如foo.sed,然后使用sed -f foo.sed filename:

    /^people/ {
      :loop
      N
      /\ndone/ ! b loop
      /target/ d
    }
    

    代码工作如下:

    /^people/ {
    

    在以“人”开头的行中

      :loop
      N
      /\ndone/ ! b loop
    

    在循环中获取更多行,直到一个以done 开头(这将是\ndone 第一次出现在模式空间中)

      /target/ d
    

    如果其中有target,请丢弃整个内容

    }
    

    否则照常进行(这意味着打印模式空间,因为我们没有将-n 传递给 sed)。

    稳健性的一个可能改进是

    sed '/^people/ { :loop; N; /\ndone$/! { $! b loop }; /target/ d }' filename
    

    或

    /^people/ {
      :loop
      N
      /\ndone/ ! {
        $ ! b loop
      }
      /target/ d
    }
    

    更改为/\ndone$/! { $! b loop }。即使没有遇到done,这也会在文件的最后一行结束循环,这会导致文件末尾未完成的people部分不会被丢弃(除非它们包含target)。

    【讨论】:

    • 这个答案看起来不错,但是当我尝试运行它时,我得到了这个错误:sed: 1: "/^people/ { :loop; N; ...": unexpected EOF (pending }'s)...有什么原因吗?顺便说一句,我正在使用 OSx。
    • 是的,我误读了错误(因此忍者删除)。不挂断。我无法测试 OSX,但也许某处有差异文档。
    • 在最后的d后面加一个;是否有效?
    • 不,仍然出现同样的错误,我认为这可能与 { 括号有关,我之前在使用 sed 时遇到过问题。
    • 好的,我在我的 FreeBSD 虚拟机(显然与 OSX 具有相同的 sed)中尝试了它。如果您将代码的可读版本放入一个文件并使用sed -f foo.sed filename,或者如果您将带有代码的字符串文字拉伸到多行(} 之前有一个换行符),它就会起作用。
    【解决方案4】:

    迟到的答复

    sed '/^foo bar *$/,/^done *$/{/^done *$/!{H;d};/^done *$/{H;g;s/.*//g;x;/.*target.*/d;s/^.//g}}'
    

    找到/^foo bar *$/,/^done *$/之间的所有行

    /foo bar/,/done/
    

    这个/^done *$/!{H;d} 从 foo bar 中取出所有行,但不是最后一行“完成”,并将其放在保持空间中。然后从模式空间中删除这些行。

    这个/^done *$/{H;g;s/.*//g;x; 取最后一行“完成”并将其附加到保留空间。现在我们有了从 foo bar 行到在保持空间中完成的行的所有行。之后,我们清除模式空间中的所有内容,并将保持空间中的行范围与模式空间中的空行交换(这是为了在瞄准“之间的另一个行范围时始终保持保持空间为空” foo bar”和“完成”。

    终于

    /.*target.*/d 
    

    我们测试“目标”是否在多模式空间中。如果是,“foo bar”和“done”之间的行范围将被删除

    这样可以避免将整个文件作为单个字符串读取

    示例

    hello world
    foo bar
    people people
    target
    something
    done
    foo bar
    .....
    .....
    .....
    done
    foo bar
    people people
    test
    something
    done
    

    结果

    hello world
    foo bar
    .....
    .....
    .....
    done
    foo bar
    people people
    test
    something
    done
    

    注意:从 "foo bar" 到 "done" 的行范围内包含 "target" 的行正在被删除

    【讨论】:

      猜你喜欢
      • 2014-11-15
      • 2013-05-28
      • 1970-01-01
      • 2014-07-18
      • 2017-01-15
      • 2018-11-22
      • 1970-01-01
      • 1970-01-01
      • 2022-06-16
      相关资源
      最近更新 更多