From 233400f1a50ee9d8d987742703e198bea6987368 Mon Sep 17 00:00:00 2001 From: Iain Samuel McLean Elder Date: Thu, 24 Apr 2025 17:15:49 +0200 Subject: [PATCH] [Resolve #1482] Make shell optional in cmd hook Now you can set the command to run as an object without also having to set the shell. In this case it uses the default shell, as if the command to run were passed as a string. This allows you to use resolvers to set the command without worrying about which shell to invoke. See @dmarra's use case in #1482. --- sceptre/hooks/cmd.py | 29 +++++++++-------- tests/test_hooks/test_cmd.py | 60 +++++++++++++++++++++--------------- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/sceptre/hooks/cmd.py b/sceptre/hooks/cmd.py index 8292e8789..84e6a19f2 100644 --- a/sceptre/hooks/cmd.py +++ b/sceptre/hooks/cmd.py @@ -24,25 +24,24 @@ def run(self): """ envs = self.stack.connection_manager.create_session_environment_variables() - if isinstance(self.argument, str) and self.argument != "": - command_to_run = self.argument - shell = None - - elif ( - isinstance(self.argument, dict) - and set(self.argument) == {"run", "shell"} - and isinstance(self.argument["run"], str) - and isinstance(self.argument["shell"], str) - and self.argument["run"] != "" - and self.argument["shell"] != "" - ): - command_to_run = self.argument["run"] - shell = self.argument["shell"] + command_to_run = None + shell = None + if isinstance(self.argument, dict): + command_to_run = self.argument.get("run") + shell = self.argument.get("shell") else: + command_to_run = self.argument + + if not isinstance(command_to_run, str) or command_to_run == "": raise InvalidHookArgumentTypeError( "A cmd hook requires either a string argument or an object with " - "`run` and `shell` keys with string values. " + f"a `run` key and a string value. You gave `{self.argument!r}`." + ) + + if shell is not None and not isinstance(shell, str) or shell == "": + raise InvalidHookArgumentTypeError( + "A cmd hook requires a `shell` key with a non-empty string. " f"You gave `{self.argument!r}`." ) diff --git a/tests/test_hooks/test_cmd.py b/tests/test_hooks/test_cmd.py index 9686f5f67..ff7688d23 100644 --- a/tests/test_hooks/test_cmd.py +++ b/tests/test_hooks/test_cmd.py @@ -7,9 +7,13 @@ from sceptre.hooks.cmd import Cmd from sceptre.stack import Stack -ERROR_MESSAGE = ( - r"^A cmd hook requires either a string argument or an object with `run` and " - r"`shell` keys with string values\. You gave `{0}`\.$" +RUN_ERROR_MESSAGE = ( + r"^A cmd hook requires either a string argument or an object with a `run` " + r"key and a string value\. You gave `{0}`\.$" +) + +SHELL_ERROR_MESSAGE = ( + r"^A cmd hook requires a `shell` key with a non-empty string\. You gave `{0}`\.$" ) @@ -32,37 +36,31 @@ def stack(): def test_null_input_raises_exception(stack): - message = ERROR_MESSAGE.format(r"None") + message = RUN_ERROR_MESSAGE.format(r"None") with pytest.raises(InvalidHookArgumentTypeError, match=message): Cmd(None, stack).run() def test_empty_string_raises_exception(stack): - message = ERROR_MESSAGE.format(r"''") + message = RUN_ERROR_MESSAGE.format(r"''") with pytest.raises(InvalidHookArgumentTypeError, match=message): Cmd("", stack).run() def test_list_raises_exception(stack): - message = ERROR_MESSAGE.format(r"\['echo', 'hello'\]") + message = RUN_ERROR_MESSAGE.format(r"\['echo', 'hello'\]") with pytest.raises(InvalidHookArgumentTypeError, match=message): Cmd(["echo", "hello"], stack).run() -def test_dict_without_shell_raises_exception(stack): - message = ERROR_MESSAGE.format(r"\{'run': 'echo hello'\}") - with pytest.raises(InvalidHookArgumentTypeError, match=message): - Cmd({"run": "echo hello"}, stack).run() - - def test_dict_without_run_raises_exception(stack): - message = ERROR_MESSAGE.format(r"\{'shell': '/bin/bash'\}") + message = RUN_ERROR_MESSAGE.format(r"\{'shell': '/bin/bash'\}") with pytest.raises(InvalidHookArgumentTypeError, match=message): Cmd({"shell": "/bin/bash"}, stack).run() def test_dict_with_list_run_raises_exception(stack): - message = ERROR_MESSAGE.format( + message = RUN_ERROR_MESSAGE.format( r"\{'run': \['echo', 'hello'\], 'shell': '/bin/bash'\}" ) with pytest.raises(InvalidHookArgumentTypeError, match=message): @@ -70,19 +68,21 @@ def test_dict_with_list_run_raises_exception(stack): def test_dict_with_empty_run_raises_exception(stack): - message = ERROR_MESSAGE.format(r"\{'run': '', 'shell': '/bin/bash'\}") + message = RUN_ERROR_MESSAGE.format(r"\{'run': '', 'shell': '/bin/bash'\}") with pytest.raises(InvalidHookArgumentTypeError, match=message): Cmd({"run": "", "shell": "/bin/bash"}, stack).run() def test_dict_with_null_run_raises_exception(stack): - message = ERROR_MESSAGE.format(r"\{'run': None, 'shell': '/bin/bash'\}") + message = RUN_ERROR_MESSAGE.format(r"\{'run': None, 'shell': '/bin/bash'\}") with pytest.raises(InvalidHookArgumentTypeError, match=message): Cmd({"run": None, "shell": "/bin/bash"}, stack).run() def test_dict_with_list_shell_raises_exception(stack): - message = ERROR_MESSAGE.format(r"\{'run': 'echo hello', 'shell': \['/bin/bash'\]\}") + message = SHELL_ERROR_MESSAGE.format( + r"\{'run': 'echo hello', 'shell': \['/bin/bash'\]\}" + ) with pytest.raises(InvalidHookArgumentTypeError, match=message): Cmd({"run": "echo hello", "shell": ["/bin/bash"]}, stack).run() @@ -101,21 +101,15 @@ def test_dict_with_non_executable_shell_raises_exception(stack): def test_dict_with_empty_shell_raises_exception(stack): - message = ERROR_MESSAGE.format(r"\{'run': 'echo hello', 'shell': ''\}") + message = SHELL_ERROR_MESSAGE.format(r"\{'run': 'echo hello', 'shell': ''\}") with pytest.raises(InvalidHookArgumentTypeError, match=message): Cmd({"run": "echo hello", "shell": ""}, stack).run() -def test_dict_with_null_shell_raises_exception(stack): - message = ERROR_MESSAGE.format(r"\{'run': 'echo hello', 'shell': None\}") - with pytest.raises(InvalidHookArgumentTypeError, match=message): - Cmd({"run": "echo hello", "shell": None}, stack).run() - - def test_input_exception_reprs_input(stack): import datetime - exception_message = ERROR_MESSAGE.format(r"datetime.date\(2023, 8, 31\)") + exception_message = RUN_ERROR_MESSAGE.format(r"datetime.date\(2023, 8, 31\)") with pytest.raises(InvalidHookArgumentTypeError, match=exception_message): Cmd(datetime.date(2023, 8, 31), stack).run() @@ -151,6 +145,22 @@ def test_default_shell_is_sh(stack, capfd): assert cap.err.strip() == "" +def test_dict_without_shell_uses_default_shell(stack, capfd): + Cmd("echo $0", stack).run() + cap1 = capfd.readouterr() + Cmd({"run": "echo $0"}, stack).run() + cap2 = capfd.readouterr() + assert cap1 == cap2 + + +def test_dict_with_null_shell_uses_default_shell(stack, capfd): + Cmd("echo $0", stack).run() + cap1 = capfd.readouterr() + Cmd({"run": "echo $0", "shell": None}, stack).run() + cap2 = capfd.readouterr() + assert cap1 == cap2 + + def test_shell_parameter_sets_the_shell(stack, capfd): # Determine the local path to bash (it's not always /bin/bash) Cmd("echo $0", stack).run()