From 1d1195f7fd9da8c2cce7deaaf7eba968840b342c Mon Sep 17 00:00:00 2001 From: "d.kovalenko" Date: Wed, 8 Jul 2026 20:24:59 +0300 Subject: [PATCH] OsOperations::join_command_arguments is added Declarations: def join_command_arguments(self, cmd: typing.Iterable[str]) -> str: It quotes all the parts of cmd and join it via " ". --- src/local_ops.py | 13 +++++++ src/os_ops.py | 5 +++ src/remote_ops.py | 17 +++++++-- tests/test_os_ops_common.py | 70 +++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/local_ops.py b/src/local_ops.py index 241d5fa..77e88d8 100644 --- a/src/local_ops.py +++ b/src/local_ops.py @@ -381,6 +381,11 @@ def quote_path(self, path: str) -> str: assert type(path) is str return __class__._quote_path(path) + def join_command_arguments(self, cmd: typing.Iterable[str]) -> str: + assert cmd is not None + assert type(cmd) is list + return __class__._join_command_arguments(cmd) + # Environment setup def environ(self, var_name: str) -> typing.Optional[str]: assert type(var_name) is str @@ -859,3 +864,11 @@ def _quote_path(path: str) -> str: def _quote_path2(path: str) -> str: assert type(path) is str return shlex.quote(path) + + @staticmethod + def _join_command_arguments(cmd: typing.Iterable[str]) -> str: + assert type(cmd) is list + for item in cmd: + assert type(item) is str + + return " ".join(__class__._quote_path(arg) for arg in cmd) diff --git a/src/os_ops.py b/src/os_ops.py index 41a3fab..190d756 100644 --- a/src/os_ops.py +++ b/src/os_ops.py @@ -119,6 +119,11 @@ def quote_path(self, path: str) -> str: assert type(path) is str raise NotImplementedError() + def join_command_arguments(self, cmd: typing.Iterable[str]) -> str: + assert cmd is not None + assert type(cmd) is list + raise NotImplementedError() + # Environment setup def environ(self, var_name: str) -> typing.Optional[str]: assert type(var_name) is str diff --git a/src/remote_ops.py b/src/remote_ops.py index 10d55c0..dd9ee9a 100644 --- a/src/remote_ops.py +++ b/src/remote_ops.py @@ -290,6 +290,11 @@ def quote_path(self, path: str) -> str: assert type(path) is str return __class__._quote_path(path) + def join_command_arguments(self, cmd: typing.Iterable[str]) -> str: + assert cmd is not None + assert type(cmd) is list + return __class__._join_command_arguments(cmd) + # Environment setup def environ(self, var_name: str) -> typing.Optional[str]: """ @@ -1229,11 +1234,11 @@ def _build_cmdline( return cmdline @staticmethod - def _ensure_cmdline(cmd) -> str: + def _ensure_cmdline(cmd: typing.Any) -> str: if type(cmd) is str: return cmd if type(cmd) is list: - return subprocess.list2cmdline(cmd) + return __class__._join_command_arguments(cmd) raise ValueError("Invalid 'cmd' argument type - {0}".format(type(cmd).__name__)) @@ -1379,6 +1384,14 @@ def _quote_path2(path: str) -> str: assert type(path) is str return shlex.quote(path) + @staticmethod + def _join_command_arguments(cmd: typing.Iterable[str]) -> str: + assert type(cmd) is list + for item in cmd: + assert type(item) is str + + return " ".join(__class__._quote_path(arg) for arg in cmd) + def normalize_error(error): if isinstance(error, bytes): diff --git a/tests/test_os_ops_common.py b/tests/test_os_ops_common.py index c147cd4..a21969d 100644 --- a/tests/test_os_ops_common.py +++ b/tests/test_os_ops_common.py @@ -3221,6 +3221,76 @@ def LOCAL__check(value, expected) -> bool: return + def test_join_command_arguments( + self, + os_ops_descr: OsOpsDescr, + ): + assert type(os_ops_descr) is OsOpsDescr + os_ops = os_ops_descr.os_ops + assert isinstance(os_ops, OsOperations) + + def LOCAL__check(value, expected) -> bool: + logging.info("Source path: [{}]".format(value)) + actual = os_ops.join_command_arguments(value) + if actual == expected: + logging.info("Result is OK: [{}].".format( + actual, + )) + else: + logging.error("Result is BAD: [{}]. Expected: [{}].".format( + actual, + expected, + )) + logging.info("") + return False + + logging.info("------------- test empty string") + LOCAL__check(["cmd", ""], "cmd ''") + + logging.info("------------- test one char") + LOCAL__check(["cmd", "a"], "cmd a") + + logging.info("------------- test path") + LOCAL__check(["cmd", "a/b/c"], "cmd a/b/c") + + logging.info("------------- test single quote") + LOCAL__check(["cmd", "'"], "cmd ''\"'\"''") + + logging.info("------------- test double quote") + LOCAL__check(["cmd", "\""], "cmd '\"'") + + logging.info("------------- test tilde") + LOCAL__check(["cmd", "~"], "cmd ~") + + logging.info("------------- test tilde and slash") + LOCAL__check(["cmd", "~/"], "cmd ~") + + logging.info("------------- test tilde and slash and path") + LOCAL__check(["cmd", "~/abc"], "cmd ~/abc") + + logging.info("------------- test tilde and slash and path_with_spaces") + LOCAL__check(["cmd", "~/a b c"], "cmd ~/'a b c'") + + logging.info("------------- test tilde and slash and path_with_spaces_and_final_slash") + LOCAL__check(["cmd", "~/a b c/"], "cmd ~/'a b c/'") + + logging.info("------------- test tilde and slash and path_with_dquote") + LOCAL__check(["cmd", "~/a\"b c/"], "cmd ~/'a\"b c/'") + + logging.info("------------- test tilde_with_user") + LOCAL__check(["cmd", "~root"], "cmd ~root") + + logging.info("------------- test tilde_with_user and slash") + LOCAL__check(["cmd", "~root/"], "cmd ~root") + + logging.info("------------- test tilde_with_user and path") + LOCAL__check(["cmd", "~root/a b c d e f "], "cmd ~root/'a b c d e f '") + + logging.info("------------- test tilde_with_user and path_and_final_slash") + LOCAL__check(["cmd", "~root/a b c d e f /"], "cmd ~root/'a b c d e f /'") + + return + def test_copytree__empty( self, os_ops_descr: OsOpsDescr,