sed:捕获恰好是可选的重复出现的正则表达式组

sed: capturing a recurring regex group that happens to be optional

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

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/__/'

结果是:

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/__/'

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

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 的新手——我想学会很好地使用它们——所以非常感谢您的指点和指导。

对于 FPAT 使用 GNU awk:

$ awk -v FPAT='^2000_[A-Z]{1,2}|V[0-9]{2}' '{out=; 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

作为纯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[=10=]')

输出:

2000_A_V22_V23
2000_BB_V14_V45
2000_C_V18
2000_DD_V88

基本 sed 有什么困难?利用 alternation | 运算符的强大功能和 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]\+\)\|.//g' sedtets
2000_A_V22_V23
2000_BB_V14_V45
2000_C_V18
2000_DD_V88

DEMO

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

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

正如我在 中指出的那样,在 sed 中完成这项工作非常困难。不过仔细使用b运行ching和测试,是可以做到的。

我使用的是经典的 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,\}$//
t
s/^\(2000_[A-Z]\{1,2\}\(_V[0-9][0-9]\)*\)_[^_]\{1,\}_/_/
t retry
  • 第一行设置一个标签retry.
  • 第一行 s/// 查找 2000_ 后跟一个或两个大写字母,然后是下划线、V 和两个数字的零个或多个实例序列(这是全部记住);然后是下划线和一系列一个或多个非下划线和行尾。这被记住的 material.
  • 取代
  • 如果第一个 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 上进行了测试,两者的工作原理相同。

您可以将 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