【问题标题】:sed: capturing a recurring regex group that happens to be optionalsed:捕获恰好是可选的重复出现的正则表达式组
【发布时间】:2019-12-06 08:01:28
【问题描述】:

我有一些文件的名称如下例所示:

2000_A_tim110_may112_AATT_V22_P001_R1_001_V23_P007_R2_001_comb.ext
2000_BB_tim110_may112_AAMM_V14_P002_R1_001_V45_P008_R2_001_comb.ext
2000_C_tim110_DDFF_V18_P006_R1_001.ext
2000_DD_may112_EEJJ_V88_P004_R1_001.ext

从这些文件名中,我想提取前导 2000_[A-Z]{1,2}V[0-9]{2} 正则表达式模式的所有实例。

也就是说,

来自

2000_A_tim110_may112_AATT_V22_P001_R1_001_V23_P007_R2_001_comb.ext

我想要

2000_A_V22_V23

2000_DD_may112_EEJJ_V88_P004_R1_001.ext

我想要

2000_DD_V88

我一直在尝试通过 sed 实现这一目标,但到目前为止我还没有取得任何成功。

起初——相当天真——我尝试过

find *.ext | sed -r 's/^(2000_[A-Z]{1,2}).*(V{1}[0-9]{2,3}).*(V{1}[0-9]{2,3}).*\.ext/\1_\2_\3/'

结果如下:

2000_A_V22_V23
2000_BB_V14_V45
2000_C_tim110_DDFF_V18_P006_R1_001.ext
2000_DD_may112_EEJJ_V88_P004_R1_001.ext

这不是我想要的,因为这里有两个文件名未经编辑返回。

然后,在阅读了this post 之后,我尝试将在中间捕获的组设为可选,如下所示:

find *.ext | sed -r 's/^(2000_[A-Z]{1,2}).*(V{1}[0-9]{2})?.*(V{1}[0-9]{2}).*\.ext/\1_\2_\3/'

但这似乎也没有用,因为它返回了

2000_A__V23
2000_BB__V45
2000_C__V18
2000_DD__V88

(即中间的捕获组似乎已被完全跳过。)

我的问题是,我如何得到以下结果?

2000_A_V22_V23
2000_BB_V14_V45
2000_C_V18
2000_DD_V88

我哪里错了?或者相反,我错过了什么?我对sedregex 很陌生--我想学习如何很好地使用这两种方法--因此非常感谢您的指点和指导。

【问题讨论】:

  • sed 中至少很难做到——我几乎已经准备好说“不可能”,但这可能不太正确。您可能不得不反复删除您不想要的位,sed 可以这样做(标签、测试和分支,尽管负面模式使生活变得复杂;这些模式可能会在前后利用下划线),但是这既不简单也不明显。
  • 如果您想学习 sed,我们可以为您提供 sed 解决方案。如果您想要一个可行的简单解决方案,您应该选择不同的工具。
  • @Beta,我不介意接受上述教育(只要对您/教育者来说不会太麻烦)。作为一个新手,很难确定哪种工具适合手头的任务——例如,Ed Morton 发布了一个简洁的答案,它使用了我以前从未真正使用过的awk,但遇到过各种被吹捧的情况作为与sed相当的工具。我觉得您在此处提供给我的任何信息都将帮助我更好地了解这些工具及其相对优势/劣势。
  • @JonathanLeffler 不需要 sed 中的硬工具,只需替换 | 操作员就可以完成这项工作。看我的回答。

标签: regex string bash sed regex-group


【解决方案1】:

使用 GNU awk 进行 FPAT:

$ awk -v FPAT='^2000_[A-Z]{1,2}|V[0-9]{2}' '{out=$1; for (i=2; i<=NF;i++) out=out "_" $i; print out}' file
2000_A_V22_V23
2000_BB_V14_V45
2000_C_V18
2000_DD_V88

