pw_cli: Fish shell completion

For the 'pw' and 'pw build' commands. This mirrors current support for
bash and zsh.

Change-Id: I187c0af27b64d67745639b8ff3f2768de4934a63
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/213734
Reviewed-by: Chad Norvell <chadnorvell@google.com>
Lint: Lint 🤖 <android-build-ayeaye@system.gserviceaccount.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
Pigweed-Auto-Submit: Anthony DiGirolamo <tonymd@google.com>
This commit is contained in:
Anthony DiGirolamo 2024-06-06 20:07:19 +00:00 committed by CQ Bot Account
parent f4da9803f0
commit 3fa03cadd9
9 changed files with 211 additions and 41 deletions

View File

@ -83,7 +83,7 @@ set -e _pw_sourced
set -e _PW_BOOTSTRAP_PATH
set -e SETUP_SH
# TODO(tonymd): Source fish pw_cli shell completion.
source $PW_ROOT/pw_cli/py/pw_cli/shell_completion/pw.fish
pw_cleanup

View File

@ -638,7 +638,7 @@ def main(
force_pw_watch: bool = False,
) -> int:
"""Build upstream Pigweed presubmit steps."""
# pylint: disable=too-many-locals
# pylint: disable=too-many-locals,too-many-branches
parser = get_parser(presubmit_programs, build_recipes)
args = parser.parse_args()
@ -665,14 +665,18 @@ def main(
else:
charset = ASCII_CHARSET
if build_recipes and args.tab_complete_recipe is not None:
_tab_complete_recipe(build_recipes, text=args.tab_complete_recipe)
if args.tab_complete_recipe is not None:
if build_recipes:
_tab_complete_recipe(build_recipes, text=args.tab_complete_recipe)
# Must exit if there are no build_recipes.
return 0
if presubmit_programs and args.tab_complete_presubmit_step is not None:
_tab_complete_presubmit_step(
presubmit_programs, text=args.tab_complete_presubmit_step
)
if args.tab_complete_presubmit_step is not None:
if presubmit_programs:
_tab_complete_presubmit_step(
presubmit_programs, text=args.tab_complete_presubmit_step
)
# Must exit if there are no presubmit_programs.
return 0
# List valid steps + recipes.

View File

@ -43,6 +43,7 @@ pw_python_package("py") {
"pw_cli/pw_command_plugins.py",
"pw_cli/requires.py",
"pw_cli/shell_completion/__init__.py",
"pw_cli/shell_completion/fish/__init__.py",
"pw_cli/shell_completion/zsh/__init__.py",
"pw_cli/shell_completion/zsh/pw/__init__.py",
"pw_cli/shell_completion/zsh/pw_build/__init__.py",
@ -63,7 +64,9 @@ pw_python_package("py") {
mypy_ini = "$dir_pigweed/.mypy.ini"
inputs = [
"pw_cli/shell_completion/common.bash",
"pw_cli/shell_completion/fish/pw.fish",
"pw_cli/shell_completion/pw.bash",
"pw_cli/shell_completion/pw.fish",
"pw_cli/shell_completion/pw.zsh",
"pw_cli/shell_completion/pw_build.bash",
"pw_cli/shell_completion/pw_build.zsh",

View File

@ -31,10 +31,15 @@ def main() -> NoReturn:
pw_cli.log.install(level=args.loglevel, debug_log=args.debug_log)
# Print the banner unless --no-banner or --tab-complete-command is provided.
# Note: args.tab_complete_command may be the empty string '' so check for
# None instead.
if args.banner and args.tab_complete_command is None:
# Print the banner unless --no-banner or a tab completion arg is
# present.
# Note: args.tab_complete_{command,option} may be the empty string
# '' so check for None instead.
if (
args.banner
and args.tab_complete_option is None
and args.tab_complete_command is None
):
arguments.print_banner()
_LOG.debug('Executing the pw command from %s', args.directory)
@ -53,7 +58,7 @@ def main() -> NoReturn:
if args.tab_complete_command is not None:
for name, plugin in sorted(pw_command_plugins.plugin_registry.items()):
if name.startswith(args.tab_complete_command):
if args.tab_complete_format == 'zsh':
if args.tab_complete_format in ('fish', 'zsh'):
print(':'.join([name, plugin.help()]))
else:
print(name)

View File

@ -14,12 +14,13 @@
"""Defines arguments for the pw command."""
import argparse
from dataclasses import dataclass, field
from dataclasses import dataclass
from enum import Enum
from functools import cached_property
import logging
from pathlib import Path
import sys
from typing import NoReturn
from typing import Any, NoReturn
from pw_cli import argument_types, plugins
from pw_cli.branding import banner
@ -41,39 +42,92 @@ class ShellCompletionFormat(Enum):
"""Supported shell tab completion modes."""
BASH = 'bash'
FISH = 'fish'
ZSH = 'zsh'
@dataclass(frozen=True)
class ShellCompletion:
option_strings: list[str] = field(default_factory=list)
help: str | None = None
choices: list[str] | None = None
flag: bool = True
"""Transforms argparse actions into bash, fish, zsh shell completions."""
def bash_completion(self, text: str) -> list[str]:
action: argparse.Action
parser: argparse.ArgumentParser
@property
def option_strings(self) -> list[str]:
return list(self.action.option_strings)
@cached_property
def help(self) -> str:
return self.parser._get_formatter()._expand_help( # pylint: disable=protected-access
self.action
)
@property
def choices(self) -> list[str]:
return list(self.action.choices) if self.action.choices else []
@property
def flag(self) -> bool:
return self.action.nargs == 0
@property
def default(self) -> Any:
return self.action.default
def bash_option(self, text: str) -> list[str]:
result: list[str] = []
for option_str in self.option_strings:
if option_str.startswith(text):
result.append(option_str)
return result
def zsh_completion(self, text: str) -> list[str]:
def zsh_option(self, text: str) -> list[str]:
result: list[str] = []
for option_str in self.option_strings:
if option_str.startswith(text):
short_and_long_opts = ' '.join(self.option_strings)
# '(-h --help)-h[Display help message and exit]'
# '(-h --help)--help[Display help message and exit]'
help_text = self.help if self.help else ''
state_str = ''
if not self.flag:
state_str = ': :->' + option_str
if not option_str.startswith(text):
continue
result.append(
f'({short_and_long_opts}){option_str}[{help_text}]'
f'{state_str}'
)
short_and_long_opts = ' '.join(self.option_strings)
# '(-h --help)-h[Display help message and exit]'
# '(-h --help)--help[Display help message and exit]'
help_text = self.help if self.help else ''
state_str = ''
if not self.flag:
state_str = ': :->' + option_str
result.append(
f'({short_and_long_opts}){option_str}[{help_text}]'
f'{state_str}'
)
return result
def fish_option(self, text: str) -> list[str]:
result: list[str] = []
for option_str in self.option_strings:
if not option_str.startswith(text):
continue
output: list[str] = []
if option_str.startswith('--'):
output.append(f'--long-option\t{option_str.lstrip("-")}')
elif option_str.startswith('-'):
output.append(f'--short-option\t{option_str.lstrip("-")}')
if self.choices:
choice_str = " ".join(self.choices)
output.append('--exclusive')
output.append('--arguments')
output.append(f'(string split " " "{choice_str}")')
elif self.action.type == Path:
output.append('--require-parameter')
output.append('--force-files')
if self.help:
output.append(f'--description\t"{self.help}"')
result.append('\t'.join(output))
return result
@ -81,12 +135,7 @@ def get_options_and_help(
parser: argparse.ArgumentParser,
) -> list[ShellCompletion]:
return list(
ShellCompletion(
option_strings=list(action.option_strings),
help=action.help,
choices=list(action.choices) if action.choices else [],
flag=action.nargs == 0,
)
ShellCompletion(action=action, parser=parser)
for action in parser._actions # pylint: disable=protected-access
)
@ -99,9 +148,11 @@ def print_completions_for_option(
matched_lines: list[str] = []
for completion in get_options_and_help(parser):
if tab_completion_format == ShellCompletionFormat.ZSH.value:
matched_lines.extend(completion.zsh_completion(text))
matched_lines.extend(completion.zsh_option(text))
if tab_completion_format == ShellCompletionFormat.FISH.value:
matched_lines.extend(completion.fish_option(text))
else:
matched_lines.extend(completion.bash_completion(text))
matched_lines.extend(completion.bash_option(text))
for line in matched_lines:
print(line)
@ -179,6 +230,7 @@ def arg_parser() -> argparse.ArgumentParser:
)
argparser.add_argument(
'--debug-log',
type=Path,
help=(
'Additionally log to this file at debug level; does not affect '
'terminal output'

View File

@ -0,0 +1,82 @@
# Copyright 2024 The Pigweed Authors
#
# 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
#
# https://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.
# Only generate completions if $PW_ROOT is set
if test -z "$PW_ROOT"
exit
end
set status_message_length 0
function print_status_message
printf '\033[s\033[1B\033[0G'
set -l status_message "Regenerating completions."
set status_message_length (math 1 + (string length --visible $status_message))
printf $status_message
printf '\033[u'
end
function print_status_indicator
printf '\033[s\033[1B\033['
printf $status_message_length
printf G
printf '.'
printf '\033[u'
set status_message_length (math $status_message_length + 1)
end
print_status_message
complete -c pw --no-files
# pw completion
set --local --append _pw_subcommands (pw --no-banner --tab-complete-format=fish --tab-complete-command "")
print_status_indicator
set --local --append _pw_options (pw --no-banner --tab-complete-format=fish --tab-complete-option "")
print_status_indicator
# pw short and long options
for option in $_pw_options
set -l complete_args (string split \t -- "$option")
complete --command pw --condition "__fish_use_subcommand; and not __fish_seen_subcommand_from $_pw_subcommand_names" $complete_args
end
# pw subcommands
for subcommand in $_pw_subcommands
set --local command_and_help (string split --max 1 ":" "$subcommand")
set --local command $command_and_help[1]
set --local help $command_and_help[2]
set --append _pw_subcommand_names $command
complete --command pw --condition __fish_use_subcommand --arguments "$command" --description "$help"
end
# pw build completion
if contains build $_pw_subcommand_names
set --local --append _pw_build_options (pw --no-banner build --tab-complete-format=fish --tab-complete-option "")
print_status_indicator
set --local --append _pw_build_recipes (pw --no-banner build --tab-complete-format=fish --tab-complete-recipe "")
print_status_indicator
set --local --append _pw_build_presubmit_steps (pw --no-banner build --tab-complete-format=fish --tab-complete-presubmit-step "")
print_status_indicator
complete --command pw --condition "__fish_seen_subcommand_from build" -s r -l recipe -x -a "$_pw_build_recipes"
complete --command pw --condition "__fish_seen_subcommand_from build" -s s -l step -x -a "$_pw_build_presubmit_steps"
for option in $_pw_build_options
set -l complete_args (string split \t -- "$option")
complete --command pw --condition "__fish_seen_subcommand_from build" $complete_args
end
end

View File

@ -0,0 +1,22 @@
# Copyright 2024 The Pigweed Authors
#
# 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
#
# https://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.
# This script must be tested on fish 3.6.0
set _pw_fish_completion_path (path resolve (status current-dirname)/fish)
# Add the fish subdirectory to the completion path
if not contains $_pw_fish_completion_path $fish_complete_path
set -x --append fish_complete_path $_pw_fish_completion_path
end

View File

@ -34,8 +34,10 @@ pw_cli =
py.typed
shell_completion/common.bash
shell_completion/pw.bash
shell_completion/pw.fish
shell_completion/pw.zsh
shell_completion/pw_build.bash
shell_completion/pw_build.zsh
shell_completion/fish/pw.fish
shell_completion/zsh/pw/_pw
shell_completion/zsh/pw_build/_pw_build