Skip to content

Commit 2f1eec3

Browse files
committed
fix: render bytes arguments as text instead of repr
local['echo'](b'test') passed b"test" through str(), so the child process received the literal repr b'test' rather than test. Command arguments now go through a _stringify helper that decodes bytes with os.fsdecode, which shquote and the unquoted formulate paths both use. Closes #600
1 parent 500cfb6 commit 2f1eec3

2 files changed

Lines changed: 37 additions & 4 deletions

File tree

plumbum/commands/base.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import contextlib
1414
import functools
15+
import os
1516
import shlex
1617
import subprocess
1718
import typing
@@ -69,10 +70,20 @@ class RedirectionError(Exception):
6970
# ===================================================================================================
7071
# Utilities
7172
# ===================================================================================================
73+
def _stringify(value: Any) -> str:
74+
"""Renders a command-line argument as text.
75+
76+
``bytes`` are decoded with :func:`os.fsdecode` rather than going through
77+
``str()``, which would render them as their ``repr`` (``b'test'``).
78+
"""
79+
if isinstance(value, (bytes, bytearray, memoryview)):
80+
return os.fsdecode(bytes(value))
81+
return str(value)
82+
83+
7284
def shquote(text: Any) -> str:
7385
"""Quotes the given text with shell escaping (assumes as syntax similar to ``sh``)"""
74-
text = str(text)
75-
return shlex.quote(text)
86+
return shlex.quote(_stringify(text))
7687

7788

7889
def shquote_list(seq: Sequence[Any]) -> list[str]:
@@ -695,10 +706,11 @@ def formulate(self, level: int = 0, args: Sequence[Any] = ()) -> list[str]:
695706
argv.extend(a.formulate(level + 1))
696707
elif isinstance(a, (list, tuple)):
697708
argv.extend(
698-
shquote(b) if level >= self.QUOTE_LEVEL else str(b) for b in a
709+
shquote(b) if level >= self.QUOTE_LEVEL else _stringify(b)
710+
for b in a
699711
)
700712
else:
701-
argv.append(shquote(a) if level >= self.QUOTE_LEVEL else str(a))
713+
argv.append(shquote(a) if level >= self.QUOTE_LEVEL else _stringify(a))
702714
return argv
703715

704716
@property

tests/test_local.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,3 +1322,24 @@ def test_is_recursive_glob(pattern, expected):
13221322
from plumbum.machines.remote import _is_recursive_glob
13231323

13241324
assert _is_recursive_glob(pattern) is expected
1325+
1326+
1327+
class TestBytesArguments:
1328+
"""``bytes`` arguments must be decoded, not rendered via ``repr``.
1329+
1330+
See https://github.com/tomerfiliba/plumbum/issues/600
1331+
"""
1332+
1333+
def test_shquote_bytes(self):
1334+
from plumbum.commands import shquote
1335+
1336+
assert shquote(b"test") == "test"
1337+
assert shquote(b"a b") == "'a b'"
1338+
1339+
@skip_on_windows
1340+
def test_bytes_argument(self):
1341+
assert local["echo"](b"test").strip() == "test"
1342+
1343+
@skip_on_windows
1344+
def test_bytes_argument_in_list(self):
1345+
assert local["echo"][[b"a", b"b"]]().split() == ["a", "b"]

0 commit comments

Comments
 (0)