contrib/testparseutil.py
changeset 40093 726cfc47f17a
child 41552 99b4c6d73a72
equal deleted inserted replaced
40092:58786930ea27 40093:726cfc47f17a
       
     1 # testparseutil.py - utilities to parse test script for check tools
       
     2 #
       
     3 #  Copyright 2018 FUJIWARA Katsunori <foozy@lares.dti.ne.jp> and others
       
     4 #
       
     5 # This software may be used and distributed according to the terms of the
       
     6 # GNU General Public License version 2 or any later version.
       
     7 
       
     8 from __future__ import absolute_import, print_function
       
     9 
       
    10 import abc
       
    11 import re
       
    12 import sys
       
    13 
       
    14 ####################
       
    15 # for Python3 compatibility (almost comes from mercurial/pycompat.py)
       
    16 
       
    17 ispy3 = (sys.version_info[0] >= 3)
       
    18 
       
    19 def identity(a):
       
    20     return a
       
    21 
       
    22 def _rapply(f, xs):
       
    23     if xs is None:
       
    24         # assume None means non-value of optional data
       
    25         return xs
       
    26     if isinstance(xs, (list, set, tuple)):
       
    27         return type(xs)(_rapply(f, x) for x in xs)
       
    28     if isinstance(xs, dict):
       
    29         return type(xs)((_rapply(f, k), _rapply(f, v)) for k, v in xs.items())
       
    30     return f(xs)
       
    31 
       
    32 def rapply(f, xs):
       
    33     if f is identity:
       
    34         # fast path mainly for py2
       
    35         return xs
       
    36     return _rapply(f, xs)
       
    37 
       
    38 if ispy3:
       
    39     import builtins
       
    40 
       
    41     # TODO: .buffer might not exist if std streams were replaced; we'll need
       
    42     # a silly wrapper to make a bytes stream backed by a unicode one.
       
    43     stdin = sys.stdin.buffer
       
    44     stdout = sys.stdout.buffer
       
    45     stderr = sys.stderr.buffer
       
    46 
       
    47     def bytestr(s):
       
    48         # tiny version of pycompat.bytestr
       
    49         return s.encode('latin1')
       
    50 
       
    51     def sysstr(s):
       
    52         if isinstance(s, builtins.str):
       
    53             return s
       
    54         return s.decode(u'latin-1')
       
    55 
       
    56     def opentext(f):
       
    57         return open(f, 'rb')
       
    58 else:
       
    59     stdin = sys.stdin
       
    60     stdout = sys.stdout
       
    61     stderr = sys.stderr
       
    62 
       
    63     bytestr = str
       
    64     sysstr = identity
       
    65 
       
    66     opentext = open
       
    67 
       
    68 def b2s(x):
       
    69     # convert BYTES elements in "x" to SYSSTR recursively
       
    70     return rapply(sysstr, x)
       
    71 
       
    72 def writeout(data):
       
    73     # write "data" in BYTES into stdout
       
    74     stdout.write(data)
       
    75 
       
    76 def writeerr(data):
       
    77     # write "data" in BYTES into stderr
       
    78     stderr.write(data)
       
    79 
       
    80 ####################
       
    81 
       
    82 class embeddedmatcher(object):
       
    83     """Base class to detect embedded code fragments in *.t test script
       
    84     """
       
    85     __metaclass__ = abc.ABCMeta
       
    86 
       
    87     def __init__(self, desc):
       
    88         self.desc = desc
       
    89 
       
    90     @abc.abstractmethod
       
    91     def startsat(self, line):
       
    92         """Examine whether embedded code starts at line
       
    93 
       
    94         This can return arbitrary object, and it is used as 'ctx' for
       
    95         subsequent method invocations.
       
    96         """
       
    97 
       
    98     @abc.abstractmethod
       
    99     def endsat(self, ctx, line):
       
   100         """Examine whether embedded code ends at line"""
       
   101 
       
   102     @abc.abstractmethod
       
   103     def isinside(self, ctx, line):
       
   104         """Examine whether line is inside embedded code, if not yet endsat
       
   105         """
       
   106 
       
   107     @abc.abstractmethod
       
   108     def ignores(self, ctx):
       
   109         """Examine whether detected embedded code should be ignored"""
       
   110 
       
   111     @abc.abstractmethod
       
   112     def filename(self, ctx):
       
   113         """Return filename of embedded code
       
   114 
       
   115         If filename isn't specified for embedded code explicitly, this
       
   116         returns None.
       
   117         """
       
   118 
       
   119     @abc.abstractmethod
       
   120     def codeatstart(self, ctx, line):
       
   121         """Return actual code at the start line of embedded code
       
   122 
       
   123         This might return None, if the start line doesn't contain
       
   124         actual code.
       
   125         """
       
   126 
       
   127     @abc.abstractmethod
       
   128     def codeatend(self, ctx, line):
       
   129         """Return actual code at the end line of embedded code
       
   130 
       
   131         This might return None, if the end line doesn't contain actual
       
   132         code.
       
   133         """
       
   134 
       
   135     @abc.abstractmethod
       
   136     def codeinside(self, ctx, line):
       
   137         """Return actual code at line inside embedded code"""
       
   138 
       
   139 def embedded(basefile, lines, errors, matchers):
       
   140     """pick embedded code fragments up from given lines
       
   141 
       
   142     This is common parsing logic, which examines specified matchers on
       
   143     given lines.
       
   144 
       
   145     :basefile: a name of a file, from which lines to be parsed come.
       
   146     :lines: to be parsed (might be a value returned by "open(basefile)")
       
   147     :errors: an array, into which messages for detected error are stored
       
   148     :matchers: an array of embeddedmatcher objects
       
   149 
       
   150     This function yields '(filename, starts, ends, code)' tuple.
       
   151 
       
   152     :filename: a name of embedded code, if it is explicitly specified
       
   153                (e.g.  "foobar" of "cat >> foobar <<EOF").
       
   154                Otherwise, this is None
       
   155     :starts: line number (1-origin), at which embedded code starts (inclusive)
       
   156     :ends: line number (1-origin), at which embedded code ends (exclusive)
       
   157     :code: extracted embedded code, which is single-stringified
       
   158 
       
   159     >>> class ambigmatcher(object):
       
   160     ...     # mock matcher class to examine implementation of
       
   161     ...     # "ambiguous matching" corner case
       
   162     ...     def __init__(self, desc, matchfunc):
       
   163     ...         self.desc = desc
       
   164     ...         self.matchfunc = matchfunc
       
   165     ...     def startsat(self, line):
       
   166     ...         return self.matchfunc(line)
       
   167     >>> ambig1 = ambigmatcher(b'ambiguous #1',
       
   168     ...                       lambda l: l.startswith(b'  $ cat '))
       
   169     >>> ambig2 = ambigmatcher(b'ambiguous #2',
       
   170     ...                       lambda l: l.endswith(b'<< EOF\\n'))
       
   171     >>> lines = [b'  $ cat > foo.py << EOF\\n']
       
   172     >>> errors = []
       
   173     >>> matchers = [ambig1, ambig2]
       
   174     >>> list(t for t in embedded(b'<dummy>', lines, errors, matchers))
       
   175     []
       
   176     >>> b2s(errors)
       
   177     ['<dummy>:1: ambiguous line for "ambiguous #1", "ambiguous #2"']
       
   178 
       
   179     """
       
   180     matcher = None
       
   181     ctx = filename = code = startline = None # for pyflakes
       
   182 
       
   183     for lineno, line in enumerate(lines, 1):
       
   184         if not line.endswith(b'\n'):
       
   185             line += b'\n' # to normalize EOF line
       
   186         if matcher: # now, inside embedded code
       
   187             if matcher.endsat(ctx, line):
       
   188                 codeatend = matcher.codeatend(ctx, line)
       
   189                 if codeatend is not None:
       
   190                     code.append(codeatend)
       
   191                 if not matcher.ignores(ctx):
       
   192                     yield (filename, startline, lineno, b''.join(code))
       
   193                 matcher = None
       
   194                 # DO NOT "continue", because line might start next fragment
       
   195             elif not matcher.isinside(ctx, line):
       
   196                 # this is an error of basefile
       
   197                 # (if matchers are implemented correctly)
       
   198                 errors.append(b'%s:%d: unexpected line for "%s"'
       
   199                               % (basefile, lineno, matcher.desc))
       
   200                 # stop extracting embedded code by current 'matcher',
       
   201                 # because appearance of unexpected line might mean
       
   202                 # that expected end-of-embedded-code line might never
       
   203                 # appear
       
   204                 matcher = None
       
   205                 # DO NOT "continue", because line might start next fragment
       
   206             else:
       
   207                 code.append(matcher.codeinside(ctx, line))
       
   208                 continue
       
   209 
       
   210         # examine whether current line starts embedded code or not
       
   211         assert not matcher
       
   212 
       
   213         matched = []
       
   214         for m in matchers:
       
   215             ctx = m.startsat(line)
       
   216             if ctx:
       
   217                 matched.append((m, ctx))
       
   218         if matched:
       
   219             if len(matched) > 1:
       
   220                 # this is an error of matchers, maybe
       
   221                 errors.append(b'%s:%d: ambiguous line for %s' %
       
   222                               (basefile, lineno,
       
   223                                b', '.join([b'"%s"' % m.desc
       
   224                                            for m, c in matched])))
       
   225                 # omit extracting embedded code, because choosing
       
   226                 # arbitrary matcher from matched ones might fail to
       
   227                 # detect the end of embedded code as expected.
       
   228                 continue
       
   229             matcher, ctx = matched[0]
       
   230             filename = matcher.filename(ctx)
       
   231             code = []
       
   232             codeatstart = matcher.codeatstart(ctx, line)
       
   233             if codeatstart is not None:
       
   234                 code.append(codeatstart)
       
   235                 startline = lineno
       
   236             else:
       
   237                 startline = lineno + 1
       
   238 
       
   239     if matcher:
       
   240         # examine whether EOF ends embedded code, because embedded
       
   241         # code isn't yet ended explicitly
       
   242         if matcher.endsat(ctx, b'\n'):
       
   243             codeatend = matcher.codeatend(ctx, b'\n')
       
   244             if codeatend is not None:
       
   245                 code.append(codeatend)
       
   246             if not matcher.ignores(ctx):
       
   247                 yield (filename, startline, lineno + 1, b''.join(code))
       
   248         else:
       
   249             # this is an error of basefile
       
   250             # (if matchers are implemented correctly)
       
   251             errors.append(b'%s:%d: unexpected end of file for "%s"'
       
   252                           % (basefile, lineno, matcher.desc))
       
   253 
       
   254 # heredoc limit mark to ignore embedded code at check-code.py or so
       
   255 heredocignorelimit = b'NO_CHECK_EOF'
       
   256 
       
   257 # the pattern to match against cases below, and to return a limit mark
       
   258 # string as 'lname' group
       
   259 #
       
   260 # - << LIMITMARK
       
   261 # - << "LIMITMARK"
       
   262 # - << 'LIMITMARK'
       
   263 heredoclimitpat = br'\s*<<\s*(?P<lquote>["\']?)(?P<limit>\w+)(?P=lquote)'
       
   264 
       
   265 class fileheredocmatcher(embeddedmatcher):
       
   266     """Detect "cat > FILE << LIMIT" style embedded code
       
   267 
       
   268     >>> matcher = fileheredocmatcher(b'heredoc .py file', br'[^<]+\.py')
       
   269     >>> b2s(matcher.startsat(b'  $ cat > file.py << EOF\\n'))
       
   270     ('file.py', '  > EOF\\n')
       
   271     >>> b2s(matcher.startsat(b'  $ cat   >>file.py   <<EOF\\n'))
       
   272     ('file.py', '  > EOF\\n')
       
   273     >>> b2s(matcher.startsat(b'  $ cat>  \\x27any file.py\\x27<<  "EOF"\\n'))
       
   274     ('any file.py', '  > EOF\\n')
       
   275     >>> b2s(matcher.startsat(b"  $ cat > file.py << 'ANYLIMIT'\\n"))
       
   276     ('file.py', '  > ANYLIMIT\\n')
       
   277     >>> b2s(matcher.startsat(b'  $ cat<<ANYLIMIT>"file.py"\\n'))
       
   278     ('file.py', '  > ANYLIMIT\\n')
       
   279     >>> start = b'  $ cat > file.py << EOF\\n'
       
   280     >>> ctx = matcher.startsat(start)
       
   281     >>> matcher.codeatstart(ctx, start)
       
   282     >>> b2s(matcher.filename(ctx))
       
   283     'file.py'
       
   284     >>> matcher.ignores(ctx)
       
   285     False
       
   286     >>> inside = b'  > foo = 1\\n'
       
   287     >>> matcher.endsat(ctx, inside)
       
   288     False
       
   289     >>> matcher.isinside(ctx, inside)
       
   290     True
       
   291     >>> b2s(matcher.codeinside(ctx, inside))
       
   292     'foo = 1\\n'
       
   293     >>> end = b'  > EOF\\n'
       
   294     >>> matcher.endsat(ctx, end)
       
   295     True
       
   296     >>> matcher.codeatend(ctx, end)
       
   297     >>> matcher.endsat(ctx, b'  > EOFEOF\\n')
       
   298     False
       
   299     >>> ctx = matcher.startsat(b'  $ cat > file.py << NO_CHECK_EOF\\n')
       
   300     >>> matcher.ignores(ctx)
       
   301     True
       
   302     """
       
   303     _prefix = b'  > '
       
   304 
       
   305     def __init__(self, desc, namepat):
       
   306         super(fileheredocmatcher, self).__init__(desc)
       
   307 
       
   308         # build the pattern to match against cases below (and ">>"
       
   309         # variants), and to return a target filename string as 'name'
       
   310         # group
       
   311         #
       
   312         # - > NAMEPAT
       
   313         # - > "NAMEPAT"
       
   314         # - > 'NAMEPAT'
       
   315         namepat = (br'\s*>>?\s*(?P<nquote>["\']?)(?P<name>%s)(?P=nquote)'
       
   316                    % namepat)
       
   317         self._fileres = [
       
   318             # "cat > NAME << LIMIT" case
       
   319             re.compile(br'  \$ \s*cat' + namepat + heredoclimitpat),
       
   320             # "cat << LIMIT > NAME" case
       
   321             re.compile(br'  \$ \s*cat' + heredoclimitpat + namepat),
       
   322         ]
       
   323 
       
   324     def startsat(self, line):
       
   325         # ctx is (filename, END-LINE-OF-EMBEDDED-CODE) tuple
       
   326         for filere in self._fileres:
       
   327             matched = filere.match(line)
       
   328             if matched:
       
   329                 return (matched.group('name'),
       
   330                         b'  > %s\n' % matched.group('limit'))
       
   331 
       
   332     def endsat(self, ctx, line):
       
   333         return ctx[1] == line
       
   334 
       
   335     def isinside(self, ctx, line):
       
   336         return line.startswith(self._prefix)
       
   337 
       
   338     def ignores(self, ctx):
       
   339         return b'  > %s\n' % heredocignorelimit == ctx[1]
       
   340 
       
   341     def filename(self, ctx):
       
   342         return ctx[0]
       
   343 
       
   344     def codeatstart(self, ctx, line):
       
   345         return None # no embedded code at start line
       
   346 
       
   347     def codeatend(self, ctx, line):
       
   348         return None # no embedded code at end line
       
   349 
       
   350     def codeinside(self, ctx, line):
       
   351         return line[len(self._prefix):] # strip prefix
       
   352 
       
   353 ####
       
   354 # for embedded python script
       
   355 
       
   356 class pydoctestmatcher(embeddedmatcher):
       
   357     """Detect ">>> code" style embedded python code
       
   358 
       
   359     >>> matcher = pydoctestmatcher()
       
   360     >>> startline = b'  >>> foo = 1\\n'
       
   361     >>> matcher.startsat(startline)
       
   362     True
       
   363     >>> matcher.startsat(b'  ... foo = 1\\n')
       
   364     False
       
   365     >>> ctx = matcher.startsat(startline)
       
   366     >>> matcher.filename(ctx)
       
   367     >>> matcher.ignores(ctx)
       
   368     False
       
   369     >>> b2s(matcher.codeatstart(ctx, startline))
       
   370     'foo = 1\\n'
       
   371     >>> inside = b'  >>> foo = 1\\n'
       
   372     >>> matcher.endsat(ctx, inside)
       
   373     False
       
   374     >>> matcher.isinside(ctx, inside)
       
   375     True
       
   376     >>> b2s(matcher.codeinside(ctx, inside))
       
   377     'foo = 1\\n'
       
   378     >>> inside = b'  ... foo = 1\\n'
       
   379     >>> matcher.endsat(ctx, inside)
       
   380     False
       
   381     >>> matcher.isinside(ctx, inside)
       
   382     True
       
   383     >>> b2s(matcher.codeinside(ctx, inside))
       
   384     'foo = 1\\n'
       
   385     >>> inside = b'  expected output\\n'
       
   386     >>> matcher.endsat(ctx, inside)
       
   387     False
       
   388     >>> matcher.isinside(ctx, inside)
       
   389     True
       
   390     >>> b2s(matcher.codeinside(ctx, inside))
       
   391     '\\n'
       
   392     >>> inside = b'  \\n'
       
   393     >>> matcher.endsat(ctx, inside)
       
   394     False
       
   395     >>> matcher.isinside(ctx, inside)
       
   396     True
       
   397     >>> b2s(matcher.codeinside(ctx, inside))
       
   398     '\\n'
       
   399     >>> end = b'  $ foo bar\\n'
       
   400     >>> matcher.endsat(ctx, end)
       
   401     True
       
   402     >>> matcher.codeatend(ctx, end)
       
   403     >>> end = b'\\n'
       
   404     >>> matcher.endsat(ctx, end)
       
   405     True
       
   406     >>> matcher.codeatend(ctx, end)
       
   407     """
       
   408     _prefix = b'  >>> '
       
   409     _prefixre = re.compile(br'  (>>>|\.\.\.) ')
       
   410 
       
   411     # If a line matches against not _prefixre but _outputre, that line
       
   412     # is "an expected output line" (= not a part of code fragment).
       
   413     #
       
   414     # Strictly speaking, a line matching against "(#if|#else|#endif)"
       
   415     # is also treated similarly in "inline python code" semantics by
       
   416     # run-tests.py. But "directive line inside inline python code"
       
   417     # should be rejected by Mercurial reviewers. Therefore, this
       
   418     # regexp does not matche against such directive lines.
       
   419     _outputre = re.compile(br'  $|  [^$]')
       
   420 
       
   421     def __init__(self):
       
   422         super(pydoctestmatcher, self).__init__(b"doctest style python code")
       
   423 
       
   424     def startsat(self, line):
       
   425         # ctx is "True"
       
   426         return line.startswith(self._prefix)
       
   427 
       
   428     def endsat(self, ctx, line):
       
   429         return not (self._prefixre.match(line) or self._outputre.match(line))
       
   430 
       
   431     def isinside(self, ctx, line):
       
   432         return True # always true, if not yet ended
       
   433 
       
   434     def ignores(self, ctx):
       
   435         return False # should be checked always
       
   436 
       
   437     def filename(self, ctx):
       
   438         return None # no filename
       
   439 
       
   440     def codeatstart(self, ctx, line):
       
   441         return line[len(self._prefix):] # strip prefix '  >>> '/'  ... '
       
   442 
       
   443     def codeatend(self, ctx, line):
       
   444         return None # no embedded code at end line
       
   445 
       
   446     def codeinside(self, ctx, line):
       
   447         if self._prefixre.match(line):
       
   448             return line[len(self._prefix):] # strip prefix '  >>> '/'  ... '
       
   449         return b'\n' # an expected output line is treated as an empty line
       
   450 
       
   451 class pyheredocmatcher(embeddedmatcher):
       
   452     """Detect "python << LIMIT" style embedded python code
       
   453 
       
   454     >>> matcher = pyheredocmatcher()
       
   455     >>> b2s(matcher.startsat(b'  $ python << EOF\\n'))
       
   456     '  > EOF\\n'
       
   457     >>> b2s(matcher.startsat(b'  $ $PYTHON   <<EOF\\n'))
       
   458     '  > EOF\\n'
       
   459     >>> b2s(matcher.startsat(b'  $ "$PYTHON"<<  "EOF"\\n'))
       
   460     '  > EOF\\n'
       
   461     >>> b2s(matcher.startsat(b"  $ $PYTHON << 'ANYLIMIT'\\n"))
       
   462     '  > ANYLIMIT\\n'
       
   463     >>> matcher.startsat(b'  $ "$PYTHON" < EOF\\n')
       
   464     >>> start = b'  $ python << EOF\\n'
       
   465     >>> ctx = matcher.startsat(start)
       
   466     >>> matcher.codeatstart(ctx, start)
       
   467     >>> matcher.filename(ctx)
       
   468     >>> matcher.ignores(ctx)
       
   469     False
       
   470     >>> inside = b'  > foo = 1\\n'
       
   471     >>> matcher.endsat(ctx, inside)
       
   472     False
       
   473     >>> matcher.isinside(ctx, inside)
       
   474     True
       
   475     >>> b2s(matcher.codeinside(ctx, inside))
       
   476     'foo = 1\\n'
       
   477     >>> end = b'  > EOF\\n'
       
   478     >>> matcher.endsat(ctx, end)
       
   479     True
       
   480     >>> matcher.codeatend(ctx, end)
       
   481     >>> matcher.endsat(ctx, b'  > EOFEOF\\n')
       
   482     False
       
   483     >>> ctx = matcher.startsat(b'  $ python << NO_CHECK_EOF\\n')
       
   484     >>> matcher.ignores(ctx)
       
   485     True
       
   486     """
       
   487     _prefix = b'  > '
       
   488 
       
   489     _startre = re.compile(br'  \$ (\$PYTHON|"\$PYTHON"|python).*' +
       
   490                           heredoclimitpat)
       
   491 
       
   492     def __init__(self):
       
   493         super(pyheredocmatcher, self).__init__(b"heredoc python invocation")
       
   494 
       
   495     def startsat(self, line):
       
   496         # ctx is END-LINE-OF-EMBEDDED-CODE
       
   497         matched = self._startre.match(line)
       
   498         if matched:
       
   499             return b'  > %s\n' % matched.group('limit')
       
   500 
       
   501     def endsat(self, ctx, line):
       
   502         return ctx == line
       
   503 
       
   504     def isinside(self, ctx, line):
       
   505         return line.startswith(self._prefix)
       
   506 
       
   507     def ignores(self, ctx):
       
   508         return b'  > %s\n' % heredocignorelimit == ctx
       
   509 
       
   510     def filename(self, ctx):
       
   511         return None # no filename
       
   512 
       
   513     def codeatstart(self, ctx, line):
       
   514         return None # no embedded code at start line
       
   515 
       
   516     def codeatend(self, ctx, line):
       
   517         return None # no embedded code at end line
       
   518 
       
   519     def codeinside(self, ctx, line):
       
   520         return line[len(self._prefix):] # strip prefix
       
   521 
       
   522 _pymatchers = [
       
   523     pydoctestmatcher(),
       
   524     pyheredocmatcher(),
       
   525     # use '[^<]+' instead of '\S+', in order to match against
       
   526     # paths including whitespaces
       
   527     fileheredocmatcher(b'heredoc .py file', br'[^<]+\.py'),
       
   528 ]
       
   529 
       
   530 def pyembedded(basefile, lines, errors):
       
   531     return embedded(basefile, lines, errors, _pymatchers)
       
   532 
       
   533 ####
       
   534 # for embedded shell script
       
   535 
       
   536 _shmatchers = [
       
   537     # use '[^<]+' instead of '\S+', in order to match against
       
   538     # paths including whitespaces
       
   539     fileheredocmatcher(b'heredoc .sh file', br'[^<]+\.sh'),
       
   540 ]
       
   541 
       
   542 def shembedded(basefile, lines, errors):
       
   543     return embedded(basefile, lines, errors, _shmatchers)
       
   544 
       
   545 ####
       
   546 # for embedded hgrc configuration
       
   547 
       
   548 _hgrcmatchers = [
       
   549     # use '[^<]+' instead of '\S+', in order to match against
       
   550     # paths including whitespaces
       
   551     fileheredocmatcher(b'heredoc hgrc file',
       
   552                        br'(([^/<]+/)+hgrc|\$HGRCPATH|\${HGRCPATH})'),
       
   553 ]
       
   554 
       
   555 def hgrcembedded(basefile, lines, errors):
       
   556     return embedded(basefile, lines, errors, _hgrcmatchers)
       
   557 
       
   558 ####
       
   559 
       
   560 if __name__ == "__main__":
       
   561     import optparse
       
   562     import sys
       
   563 
       
   564     def showembedded(basefile, lines, embeddedfunc, opts):
       
   565         errors = []
       
   566         for name, starts, ends, code in embeddedfunc(basefile, lines, errors):
       
   567             if not name:
       
   568                 name = b'<anonymous>'
       
   569             writeout(b"%s:%d: %s starts\n" % (basefile, starts, name))
       
   570             if opts.verbose and code:
       
   571                 writeout(b"  |%s\n" %
       
   572                          b"\n  |".join(l for l in code.splitlines()))
       
   573             writeout(b"%s:%d: %s ends\n" % (basefile, ends, name))
       
   574         for e in errors:
       
   575             writeerr(b"%s\n" % e)
       
   576         return len(errors)
       
   577 
       
   578     def applyembedded(args, embeddedfunc, opts):
       
   579         ret = 0
       
   580         if args:
       
   581             for f in args:
       
   582                 with opentext(f) as fp:
       
   583                     if showembedded(bytestr(f), fp, embeddedfunc, opts):
       
   584                         ret = 1
       
   585         else:
       
   586             lines = [l for l in stdin.readlines()]
       
   587             if showembedded(b'<stdin>', lines, embeddedfunc, opts):
       
   588                 ret = 1
       
   589         return ret
       
   590 
       
   591     commands = {}
       
   592     def command(name, desc):
       
   593         def wrap(func):
       
   594             commands[name] = (desc, func)
       
   595         return wrap
       
   596 
       
   597     @command("pyembedded", "detect embedded python script")
       
   598     def pyembeddedcmd(args, opts):
       
   599         return applyembedded(args, pyembedded, opts)
       
   600 
       
   601     @command("shembedded", "detect embedded shell script")
       
   602     def shembeddedcmd(args, opts):
       
   603         return applyembedded(args, shembedded, opts)
       
   604 
       
   605     @command("hgrcembedded", "detect embedded hgrc configuration")
       
   606     def hgrcembeddedcmd(args, opts):
       
   607         return applyembedded(args, hgrcembedded, opts)
       
   608 
       
   609     availablecommands = "\n".join(["  - %s: %s" % (key, value[0])
       
   610                                    for key, value in commands.items()])
       
   611 
       
   612     parser = optparse.OptionParser("""%prog COMMAND [file ...]
       
   613 
       
   614 Pick up embedded code fragments from given file(s) or stdin, and list
       
   615 up start/end lines of them in standard compiler format
       
   616 ("FILENAME:LINENO:").
       
   617 
       
   618 Available commands are:
       
   619 """ + availablecommands + """
       
   620 """)
       
   621     parser.add_option("-v", "--verbose",
       
   622                       help="enable additional output (e.g. actual code)",
       
   623                       action="store_true")
       
   624     (opts, args) = parser.parse_args()
       
   625 
       
   626     if not args or args[0] not in commands:
       
   627         parser.print_help()
       
   628         sys.exit(255)
       
   629 
       
   630     sys.exit(commands[args[0]][1](args[1:], opts))