From e6dc88201b920975f15c3e6bbd5f49933ec36661 Mon Sep 17 00:00:00 2001 From: Federico Ressi Date: Fri, 4 Jun 2021 11:31:39 +0200 Subject: [PATCH] Update shell command tool Improve typing hints for shell command tool Add unit test for ShellCommand class Change-Id: Ibe3d2cfeddd13fb96831c6343c1a7585fc39536e --- tobiko/shell/sh/_command.py | 36 ++++---- tobiko/tests/unit/shell/sh/test_command.py | 97 ++++++++++++++++++++++ 2 files changed, 118 insertions(+), 15 deletions(-) create mode 100644 tobiko/tests/unit/shell/sh/test_command.py diff --git a/tobiko/shell/sh/_command.py b/tobiko/shell/sh/_command.py index f76885145..d95fb8d5f 100644 --- a/tobiko/shell/sh/_command.py +++ b/tobiko/shell/sh/_command.py @@ -19,35 +19,37 @@ import shlex import typing # noqa +ShellCommandType = typing.Union['ShellCommand', str, typing.Iterable[str]] + + class ShellCommand(tuple): - def __repr__(self): - return "ShellCommand([{!s}])".format(', '.join(self)) + def __repr__(self) -> str: + return f"ShellCommand({str(self)!r})" - def __str__(self): - return list_to_command_line(self) + def __str__(self) -> str: + return join_command(self) - def __add__(self, other): - other = shell_command(other) - return shell_command(tuple(self) + other) - - -ShellCommandType = typing.Union[ShellCommand, str, typing.Iterable] + def __add__(self, other: ShellCommandType) -> 'ShellCommand': + return shell_command(tuple(self) + shell_command(other)) def shell_command(command: ShellCommandType) -> ShellCommand: if isinstance(command, ShellCommand): return command elif isinstance(command, str): - return ShellCommand(shlex.split(command)) + return ShellCommand(split_command(command)) else: return ShellCommand(str(a) for a in command) -def list_to_command_line(seq): - result = [] - for arg in seq: - bs_buf = [] +NEED_QUOTE_CHARS = {' ', '\t', '\n', '\r', "'", '"'} + + +def join_command(sequence: typing.Iterable[str]) -> str: + result: typing.List[str] = [] + for arg in sequence: + bs_buf: typing.List[str] = [] # Add a space to separate this argument from the others if result: @@ -82,3 +84,7 @@ def list_to_command_line(seq): result.append("'") return ''.join(result) + + +def split_command(command: str) -> typing.Sequence[str]: + return shlex.split(command) diff --git a/tobiko/tests/unit/shell/sh/test_command.py b/tobiko/tests/unit/shell/sh/test_command.py new file mode 100644 index 000000000..450c09f57 --- /dev/null +++ b/tobiko/tests/unit/shell/sh/test_command.py @@ -0,0 +1,97 @@ +# Copyright (c) 2021 Red Hat, Inc. +# +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import absolute_import + + +from tobiko.shell import sh +from tobiko.tests import unit + + +class ShellCommandTest(unit.TobikoUnitTest): + + def test_from_str(self): + result = sh.shell_command('ls -lh *.py') + self.assertIsInstance(result, sh.ShellCommand) + self.assertEqual(('ls', '-lh', '*.py'), result) + self.assertEqual("ls -lh *.py", str(result)) + + def test_from_str_with_quotes(self): + result = sh.shell_command('ls -lh "with quotes"') + self.assertIsInstance(result, sh.ShellCommand) + self.assertEqual(('ls', '-lh', "with quotes"), result) + self.assertEqual("ls -lh 'with quotes'", str(result)) + + def test_from_sequence(self): + result = sh.shell_command(['ls', '-lh', '*.py']) + self.assertIsInstance(result, sh.ShellCommand) + self.assertEqual(('ls', '-lh', '*.py'), result) + self.assertEqual("ls -lh *.py", str(result)) + + def test_from_sequence_with_quotes(self): + result = sh.shell_command(['ls', '-lh', "with quotes"]) + self.assertIsInstance(result, sh.ShellCommand) + self.assertEqual(('ls', '-lh', "with quotes"), result) + self.assertEqual("ls -lh 'with quotes'", str(result)) + + def test_from_shell_command(self): + other = sh.shell_command(['ls', '-lh', '*.py']) + result = sh.shell_command(other) + self.assertIs(other, result) + + def test_add_str(self): + base = sh.shell_command('ssh pippo@clubhouse.mouse') + result = base + 'ls -lh *.py' + self.assertEqual(('ssh', 'pippo@clubhouse.mouse', 'ls', '-lh', '*.py'), + result) + self.assertEqual("ssh pippo@clubhouse.mouse ls -lh *.py", + str(result)) + + def test_add_str_with_quotes(self): + base = sh.shell_command('sh -c') + result = base + "'echo Hello!'" + self.assertIsInstance(result, sh.ShellCommand) + self.assertEqual(('sh', '-c', "echo Hello!"), result) + self.assertEqual("sh -c 'echo Hello!'", str(result)) + + def test_add_sequence(self): + base = sh.shell_command('ssh pippo@clubhouse.mouse') + result = base + ['ls', '-lh', '*.py'] + self.assertEqual(('ssh', 'pippo@clubhouse.mouse', 'ls', '-lh', '*.py'), + result) + self.assertEqual("ssh pippo@clubhouse.mouse ls -lh *.py", + str(result)) + + def test_add_sequence_with_quotes(self): + base = sh.shell_command('sh -c') + result = base + ['echo Hello!'] + self.assertIsInstance(result, sh.ShellCommand) + self.assertEqual(('sh', '-c', "echo Hello!"), result) + self.assertEqual("sh -c 'echo Hello!'", str(result)) + + def test_add_shell_command(self): + base = sh.shell_command('ssh pippo@clubhouse.mouse') + result = base + sh.shell_command(['ls', '-lh', '*.py']) + self.assertEqual(('ssh', 'pippo@clubhouse.mouse', 'ls', '-lh', '*.py'), + result) + self.assertEqual("ssh pippo@clubhouse.mouse ls -lh *.py", + str(result)) + + def test_add_shell_command_with_quotes(self): + base = sh.shell_command('sh -c') + result = base + sh.shell_command(['echo Hello!']) + self.assertIsInstance(result, sh.ShellCommand) + self.assertEqual(('sh', '-c', "echo Hello!"), result) + self.assertEqual("sh -c 'echo Hello!'", str(result))