| # Copyright 2020 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. |
| """Defines arguments for the pw command.""" |
| |
| import argparse |
| from dataclasses import dataclass |
| from enum import Enum |
| from functools import cached_property |
| import logging |
| from pathlib import Path |
| import sys |
| from typing import Any, NoReturn |
| |
| from pw_cli import argument_types, plugins |
| from pw_cli.branding import banner |
| import pw_cli.env |
| |
| _HELP_HEADER = '''The Pigweed command line interface (CLI). |
| |
| Example uses: |
| pw logdemo |
| pw --loglevel debug watch -C out |
| ''' |
| |
| |
| def parse_args() -> argparse.Namespace: |
| return arg_parser().parse_args() |
| |
| |
| class ShellCompletionFormat(Enum): |
| """Supported shell tab completion modes.""" |
| |
| BASH = 'bash' |
| FISH = 'fish' |
| ZSH = 'zsh' |
| |
| |
| @dataclass(frozen=True) |
| class ShellCompletion: |
| """Transforms argparse actions into bash, fish, zsh shell completions.""" |
| |
| 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_option(self, text: str) -> list[str]: |
| result: list[str] = [] |
| for option_str in self.option_strings: |
| if not option_str.startswith(text): |
| continue |
| |
| 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 |
| |
| |
| def get_options_and_help( |
| parser: argparse.ArgumentParser, |
| ) -> list[ShellCompletion]: |
| return list( |
| ShellCompletion(action=action, parser=parser) |
| for action in parser._actions # pylint: disable=protected-access |
| ) |
| |
| |
| def print_completions_for_option( |
| parser: argparse.ArgumentParser, |
| text: str = '', |
| tab_completion_format: str = ShellCompletionFormat.BASH.value, |
| ) -> None: |
| matched_lines: list[str] = [] |
| for completion in get_options_and_help(parser): |
| if tab_completion_format == ShellCompletionFormat.ZSH.value: |
| 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_option(text)) |
| |
| for line in matched_lines: |
| print(line) |
| |
| |
| def print_banner() -> None: |
| """Prints the PIGWEED (or project specific) banner to stderr.""" |
| parsed_env = pw_cli.env.pigweed_environment() |
| if parsed_env.PW_ENVSETUP_NO_BANNER or parsed_env.PW_ENVSETUP_QUIET: |
| return |
| print(banner() + '\n', file=sys.stderr) |
| |
| |
| def format_help(registry: plugins.Registry) -> str: |
| """Returns the pw help information as a string.""" |
| return f'{arg_parser().format_help()}\n{registry.short_help()}' |
| |
| |
| class _ArgumentParserWithBanner(argparse.ArgumentParser): |
| """Parser that the Pigweed banner when there are parsing errors.""" |
| |
| def error(self, message: str) -> NoReturn: |
| print_banner() |
| self.print_usage(sys.stderr) |
| self.exit(2, f'{self.prog}: error: {message}\n') |
| |
| |
| def add_tab_complete_arguments( |
| parser: argparse.ArgumentParser, |
| ) -> argparse.ArgumentParser: |
| parser.add_argument( |
| '--tab-complete-option', |
| nargs='?', |
| help='Print tab completions for the supplied option text.', |
| ) |
| parser.add_argument( |
| '--tab-complete-format', |
| choices=list(shell.value for shell in ShellCompletionFormat), |
| default='bash', |
| help='Output format for tab completion results.', |
| ) |
| return parser |
| |
| |
| def arg_parser() -> argparse.ArgumentParser: |
| """Creates an argument parser for the pw command.""" |
| argparser = _ArgumentParserWithBanner( |
| prog='pw', |
| add_help=False, |
| description=_HELP_HEADER, |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| ) |
| |
| # Do not use the built-in help argument so that displaying the help info can |
| # be deferred until the pw plugins have been registered. |
| argparser.add_argument( |
| '-h', |
| '--help', |
| action='store_true', |
| help='Display this help message and exit', |
| ) |
| argparser.add_argument( |
| '-C', |
| '--directory', |
| type=argument_types.directory, |
| default=Path.cwd(), |
| help='Change to this directory before doing anything', |
| ) |
| argparser.add_argument( |
| '-l', |
| '--loglevel', |
| type=argument_types.log_level, |
| default=logging.INFO, |
| help='Set the log level (debug, info, warning, error, critical)', |
| ) |
| argparser.add_argument( |
| '--debug-log', |
| type=Path, |
| help=( |
| 'Additionally log to this file at debug level; does not affect ' |
| 'terminal output' |
| ), |
| ) |
| argparser.add_argument( |
| '--banner', |
| action=argparse.BooleanOptionalAction, |
| default=True, |
| help='Whether to print the Pigweed banner', |
| ) |
| argparser.add_argument( |
| '--tab-complete-command', |
| nargs='?', |
| help='Print tab completions for the supplied command text.', |
| ) |
| argparser.add_argument( |
| 'command', |
| nargs='?', |
| help='Which command to run; see supported commands below', |
| ) |
| argparser.add_argument( |
| 'plugin_args', |
| metavar='...', |
| nargs=argparse.REMAINDER, |
| help='Remaining arguments are forwarded to the command', |
| ) |
| return add_tab_complete_arguments(argparser) |