【问题标题】:gawk or grep: single line and ungreedygawk 或 grep:单行且不贪婪
【发布时间】:2021-03-04 06:09:06
【问题描述】:

我想在所有具有两个以上类型参数的子目录中递归地打印 *.java 文件的标题(即下面示例中 <R ... H> 中的参数)。其中一个文件看起来像(为简洁起见减少了名称):

multiple-lines.java

class ClazzA<R extends A,
    S extends B<T>, T extends C<T>,
    U extends D, W extends E,
    X extends F, Y extends G, Z extends H>
    extends OtherClazz<S> implements I<T> {

  public void method(Type<Q, R> x) { 
    // ... code ...
  }
}

预期输出:

ClazzA.java:10: class ClazzA<R extends A,
ClazzA.java:11:     S extends B<T>, T extends C<T>,
ClazzA.java:12:     U extends D, W extends E,
ClazzA.java:13:     X extends F, Y extends G, Z extends H>
ClazzA.java:14:     extends OtherClazz<S> implements I<T> {

但另一个也可能是这样的:

single-line.java

class ClazzB<R extends A, S extends B<T>, T extends C<T>, U extends D, W extends E, X extends F, Y extends G, Z extends H> extends OtherClazz<S> implements I<T> {

  public void method(Type<Q, R> x) { 
    // ... code ...
  }
}

预期输出:

ClazzB.java:42: class ClazzB<R extends A, S extends B<T>, T extends C<T>, U extends D, W extends E, X extends F, Y extends G, Z extends H> extends OtherClazz<S> implements I<T> {

不应考虑/打印的文件:

X-no-parameter.java

class ClazzC /* no type parameter */ extends OtherClazz<S> implements I<T> {

  public void method(Type<A, B> x) { 
    // ... code ...
  }
}

X-one-parameter.java

class ClazzD<R extends A>  // only one type parameter
    extends OtherClazz<S> implements I<T> {

  public void method(Type<X, Y> x) { 
    // ... code ...
  }
}

X-two-parameters.java

class ClazzE<R extends A, S extends B<T>>  // only two type parameters
    extends OtherClazz<S> implements I<T> {

  public void method(Type<X, Y> x) { 
    // ... code ...
  }
}

X-two-line-parameters.java

class ClazzF<R extends A,  // only two type parameters
    S extends B<T>>        // on two lines
    extends OtherClazz<S> implements I<T> {

  public void method(Type<X, Y> x) { 
    // ... code ...
  }
}

文件中的所有空格都可以是\s+。紧接在{ 之前的extends [...]implements [...] 是可选的。 extends [...] 在每个类型参数中也是可选的。详情请见The Java® Language Specification, 8.1. Class Declarations

我在 Git Bash 中使用gawk

$ gawk --version
GNU Awk 5.0.0, API: 2.0 (GNU MPFR 4.0.2, GNU MP 6.2.0)

与:

find . -type f -name '*.java' | xargs gawk -f ws-class-type-parameter.awk > ws-class-type-parameter.log

ws-class-type-parameter.awk:

# /start/ , /end/ ... pattern

#/class ClazzA<.*,.*/      , /{/  {    # 5 lines, OK for ClazzA, but in real it prints classes with 2 or less type parameters, too
#/class ClazzA<.*,.*,/     , /{/  {    # no line with ClazzA, since there's no second ',' on its first line
#/class ClazzA<.*,.*,/s    , /{/  {    # 500.000+(!) lines
#/class ClazzA<.*,.*,/s    , /{/U {    # 500.000+(!) lines
#/class ClazzA<.*,.*,/sU   , /{/U {    # 500.000+(!) lines
 /(?s)class ClazzA<.*,.*,/ , /{/  {    # no line

    match( FILENAME, "/.*/.." )
    print substr( FILENAME, RLENGTH ) ":" FNR ": " $0
}

这会找到所有*.java 文件...很好,对每个文件执行gawk...很好,但是在我尝试之后您会看到结果为 cmets。请注意:ClazzA 文字仅用于测试,MCVE 在这里。实际可能是\w+,但在测试时,数千个文件中有 500.000 多行...

如果我在regex101.com 上尝试它会起作用。嗯,有点。我没有找到如何在那里定义/start-regex/,/end-regex/,所以我在两者之间添加了另一个.*

我从那里拿到了标志,但我找不到gawk 是否支持标志语法/.../sU , /.../U 的描述,所以我试了一下。一条现已删除的评论告诉我,没有任何 awk 支持这一点。

我也试过grep:

$ grep --version
grep (GNU grep) 3.1
...
$ grep -nrPf types.grep *.java

使用 types.grep

(?s).*class\s+\w+\s*<.*,.*,.*>.*{

这只会导致 singleline.java 的输出。

(?s)--perl-regexp, -P 语法,grep --help 声称支持这一点。

更新

Ed Morton 的答案中的解决方案效果很好,但事实证明有自动生成的文件具有以下方法:

    /** more code before here */    
    public void setId(String value) {
        this.id = value;
    }

    /**
     * Gets a map that contains attributes that aren't bound to any typed property on this class.
     * 
     * <p>
     * the map is keyed by the name of the attribute and 
     * the value is the string value of the attribute.
     * 
     * the map returned by this method is live, and you can add new attribute
     * by updating the map directly. Because of this design, there's no setter.
     * 
     * 
     * @return
     *     always non-null
     */
    public Map<QName, String> getOtherAttributes() {
        return otherAttributes;
    }

输出例如:

AbstractAddressType.java:81:      * Gets a map that contains attributes that aren't bound to any typed property on this class.
AbstractAddressType.java:82:      * 
AbstractAddressType.java:83:      * <p>
AbstractAddressType.java:84:      * the map is keyed by the name of the attribute and 
AbstractAddressType.java:85:      * the value is the string value of the attribute.
AbstractAddressType.java:86:      * 
AbstractAddressType.java:87:      * the map returned by this method is live, and you can add new attribute
AbstractAddressType.java:88:      * by updating the map directly. Because of this design, there's no setter.
AbstractAddressType.java:89:      * 
AbstractAddressType.java:90:      * 
AbstractAddressType.java:91:      * @return
AbstractAddressType.java:92:      *     always non-null
AbstractAddressType.java:93:      */
AbstractAddressType.java:94:     public Map<QName, String> getOtherAttributes() {

以及其他具有类 cmets 和注释的人,例如:

/**
 * This class was generated by Apache CXF 3.3.4
 * 2020-11-30T12:03:21.251+01:00
 * Generated source version: 3.3.4
 *
 */
@WebService(targetNamespace = "urn:SZRServices", name = "SZR")
@XmlSeeAlso({at.gv.egov.pvp1.ObjectFactory.class, org.w3._2001._04.xmldsig_more_.ObjectFactory.class, ObjectFactory.class, org.xmlsoap.schemas.ws._2002._04.secext.ObjectFactory.class, org.w3._2000._09.xmldsig_.ObjectFactory.class, at.gv.e_government.reference.namespace.persondata._20020228_.ObjectFactory.class})
public interface SZR {
// more code after here

输出例如:

SZR.java:13:  * This class was generated by Apache CXF 3.3.4
SZR.java:14:  * 2020-10-12T11:51:35.175+02:00
SZR.java:15:  * Generated source version: 3.3.4
SZR.java:16:  *
SZR.java:17:  */
SZR.java:18: @WebService(targetNamespace = "urn:SZRServices", name = "SZR")
SZR.java:19: @XmlSeeAlso({at.gv.egov.pvp1.ObjectFactory.class, org.w3._2001._04.xmldsig_more_.ObjectFactory.class, ObjectFactory.class, org.xmlsoap.schemas.ws._2002._04.secext.ObjectFactory.class, org.w3._2000._09.xmldsig_.ObjectFactory.class, at.gv.e_government.reference.namespace.persondata._20020228_.ObjectFactory.class})

【问题讨论】:

  • 如果我必须做这样的事情,我不会尝试单个正则表达式,我会用 perl 编写一个小型状态机。我的头顶有 3 个状态“寻找 ” - 这处理了将用单个正则表达式造成严重破坏的多行问题 :-)
  • @John3136 感谢您的评论。一条现已删除的评论也提议编写 Java 类解析器。您(或其他人,就此而言)能否至少确认它应该与 Perl 正则表达式 (?s) 一起工作——这个想法是正确的吗?
  • 对于您尝试过的 grep 命令,您还需要 -z 选项来匹配多行(我没有尝试完全理解这个问题,只是给出一个建议).. 但是,您还需要 -o 选项来仅显示匹配的部分,您不会在输出中获得行号,每次匹配结束时都会添加一个 ASCII NUL 字符,等等
  • 如果你可以安装github.com/BurntSushi/ripgrep,你会得到更快的结果,你可以使用-U选项进行多行匹配(不需要使用NUL作为输入行分隔符)以及行号输出等

标签: java regex awk grep git-bash


【解决方案1】:

在每个 UNIX 机器上的任何 shell 中使用任何 POSIX awk:

$ cat tst.awk
/[[:space:]]*class[[:space:]]*/ {
    inDef = 1
    fname = FILENAME
    sub(".*/","",fname)
    def = out = ""
}
inDef {
    out = out fname ":" FNR ": " $0 ORS

    # Remove comments (not perfect but should work for 99.9% of cases)
    sub("//.*","")
    gsub("/[*]|[*]/","\n")
    gsub(/\n[^\n]*\n/,"")

    def = def $0 ORS
    if ( /{/ ) {
        if ( gsub(/,/,"&",def) > 2 ) {
            printf "%s", out
        }
        inDef = 0
    }
}

$ find tmp -type f -name '*.java' -exec awk -f tst.awk {} +
multiple-lines.java:1: class ClazzA<R extends A,
multiple-lines.java:2:     S extends B<T>, T extends C<T>,
multiple-lines.java:3:     U extends D, W extends E,
multiple-lines.java:4:     X extends F, Y extends G, Z extends H>
multiple-lines.java:5:     extends OtherClazz<S> implements I<T> {
single-line.java:1: class ClazzB<R extends A, S extends B<T>, T extends C<T>, U extends D, W extends E, X extends F, Y extends G, Z extends H> extends OtherClazz<S> implements I<T> {

上面是使用这个输入运行的:

$ head tmp/*
==> tmp/X-no-parameter.java <==
class ClazzC /* no type parameter */ extends OtherClazz<S> implements I<T> {

  public void method(Type<A, B> x) {
    // ... code ...
  }
}

==> tmp/X-one-parameter.java <==
class ClazzD<R extends A>  // only one type parameter
    extends OtherClazz<S> implements I<T> {

  public void method(Type<X, Y> x) {
    // ... code ...
  }
}

==> tmp/X-two-line-parameters.java <==
class ClazzF<R extends A,  // only two type parameters
    S extends B<T>>        // on two lines
    extends OtherClazz<S> implements I<T> {

  public void method(Type<X, Y> x) {
    // ... code ...
  }
}

==> tmp/X-two-parameters.java <==
class ClazzE<R extends A, S extends B<T>>  // only two type parameters
    extends OtherClazz<S> implements I<T> {

  public void method(Type<X, Y> x) {
    // ... code ...
  }
}

==> tmp/multiple-lines.java <==
class ClazzA<R extends A,
    S extends B<T>, T extends C<T>,
    U extends D, W extends E,
    X extends F, Y extends G, Z extends H>
    extends OtherClazz<S> implements I<T> {

  public void method(Type<Q, R> x) {
    // ... code ...
  }
}

==> tmp/single-line.java <==
class ClazzB<R extends A, S extends B<T>, T extends C<T>, U extends D, W extends E, X extends F, Y extends G, Z extends H> extends OtherClazz<S> implements I<T> {

  public void method(Type<Q, R> x) {
    // ... code ...
  }
}

以上只是尽力而为,没有为该语言编写解析器,只是让 OP 发布示例输入/输出以继续处理需要处理的内容。

【讨论】:

  • 虽然我还不知道这在每个细节上都有什么作用,但在删除^ 之前的class 之后,它就像一个魅力。非常感谢!
  • 那么你的源代码中必须在class 之前有一些东西。如果它是空白,则将 ^ 替换为 ^[[:space:]]* 而不是删除它。如果它不是空白,那么让我知道它是什么,我会告诉你如何处理它。不要只是删除^,否则您将开始获得部分匹配的不良结果。不客气。
  • 是的,我愿意。这就是为什么我在问题中只使用了/class。它可能是(零个或多个)class modifiers,在文字之前带有注释。虽然到目前为止我还没有在同一行看到任何带有注释的类。它们通常在前几行。
  • 好的,那么使用/[[:space:]]*class[[:space:]]*/ 可能就足够了,因为无论如何这只是一个尽力而为的解决方案。
  • ...有点像我在 cmets 但在 awk 中建议的状态机 :-)
【解决方案2】:

注意:存在 cmets 可能会导致这些解决方案失败。

ripgrep (https://github.com/BurntSushi/ripgrep)

rg -nU --no-heading '(?s)class\s+\w+\s*<[^{]*,[^{]*,[^{]*>[^{]*\{' *.java
  • -n 启用行号(如果输出到终端,这是默认设置)
  • -U 启用多行匹配
  • --no-heading 默认情况下,ripgrep 显示在文件名下分组的匹配行作为标题,此选项使ripgrep 的行为类似于GNU grep,每个输出行都有文件名前缀
  • 使用[^{]* 代替.* 以防止匹配文件中其他位置的,&gt;,否则将匹配public void method(Type&lt;Q, R&gt; x) { 之类的行
  • -m 选项可用于限制每个输入文件的匹配数,这将带来一个额外的好处,即不必搜索整个输入文件

如果您将上述正则表达式与GNU grep 一起使用,请注意:

  • grep 一次只匹配一行。如果您使用-z 选项,grep 将考虑 ASCII NUL 作为记录分隔符,这有效地使您能够跨多行匹配,假设输入没有可以阻止这种匹配的 NUL 字符。 -z 选项的另一个效果是 NUL 字符将附加到每个输出结果(这可以通过管道结果到 tr '\0' '\n' 来解决)
  • 仅打印匹配部分需要-o 选项,这意味着您将无法获得行号前缀
  • 对于给定的任务,-P 不是必需的,grep -zoE 'class\s+\w+\s*&lt;[^{]*,[^{]*,[^{]*&gt;[^{]*\{' *.java | tr '\0' '\n' 将为您提供与ripgrep 命令相似的结果。但是,您不会获得行号前缀,文件名前缀将仅适用于每个匹配部分而不是每个匹配行,并且您不会在 class 之前和 { 之后获得其余行

【讨论】:

  • 感谢您的回答,包括如此详细的解释。我真的很想给你额外的赏金,表达我的感激之情,但是I don't see anything I can do to achieve this。 (甚至创建一些虚假的 SO 帐户来支持您的 A 都不会这样做。:)
  • 获得赏金会感觉很好。但总的来说,我喜欢回答 SO,而且这个问题也是一次很好的学习经历(尤其是 Ed 的回答,一如既往)。
  • 如果在 cmets 中出现 ,{,这些正则表达式将失败。不确定是否/如何使用正则表达式解决该问题。
  • @EdMorton 刚刚添加了一条注释。当前的解决方案将失败,class&lt; 之间也存在注释。解决的一种方法是在应用之前删除 cmets用于匹配的正则表达式.. PCRE 可能有其他方法,但我不会考虑/尝试它.. 我修改了 OP 对此答案的尝试..
  • 好点,如果您先删除 cmets,则行号可能会关闭。也许有一种方法可以使用 PCRE,idk,而不是我曾经涉足或想要涉足的东西!
猜你喜欢
  • 2020-03-26
  • 2011-11-17
  • 2011-03-02
  • 2015-01-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多