diff --git a/subcmds/grep.py b/subcmds/grep.py new file mode 100644 index 00000000..43f5e968 --- /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} [...] +""" + 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)