Patchwork [1,of,2] templatefuncs: add regexp search() function that extracts substring

login
register
mail settings
Submitter Yuya Nishihara
Date Dec. 13, 2018, 1:35 p.m.
Message ID <eeb0c3a6209241471368.1544708116@mimosa>
Download mbox | patch
Permalink /patch/37133/
State Accepted
Headers show

Comments

Yuya Nishihara - Dec. 13, 2018, 1:35 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1544620797 -32400
#      Wed Dec 12 22:19:57 2018 +0900
# Node ID eeb0c3a62092414713689e9dce6e8949bd27692b
# Parent  a6ba978d9ffb0dc8b0edb8f892a947776b6377c5
templatefuncs: add regexp search() function that extracts substring

This can be used to extract an issue number from a commit message, for
example:

  {search(r'\(issue([0-9]*)\)', desc) % '{1}'}
Pulkit Goyal - Dec. 13, 2018, 5:01 p.m.
On Thu, Dec 13, 2018 at 4:36 PM Yuya Nishihara <yuya@tcha.org> wrote:

> # HG changeset patch
> # User Yuya Nishihara <yuya@tcha.org>
> # Date 1544620797 -32400
> #      Wed Dec 12 22:19:57 2018 +0900
> # Node ID eeb0c3a62092414713689e9dce6e8949bd27692b
> # Parent  a6ba978d9ffb0dc8b0edb8f892a947776b6377c5
> templatefuncs: add regexp search() function that extracts substring
>
> This can be used to extract an issue number from a commit message, for
> example:
>
>   {search(r'\(issue([0-9]*)\)', desc) % '{1}'}
>

Nice, queued the series, many thanks!
Pulkit Goyal - Dec. 13, 2018, 5:13 p.m.
On Thu, Dec 13, 2018 at 4:36 PM Yuya Nishihara <yuya@tcha.org> wrote:

