blob: cac04b83284ca6360b73cbbffa1a4af875767d34 [file] [log] [blame]
# 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)