Add 'repo grep' to support searching all projects
Users can now use 'repo grep' to search all projects, rather than
'repo forall -c git grep'. Its not only shorter to type, but it
also filters results better by highlighting which projects matched
in the client workspace.
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/subcmds/grep.py b/subcmds/grep.py
new file mode 100644
index 0000000..43f5e96
--- /dev/null
+++ b/subcmds/grep.py
@@ -0,0 +1,243 @@
+#
+# Copyright (C) 2009 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+from optparse import SUPPRESS_HELP
+from color import Coloring
+from command import PagedCommand
+from git_command import GitCommand
+
+class GrepColoring(Coloring):
+ def __init__(self, config):
+ Coloring.__init__(self, config, 'grep')
+ self.project = self.printer('project', attr='bold')
+
+class Grep(PagedCommand):
+ common = True
+ helpSummary = "Print lines matching a pattern"
+ helpUsage = """
+%prog {pattern | -e pattern} [<project>...]
+"""
+ helpDescription = """
+Search for the specified patterns in all project files.
+
+Options
+-------
+
+The following options can appear as often as necessary to express
+the pattern to locate:
+
+ -e PATTERN
+ --and, --or, --not, -(, -)
+
+Further, the -r/--revision option may be specified multiple times
+in order to scan multiple trees. If the same file matches in more
+than one tree, only the first result is reported, prefixed by the
+revision name it was found under.
+
+Examples
+-------
+
+Look for a line that has '#define' and either 'MAX_PATH or 'PATH_MAX':
+
+ repo grep -e '#define' --and -\( -e MAX_PATH -e PATH_MAX \)
+
+Look for a line that has 'NODE' or 'Unexpected' in files that
+contain a line that matches both expressions:
+
+ repo grep --all-match -e NODE -e Unexpected
+
+"""
+
+ def _Options(self, p):
+ def carry(option,
+ opt_str,
+ value,
+ parser):
+ pt = getattr(parser.values, 'cmd_argv', None)
+ if pt is None:
+ pt = []
+ setattr(parser.values, 'cmd_argv', pt)
+
+ if opt_str == '-(':
+ pt.append('(')
+ elif opt_str == '-)':
+ pt.append(')')
+ else:
+ pt.append(opt_str)
+
+ if value is not None:
+ pt.append(value)
+
+ g = p.add_option_group('Sources')
+ g.add_option('--cached',
+ action='callback', callback=carry,
+ help='Search the index, instead of the work tree')
+ g.add_option('-r','--revision',
+ dest='revision', action='append', metavar='TREEish',
+ help='Search TREEish, instead of the work tree')
+
+ g = p.add_option_group('Pattern')
+ g.add_option('-e',
+ action='callback', callback=carry,
+ metavar='PATTERN', type='str',
+ help='Pattern to search for')
+ g.add_option('-i', '--ignore-case',
+ action='callback', callback=carry,
+ help='Ignore case differences')
+ g.add_option('-a','--text',
+ action='callback', callback=carry,
+ help="Process binary files as if they were text")
+ g.add_option('-I',
+ action='callback', callback=carry,
+ help="Don't match the pattern in binary files")
+ g.add_option('-w', '--word-regexp',
+ action='callback', callback=carry,
+ help='Match the pattern only at word boundaries')
+ g.add_option('-v', '--invert-match',
+ action='callback', callback=carry,
+ help='Select non-matching lines')
+ g.add_option('-G', '--basic-regexp',
+ action='callback', callback=carry,
+ help='Use POSIX basic regexp for patterns (default)')
+ g.add_option('-E', '--extended-regexp',
+ action='callback', callback=carry,
+ help='Use POSIX extended regexp for patterns')
+ g.add_option('-F', '--fixed-strings',
+ action='callback', callback=carry,
+ help='Use fixed strings (not regexp) for pattern')
+
+ g = p.add_option_group('Pattern Grouping')
+ g.add_option('--all-match',
+ action='callback', callback=carry,
+ help='Limit match to lines that have all patterns')
+ g.add_option('--and', '--or', '--not',
+ action='callback', callback=carry,
+ help='Boolean operators to combine patterns')
+ g.add_option('-(','-)',
+ action='callback', callback=carry,
+ help='Boolean operator grouping')
+
+ g = p.add_option_group('Output')
+ g.add_option('-n',
+ action='callback', callback=carry,
+ help='Prefix the line number to matching lines')
+ g.add_option('-C',
+ action='callback', callback=carry,
+ metavar='CONTEXT', type='str',
+ help='Show CONTEXT lines around match')
+ g.add_option('-B',
+ action='callback', callback=carry,
+ metavar='CONTEXT', type='str',
+ help='Show CONTEXT lines before match')
+ g.add_option('-A',
+ action='callback', callback=carry,
+ metavar='CONTEXT', type='str',
+ help='Show CONTEXT lines after match')
+ g.add_option('-l','--name-only','--files-with-matches',
+ action='callback', callback=carry,
+ help='Show only file names containing matching lines')
+ g.add_option('-L','--files-without-match',
+ action='callback', callback=carry,
+ help='Show only file names not containing matching lines')
+
+
+ def Execute(self, opt, args):
+ out = GrepColoring(self.manifest.manifestProject.config)
+
+ cmd_argv = ['grep']
+ if out.is_on:
+ cmd_argv.append('--color')
+ cmd_argv.extend(getattr(opt,'cmd_argv',[]))
+
+ if '-e' not in cmd_argv:
+ if not args:
+ self.Usage()
+ cmd_argv.append('-e')
+ cmd_argv.append(args[0])
+ args = args[1:]
+
+ projects = self.GetProjects(args)
+
+ full_name = False
+ if len(projects) > 1:
+ cmd_argv.append('--full-name')
+ full_name = True
+
+ have_rev = False
+ if opt.revision:
+ if '--cached' in cmd_argv:
+ print >>sys.stderr,\
+ 'fatal: cannot combine --cached and --revision'
+ sys.exit(1)
+ have_rev = True
+ cmd_argv.extend(opt.revision)
+ cmd_argv.append('--')
+
+ bad_rev = False
+ have_match = False
+
+ for project in projects:
+ p = GitCommand(project,
+ cmd_argv,
+ bare = False,
+ capture_stdout = True,
+ capture_stderr = True)
+ if p.Wait() != 0:
+ # no results
+ #
+ if p.stderr:
+ if have_rev and 'fatal: ambiguous argument' in p.stderr:
+ bad_rev = True
+ else:
+ out.project('--- project %s ---' % project.relpath)
+ out.nl()
+ out.write(p.stderr)
+ out.nl()
+ continue
+ have_match = True
+
+ # We cut the last element, to avoid a blank line.
+ #
+ r = p.stdout.split('\n')
+ r = r[0:-1]
+
+ if have_rev and full_name:
+ for line in r:
+ rev, line = line.split(':', 1)
+ out.write(rev)
+ out.write(':')
+ out.project(project.relpath)
+ out.write('/')
+ out.write(line)
+ out.nl()
+ elif full_name:
+ for line in r:
+ out.project(project.relpath)
+ out.write('/')
+ out.write(line)
+ out.nl()
+ else:
+ for line in r:
+ print line
+
+ if have_match:
+ sys.exit(0)
+ elif have_rev and bad_rev:
+ for r in opt.revision:
+ print >>sys.stderr, "error: can't search revision %s" % r
+ sys.exit(1)
+ else:
+ sys.exit(1)