app: handle unexpected command name better

Right now if you run a zephyr extension like 'west build' outside of a
workspace, argparse says:

  [...] invalid choice: ‘build’ [...]

This is because argparse's subcommand parser doesn't seem to have any
API to add a catch-all value for when the user provides an unknown
command, so it expects that exactly the subcommands we told it about
are available.

This is confusing to users, and now that we have our own EarlyArgs
parsed, we can do better by printing some west-specific help if we
aren't in a workspace:

  usage: west [-h] [-z ZEPHYR_BASE] [-v] [-V] <command> ...
  west: unknown command "build"; do you need to run this inside a
  workspace?

as well as if you are:

  usage: west [-h] [-z ZEPHYR_BASE] [-v] [-V] <command> ...
  west: unknown command "foo"; workspace /home/mbolivar/zp does
  not define this extension command -- try "west help"

Signed-off-by: Martí Bolívar <marti.bolivar@nordicsemi.no>
This commit is contained in:
Martí Bolívar 2023-05-31 16:27:08 -07:00 committed by Carles Cufí
parent f9630e37be
commit d842371089
1 changed files with 29 additions and 2 deletions

View File

@ -184,6 +184,8 @@ class WestApp:
def run(self, argv): def run(self, argv):
# Run the command-line application with argument list 'argv'. # Run the command-line application with argument list 'argv'.
early_args = parse_early_args(argv)
# Silence validation errors from pykwalify, which are logged at # Silence validation errors from pykwalify, which are logged at
# logging.ERROR level. We want to handle those ourselves as # logging.ERROR level. We want to handle those ourselves as
# needed. # needed.
@ -218,7 +220,7 @@ class WestApp:
self.setup_parsers() self.setup_parsers()
# OK, we are all set. Run the command. # OK, we are all set. Run the command.
self.run_command(argv) self.run_command(argv, early_args)
def load_manifest(self): def load_manifest(self):
# Try to parse the manifest. We'll save it if that works, so # Try to parse the manifest. We'll save it if that works, so
@ -451,11 +453,12 @@ class WestApp:
return parser, subparser_gen return parser, subparser_gen
def run_command(self, argv): def run_command(self, argv, early_args):
# Parse command line arguments and run the WestCommand. # Parse command line arguments and run the WestCommand.
# If we're running an extension, instantiate it from its # If we're running an extension, instantiate it from its
# spec and re-parse arguments before running. # spec and re-parse arguments before running.
self.handle_early_arg_errors(early_args)
args, unknown = self.west_parser.parse_known_args(args=argv) args, unknown = self.west_parser.parse_known_args(args=argv)
# Set up logging verbosity before running the command, for # Set up logging verbosity before running the command, for
@ -537,6 +540,30 @@ class WestApp:
except WestNotFound as wnf: except WestNotFound as wnf:
self.cmd.die(str(wnf)) self.cmd.die(str(wnf))
def handle_early_arg_errors(self, early_args):
# If early_args indicates we should error out, handle it
# gracefully. This provides more user-friendly output than
# argparse can do on its own.
if (early_args.command_name and
(early_args.command_name not in self.builtins and
(not self.extensions or
early_args.command_name not in self.extensions))):
self.handle_unknown_command(early_args.command_name)
def handle_unknown_command(self, command_name):
if self.topdir:
extra_help = (f'workspace {self.topdir} does not define '
'this extension command -- try "west help"')
else:
extra_help = 'do you need to run this inside a workspace?'
self.print_usage_and_exit(f'west: unknown command "{command_name}"; '
f'{extra_help}')
def print_usage_and_exit(self, message):
self.west_parser.print_usage(file=sys.stderr)
sys.exit(message)
def run_builtin(self, args, unknown): def run_builtin(self, args, unknown):
self.queued_io.append( self.queued_io.append(
lambda cmd: cmd.dbg('args namespace:', args, lambda cmd: cmd.dbg('args namespace:', args,