【讨论】:

    【解决方案2】:

    正如我在comment 中指出的那样,在sed 中完成这项工作非常困难。但是,通过仔细使用分支和测试,是可以做到的。

    我使用的是经典的sed BRE 表示法;如果您选择使用更现代但不一定作为可移植 ERE 表示法,您可以消除相当数量的反斜杠。我还将脚本保存在文件sed.script 中,并将示例数据保存在文件data 中,并使用以下命令运行命令:

    $ sed -f sed.script data
    2000_A_V22_V23
    2000_BB_V14_V45
    2000_C_V18
    2000_DD_V88
    $
    

    脚本包含:

    :retry
    s/^\(2000_[A-Z]\{1,2\}\(_V[0-9][0-9]\)*\)_[^_]\{1,\}$/\1/
    t
    s/^\(2000_[A-Z]\{1,2\}\(_V[0-9][0-9]\)*\)_[^_]\{1,\}_/\1_/
    t retry
    
    • 第一行设置标签retry
    • 第一行s/// 查找2000_,后跟一个或两个大写字母,然后是零个或多个下划线、V 和两个数字的序列(这都记住了);然后是下划线和一个或多个非下划线的序列和行尾。这被记住的材料所取代。
    • 如果第一个 s/// 匹配,则它分支到脚本的末尾(t 没有标签名称)。这将导致打印行。
    • 第二行s/// 与第一行非常相似,不同之处在于它不是查找行尾,而是在下划线和非下划线序列之后查找另一个下划线。请注意,查找_V##(其中# 表示一个数字)的术语会尽可能多地找到它们,因此_xxx_ 术语与_V##_ 不匹配。替换为记住的术语和下划线,因此它会从字符串中删除一个单位 _xxx_
    • 如果第二个 s/// 匹配,则分支回到脚本的开头。
    • 理论上,如果第二个s/// 不匹配,则循环中断并打印剩余的内容。在实践中,样本数据并没有达到,但如果输入行根本不匹配(例如,它以 2001 而不是 2000 开始),那么在没有被任何一个处理之后,它将被打印出来并保持不变s/// 操作。
    • 如果应该删除与开始模式不匹配的行,可以通过在脚本开头添加一行来处理:

      /^2000_[A-Z]\{1,2\}/!d
      
    • 如果行不包含任何_V##_ 序列,也可以处理,在retry 标签前添加更多行。如果在一行的末尾有_V##(并且没有更早的地方),那么它会跳过下一行。下一行在一行中间查找_V##_,如果不匹配则删除该行。

      /_V[0-9][0-9]$/b skip
      /_V[0-9][0-9]_/!d
      :skip
      

    您可以通过在每个s/// 操作之后添加p 来查看其进展情况,这也显示了中间结果:

    2000_A_may112_AATT_V22_P001_R1_001_V23_P007_R2_001_comb.ext
    2000_A_AATT_V22_P001_R1_001_V23_P007_R2_001_comb.ext
    2000_A_V22_P001_R1_001_V23_P007_R2_001_comb.ext
    2000_A_V22_R1_001_V23_P007_R2_001_comb.ext
    2000_A_V22_001_V23_P007_R2_001_comb.ext
    2000_A_V22_V23_P007_R2_001_comb.ext
    2000_A_V22_V23_R2_001_comb.ext
    2000_A_V22_V23_001_comb.ext
    2000_A_V22_V23_comb.ext
    2000_A_V22_V23
    2000_A_V22_V23
    2000_BB_may112_AAMM_V14_P002_R1_001_V45_P008_R2_001_comb.ext
    2000_BB_AAMM_V14_P002_R1_001_V45_P008_R2_001_comb.ext
    2000_BB_V14_P002_R1_001_V45_P008_R2_001_comb.ext
    2000_BB_V14_R1_001_V45_P008_R2_001_comb.ext
    2000_BB_V14_001_V45_P008_R2_001_comb.ext
    2000_BB_V14_V45_P008_R2_001_comb.ext
    2000_BB_V14_V45_R2_001_comb.ext
    2000_BB_V14_V45_001_comb.ext
    2000_BB_V14_V45_comb.ext
    2000_BB_V14_V45
    2000_BB_V14_V45
    2000_C_DDFF_V18_P006_R1_001.ext
    2000_C_V18_P006_R1_001.ext
    2000_C_V18_R1_001.ext
    2000_C_V18_001.ext
    2000_C_V18
    2000_C_V18
    2000_DD_EEJJ_V88_P004_R1_001.ext
    2000_DD_V88_P004_R1_001.ext
    2000_DD_V88_R1_001.ext
    2000_DD_V88_001.ext
    2000_DD_V88
    2000_DD_V88
    

    如果您的 sed 支持 POSIX sed 要求的扩展,您可以简化脚本。例如,如果您可以使用|+,则可能有简化脚本的选项。这应该适用于任何版本的sed

    此代码已使用 macOS (BSD) sed 和 GNU sed 进行了测试,并且两者的工作方式相同。

    【讨论】:

    • 感谢@Jonathan Leffler 的详细回答。我非常感谢您的分步解释,而且您涵盖异常情况的事实也给我留下了深刻的印象。
    • 我认为更简单的解决方案是可能的::a&lt;CR&gt;/^2000_[A-Z]*\(_V[0-9]\{2\}\)*$/b&lt;CR&gt;s/\(^2000_[A-Z]*\(_V[0-9]\{2\}\)*\)_[^_]*/\1/&lt;CR&gt;ba
    【解决方案3】:

    作为纯bash 解决方案(对不起,没有sed),怎么样:

    #!/bin/bash
    
    pat='((^2000_[A-Z]{1,2})|(_V[0-9]{2}))(.*)'
    while IFS= read -r -d '' line; do
        result=
        while [[ $line =~ $pat ]]; do
            result+="${BASH_REMATCH[1]}"
            line="${BASH_REMATCH[4]}"
        done
        [[ -n "$result" ]] && echo "$result"
    done < <(find . -type f -name '*.ext' -printf '%f\0')
    

    输出:

    2000_A_V22_V23
    2000_BB_V14_V45
    2000_C_V18
    2000_DD_V88
    

    【讨论】:

      【解决方案4】:

      您可以将grep 与循环一起使用:

      for f in $(find 2000* -regex '2000_[A-Z].*ext'); do
          printf "%s\n" $(grep -Eo "^2000_[A-Z]{1,2}|_V[0-9]{2}" <<<"$f" | tr -d "\n")
      done
      

      【讨论】:

        【解决方案5】:

        基本 sed 有什么困难?通过 sed 的替代功能利用交替 | 运算符的强大功能。

        $ cat sedtets 
        2000_A_tim110_may112_AATT_V22_P001_R1_001_V23_P007_R2_001_comb.ext
        2000_BB_tim110_may112_AAMM_V14_P002_R1_001_V45_P008_R2_001_comb.ext
        2000_C_tim110_DDFF_V18_P006_R1_001.ext
        2000_DD_may112_EEJJ_V88_P004_R1_001.ext
        
        $ sed 's/\(2000_[A-Z]\{1,2\}\|_V[0-9]\+\)\|./\1/g' sedtets
        2000_A_V22_V23
        2000_BB_V14_V45
        2000_C_V18
        2000_DD_V88
        

        DEMO

        这里的逻辑是使用单个捕获组捕获所有必要的部分,然后匹配所有剩余的字符。

        然后用捕获的字符替换所有匹配的捕获字符。这将只保留捕获的字符并删除所有匹配的字符。

        【讨论】:

        • 显示的 sed 脚本不适用于 macOS (BSD) sed,无论有无 -E(扩展正则表达式)选项。 GNU sed 确实接受它而不需要 -E 选项。但是,给定一行2001_DD_V96,它会输出_V96。给定一行2000_BB_tim110_may112_AAMM_P002_R1_001_P008_R2_001_comb.ext,其中没有_V##,它输出2000_BB。当然,这些不在问题的数据中,所以不清楚正确的行为是什么,但很可能这两行都不应该产生任何输出。 2001_DD… 行肯定与所需的 2000_… 前缀不匹配。
        • 2000_ 前缀可以通过将find *.ext | 更改为find 2000_*.ext | 来处理
        • 如果我们在重命名实用程序中使用相同的正则表达式(不带斜杠),我们甚至不需要查找
        • 您能解释一下为什么这里需要使用| 运算符吗?尽管我接受了@Jonathan Leffler 的回答,因为它具有教育意义,但我必须承认,您在此处提出的解决方案将是最直接的解决方案。正如你们都可以猜到的那样,文件名中的数据没有差异,例如 2001_ 而不是 2000_ 等等。也许我应该在 OP 中澄清这一点;我没有预见到人们会从问题中提出的内容思考这么远。 : ) : )
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-11-21
        • 2021-12-12
        • 1970-01-01
        • 2017-09-13
        • 2011-03-11
        相关资源
        最近更新 更多