mirror of
https://github.com/espressif/esptool.git
synced 2025-10-20 13:23:38 +08:00
feat(espefuse): Add support for chaining commands with click parser
This commit is contained in:
@@ -67,7 +67,7 @@ def get_efuses(
|
|||||||
|
|
||||||
@click.group(
|
@click.group(
|
||||||
cls=Group,
|
cls=Group,
|
||||||
# chain=True, # allow using multiple commands in a single run
|
chain=True, # allow using multiple commands in a single run
|
||||||
no_args_is_help=True,
|
no_args_is_help=True,
|
||||||
context_settings=dict(help_option_names=["-h", "--help"], max_content_width=120),
|
context_settings=dict(help_option_names=["-h", "--help"], max_content_width=120),
|
||||||
help=f"espefuse.py v{esptool.__version__} - ESP32xx eFuse get/set tool",
|
help=f"espefuse.py v{esptool.__version__} - ESP32xx eFuse get/set tool",
|
||||||
|
@@ -4,8 +4,10 @@
|
|||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import rich_click as click
|
import rich_click as click
|
||||||
|
from click.parser import OptionParser, ParsingState, _unpack_args
|
||||||
from espefuse.efuse.base_operations import BaseCommands
|
from espefuse.efuse.base_operations import BaseCommands
|
||||||
import esptool
|
import esptool
|
||||||
from esptool.cli_util import Group as EsptoolGroup
|
from esptool.cli_util import Group as EsptoolGroup
|
||||||
@@ -85,11 +87,110 @@ click.rich_click.COMMAND_GROUPS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ChainParser(OptionParser):
|
||||||
|
"""
|
||||||
|
This is a modified version of the OptionParser class from click.parser.
|
||||||
|
It allows for the processing of arguments and options in interspersed order
|
||||||
|
together with chaining commands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _process_args_for_options(self, state: ParsingState) -> None:
|
||||||
|
while state.rargs:
|
||||||
|
arg = state.rargs.pop(0)
|
||||||
|
arglen = len(arg)
|
||||||
|
# Double dashes always handled explicitly regardless of what
|
||||||
|
# prefixes are valid.
|
||||||
|
if arg == "--":
|
||||||
|
return
|
||||||
|
# if the argument is a command, stop parsing options
|
||||||
|
elif arg.replace("_", "-") in SUPPORTED_COMMANDS:
|
||||||
|
state.largs.append(arg)
|
||||||
|
return
|
||||||
|
elif arg[:1] in self._opt_prefixes and arglen > 1:
|
||||||
|
self._process_opts(arg, state)
|
||||||
|
elif self.allow_interspersed_args:
|
||||||
|
state.largs.append(arg)
|
||||||
|
else:
|
||||||
|
state.rargs.insert(0, arg)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _process_args_for_args(self, state: ParsingState) -> None:
|
||||||
|
pargs, args = _unpack_args(
|
||||||
|
state.largs + state.rargs, [x.nargs for x in self._args]
|
||||||
|
)
|
||||||
|
|
||||||
|
# This check is required because of the way we modify nargs in ChainingCommand
|
||||||
|
if len(pargs) > 0:
|
||||||
|
for idx, arg in enumerate(self._args):
|
||||||
|
arg.process(pargs[idx], state)
|
||||||
|
|
||||||
|
state.largs = args
|
||||||
|
state.rargs = []
|
||||||
|
|
||||||
|
|
||||||
|
class ChainingCommand(click.RichCommand, click.Command):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _is_option(self, arg: str) -> bool:
|
||||||
|
return arg.startswith("--") or arg.startswith("-")
|
||||||
|
|
||||||
|
def invoke(self, ctx: click.Context) -> Any:
|
||||||
|
log.print(f'\n=== Run "{self.name}" command ===')
|
||||||
|
return super().invoke(ctx)
|
||||||
|
|
||||||
|
def parse_args(self, ctx: click.Context, args: list[str]):
|
||||||
|
# This is a hack to set nargs of the last argument to the number of arguments
|
||||||
|
# that will be processed separately
|
||||||
|
param_changed = None
|
||||||
|
for idx, arg in enumerate(args):
|
||||||
|
# command found in args or option found after argument
|
||||||
|
if arg.replace("_", "-") in SUPPORTED_COMMANDS or (
|
||||||
|
self._is_option(arg) and idx > 0
|
||||||
|
):
|
||||||
|
arguments_count = sum(
|
||||||
|
isinstance(param, click.Argument) for param in self.params
|
||||||
|
)
|
||||||
|
for param in self.params:
|
||||||
|
if param.nargs != -1:
|
||||||
|
continue
|
||||||
|
# set nargs of parameter to actual count of arguments and deduct
|
||||||
|
# arguments_count as each argument will be processed separately,
|
||||||
|
# we only care about the last one with nargs=-1
|
||||||
|
# at the end we add 1 to account for the processedargument itself
|
||||||
|
# e.g. if we have burn-bit BLOCK2 1 2 3, we want to set nargs to 3,
|
||||||
|
# so we need to account for BLOCK2 being processed separately
|
||||||
|
param.nargs = args.index(arg) - arguments_count + 1
|
||||||
|
param_changed = param
|
||||||
|
if param.nargs == 0 and param.required:
|
||||||
|
raise click.UsageError(
|
||||||
|
f"Command `{self.name}` requires the `{param.name}` "
|
||||||
|
"argument."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
break
|
||||||
|
ret = super().parse_args(ctx, args)
|
||||||
|
# restore nargs of the last argument to -1, in case it is going to be used again
|
||||||
|
if param_changed is not None:
|
||||||
|
param.nargs = -1
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def make_parser(self, ctx: click.Context) -> OptionParser:
|
||||||
|
"""Creates the underlying option parser for this command."""
|
||||||
|
parser = ChainParser(ctx)
|
||||||
|
parser.allow_interspersed_args = True
|
||||||
|
for param in self.get_params(ctx):
|
||||||
|
param.add_to_parser(parser, ctx)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
class Group(EsptoolGroup):
|
class Group(EsptoolGroup):
|
||||||
DEPRECATED_OPTIONS = {
|
DEPRECATED_OPTIONS = {
|
||||||
"--file_name": "--file-name",
|
"--file_name": "--file-name",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
command_class = ChainingCommand
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _split_to_groups(args: list[str]) -> tuple[list[list[str]], list[str]]:
|
def _split_to_groups(args: list[str]) -> tuple[list[list[str]], list[str]]:
|
||||||
"""
|
"""
|
||||||
|
@@ -98,7 +98,7 @@ class TupleParameter(click.Argument):
|
|||||||
ctx.exit()
|
ctx.exit()
|
||||||
|
|
||||||
# Check if we have more values than allowed by max_arity
|
# Check if we have more values than allowed by max_arity
|
||||||
if len(value) > self.max_arity * self.type.arity:
|
if self.max_arity is not None and len(value) > self.max_arity * self.type.arity:
|
||||||
raise click.BadParameter(
|
raise click.BadParameter(
|
||||||
f"Expected at most {self.max_arity} groups ({self.type.arity} values "
|
f"Expected at most {self.max_arity} groups ({self.type.arity} values "
|
||||||
f"each), got {len(value)} (values: {value})"
|
f"each), got {len(value)} (values: {value})"
|
||||||
@@ -294,8 +294,8 @@ class BaseCommands:
|
|||||||
"split - each eFuse block is placed into its own file.",
|
"split - each eFuse block is placed into its own file.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--file_name",
|
"--file-name",
|
||||||
type=click.Path(exists=True, writable=True),
|
type=click.File("w"),
|
||||||
default=sys.stdout,
|
default=sys.stdout,
|
||||||
help="The path to the file in which to save the dump, if not specified, "
|
help="The path to the file in which to save the dump, if not specified, "
|
||||||
"output to the console.",
|
"output to the console.",
|
||||||
|
Reference in New Issue
Block a user