Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 37 additions & 31 deletions src/manage/scriptutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,44 +344,50 @@ def split_args(arg_string, argv0=False):
"""Splits a single argument string into separate unquoted items.

If argv0 is True, the first argument is parsed as if it is the executable name.

Arguments are split using the same rules as the C runtime, so quotes may begin
and end anywhere within an argument.
"""
args = []
if argv0 and arg_string[:1] == '"':
a, _, arg_string = arg_string[1:].partition('"')
args.append(a)

arg_buffer = None
quoted_arg = []
bits = arg_string.strip().split(' ')
while bits:
a = bits.pop(0)
pre, quot, post = a.partition('"')
if arg_buffer:
pre = arg_buffer + pre
arg_buffer = None
if not quot:
if quoted_arg:
quoted_arg.append(pre)
quoted_arg.append(' ')
else:
args.append(pre.replace('\\\\', '\\'))
arg = None
in_quotes = False
backslashes = 0
i = 0
while i < len(arg_string):
c = arg_string[i]
i += 1
if c == '\\':
backslashes += 1
continue
if pre[-1:] == '\\' and (len(pre) - len(pre.rstrip('\\'))) % 2 == 1:
arg_buffer = pre[:-1] + quot
if post:
bits.insert(0, post)
if c == '"':
# 2n backslashes + quote -> n backslashes and toggle quoting
# 2n+1 backslashes + quote -> n backslashes and a literal quote
arg = (arg or '') + '\\' * (backslashes // 2)
if backslashes % 2:
arg += '"'
elif in_quotes and arg_string[i:i + 1] == '"':
# A doubled quote inside quotes is a literal quote
arg += '"'
i += 1
else:
in_quotes = not in_quotes
backslashes = 0
continue
elif quoted_arg:
quoted_arg.append(pre)
args.append(''.join(quoted_arg).replace('\\\\', '\\'))
quoted_arg.clear()
if backslashes:
arg = (arg or '') + '\\' * backslashes
backslashes = 0
if c in ' \t' and not in_quotes:
if arg is not None:
args.append(arg)
arg = None
continue

quoted_arg.append(pre)
if pre:
quoted_arg.append(quot)
if post:
bits.insert(0, post)
if quoted_arg:
args.append(''.join(quoted_arg))
arg = (arg or '') + c
if backslashes:
arg = (arg or '') + '\\' * backslashes
if arg is not None:
args.append(arg)
return args
20 changes: 20 additions & 0 deletions tests/test_scriptutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,26 @@ def test_quote_args(args, expect):
assert args == split_args(expect), expect


# Examples from "Parsing C command-line arguments" in the Microsoft C runtime
# documentation, plus quotes that begin or end partway through an argument.
@pytest.mark.parametrize("arg_string, expect", [pytest.param(*a, id=a[0]) for a in [
('"a b c" d e', ['a b c', 'd', 'e']),
('"ab\\"c" "\\\\" d', ['ab"c', '\\', 'd']),
('a\\\\\\b d"e f"g h', ['a\\\\\\b', 'de fg', 'h']),
('a\\\\\\"b c d', ['a\\"b', 'c', 'd']),
('a\\\\\\\\"b c" d e', ['a\\\\b c', 'd', 'e']),
('a"b"" c d', ['ab" c d']),
('--opt="a b" -E', ['--opt=a b', '-E']),
('-W"ignore"', ['-Wignore']),
('"a b"c d', ['a bc', 'd']),
('-X utf8', ['-X', 'utf8']),
('x "" y', ['x', '', 'y']),
('a\tb', ['a', 'b']),
]])
def test_split_args(arg_string, expect):
assert expect == split_args(arg_string)


@pytest.mark.parametrize("line, expect_id, expect_line", [pytest.param(*a, id=a[0]) for a in [
("#!/usr/bin/python", "Test1", None),
("#!/usr/bin/python -Es", "Test1", None),
Expand Down