diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 0842b85372..a2f62e5ed5 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -71,7 +71,7 @@ jobs: WSLENV: FORCE_COLOR:PYTEST_REQPASS:TOXENV:GITHUB_STEP_SUMMARY # Number of expected test passes, safety measure for accidental skip of # tests. Update value if you add/remove tests. - PYTEST_REQPASS: 811 + PYTEST_REQPASS: 812 steps: - name: Activate WSL1 if: "contains(matrix.shell, 'wsl')" diff --git a/examples/playbooks/transform_command_instead_of_shell.transformed.yml b/examples/playbooks/transform_command_instead_of_shell.transformed.yml new file mode 100644 index 0000000000..f2477a5b16 --- /dev/null +++ b/examples/playbooks/transform_command_instead_of_shell.transformed.yml @@ -0,0 +1,25 @@ +--- +- name: Fixture + hosts: localhost + tasks: + - name: Shell no pipe + ansible.builtin.command: + cmd: echo hello + changed_when: false + + - name: Shell with jinja filter + ansible.builtin.command: + cmd: echo {{ "hello" | upper }} + changed_when: false + + - name: Shell with jinja filter (fqcn) + ansible.builtin.command: + cmd: echo {{ "hello" | upper }} + changed_when: false + + - name: Command with executable parameter + ansible.builtin.shell: + cmd: clear + args: + executable: /bin/bash + changed_when: false diff --git a/examples/playbooks/transform_command_instead_of_shell.yml b/examples/playbooks/transform_command_instead_of_shell.yml new file mode 100644 index 0000000000..278f5d764b --- /dev/null +++ b/examples/playbooks/transform_command_instead_of_shell.yml @@ -0,0 +1,25 @@ +--- +- name: Fixture + hosts: localhost + tasks: + - name: Shell no pipe + ansible.builtin.shell: + cmd: echo hello + changed_when: false + + - name: Shell with jinja filter + ansible.builtin.shell: + cmd: echo {{ "hello" | upper }} + changed_when: false + + - name: Shell with jinja filter (fqcn) + ansible.builtin.shell: + cmd: echo {{ "hello" | upper }} + changed_when: false + + - name: Command with executable parameter + ansible.builtin.shell: + cmd: clear + args: + executable: /bin/bash + changed_when: false diff --git a/src/ansiblelint/rules/command_instead_of_shell.py b/src/ansiblelint/rules/command_instead_of_shell.py index 346a071c1a..09675c431d 100644 --- a/src/ansiblelint/rules/command_instead_of_shell.py +++ b/src/ansiblelint/rules/command_instead_of_shell.py @@ -23,15 +23,18 @@ import sys from typing import TYPE_CHECKING -from ansiblelint.rules import AnsibleLintRule +from ansiblelint.rules import AnsibleLintRule, TransformMixin from ansiblelint.utils import get_cmd_args if TYPE_CHECKING: + from ruamel.yaml.comments import CommentedMap, CommentedSeq + + from ansiblelint.errors import MatchError from ansiblelint.file_utils import Lintable from ansiblelint.utils import Task -class UseCommandInsteadOfShellRule(AnsibleLintRule): +class UseCommandInsteadOfShellRule(AnsibleLintRule, TransformMixin): """Use shell only when shell functionality is required.""" id = "command-instead-of-shell" @@ -62,6 +65,19 @@ def matchtask( return not any(ch in jinja_stripped_cmd for ch in "&|<>;$\n*[]{}?`") return False + def transform( + self, + match: MatchError, + lintable: Lintable, + data: CommentedMap | CommentedSeq | str, + ) -> None: + if match.tag == "command-instead-of-shell": + target_task = self.seek(match.yaml_path, data) + for _ in range(len(target_task)): + k, v = target_task.popitem(False) + target_task["ansible.builtin.command" if "shell" in k else k] = v + match.fixed = True + # testing code to be loaded only with pytest or when executed the rule file if "pytest" in sys.modules: diff --git a/test/test_transformer.py b/test/test_transformer.py index 3c83f3b586..f66c96bbcd 100644 --- a/test/test_transformer.py +++ b/test/test_transformer.py @@ -84,6 +84,12 @@ def fixture_runner_result( pytest.param("examples/playbooks/vars/empty.yml", 1, False, id="empty"), pytest.param("examples/playbooks/name-case.yml", 1, True, id="name_case"), pytest.param("examples/playbooks/fqcn.yml", 3, True, id="fqcn"), + pytest.param( + "examples/playbooks/transform_command_instead_of_shell.yml", + 3, + True, + id="cmd_instead_of_shell", + ), ), ) def test_transformer( # pylint: disable=too-many-arguments, too-many-locals