mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-08 14:18:26 +02:00
Cache parsing of command line specifications
Change option specification from dict to tuple for better performance and immutability and better type checking.
This commit is contained in:
@@ -247,7 +247,7 @@ def clone_safe_launch_opts() -> Sequence[GoOption]:
|
||||
ans = []
|
||||
allowed = clone_safe_opts()
|
||||
for o in go_options_for_seq(parse_option_spec(options_spec())[0]):
|
||||
if o.obj_dict['name'] in allowed:
|
||||
if o.obj_defn.name in allowed:
|
||||
ans.append(o)
|
||||
return tuple(ans)
|
||||
|
||||
|
||||
@@ -29,16 +29,16 @@ def migrate_help(x: str) -> str:
|
||||
|
||||
|
||||
def help_of(x: str) -> str:
|
||||
return migrate_help(panel_opts[x]['help'])
|
||||
return migrate_help(panel_opts[x].help)
|
||||
|
||||
|
||||
agr('qat', 'Window appearance')
|
||||
|
||||
opt('lines', '25', long_text=panel_opts['lines']['help'])
|
||||
opt('lines', '25', long_text=panel_opts['lines'].help)
|
||||
|
||||
opt('columns', '80', long_text=panel_opts['columns']['help'])
|
||||
opt('columns', '80', long_text=panel_opts['columns'].help)
|
||||
|
||||
opt('edge', 'top', choices=panel_opts['edge']['choices'], long_text=help_of('edge'))
|
||||
opt('edge', 'top', choices=panel_opts['edge'].choices, long_text=help_of('edge'))
|
||||
|
||||
opt('background_opacity', '0.85', option_type='unit_float', long_text='''
|
||||
The background opacity of the window. This works the same as the kitty
|
||||
@@ -80,7 +80,7 @@ opt('output_name', '', long_text=help_of('output_name'))
|
||||
opt('start_as_hidden', 'no', option_type='to_bool',
|
||||
long_text='Whether to start the quick access terminal hidden. Useful if you are starting it as part of system startup.')
|
||||
|
||||
opt('focus_policy', 'exclusive', choices=panel_opts['focus_policy']['choices'], long_text=help_of('focus_policy'))
|
||||
opt('focus_policy', 'exclusive', choices=panel_opts['focus_policy'].choices, long_text=help_of('focus_policy'))
|
||||
|
||||
|
||||
|
||||
|
||||
80
kitty/cli.py
80
kitty/cli.py
@@ -15,7 +15,7 @@ from .fast_data_types import parse_cli_from_spec, wcswidth
|
||||
from .options.types import Options as KittyOpts
|
||||
from .simple_cli_definitions import (
|
||||
CompletionType,
|
||||
OptionDict,
|
||||
OptionDefinition,
|
||||
OptionSpecSeq,
|
||||
defval_for_opt,
|
||||
get_option_maps,
|
||||
@@ -34,34 +34,34 @@ go_type_map = {
|
||||
|
||||
class GoOption:
|
||||
|
||||
def __init__(self, x: OptionDict) -> None:
|
||||
flags = sorted(x['aliases'], key=len)
|
||||
def __init__(self, x: OptionDefinition) -> None:
|
||||
flags = sorted(x.aliases, key=len)
|
||||
short = ''
|
||||
self.aliases = []
|
||||
if len(flags) > 1 and not flags[0].startswith("--"):
|
||||
short = flags[0][1:]
|
||||
self.short, self.long = short, x['name'].replace('_', '-')
|
||||
self.short, self.long = short, x.name.replace('_', '-')
|
||||
for f in flags:
|
||||
q = f[2:] if f.startswith('--') else f[1:]
|
||||
self.aliases.append(q)
|
||||
self.type = x['type']
|
||||
if x['choices']:
|
||||
self.type = x.type
|
||||
if x.choices:
|
||||
self.type = 'choices'
|
||||
self.default = x['default']
|
||||
self.obj_dict = x
|
||||
self.default = x.default
|
||||
self.obj_defn = x
|
||||
self.go_type = go_type_map[self.type]
|
||||
if x['dest']:
|
||||
self.go_var_name = ''.join(x.capitalize() for x in x['dest'].replace('-', '_').split('_'))
|
||||
if x.dest:
|
||||
self.go_var_name = ''.join(x.capitalize() for x in x.dest.replace('-', '_').split('_'))
|
||||
else:
|
||||
self.go_var_name = ''.join(x.capitalize() for x in self.long.replace('-', '_').split('_'))
|
||||
self.help_text = serialize_as_go_string(self.obj_dict['help'].strip())
|
||||
self.help_text = serialize_as_go_string(self.obj_defn.help.strip())
|
||||
|
||||
def struct_declaration(self) -> str:
|
||||
return f'{self.go_var_name} {self.go_type}'
|
||||
|
||||
@property
|
||||
def flags(self) -> list[str]:
|
||||
return sorted(self.obj_dict['aliases'])
|
||||
return sorted(self.obj_defn.aliases)
|
||||
|
||||
def as_option(self, cmd_name: str = 'cmd', depth: int = 0, group: str = '') -> str:
|
||||
add = f'AddToGroup("{serialize_as_go_string(group)}", ' if group else 'Add('
|
||||
@@ -77,8 +77,8 @@ class GoOption:
|
||||
cx = ', '.join(f'"{serialize_as_go_string(x)}"' for x in self.sorted_choices)
|
||||
ans += f'\nChoices: "{serialize_as_go_string(c)}",\n'
|
||||
ans += f'\nCompleter: cli.NamesCompleter("Choices for {self.long}", {cx}),'
|
||||
elif self.obj_dict['completion'].type is not CompletionType.none:
|
||||
ans += ''.join(self.obj_dict['completion'].as_go_code('Completer', ': ')) + ','
|
||||
elif self.obj_defn.completion.type is not CompletionType.none:
|
||||
ans += ''.join(self.obj_defn.completion.as_go_code('Completer', ': ')) + ','
|
||||
if depth > 0:
|
||||
ans += f'\n\tDepth: {depth},\n'
|
||||
if self.default:
|
||||
@@ -111,7 +111,7 @@ class GoOption:
|
||||
|
||||
@property
|
||||
def sorted_choices(self) -> list[str]:
|
||||
choices = sorted(self.obj_dict['choices'])
|
||||
choices = sorted(self.obj_defn.choices)
|
||||
choices.remove(self.default or '')
|
||||
choices.insert(0, self.default or '')
|
||||
return choices
|
||||
@@ -374,7 +374,7 @@ def get_defaults_from_seq(seq: OptionSpecSeq) -> dict[str, Any]:
|
||||
ans: dict[str, Any] = {}
|
||||
for opt in seq:
|
||||
if not isinstance(opt, str):
|
||||
ans[opt['dest']] = defval_for_opt(opt)
|
||||
ans[opt.dest] = defval_for_opt(opt)
|
||||
return ans
|
||||
|
||||
|
||||
@@ -432,24 +432,24 @@ class PrintHelpForSeq:
|
||||
if isinstance(opt, str):
|
||||
a(f'{title(opt)}:')
|
||||
continue
|
||||
help_text = opt['help']
|
||||
help_text = opt.help
|
||||
if help_text == '!':
|
||||
continue # hidden option
|
||||
a(' ' + ', '.join(map(green, sorted(opt['aliases'], reverse=True))))
|
||||
defval = opt.get('default')
|
||||
if (otype := opt.get('type', '')).startswith('bool-'):
|
||||
a(' ' + ', '.join(map(green, sorted(opt.aliases, reverse=True))))
|
||||
defval = opt.default
|
||||
if (otype := opt.type).startswith('bool-'):
|
||||
blocks[-1] += italic(f'[={help_defval_for_bool(otype)}]')
|
||||
else:
|
||||
dt = f'''=[{italic(defval or '""')}]'''
|
||||
blocks[-1] += dt
|
||||
if opt.get('help'):
|
||||
if opt.help:
|
||||
t = help_text.replace('%default', str(defval)).strip()
|
||||
# replace rst literal code block syntax
|
||||
t = t.replace('::\n\n', ':\n\n')
|
||||
t = t.replace('#placeholder_for_formatting#', '')
|
||||
wa(prettify(t), indent=4)
|
||||
if opt.get('choices'):
|
||||
wa('Choices: {}'.format(', '.join(opt['choices'])), indent=4)
|
||||
if opt.choices:
|
||||
wa('Choices: {}'.format(', '.join(opt.choices)), indent=4)
|
||||
a('')
|
||||
|
||||
text = '\n'.join(blocks) + '\n\n' + version()
|
||||
@@ -510,25 +510,25 @@ def seq_as_rst(
|
||||
a(opt)
|
||||
a('~' * (len(opt) + 10))
|
||||
continue
|
||||
help_text = opt['help']
|
||||
help_text = opt.help
|
||||
if help_text == '!':
|
||||
continue # hidden option
|
||||
defn = '.. option:: '
|
||||
if (otype := opt.get('type', '')).startswith('bool-'):
|
||||
if (otype := opt.type).startswith('bool-'):
|
||||
val_name = f' [={help_defval_for_bool(otype)}]'
|
||||
else:
|
||||
val_name = ' <{}>'.format(opt['dest'].upper())
|
||||
a(defn + ', '.join(o + val_name for o in sorted(opt['aliases'])))
|
||||
if opt.get('help'):
|
||||
defval = opt.get('default')
|
||||
val_name = ' <{}>'.format(opt.dest.upper())
|
||||
a(defn + ', '.join(o + val_name for o in sorted(opt.aliases)))
|
||||
if opt.help:
|
||||
defval = opt.default
|
||||
t = help_text.replace('%default', ':code:`' + escape_rst(str(defval)) + '`').strip()
|
||||
t = t.replace('#placeholder_for_formatting#', '')
|
||||
a('')
|
||||
a(textwrap.indent(prettify_rst(t), ' ' * 4))
|
||||
if defval is not None:
|
||||
a(textwrap.indent(f'Default: :code:`{escape_rst(str(defval))}`', ' ' * 4))
|
||||
if opt.get('choices'):
|
||||
a(textwrap.indent('Choices: {}'.format(', '.join(f':code:`{escape_rst(c)}`' for c in sorted(opt['choices']))), ' ' * 4))
|
||||
if opt.choices:
|
||||
a(textwrap.indent('Choices: {}'.format(', '.join(f':code:`{escape_rst(c)}`' for c in sorted(opt.choices))), ' ' * 4))
|
||||
a('')
|
||||
|
||||
text = '\n'.join(blocks)
|
||||
@@ -541,8 +541,8 @@ def as_type_stub(seq: OptionSpecSeq, disabled: OptionSpecSeq, class_name: str, e
|
||||
for opt in chain(seq, disabled):
|
||||
if isinstance(opt, str):
|
||||
continue
|
||||
name = opt['dest']
|
||||
otype = opt['type'] or 'str'
|
||||
name = opt.dest
|
||||
otype = opt.type or 'str'
|
||||
if otype in ('str', 'int', 'float'):
|
||||
t = otype
|
||||
if t == 'str' and defval_for_opt(opt) is None:
|
||||
@@ -550,8 +550,8 @@ def as_type_stub(seq: OptionSpecSeq, disabled: OptionSpecSeq, class_name: str, e
|
||||
elif otype == 'list':
|
||||
t = 'typing.Sequence[str]'
|
||||
elif otype in ('choice', 'choices'):
|
||||
if opt['choices']:
|
||||
t = 'typing.Literal[{}]'.format(','.join(f'{x!r}' for x in opt['choices']))
|
||||
if opt.choices:
|
||||
t = 'typing.Literal[{}]'.format(','.join(f'{x!r}' for x in opt.choices))
|
||||
else:
|
||||
t = 'str'
|
||||
elif otype.startswith('bool-'):
|
||||
@@ -616,14 +616,14 @@ def apply_preparsed_cli_flags(
|
||||
|
||||
|
||||
def parse_cmdline_inner(
|
||||
args: list[str], oc: Options, disabled: OptionSpecSeq, names_map: dict[str, OptionDict],
|
||||
values_map: dict[str, OptionDict], ans: Any, track_seen_options: dict[str, Any] | None = None
|
||||
args: list[str], oc: Options, disabled: OptionSpecSeq, names_map: dict[str, OptionDefinition],
|
||||
values_map: dict[str, OptionDefinition], ans: Any, track_seen_options: dict[str, Any] | None = None
|
||||
) -> list[str]:
|
||||
preparsed = parse_cli_from_spec(args, names_map, values_map)
|
||||
leftover_args = apply_preparsed_cli_flags(preparsed, ans, lambda: oc, track_seen_options)
|
||||
for opt in disabled:
|
||||
if not isinstance(opt, str):
|
||||
setattr(ans, opt['dest'], defval_for_opt(opt))
|
||||
setattr(ans, opt.dest, defval_for_opt(opt))
|
||||
return leftover_args
|
||||
|
||||
|
||||
@@ -634,10 +634,10 @@ def parse_cmdline(
|
||||
names_map = oc.names_map.copy()
|
||||
values_map = oc.values_map.copy()
|
||||
if 'help' not in names_map:
|
||||
names_map['help'] = {'type': 'bool-set', 'aliases': ('--help', '-h')} # type: ignore
|
||||
names_map['help'] = OptionDefinition(type='bool-set', aliases=('--help', '-h'))
|
||||
values_map['help'] = False
|
||||
if 'version' not in names_map:
|
||||
names_map['version'] = {'type': 'bool-set', 'aliases': ('--version', '-v')} # type: ignore
|
||||
names_map['version'] = OptionDefinition(type='bool-set', aliases=('--version', '-v'))
|
||||
values_map['version'] = False
|
||||
try:
|
||||
return parse_cmdline_inner(sys.argv[1:] if args is None else args, oc, disabled, names_map, values_map, ans, track_seen_options)
|
||||
|
||||
@@ -8,7 +8,7 @@ from kitty.fonts.render import FontObject
|
||||
from kitty.marks import MarkerFunc
|
||||
from kitty.notifications import MacOSNotificationCategory
|
||||
from kitty.options.types import Options
|
||||
from kitty.simple_cli_definitions import OptionDict
|
||||
from kitty.simple_cli_definitions import OptionDefinition
|
||||
from kitty.types import LayerShellConfig, SignalInfo
|
||||
from kitty.typing_compat import EdgeLiteral, NotRequired, ReadableBuffer, WriteableBuffer
|
||||
|
||||
@@ -1720,7 +1720,9 @@ def set_clipboard_data_types(ct: int, mime_types: Tuple[str, ...]) -> None: ...
|
||||
def get_clipboard_mime(ct: int, mime: Optional[str], callback: Callable[[bytes], None]) -> None: ...
|
||||
def run_with_activation_token(func: Callable[[str], None]) -> bool: ...
|
||||
def toggle_os_window_visibility(os_window_id: int, visible: bool | Literal[-1] = -1, move_to_active_screen: bool = False) -> bool: ...
|
||||
def parse_cli_from_spec(args: list[str], names_map: dict[str, OptionDict], defval_map: dict[str, Any]) -> tuple[dict[str, tuple[Any, bool]], list[str]]: ...
|
||||
def parse_cli_from_spec(
|
||||
args: list[str], names_map: dict[str, OptionDefinition], defval_map: dict[str, Any]
|
||||
) -> tuple[dict[str, tuple[Any, bool]], list[str]]: ...
|
||||
def layer_shell_config_for_os_window(os_window_id: int) -> dict[str, Any] | None: ...
|
||||
def set_layer_shell_config(os_window_id: int, cfg: LayerShellConfig) -> bool: ...
|
||||
def wrapped_kitten_names() -> List[str]: ...
|
||||
|
||||
@@ -559,14 +559,16 @@ parse_cli_from_python_spec(PyObject *self, PyObject *args) {
|
||||
memcpy(argv[i + 1], src, sz);
|
||||
}
|
||||
argv[++argc] = 0;
|
||||
PyObject *key = NULL, *opt = NULL;
|
||||
PyObject *key = NULL, *optdef = NULL;
|
||||
Py_ssize_t pos = 0;
|
||||
while (PyDict_Next(names_map, &pos, &key, &opt)) {
|
||||
while (PyDict_Next(names_map, &pos, &key, &optdef)) {
|
||||
FlagSpec flag = {.dest=PyUnicode_AsUTF8(key)};
|
||||
PyObject *pytype = PyDict_GetItemString(opt, "type");
|
||||
const char *type = pytype ? PyUnicode_AsUTF8(pytype) : "";
|
||||
RAII_PyObject(pytype, PyObject_GetAttrString(optdef, "type"));
|
||||
if (!pytype) return NULL;
|
||||
const char *type = PyUnicode_AsUTF8(pytype);
|
||||
PyObject *defval = PyDict_GetItemWithError(defval_map, key); if (!defval && PyErr_Occurred()) return NULL;
|
||||
PyObject *pyaliases = PyDict_GetItemString(opt, "aliases");
|
||||
RAII_PyObject(pyaliases, PyObject_GetAttrString(optdef, "aliases"));
|
||||
if (!pyaliases) return NULL;
|
||||
for (int a = 0; a < PyTuple_GET_SIZE(pyaliases); a++) {
|
||||
const char *alias = PyUnicode_AsUTF8(PyTuple_GET_ITEM(pyaliases, a));
|
||||
if (vt_is_end(vt_insert(&spec.alias_map, alias, flag.dest))) return PyErr_NoMemory();
|
||||
@@ -588,7 +590,8 @@ parse_cli_from_python_spec(PyObject *self, PyObject *args) {
|
||||
} else if (strcmp(type, "choices") == 0) {
|
||||
flag.defval.type = CLI_VALUE_CHOICE;
|
||||
flag.defval.strval = PyUnicode_AsUTF8(defval);
|
||||
PyObject *pyc = PyDict_GetItemString(opt, "choices");
|
||||
RAII_PyObject(pyc, PyObject_GetAttrString(optdef, "choices"));
|
||||
if (!pyc) return NULL;
|
||||
flag.defval.listval.items = alloc_for_cli(&spec, PyTuple_GET_SIZE(pyc) * sizeof(char*));
|
||||
if (!flag.defval.listval.items) return PyErr_NoMemory();
|
||||
flag.defval.listval.count = PyTuple_GET_SIZE(pyc);
|
||||
|
||||
@@ -82,8 +82,8 @@ CmdGenerator = Iterator[CmdReturnType]
|
||||
PayloadType = Optional[Union[CmdReturnType, CmdGenerator]]
|
||||
PayloadGetType = PayloadGetter
|
||||
ArgsType = list[str]
|
||||
ImageCompletion = CompletionSpec.from_string('type:file group:"Images"')
|
||||
ImageCompletion.extensions = 'png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff'
|
||||
ImageCompletion = CompletionSpec.from_string('type:file group:"Images"')._replace(
|
||||
extensions=('png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff'))
|
||||
SUPPORTED_IMAGE_FORMATS = tuple(x.upper() for x in ImageCompletion.extensions if x != 'jpg')
|
||||
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
from typing import Any, Iterator, Sequence, TypedDict
|
||||
from functools import lru_cache
|
||||
from typing import Any, Iterator, NamedTuple, Sequence
|
||||
|
||||
try:
|
||||
from kitty.constants import appname, is_macos
|
||||
@@ -42,8 +42,7 @@ class CompletionRelativeTo(Enum):
|
||||
config_dir = auto()
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompletionSpec:
|
||||
class CompletionSpec(NamedTuple):
|
||||
|
||||
type: CompletionType = CompletionType.none
|
||||
kwds: tuple[str,...] = ()
|
||||
@@ -54,27 +53,33 @@ class CompletionSpec:
|
||||
|
||||
@staticmethod
|
||||
def from_string(raw: str) -> 'CompletionSpec':
|
||||
self = CompletionSpec()
|
||||
typ = CompletionType.none
|
||||
kwds: tuple[str, ...] = ()
|
||||
extensions: tuple[str, ...] = ()
|
||||
mime_patterns: tuple[str, ...] = ()
|
||||
group = ''
|
||||
relative_to = CompletionRelativeTo.cwd
|
||||
for x in shlex_split(raw):
|
||||
ck, vv = x.split(':', 1)
|
||||
if ck == 'type':
|
||||
self.type = getattr(CompletionType, vv)
|
||||
typ = getattr(CompletionType, vv)
|
||||
elif ck == 'kwds':
|
||||
self.kwds += tuple(vv.split(','))
|
||||
kwds += tuple(vv.split(','))
|
||||
elif ck == 'ext':
|
||||
self.extensions += tuple(vv.split(','))
|
||||
extensions += tuple(vv.split(','))
|
||||
elif ck == 'group':
|
||||
self.group = vv
|
||||
group = vv
|
||||
elif ck == 'mime':
|
||||
self.mime_patterns += tuple(vv.split(','))
|
||||
mime_patterns += tuple(vv.split(','))
|
||||
elif ck == 'relative':
|
||||
if vv == 'conf':
|
||||
self.relative_to = CompletionRelativeTo.config_dir
|
||||
relative_to = CompletionRelativeTo.config_dir
|
||||
else:
|
||||
raise ValueError(f'Unknown completion relative to value: {vv}')
|
||||
else:
|
||||
raise KeyError(f'Unknown completion property: {ck}')
|
||||
return self
|
||||
return CompletionSpec(
|
||||
type=typ, kwds=kwds, extensions=extensions, mime_patterns=mime_patterns, group=group, relative_to=relative_to)
|
||||
|
||||
def as_go_code(self, go_name: str, sep: str = ': ') -> Iterator[str]:
|
||||
completers = []
|
||||
@@ -106,21 +111,23 @@ class CompletionSpec:
|
||||
yield f'{go_name}{sep}{completers[0]}'
|
||||
|
||||
|
||||
class OptionDict(TypedDict):
|
||||
dest: str
|
||||
name: str
|
||||
aliases: tuple[str, ...]
|
||||
help: str
|
||||
choices: tuple[str, ...]
|
||||
type: str
|
||||
default: str | None
|
||||
condition: bool
|
||||
completion: CompletionSpec
|
||||
class OptionDefinition(NamedTuple):
|
||||
dest: str = ''
|
||||
name: str = ''
|
||||
aliases: tuple[str, ...] = ()
|
||||
help: str = ''
|
||||
choices: tuple[str, ...] = ()
|
||||
type: str = ''
|
||||
default: str | None = None
|
||||
condition: bool = False
|
||||
completion: CompletionSpec = CompletionSpec()
|
||||
|
||||
|
||||
OptionSpecSeq = Sequence[str | OptionDict]
|
||||
|
||||
OptionSpecSeq = Sequence[str | OptionDefinition]
|
||||
|
||||
|
||||
@lru_cache(64)
|
||||
def parse_option_spec(spec: str | None = None) -> tuple[OptionSpecSeq, OptionSpecSeq]:
|
||||
if spec is None:
|
||||
spec = kitty_options_spec()
|
||||
@@ -129,14 +136,10 @@ def parse_option_spec(spec: str | None = None) -> tuple[OptionSpecSeq, OptionSpe
|
||||
lines = spec.splitlines()
|
||||
prev_line = ''
|
||||
prev_indent = 0
|
||||
seq: list[str | OptionDict] = []
|
||||
disabled: list[str | OptionDict] = []
|
||||
seq: list[str | OptionDefinition] = []
|
||||
disabled: list[str | OptionDefinition] = []
|
||||
mpat = re.compile('([a-z]+)=(.+)')
|
||||
current_cmd: OptionDict = {
|
||||
'dest': '', 'aliases': (), 'help': '', 'choices': (),
|
||||
'type': '', 'condition': False, 'default': None, 'completion': CompletionSpec(), 'name': ''
|
||||
}
|
||||
empty_cmd = current_cmd
|
||||
current_cmd = empty_cmd = OptionDefinition()
|
||||
|
||||
def indent_of_line(x: str) -> int:
|
||||
return len(x) - len(x.lstrip())
|
||||
@@ -152,11 +155,7 @@ def parse_option_spec(spec: str | None = None) -> tuple[OptionSpecSeq, OptionSpe
|
||||
if line.startswith('--'):
|
||||
parts = line.split(' ')
|
||||
defdest = parts[0][2:].replace('-', '_')
|
||||
current_cmd = {
|
||||
'dest': defdest, 'aliases': tuple(parts), 'help': '',
|
||||
'choices': tuple(), 'type': '', 'name': defdest,
|
||||
'default': None, 'condition': True, 'completion': CompletionSpec(),
|
||||
}
|
||||
current_cmd = OptionDefinition(dest=defdest, aliases=tuple(parts), name=defdest, condition=True)
|
||||
state = METADATA
|
||||
continue
|
||||
raise ValueError(f'Invalid option spec, unexpected line: {line}')
|
||||
@@ -164,60 +163,61 @@ def parse_option_spec(spec: str | None = None) -> tuple[OptionSpecSeq, OptionSpe
|
||||
m = mpat.match(line)
|
||||
if m is None:
|
||||
state = HELP
|
||||
current_cmd['help'] += line
|
||||
current_cmd = current_cmd._replace(help=current_cmd.help + line)
|
||||
else:
|
||||
k, v = m.group(1), m.group(2)
|
||||
if k == 'choices':
|
||||
vals = tuple(x.strip() for x in v.split(','))
|
||||
if not current_cmd['type']:
|
||||
current_cmd['type'] = 'choices'
|
||||
if current_cmd['type'] != 'choices':
|
||||
raise ValueError(f'Cannot specify choices for an option of type: {current_cmd["type"]}')
|
||||
current_cmd['choices'] = tuple(vals)
|
||||
if current_cmd['default'] is None:
|
||||
current_cmd['default'] = vals[0]
|
||||
if not current_cmd.type:
|
||||
current_cmd = current_cmd._replace(type='choices')
|
||||
if current_cmd.type != 'choices':
|
||||
raise ValueError(f'Cannot specify choices for an option of type: {current_cmd.type}')
|
||||
current_cmd = current_cmd._replace(choices=tuple(vals))
|
||||
if current_cmd.default is None:
|
||||
current_cmd = current_cmd._replace(default=vals[0])
|
||||
else:
|
||||
if k == 'default':
|
||||
current_cmd['default'] = v
|
||||
current_cmd = current_cmd._replace(default=v)
|
||||
elif k == 'type':
|
||||
if v == 'choice':
|
||||
v = 'choices'
|
||||
current_cmd['type'] = v
|
||||
current_cmd = current_cmd._replace(type=v)
|
||||
elif k == 'dest':
|
||||
current_cmd['dest'] = v
|
||||
current_cmd = current_cmd._replace(dest=v)
|
||||
elif k == 'condition':
|
||||
current_cmd['condition'] = bool(eval(v))
|
||||
current_cmd = current_cmd._replace(condition=bool(eval(v)))
|
||||
elif k == 'completion':
|
||||
current_cmd['completion'] = CompletionSpec.from_string(v)
|
||||
current_cmd = current_cmd._replace(completion=CompletionSpec.from_string(v))
|
||||
elif state is HELP:
|
||||
if line:
|
||||
current_indent = indent_of_line(line)
|
||||
if current_indent > 1:
|
||||
if prev_indent == 0:
|
||||
current_cmd['help'] += '\n'
|
||||
current_cmd = current_cmd._replace(help=current_cmd.help + '\n')
|
||||
else:
|
||||
line = line.strip()
|
||||
prev_indent = current_indent
|
||||
spc = '' if current_cmd['help'].endswith('\n') else ' '
|
||||
current_cmd['help'] += spc + line
|
||||
spc = '' if current_cmd.help.endswith('\n') else ' '
|
||||
current_cmd = current_cmd._replace(help=current_cmd.help + spc + line)
|
||||
else:
|
||||
prev_indent = 0
|
||||
if prev_line:
|
||||
current_cmd['help'] += '\n' if current_cmd['help'].endswith('::') else '\n\n'
|
||||
h = '\n' if current_cmd.help.endswith('::') else '\n\n'
|
||||
current_cmd = current_cmd._replace(help=current_cmd.help + h)
|
||||
else:
|
||||
state = NORMAL
|
||||
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
|
||||
(seq if current_cmd.condition else disabled).append(current_cmd)
|
||||
current_cmd = empty_cmd
|
||||
prev_line = line
|
||||
if current_cmd is not empty_cmd:
|
||||
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
|
||||
(seq if current_cmd.condition else disabled).append(current_cmd)
|
||||
|
||||
return seq, disabled
|
||||
|
||||
|
||||
def defval_for_opt(opt: OptionDict) -> Any:
|
||||
dv: Any = opt.get('default')
|
||||
typ = opt.get('type', '')
|
||||
def defval_for_opt(opt: OptionDefinition) -> Any:
|
||||
dv: Any = opt.default
|
||||
typ = opt.type
|
||||
if typ.startswith('bool-'):
|
||||
if dv is None:
|
||||
dv = False if typ == 'bool-set' else True
|
||||
@@ -230,16 +230,16 @@ def defval_for_opt(opt: OptionDict) -> Any:
|
||||
return dv
|
||||
|
||||
|
||||
def get_option_maps(seq: OptionSpecSeq) -> tuple[dict[str, OptionDict], dict[str, OptionDict], dict[str, Any]]:
|
||||
names_map: dict[str, OptionDict] = {}
|
||||
alias_map: dict[str, OptionDict] = {}
|
||||
def get_option_maps(seq: OptionSpecSeq) -> tuple[dict[str, OptionDefinition], dict[str, OptionDefinition], dict[str, Any]]:
|
||||
names_map: dict[str, OptionDefinition] = {}
|
||||
alias_map: dict[str, OptionDefinition] = {}
|
||||
values_map: dict[str, Any] = {}
|
||||
for opt in seq:
|
||||
if isinstance(opt, str):
|
||||
continue
|
||||
for alias in opt['aliases']:
|
||||
for alias in opt.aliases:
|
||||
alias_map[alias] = opt
|
||||
name = opt['dest']
|
||||
name = opt.dest
|
||||
names_map[name] = opt
|
||||
values_map[name] = defval_for_opt(opt)
|
||||
return names_map, alias_map, values_map
|
||||
@@ -259,9 +259,9 @@ def add_list_values(*values: str) -> Iterator[str]:
|
||||
yield f'\tflag.defval.listval.items[{n}] = {c_str(value)};'
|
||||
|
||||
|
||||
def generate_c_for_opt(name: str, defval: Any, opt: OptionDict) -> Iterator[str]:
|
||||
def generate_c_for_opt(name: str, defval: Any, opt: OptionDefinition) -> Iterator[str]:
|
||||
yield f'\tflag = (FlagSpec){{.dest={c_str(name)},}};'
|
||||
match opt['type']:
|
||||
match opt.type:
|
||||
case 'bool-set' | 'bool-reset':
|
||||
yield '\tflag.defval.type = CLI_VALUE_BOOL;'
|
||||
yield f'\tflag.defval.boolval = {"true" if defval else "false"};'
|
||||
@@ -278,7 +278,7 @@ def generate_c_for_opt(name: str, defval: Any, opt: OptionDict) -> Iterator[str]
|
||||
case 'choices':
|
||||
yield '\tflag.defval.type = CLI_VALUE_CHOICE;'
|
||||
yield f'\tflag.defval.strval = {c_str(defval)};'
|
||||
yield from add_list_values(*opt['choices'])
|
||||
yield from add_list_values(*opt.choices)
|
||||
case _:
|
||||
yield '\tflag.defval.type = CLI_VALUE_STRING;'
|
||||
if defval is not None:
|
||||
@@ -289,22 +289,22 @@ def generate_c_parser_for(funcname: str, spec: str) -> Iterator[str]:
|
||||
seq, disabled = parse_option_spec(spec)
|
||||
names_map, _, defaults_map = get_option_maps(seq)
|
||||
if 'help' not in names_map:
|
||||
names_map['help'] = {'type': 'bool-set', 'aliases': ('--help', '-h')} # type: ignore
|
||||
names_map['help'] = OptionDefinition(type='bool-set', aliases=('--help', '-h'))
|
||||
defaults_map['help'] = False
|
||||
if 'version' not in names_map:
|
||||
names_map['version'] = {'type': 'bool-set', 'aliases': ('--version', '-v')} # type: ignore
|
||||
names_map['version'] = OptionDefinition(type='bool-set', aliases=('--version', '-v'))
|
||||
defaults_map['version'] = False
|
||||
|
||||
yield f'static void\nparse_cli_for_{funcname}(CLISpec *spec, int argc, char **argv) {{' # }}
|
||||
yield '\tFlagSpec flag;'
|
||||
for name, opt in names_map.items():
|
||||
for alias in opt['aliases']:
|
||||
for alias in opt.aliases:
|
||||
yield f'\tif (vt_is_end(vt_insert(&spec->alias_map, {c_str(alias)}, {c_str(name)}))) OOM;'
|
||||
yield from generate_c_for_opt(name, defaults_map[name], opt)
|
||||
yield '\tif (vt_is_end(vt_insert(&spec->flag_map, flag.dest, flag))) OOM;'
|
||||
for d in disabled:
|
||||
if not isinstance(d, str):
|
||||
yield from generate_c_for_opt(d['dest'], defval_for_opt(d), d)
|
||||
yield from generate_c_for_opt(d.dest, defval_for_opt(d), d)
|
||||
yield '\tif (vt_is_end(vt_insert(&spec->disabled_map, flag.dest, flag))) OOM;'
|
||||
|
||||
yield '\tparse_cli_loop(spec, true, argc, argv);'
|
||||
|
||||
Reference in New Issue
Block a user