Skip to content

Commit

Permalink
Disable globbing and variable resolution for extra task args
Browse files Browse the repository at this point in the history
Also disable globbing for quoted command tokens within a cmd task definition
to bring behavoir closer to what would be expected, and make it possible to
pass strings with glob chars as arguments

Resolves #16

In retrospect the extra interpretation of args passed from the command line
never really made sense becuase the host shell should handle this.
  • Loading branch information
nat-n committed Jan 26, 2021
1 parent 8ee3ff2 commit 2f2e6a3
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 63 deletions.
50 changes: 32 additions & 18 deletions poethepoet/task/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from ..context import RunContext

_GLOBCHARS_PATTERN = re.compile(r".*[\*\?\[]")
_QUOTED_TOKEN_PATTERN = re.compile(r"(^\".*\"$|^'.*'$)")


class CmdTask(PoeTask):
Expand All @@ -33,32 +34,45 @@ def _handle_run(
extra_args: Iterable[str],
env: MutableMapping[str, str],
) -> int:
cmd = self._resolve_args(context, extra_args, env)
cmd = (*self._resolve_args(context, env), *extra_args)
self._print_action(" ".join(cmd), context.dry)
return context.get_executor(env, self.options.get("executor")).execute(cmd)

def _resolve_args(
self,
context: "RunContext",
extra_args: Iterable[str],
env: MutableMapping[str, str],
self, context: "RunContext", env: MutableMapping[str, str],
):
# Parse shell command tokens
cmd_tokens = shlex.split(
self._resolve_envvars(self.content, context, env),
comments=True,
posix=not self._is_windows,
)
extra_args = [
self._resolve_envvars(token, context, env) for token in extra_args
]
# Resolve any glob pattern paths
# Parse shell command tokens and check if they're quoted
if self._is_windows:
cmd_tokens = (
(compat_token, bool(_QUOTED_TOKEN_PATTERN.match(compat_token)))
for compat_token in shlex.split(
self._resolve_envvars(self.content, context, env),
posix=False,
comments=True,
)
)
else:
cmd_tokens = (
(posix_token, bool(_QUOTED_TOKEN_PATTERN.match(compat_token)))
for (posix_token, compat_token) in zip(
shlex.split(
self._resolve_envvars(self.content, context, env),
posix=True,
comments=True,
),
shlex.split(
self._resolve_envvars(self.content, context, env),
posix=False,
comments=True,
),
)
)
# Resolve any unquoted glob pattern paths
result = []
for cmd_token in (*cmd_tokens, *extra_args):
if _GLOBCHARS_PATTERN.match(cmd_token):
for (cmd_token, is_quoted) in cmd_tokens:
if not is_quoted and _GLOBCHARS_PATTERN.match(cmd_token):
# looks like a glob path so resolve it
result.extend(glob(cmd_token, recursive=True))
else:
result.append(cmd_token)
# Finally add the extra_args from the invoking command and we're done
return result
3 changes: 0 additions & 3 deletions poethepoet/task/shell.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os
import re
import shutil
import subprocess
from typing import Dict, Iterable, MutableMapping, Type, TYPE_CHECKING
Expand All @@ -10,8 +9,6 @@
from ..config import PoeConfig
from ..context import RunContext

_GLOBCHARS_PATTERN = re.compile(r".*[\*\?\[]")


class ShellTask(PoeTask):
"""
Expand Down
48 changes: 6 additions & 42 deletions tests/test_task_running.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
def test_call_echo_task(run_poe_subproc, dummy_project_path, esc_prefix):
# The $ has to be escaped or it'll be evaluated by the outer shell and poe will
# never see it
result = run_poe_subproc("echo", "foo", esc_prefix + r"${POE_ROOT} !")
result = run_poe_subproc("echo", "foo", "!")
assert (
result.capture
== f"Poe => echo POE_ROOT:{dummy_project_path} Password1, task_args: foo {dummy_project_path} !\n"
== f"Poe => echo POE_ROOT:{dummy_project_path} Password1, task_args: foo !\n"
)
assert (
result.stdout
== f"POE_ROOT:{dummy_project_path} Password1, task_args: foo {dummy_project_path} !\n"
result.stdout == f"POE_ROOT:{dummy_project_path} Password1, task_args: foo !\n"
)
assert result.stderr == ""

Expand All @@ -25,40 +24,6 @@ def test_setting_envvar_in_task(run_poe_subproc, dummy_project_path):
assert result.stderr == ""


@pytest.mark.parametrize(
"input_var, output_var",
(
(r"\${POE_ROOT}", "dummy_project_path"),
(r"\\\$POE_ROOT", r"$" + "POE_ROOT"),
(r"\${POE_ROOT}", "dummy_project_path"),
(r"ABC\\\${POE_ROOT}D", r"ABC${" + "POE_ROOT" + "}D"),
(
r"1 \${POE_ROOT} 2 \${POE_ROOT} 3 \\\${POE_ROOT} ",
r"1 dummy_project_path 2 dummy_project_path 3 ${POE_ROOT} ",
),
# It's difficult to add more cases without getting lost in the process of
# templating and running the subprocess script
),
)
def test_passing_envvar_str_to_task(
input_var, output_var, run_poe_subproc, dummy_project_path, is_windows
):
if is_windows:
# the escapes doesn't work the same on windows so remove the extras
input_var = input_var.replace(r"\$", "$").replace(r"\\", "\\")
# A further (outer shell escaped) escape can be added to indicate to POE that the $
# is escaped
# For the sake of sanity poe leaves the remaining backslashes alone
result = run_poe_subproc("echo", input_var, "!")
output_var_ = output_var.replace("dummy_project_path", str(dummy_project_path))
assert (
result.capture
== f"Poe => echo POE_ROOT:{dummy_project_path} Password1, task_args: {output_var_} !\n"
)
# assert result.stdout == f"POE_ROOT:{dummy_project_path} task_args: {output_var} !\n"
assert result.stderr == ""


def test_shell_task(run_poe_subproc):
result = run_poe_subproc("count")
assert (
Expand Down Expand Up @@ -104,14 +69,13 @@ def test_script_task_with_hard_coded_args(

def test_ref_task(run_poe_subproc, dummy_project_path, esc_prefix):
# This should be exactly the same as calling the echo task directly
result = run_poe_subproc("also_echo", "foo", esc_prefix + r"${POE_ROOT} !")
result = run_poe_subproc("also_echo", "foo", "!")
assert (
result.capture
== f"Poe => echo POE_ROOT:{dummy_project_path} Password1, task_args: foo {dummy_project_path} !\n"
== f"Poe => echo POE_ROOT:{dummy_project_path} Password1, task_args: foo !\n"
)
assert (
result.stdout
== f"POE_ROOT:{dummy_project_path} Password1, task_args: foo {dummy_project_path} !\n"
result.stdout == f"POE_ROOT:{dummy_project_path} Password1, task_args: foo !\n"
)
assert result.stderr == ""

Expand Down

0 comments on commit 2f2e6a3

Please sign in to comment.