> # HG changeset patch
> # User Yuya Nishihara <yuya@tcha.org>
> # Date 1544620797 -32400
> #      Wed Dec 12 22:19:57 2018 +0900
> # Node ID eeb0c3a62092414713689e9dce6e8949bd27692b
> # Parent  a6ba978d9ffb0dc8b0edb8f892a947776b6377c5
> templatefuncs: add regexp search() function that extracts substring
>
> This can be used to extract an issue number from a commit message, for
> example:
>
>   {search(r'\(issue([0-9]*)\)', desc) % '{1}'}
>
> diff --git a/mercurial/templatefuncs.py b/mercurial/templatefuncs.py
> --- a/mercurial/templatefuncs.py
> +++ b/mercurial/templatefuncs.py
> @@ -20,6 +20,7 @@ from . import (
>      error,
>      minirst,
>      obsutil,
> +    pycompat,
>      registrar,
>      revset as revsetmod,
>      revsetlang,
> @@ -581,6 +582,40 @@ def rstdoc(context, mapping, args):
>
>      return minirst.format(text, style=style, keep=['verbose'])
>
> +@templatefunc('search(pattern, text)')
> +def search(context, mapping, args):
> +    """Look for the first text matching the regular expression pattern.
> +    Groups are accessible as ``{1}``, ``{2}``, ... in %-mapped
> template."""
> +    if len(args) != 2:
> +        # i18n: "search" is a keyword
> +        raise error.ParseError(_(b'search expects two arguments'))
> +
> +    pat = evalstring(context, mapping, args[0])
> +    src = evalstring(context, mapping, args[1])
> +    try:
> +        patre = re.compile(pat)
> +    except re.error:
> +        # i18n: "search" is a keyword
> +        raise error.ParseError(_(b'search got an invalid pattern: %s') %
> pat)
> +    # named groups shouldn't shadow *reserved* resource keywords
> +    badgroups = (context.knownresourcekeys()
> +                 & set(pycompat.byteskwargs(patre.groupindex)))
> +    if badgroups:
> +        raise error.ParseError(
> +            # i18n: "search" is a keyword
> +            _(b'invalid group %(group)s in search pattern: %(pat)s')
> +            % {b'group': b', '.join("'%s'" % g for g in
> sorted(badgroups)),
> +               b'pat': pat})
> +
> +    match = patre.search(src)
> +    if not match:
> +        return
> +
> +    lm = {b'0': match.group(0)}
> +    lm.update((b'%d' % i, v) for i, v in enumerate(match.groups(), 1))
> +    lm.update(pycompat.byteskwargs(match.groupdict()))
> +    return templateutil.mappingdict(lm, tmpl=b'{0}')
> +
>  @templatefunc('separate(sep, args...)', argspec='sep *args')
>  def separate(context, mapping, args):
>      """Add a separator between non-empty arguments."""
> diff --git a/tests/test-template-functions.t
> b/tests/test-template-functions.t
> --- a/tests/test-template-functions.t
> +++ b/tests/test-template-functions.t
> @@ -603,6 +603,50 @@ Test laziness of if() then/else clause
>    $ hg debugtemplate '{ifeq(0, 0, "", count(0))}'
>    $ hg debugtemplate '{ifeq(0, 1, count(0), "")}'
>
> +Test search() function:
>

added a `hg log -R a -r2 -T '{desc}\n'` here to make it more easy to
understand the test.

> +
> +  $ hg log -R a -r2 -T '{search(r"p.*", desc)}\n'
> +  person
> +
> + as bool
> +
> +  $ hg log -R a -r2 -T '{if(search(r"p.*", desc), "", "not ")}found\n'
> +  found
> +  $ hg log -R a -r2 -T '{if(search(r"q", desc), "", "not ")}found\n'
> +  not found
> +
> + match as json
> +
> +  $ hg log -R a -r2 -T '{search(r"(no) p.*", desc)|json}\n'
> +  {"0": "no person", "1": "no"}
> +  $ hg log -R a -r2 -T '{search(r"q", desc)|json}\n'
> +  null
> +
> + group reference
> +
> +  $ hg log -R a -r2 -T '{search(r"(no) (p.*)", desc) % "{1|upper}
> {2|hex}"}\n'
> +  NO 706572736f6e
> +  $ hg log -R a -r2 -T '{search(r"(?P<foo>[a-z]*)", desc) % "{foo}"}\n'
> +  no
> +  $ hg log -R a -r2 -T '{search(r"(?P<foo>[a-z]*)", desc).foo}\n'
> +  no
> +
> + group reference with no match
> + (TODO: we'll probably want to map it to an empty value)
> +
> +  $ hg log -R a -r2 -T '{search(r"q", desc) % "match: {0}"}\n'
> +  hg: parse error: None is not iterable of mappings
> +  [255]
> +
> + bad group names
> +
> +  $ hg log -R a -r2 -T '{search(r"(?P<0>.)", desc) % "{0}"}\n'
> +  hg: parse error: search got an invalid pattern: (?P<0>.)
> +  [255]
> +  $ hg log -R a -r2 -T '{search(r"(?P<repo>.)", desc) % "{repo}"}\n'
> +  hg: parse error: invalid group 'repo' in search pattern: (?P<repo>.)
> +  [255]
> +
>  Test the sub function of templating for expansion:
>
>    $ hg log -R latesttag -r 10 --template '{sub("[0-9]", "x", "{rev}")}\n'
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
>

Patch

diff --git a/mercurial/templatefuncs.py b/mercurial/templatefuncs.py
--- a/mercurial/templatefuncs.py
+++ b/mercurial/templatefuncs.py
@@ -20,6 +20,7 @@  from . import (
     error,
     minirst,
     obsutil,
+    pycompat,
     registrar,
     revset as revsetmod,
     revsetlang,
@@ -581,6 +582,40 @@  def rstdoc(context, mapping, args):
 
     return minirst.format(text, style=style, keep=['verbose'])
 
+@templatefunc('search(pattern, text)')
+def search(context, mapping, args):
+    """Look for the first text matching the regular expression pattern.
+    Groups are accessible as ``{1}``, ``{2}``, ... in %-mapped template."""
+    if len(args) != 2:
+        # i18n: "search" is a keyword
+        raise error.ParseError(_(b'search expects two arguments'))
+
+    pat = evalstring(context, mapping, args[0])
+    src = evalstring(context, mapping, args[1])
+    try:
+        patre = re.compile(pat)
+    except re.error:
+        # i18n: "search" is a keyword
+        raise error.ParseError(_(b'search got an invalid pattern: %s') % pat)
+    # named groups shouldn't shadow *reserved* resource keywords
+    badgroups = (context.knownresourcekeys()
+                 & set(pycompat.byteskwargs(patre.groupindex)))
+    if badgroups:
+        raise error.ParseError(
+            # i18n: "search" is a keyword
+            _(b'invalid group %(group)s in search pattern: %(pat)s')
+            % {b'group': b', '.join("'%s'" % g for g in sorted(badgroups)),
+               b'pat': pat})
+
+    match = patre.search(src)
+    if not match:
+        return
+
+    lm = {b'0': match.group(0)}
+    lm.update((b'%d' % i, v) for i, v in enumerate(match.groups(), 1))
+    lm.update(pycompat.byteskwargs(match.groupdict()))
+    return templateutil.mappingdict(lm, tmpl=b'{0}')
+
 @templatefunc('separate(sep, args...)', argspec='sep *args')
 def separate(context, mapping, args):
     """Add a separator between non-empty arguments."""
diff --git a/tests/test-template-functions.t b/tests/test-template-functions.t
--- a/tests/test-template-functions.t
+++ b/tests/test-template-functions.t
@@ -603,6 +603,50 @@  Test laziness of if() then/else clause
   $ hg debugtemplate '{ifeq(0, 0, "", count(0))}'
   $ hg debugtemplate '{ifeq(0, 1, count(0), "")}'
 
+Test search() function:
+
+  $ hg log -R a -r2 -T '{search(r"p.*", desc)}\n'
+  person
+
+ as bool
+
+  $ hg log -R a -r2 -T '{if(search(r"p.*", desc), "", "not ")}found\n'
+  found
+  $ hg log -R a -r2 -T '{if(search(r"q", desc), "", "not ")}found\n'
+  not found
+
+ match as json
+
+  $ hg log -R a -r2 -T '{search(r"(no) p.*", desc)|json}\n'
+  {"0": "no person", "1": "no"}
+  $ hg log -R a -r2 -T '{search(r"q", desc)|json}\n'
+  null
+
+ group reference
+
+  $ hg log -R a -r2 -T '{search(r"(no) (p.*)", desc) % "{1|upper} {2|hex}"}\n'
+  NO 706572736f6e
+  $ hg log -R a -r2 -T '{search(r"(?P<foo>[a-z]*)", desc) % "{foo}"}\n'
+  no
+  $ hg log -R a -r2 -T '{search(r"(?P<foo>[a-z]*)", desc).foo}\n'
+  no
+
+ group reference with no match
+ (TODO: we'll probably want to map it to an empty value)
+
+  $ hg log -R a -r2 -T '{search(r"q", desc) % "match: {0}"}\n'
+  hg: parse error: None is not iterable of mappings
+  [255]
+
+ bad group names
+
+  $ hg log -R a -r2 -T '{search(r"(?P<0>.)", desc) % "{0}"}\n'
+  hg: parse error: search got an invalid pattern: (?P<0>.)
+  [255]
+  $ hg log -R a -r2 -T '{search(r"(?P<repo>.)", desc) % "{repo}"}\n'
+  hg: parse error: invalid group 'repo' in search pattern: (?P<repo>.)
+  [255]
+
 Test the sub function of templating for expansion:
 
   $ hg log -R latesttag -r 10 --template '{sub("[0-9]", "x", "{rev}")}\n'