Work on porting config file parsing to Go
This commit is contained in:
parent
6f63d9c5d4
commit
5822bb23f0
@ -525,9 +525,9 @@ def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None:
|
|||||||
|
|
||||||
from kittens.runner import get_kitten_conf_docs
|
from kittens.runner import get_kitten_conf_docs
|
||||||
for kitten in all_kitten_names:
|
for kitten in all_kitten_names:
|
||||||
definition = get_kitten_conf_docs(kitten)
|
defn = get_kitten_conf_docs(kitten)
|
||||||
if definition:
|
if defn is not None:
|
||||||
generate_default_config(definition, f'kitten-{kitten}')
|
generate_default_config(defn, f'kitten-{kitten}')
|
||||||
|
|
||||||
from kitty.actions import as_rst
|
from kitty.actions import as_rst
|
||||||
with open('generated/actions.rst', 'w', encoding='utf-8') as f:
|
with open('generated/actions.rst', 'w', encoding='utf-8') as f:
|
||||||
|
|||||||
@ -51,8 +51,6 @@ def main() -> None:
|
|||||||
|
|
||||||
from kittens.diff.options.definition import definition as kd
|
from kittens.diff.options.definition import definition as kd
|
||||||
write_output('kittens.diff', kd)
|
write_output('kittens.diff', kd)
|
||||||
from kittens.ssh.options.definition import definition as sd
|
|
||||||
write_output('kittens.ssh', sd)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -22,6 +22,8 @@ from kitty.cli import (
|
|||||||
parse_option_spec,
|
parse_option_spec,
|
||||||
serialize_as_go_string,
|
serialize_as_go_string,
|
||||||
)
|
)
|
||||||
|
from kitty.conf.generate import gen_go_code
|
||||||
|
from kitty.conf.types import Definition
|
||||||
from kitty.guess_mime_type import text_mimes
|
from kitty.guess_mime_type import text_mimes
|
||||||
from kitty.key_encoding import config_mod_map
|
from kitty.key_encoding import config_mod_map
|
||||||
from kitty.key_names import character_key_name_aliases, functional_key_name_aliases
|
from kitty.key_names import character_key_name_aliases, functional_key_name_aliases
|
||||||
@ -318,8 +320,18 @@ def wrapped_kittens() -> Sequence[str]:
|
|||||||
raise Exception('Failed to read wrapped kittens from kitty wrapper script')
|
raise Exception('Failed to read wrapped kittens from kitty wrapper script')
|
||||||
|
|
||||||
|
|
||||||
|
def generate_conf_parser(kitten: str, defn: Definition) -> None:
|
||||||
|
with replace_if_needed(f'tools/cmd/{kitten}/conf_generated.go'):
|
||||||
|
print(f'package {kitten}')
|
||||||
|
print(gen_go_code(defn))
|
||||||
|
|
||||||
|
|
||||||
def kitten_clis() -> None:
|
def kitten_clis() -> None:
|
||||||
|
from kittens.runner import get_kitten_conf_docs
|
||||||
for kitten in wrapped_kittens():
|
for kitten in wrapped_kittens():
|
||||||
|
defn = get_kitten_conf_docs(kitten)
|
||||||
|
if defn is not None:
|
||||||
|
generate_conf_parser(kitten, defn)
|
||||||
with replace_if_needed(f'tools/cmd/{kitten}/cli_generated.go'):
|
with replace_if_needed(f'tools/cmd/{kitten}/cli_generated.go'):
|
||||||
od = []
|
od = []
|
||||||
kcd = kitten_cli_docs(kitten)
|
kcd = kitten_cli_docs(kitten)
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, List, cast
|
from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, List, Optional, cast
|
||||||
|
|
||||||
from kitty.constants import list_kitty_resources
|
from kitty.constants import list_kitty_resources
|
||||||
from kitty.types import run_once
|
from kitty.types import run_once
|
||||||
@ -171,7 +171,7 @@ def get_kitten_completer(kitten: str) -> Any:
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
def get_kitten_conf_docs(kitten: str) -> Definition:
|
def get_kitten_conf_docs(kitten: str) -> Optional[Definition]:
|
||||||
setattr(sys, 'options_definition', None)
|
setattr(sys, 'options_definition', None)
|
||||||
run_kitten(kitten, run_name='__conf__')
|
run_kitten(kitten, run_name='__conf__')
|
||||||
ans = getattr(sys, 'options_definition')
|
ans = getattr(sys, 'options_definition')
|
||||||
|
|||||||
@ -36,7 +36,6 @@ from ..tui.utils import kitty_opts, running_in_tmux
|
|||||||
from .config import init_config
|
from .config import init_config
|
||||||
from .copy import CopyInstruction
|
from .copy import CopyInstruction
|
||||||
from .options.types import Options as SSHOptions
|
from .options.types import Options as SSHOptions
|
||||||
from .options.utils import DELETE_ENV_VAR
|
|
||||||
from .utils import create_shared_memory, get_ssh_cli, is_extra_arg, passthrough_args
|
from .utils import create_shared_memory, get_ssh_cli, is_extra_arg, passthrough_args
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ remote. Directories are copied recursively. If absolute paths are used, they are
|
|||||||
copied as is.'''
|
copied as is.'''
|
||||||
|
|
||||||
definition = Definition(
|
definition = Definition(
|
||||||
'kittens.ssh',
|
'!kittens.ssh',
|
||||||
)
|
)
|
||||||
|
|
||||||
agr = definition.add_group
|
agr = definition.add_group
|
||||||
@ -22,7 +22,7 @@ opt = definition.add_option
|
|||||||
|
|
||||||
agr('bootstrap', 'Host bootstrap configuration') # {{{
|
agr('bootstrap', 'Host bootstrap configuration') # {{{
|
||||||
|
|
||||||
opt('hostname', '*', option_type='hostname', long_text='''
|
opt('hostname', '*', long_text='''
|
||||||
The hostname that the following options apply to. A glob pattern to match
|
The hostname that the following options apply to. A glob pattern to match
|
||||||
multiple hosts can be used. Multiple hostnames can also be specified, separated
|
multiple hosts can be used. Multiple hostnames can also be specified, separated
|
||||||
by spaces. The hostname can include an optional username in the form
|
by spaces. The hostname can include an optional username in the form
|
||||||
@ -44,7 +44,7 @@ The location on the remote host where the files needed for this kitten are
|
|||||||
installed. Relative paths are resolved with respect to :code:`$HOME`.
|
installed. Relative paths are resolved with respect to :code:`$HOME`.
|
||||||
''')
|
''')
|
||||||
|
|
||||||
opt('+copy', '', option_type='copy', add_to_default=False, long_text=f'''
|
opt('+copy', '', add_to_default=False, long_text=f'''
|
||||||
{copy_message} For example::
|
{copy_message} For example::
|
||||||
|
|
||||||
copy .vimrc .zshrc .config/some-dir
|
copy .vimrc .zshrc .config/some-dir
|
||||||
@ -80,7 +80,7 @@ The login shell to execute on the remote host. By default, the remote user
|
|||||||
account's login shell is used.
|
account's login shell is used.
|
||||||
''')
|
''')
|
||||||
|
|
||||||
opt('+env', '', option_type='env', add_to_default=False, long_text='''
|
opt('+env', '', add_to_default=False, long_text='''
|
||||||
Specify the environment variables to be set on the remote host. Using the
|
Specify the environment variables to be set on the remote host. Using the
|
||||||
name with an equal sign (e.g. :code:`env VAR=`) will set it to the empty string.
|
name with an equal sign (e.g. :code:`env VAR=`) will set it to the empty string.
|
||||||
Specifying only the name (e.g. :code:`env VAR`) will remove the variable from
|
Specifying only the name (e.g. :code:`env VAR`) will remove the variable from
|
||||||
|
|||||||
@ -1,93 +0,0 @@
|
|||||||
# generated by gen-config.py DO NOT edit
|
|
||||||
|
|
||||||
# isort: skip_file
|
|
||||||
import typing
|
|
||||||
import kittens.ssh.copy
|
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
|
||||||
choices_for_askpass = typing.Literal['unless-set', 'ssh', 'native']
|
|
||||||
choices_for_remote_kitty = typing.Literal['if-needed', 'no', 'yes']
|
|
||||||
else:
|
|
||||||
choices_for_askpass = str
|
|
||||||
choices_for_remote_kitty = str
|
|
||||||
|
|
||||||
option_names = ( # {{{
|
|
||||||
'askpass',
|
|
||||||
'color_scheme',
|
|
||||||
'copy',
|
|
||||||
'cwd',
|
|
||||||
'env',
|
|
||||||
'hostname',
|
|
||||||
'interpreter',
|
|
||||||
'login_shell',
|
|
||||||
'remote_dir',
|
|
||||||
'remote_kitty',
|
|
||||||
'share_connections',
|
|
||||||
'shell_integration') # }}}
|
|
||||||
|
|
||||||
|
|
||||||
class Options:
|
|
||||||
askpass: choices_for_askpass = 'unless-set'
|
|
||||||
color_scheme: str = ''
|
|
||||||
cwd: str = ''
|
|
||||||
hostname: str = '*'
|
|
||||||
interpreter: str = 'sh'
|
|
||||||
login_shell: str = ''
|
|
||||||
remote_dir: str = '.local/share/kitty-ssh-kitten'
|
|
||||||
remote_kitty: choices_for_remote_kitty = 'if-needed'
|
|
||||||
share_connections: bool = True
|
|
||||||
shell_integration: str = 'inherited'
|
|
||||||
copy: typing.Dict[str, kittens.ssh.copy.CopyInstruction] = {}
|
|
||||||
env: typing.Dict[str, str] = {}
|
|
||||||
config_paths: typing.Tuple[str, ...] = ()
|
|
||||||
config_overrides: typing.Tuple[str, ...] = ()
|
|
||||||
|
|
||||||
def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:
|
|
||||||
if options_dict is not None:
|
|
||||||
null = object()
|
|
||||||
for key in option_names:
|
|
||||||
val = options_dict.get(key, null)
|
|
||||||
if val is not null:
|
|
||||||
setattr(self, key, val)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _fields(self) -> typing.Tuple[str, ...]:
|
|
||||||
return option_names
|
|
||||||
|
|
||||||
def __iter__(self) -> typing.Iterator[str]:
|
|
||||||
return iter(self._fields)
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self._fields)
|
|
||||||
|
|
||||||
def _copy_of_val(self, name: str) -> typing.Any:
|
|
||||||
ans = getattr(self, name)
|
|
||||||
if isinstance(ans, dict):
|
|
||||||
ans = ans.copy()
|
|
||||||
elif isinstance(ans, list):
|
|
||||||
ans = ans[:]
|
|
||||||
return ans
|
|
||||||
|
|
||||||
def _asdict(self) -> typing.Dict[str, typing.Any]:
|
|
||||||
return {k: self._copy_of_val(k) for k in self}
|
|
||||||
|
|
||||||
def _replace(self, **kw: typing.Any) -> "Options":
|
|
||||||
ans = Options()
|
|
||||||
for name in self:
|
|
||||||
setattr(ans, name, self._copy_of_val(name))
|
|
||||||
for name, val in kw.items():
|
|
||||||
setattr(ans, name, val)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
def __getitem__(self, key: typing.Union[int, str]) -> typing.Any:
|
|
||||||
k = option_names[key] if isinstance(key, int) else key
|
|
||||||
try:
|
|
||||||
return getattr(self, k)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
raise KeyError(f"No option named: {k}")
|
|
||||||
|
|
||||||
|
|
||||||
defaults = Options()
|
|
||||||
defaults.copy = {}
|
|
||||||
defaults.env = {}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
|
||||||
|
|
||||||
from typing import Any, Dict, Iterable, Optional, Tuple
|
|
||||||
|
|
||||||
from ..copy import CopyInstruction, parse_copy_instructions
|
|
||||||
|
|
||||||
DELETE_ENV_VAR = '_delete_this_env_var_'
|
|
||||||
|
|
||||||
|
|
||||||
def env(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, str]]:
|
|
||||||
val = val.strip()
|
|
||||||
if val:
|
|
||||||
if '=' in val:
|
|
||||||
key, v = val.split('=', 1)
|
|
||||||
key, v = key.strip(), v.strip()
|
|
||||||
if key:
|
|
||||||
yield key, v
|
|
||||||
else:
|
|
||||||
yield val, DELETE_ENV_VAR
|
|
||||||
|
|
||||||
|
|
||||||
def copy(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, CopyInstruction]]:
|
|
||||||
yield from parse_copy_instructions(val, current_val)
|
|
||||||
|
|
||||||
|
|
||||||
def init_results_dict(ans: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
ans['hostname'] = '*'
|
|
||||||
ans['per_host_dicts'] = {}
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
def get_per_hosts_dict(results_dict: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
|
||||||
ans: Dict[str, Dict[str, Any]] = results_dict.get('per_host_dicts', {}).copy()
|
|
||||||
h = results_dict['hostname']
|
|
||||||
hd = {k: v for k, v in results_dict.items() if k != 'per_host_dicts'}
|
|
||||||
ans[h] = hd
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
first_seen_positions: Dict[str, int] = {}
|
|
||||||
|
|
||||||
|
|
||||||
def hostname(val: str, dict_with_parse_results: Optional[Dict[str, Any]] = None) -> str:
|
|
||||||
if dict_with_parse_results is not None:
|
|
||||||
ch = dict_with_parse_results['hostname']
|
|
||||||
if val != ch:
|
|
||||||
from .parse import create_result_dict
|
|
||||||
phd = get_per_hosts_dict(dict_with_parse_results)
|
|
||||||
dict_with_parse_results.clear()
|
|
||||||
dict_with_parse_results.update(phd.pop(val, create_result_dict()))
|
|
||||||
dict_with_parse_results['per_host_dicts'] = phd
|
|
||||||
dict_with_parse_results['hostname'] = val
|
|
||||||
if val not in first_seen_positions:
|
|
||||||
first_seen_positions[val] = len(first_seen_positions)
|
|
||||||
return val
|
|
||||||
@ -9,7 +9,7 @@ import re
|
|||||||
import textwrap
|
import textwrap
|
||||||
from typing import Any, Callable, Dict, Iterator, List, Set, Tuple, Union, get_type_hints
|
from typing import Any, Callable, Dict, Iterator, List, Set, Tuple, Union, get_type_hints
|
||||||
|
|
||||||
from kitty.conf.types import Definition, MultiOption, Option, unset
|
from kitty.conf.types import Definition, MultiOption, Option, ParserFuncType, unset
|
||||||
from kitty.types import _T
|
from kitty.types import _T
|
||||||
|
|
||||||
|
|
||||||
@ -442,6 +442,114 @@ def write_output(loc: str, defn: Definition) -> None:
|
|||||||
f.write(f'{c}\n')
|
f.write(f'{c}\n')
|
||||||
|
|
||||||
|
|
||||||
|
def go_type_data(parser_func: ParserFuncType, type_defs: List[str]) -> Tuple[str, str]:
|
||||||
|
p = parser_func.__name__
|
||||||
|
if p == 'int':
|
||||||
|
return 'int64', 'strconv.ParseInt(val, 10, 64)'
|
||||||
|
if p == 'str':
|
||||||
|
return 'string', 'val, nil'
|
||||||
|
if p == 'float':
|
||||||
|
return 'float64', 'strconv.ParseFloat(val, 10, 64)'
|
||||||
|
if p == 'to_bool':
|
||||||
|
return 'bool', 'utils.StringToBool(val), nil'
|
||||||
|
th = get_type_hints(parser_func)
|
||||||
|
rettype = th['return']
|
||||||
|
return {int: 'int64', str: 'string', float: 'float64'}[rettype], f'{p}(val)'
|
||||||
|
|
||||||
|
|
||||||
|
def gen_go_code(defn: Definition) -> str:
|
||||||
|
lines = ['import "fmt"', 'import "strconv"', 'import "kitty/tools/utils"', 'var _ = fmt.Println', 'var _ = utils.StringToBool', 'var _ = strconv.Atoi']
|
||||||
|
a = lines.append
|
||||||
|
choices = {}
|
||||||
|
go_types = {}
|
||||||
|
go_parsers = {}
|
||||||
|
defaults = {}
|
||||||
|
multiopts = {''}
|
||||||
|
type_defs = ['']
|
||||||
|
for option in sorted(defn.iter_all_options(), key=lambda a: natural_keys(a.name)):
|
||||||
|
name = option.name.capitalize()
|
||||||
|
if isinstance(option, MultiOption):
|
||||||
|
go_types[name], go_parsers[name] = go_type_data(option.parser_func, type_defs)
|
||||||
|
multiopts.add(name)
|
||||||
|
else:
|
||||||
|
defaults[name] = option.defval_as_string
|
||||||
|
if option.choices:
|
||||||
|
choices[name] = option.choices
|
||||||
|
go_types[name] = f'{name}_Choice_Type'
|
||||||
|
go_parsers[name] = f'Parse_{name}(val)'
|
||||||
|
continue
|
||||||
|
go_types[name], go_parsers[name] = go_type_data(option.parser_func, type_defs)
|
||||||
|
|
||||||
|
for oname in choices:
|
||||||
|
a(f'type {go_types[oname]} int')
|
||||||
|
for td in type_defs:
|
||||||
|
a(td)
|
||||||
|
a('type Config struct {')
|
||||||
|
for name, gotype in go_types.items():
|
||||||
|
if name in multiopts:
|
||||||
|
a(f'{name} []{gotype}')
|
||||||
|
else:
|
||||||
|
a(f'{name} {gotype}')
|
||||||
|
a('}')
|
||||||
|
|
||||||
|
a('func NewConfig() *Config {')
|
||||||
|
a('ans := Config{}')
|
||||||
|
a('var err error')
|
||||||
|
a('var val string')
|
||||||
|
for name, pname in go_parsers.items():
|
||||||
|
if name in multiopts:
|
||||||
|
a(f'ans.{name} = make([]{go_types[name]}, 0, 8)')
|
||||||
|
continue
|
||||||
|
a(f'val = `{defaults[name]}`')
|
||||||
|
a(f'ans.{name}, err = {pname}')
|
||||||
|
a('if err != nil { panic(err) }')
|
||||||
|
a('return &ans')
|
||||||
|
a('}')
|
||||||
|
|
||||||
|
def cval(x: str) -> str:
|
||||||
|
return x.replace('-', '_')
|
||||||
|
|
||||||
|
for oname, choice_vals in choices.items():
|
||||||
|
a('const (')
|
||||||
|
for i, c in enumerate(choice_vals):
|
||||||
|
c = cval(c)
|
||||||
|
if i == 0:
|
||||||
|
a(f'{oname}_{c} {oname}_Choice_Type = iota')
|
||||||
|
else:
|
||||||
|
a(f'{oname}_{c}')
|
||||||
|
a(')')
|
||||||
|
a(f'func (x {oname}_Choice_Type) String() string'' {')
|
||||||
|
a('switch x {')
|
||||||
|
a('default: return ""')
|
||||||
|
for c in choice_vals:
|
||||||
|
a(f'case {oname}_{cval(c)}: return "{c}"')
|
||||||
|
a('}''}')
|
||||||
|
a(f'func {go_parsers[oname].split("(")[0]}(val string) (ans {go_types[oname]}, err error) ''{')
|
||||||
|
a('switch val {')
|
||||||
|
for c in choice_vals:
|
||||||
|
a(f'case "{c}": return {oname}_{cval(c)}, nil')
|
||||||
|
vals = ', '.join(choice_vals)
|
||||||
|
a(f'default: return ans, fmt.Errorf("%#v is not a valid value for %s. Valid values are: %s", val, "{c}", "{vals}")')
|
||||||
|
a('}''}')
|
||||||
|
|
||||||
|
a('func (c *Config) Parse(key, val string) (err error) {')
|
||||||
|
a('switch key {')
|
||||||
|
a('default: return fmt.Errorf("Unknown configuration key: %#v", key)')
|
||||||
|
for oname, pname in go_parsers.items():
|
||||||
|
ol = oname.lower()
|
||||||
|
a(f'case "{ol}":')
|
||||||
|
a(f'var temp_val {go_types[oname]}')
|
||||||
|
a(f'temp_val, err = {pname}')
|
||||||
|
a(f'if err != nil {{ return fmt.Errorf("Failed to parse {ol} = %#v with error: %w", val, err) }}')
|
||||||
|
if oname in multiopts:
|
||||||
|
a(f'c.{oname} = append(c.{oname}, temp_val)')
|
||||||
|
else:
|
||||||
|
a(f'c.{oname} = temp_val')
|
||||||
|
a('}')
|
||||||
|
a('return}')
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
# To use run it as:
|
# To use run it as:
|
||||||
# kitty +runpy 'from kitty.conf.generate import main; main()' /path/to/kitten/file.py
|
# kitty +runpy 'from kitty.conf.generate import main; main()' /path/to/kitten/file.py
|
||||||
|
|||||||
32
tools/utils/config.go
Normal file
32
tools/utils/config.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = fmt.Print
|
||||||
|
|
||||||
|
func StringToBool(x string) bool {
|
||||||
|
x = strings.ToLower(x)
|
||||||
|
return x == "y" || x == "yes" || x == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseConfData(src io.Reader, callback func(key, val string, line int)) error {
|
||||||
|
scanner := bufio.NewScanner(src)
|
||||||
|
lnum := 0
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimLeft(scanner.Text(), " ")
|
||||||
|
lnum++
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, val, _ := strings.Cut(line, " ")
|
||||||
|
callback(key, val, lnum)
|
||||||
|
}
|
||||||
|
return scanner.Err()
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user