diff --git a/kittens/ssh/copy.py b/kittens/ssh/copy.py index 19dda070b..2c2590473 100644 --- a/kittens/ssh/copy.py +++ b/kittens/ssh/copy.py @@ -2,5 +2,70 @@ # License: GPLv3 Copyright: 2022, Kovid Goyal -class CopyInstruction: +import glob +import os +import shlex +from typing import Iterable, Iterator, List, Optional, Sequence, Tuple + +from kitty.cli import parse_args +from kitty.cli_stub import CopyCLIOptions +from kitty.types import run_once + + +@run_once +def option_text() -> str: + return ''' +--glob +type=bool-set +Interpret file arguments as glob patterns. + + +--dest +The destination on the remote computer to copy to. Relative paths are resolved +relative to HOME on the remote machine. When this option is not specified, the +local file path is used as the remote destination (with the HOME directory +getting automatically replaced by the remote HOME). Note that enviroment +variables and ~ are not expanded. +''' + + +def parse_copy_args(args: Optional[Sequence[str]] = None) -> Tuple[CopyCLIOptions, List[str]]: + args = list(args or ()) + try: + opts, args = parse_args(result_class=CopyCLIOptions, args=args, ospec=option_text) + except SystemExit as e: + raise CopyCLIError from e + return opts, args + + +def resolve_file_spec(spec: str, is_glob: bool) -> Iterator[str]: + ans = os.path.expandvars(os.path.expanduser(spec)) + if not os.path.isabs(ans): + ans = os.path.expanduser(f'~/{ans}') + if is_glob: + files = glob.glob(ans) + if not files: + raise CopyCLIError(f'{spec} does not exist') + else: + if not os.path.exists(ans): + raise CopyCLIError(f'{spec} does not exist') + files = [ans] + for x in files: + yield os.path.normpath(x).replace(os.sep, '/') + + +class CopyCLIError(ValueError): pass + + +def parse_copy_instructions(val: str) -> Iterable[Tuple[str, CopyCLIOptions]]: + opts, args = parse_copy_args(shlex.split(val)) + locations: List[str] = [] + for a in args: + locations.extend(resolve_file_spec(a, opts.glob)) + if not locations: + raise CopyCLIError('No files to copy specified') + if len(locations) > 1 and opts.dest: + raise CopyCLIError('Specifying a remote location with more than one file is not supported') + for loc in locations: + yield loc, opts diff --git a/kittens/ssh/options/types.py b/kittens/ssh/options/types.py index 03922bafb..d2d472099 100644 --- a/kittens/ssh/options/types.py +++ b/kittens/ssh/options/types.py @@ -1,7 +1,7 @@ # generated by gen-config.py DO NOT edit import typing -import kittens.ssh.copy +import kitty.cli_stub option_names = ( # {{{ @@ -12,7 +12,7 @@ class Options: hostname: str = '*' remote_dir: str = '.local/share/kitty-ssh-kitten' shell_integration: str = 'inherit' - copy: typing.Dict[str, kittens.ssh.copy.CopyInstruction] = {} + copy: typing.Dict[str, kitty.cli_stub.CLIOptions] = {} env: typing.Dict[str, str] = {} config_paths: typing.Tuple[str, ...] = () config_overrides: typing.Tuple[str, ...] = () diff --git a/kittens/ssh/options/utils.py b/kittens/ssh/options/utils.py index fa3eed8af..936aa9533 100644 --- a/kittens/ssh/options/utils.py +++ b/kittens/ssh/options/utils.py @@ -1,11 +1,12 @@ #!/usr/bin/env python # License: GPLv3 Copyright: 2022, Kovid Goyal -from typing import Any, Dict, Optional, Iterable, Tuple import posixpath +from typing import Any, Dict, Iterable, Optional, Tuple -from ..copy import CopyInstruction +from kitty.cli_stub import CopyCLIOptions +from ..copy import parse_copy_instructions DELETE_ENV_VAR = '_delete_this_env_var_' @@ -32,8 +33,8 @@ def env(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, str]]: yield val, DELETE_ENV_VAR -def copy(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, CopyInstruction]]: - pass +def copy(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, CopyCLIOptions]]: + yield from parse_copy_instructions(val) def init_results_dict(ans: Dict[str, Any]) -> Dict[str, Any]: diff --git a/kitty/cli_stub.py b/kitty/cli_stub.py index 53714af3d..b22dae398 100644 --- a/kitty/cli_stub.py +++ b/kitty/cli_stub.py @@ -13,7 +13,7 @@ LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOpt HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions QueryTerminalCLIOptions = BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions -ThemesCLIOptions = TransferCLIOptions = CLIOptions +ThemesCLIOptions = TransferCLIOptions = CopyCLIOptions = CLIOptions def generate_stub() -> None: @@ -78,6 +78,9 @@ def generate_stub() -> None: from kittens.transfer.main import option_text as OPTIONS do(OPTIONS(), 'TransferCLIOptions') + from kittens.ssh.copy import option_text as OPTIONS + do(OPTIONS(), 'CopyCLIOptions') + from kitty.rc.base import all_command_names, command_for_name for cmd_name in all_command_names(): cmd = command_for_name(cmd_name)