Skip to content

Commit

Permalink
commands: add grep
Browse files Browse the repository at this point in the history
This has been requested various times. I haven't done it because 'west
forall -c "git grep"' has been a reasonable workaround for me, and I
never had time or need to think about what an ergonomic alternative
might look like.

That's changed now and I wanted something that would only print output
for projects where a result was found, similarly to the way "west
diff" only prints for projects with nonempty "git diff", etc.

Add a "grep" command that does similar, defaulting to use of "git
grep" to get the job done. Also support (my favorite) ripgrep and
plain "grep --recursive". See the GREP_EPILOG variable in the patch
for detailed usage information.

Signed-off-by: Martí Bolívar <[email protected]>
  • Loading branch information
mbolivar-ampere committed Aug 25, 2023
1 parent 9e8f500 commit f2b9735
Show file tree
Hide file tree
Showing 2 changed files with 200 additions and 1 deletion.
3 changes: 2 additions & 1 deletion src/west/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from west.commands import WestCommand, extension_commands, \
CommandError, ExtensionCommandError, Verbosity
from west.app.project import List, ManifestCommand, Compare, Diff, Status, \
SelfUpdate, ForAll, Init, Update, Topdir
SelfUpdate, ForAll, Grep, Init, Update, Topdir
from west.app.config import Config
from west.manifest import Manifest, MalformedConfig, MalformedManifest, \
ManifestVersionError, ManifestImportFailed, _ManifestImportDepth, \
Expand Down Expand Up @@ -1097,6 +1097,7 @@ def main(argv=None):
Diff,
Status,
ForAll,
Grep,
],

'other built-in commands': [
Expand Down
198 changes: 198 additions & 0 deletions src/west/app/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,204 @@ def do_run(self, args, user_args):
failed.append(project)
self._handle_failed(args, failed)

GREP_EPILOG = '''
EXAMPLES
--------
To get "git grep foo" results from all cloned, active projects:
west grep foo
To do the same:
- with "git grep --untracked": west grep --untracked foo
- with "ripgrep": west grep --tool ripgrep foo
- with "grep --recursive": west grep --tool grep foo
To switch the default tool:
- to ripgrep: west config grep.tool ripgrep
- to grep: west config grep.tool grep
GREP TOOLS
----------
This command runs a "grep tool" to search inside projects.
Supported tools:
- git-grep (default)
- ripgrep
- grep
Set the "grep.tool" configuration option to change the default.
Use "--tool" to switch the tool from its default.
TOOL PATH
---------
Use --tool-path to override the path to the tool. For example:
west grep --tool ripgrep --tool-path /my/special/ripgrep
Without --tool-path, the "grep.<TOOL>-path" configuration option
is checked next. For example, to set the default path to ripgrep:
west config grep.ripgrep-path /my/special/ripgrep
If the option is not set, "west grep" searches for the tool as follows:
- git-grep: search for "git" (and run as "git grep")
- ripgrep: search for "rg", then "ripgrep"
- grep: search for "grep"
TOOL ARGUMENTS
--------------
The "grep.<TOOL>-args" configuration options, if set, contain arguments
that are always passed to the tool. The defaults for these are:
- git-grep: (none)
- ripgrep: (none)
- grep: "--recursive"
Additional command line arguments not recognized by "west grep" are
passed to the tool after that. For example:
west grep --this-option-is-passed-to-the-tool
To force an argument to be given to the tool instead of west, put
it after a "--", like this:
west grep -- --project=val
COLORS
------
By default, west will force the tool to print colored output as long
as the "color.ui" configuration option is true. If color.ui is false,
west forces the tool not to print colored output.
Since all supported tools have similar --color options, you can
override this behavior on the command line, for example with:
west grep --color=never
To do this permanently, set "grep.color":
west config grep.color never
'''

class Grep(_ProjectCommand):

TOOLS = ['git-grep', 'ripgrep', 'grep']

DEFAULT_TOOL = 'git-grep'

DEFAULT_TOOL_ARGS = {
'git-grep': [],
'ripgrep': [],
'grep': ['--recursive'],
}

DEFAULT_TOOL_EXECUTABLES = {
# git-grep is intentionally omitted: use self._git
'ripgrep': ['rg', 'ripgrep'],
'grep': ['grep'],
}

def __init__(self):
super().__init__(
'grep',
'run grep or a grep-like tool in one or more local projects',
'Run grep or a grep-like tool in one or more local projects.',
accepts_unknown_args=True)

def do_add_parser(self, parser_adder):
parser = self._parser(parser_adder, epilog=GREP_EPILOG)
parser.add_argument('--tool', choices=self.TOOLS, help='grep tool')
parser.add_argument('--tool-path', help='path to grep tool executable')
# Unlike other project commands, we don't take the project as
# a positional argument. This inconsistency makes the usual
# use case of "search the entire workspace" faster to type.
parser.add_argument('-p', '--project', metavar='PROJECT',
dest='projects', default=[], action='append',
help='''project to run grep tool in (may be given
more than once); default is all cloned active
projects''')
return parser

def do_run(self, args, tool_cmdline_args):
tool = self.tool(args)
command_list = ([self.tool_path(tool, args)] +
self.tool_args(tool, tool_cmdline_args))
failed = []
for project in self._cloned_projects(args,
only_active=not args.projects):
try:
output = self.check_output(command_list, cwd=project.abspath)
except subprocess.CalledProcessError as e:
# By default, supported tools exit 1 if nothing is
# found. Other return codes indicate error.
#
# This changes if you give the --quiet option, but
# that doesn't make much sense in this context, so
# assume it hasn't been passed.
if e.returncode == 1:
continue
else:
failed.append(project)
self.banner(f'{project.name_and_path}:')
self.inf(output.decode())
self._handle_failed(failed)

def tool(self, args):
if args.tool:
return args.tool

return self.config.get('grep.tool', self.DEFAULT_TOOL)

def tool_path(self, tool, args):
if args.tool_path:
return args.tool_path

config_option = self.config.get(f'grep.{tool}-path')
if config_option:
return config_option

if tool == 'git-grep':
self.die_if_no_git()
return self._git

for executable in self.DEFAULT_TOOL_EXECUTABLES[tool]:
path = shutil.which(executable)
if path is not None:
return path

self.die(f'grep tool "{tool}" not found, please use --tool-path')

def tool_args(self, tool, cmdline_args):
ret = []

if tool == 'git-grep':
ret.append('grep')

config_color = self.config.get('grep.color')
if config_color:
color = config_color
else:
if self.color_ui:
color = 'always'
else:
color = 'never'

ret.extend([f'--color={color}'])
ret.extend(self.config.get(f'grep.{tool}-args',
self.DEFAULT_TOOL_ARGS[tool]))
ret.extend(cmdline_args)

return ret

class Topdir(_ProjectCommand):
def __init__(self):
super().__init__(
Expand Down

0 comments on commit f2b9735

Please sign in to comment.