Spaces:
Build error
Build error
import pytest | |
from openhands.runtime.utils.bash import escape_bash_special_chars, split_bash_commands | |
def test_split_commands_util(): | |
cmds = [ | |
'ls -l', | |
'echo -e "hello\nworld"', | |
""" | |
echo -e "hello it\\'s me" | |
""".strip(), | |
""" | |
echo \\ | |
-e 'hello' \\ | |
-v | |
""".strip(), | |
""" | |
echo -e 'hello\\nworld\\nare\\nyou\\nthere?' | |
""".strip(), | |
""" | |
echo -e 'hello | |
world | |
are | |
you\\n | |
there?' | |
""".strip(), | |
""" | |
echo -e 'hello | |
world " | |
' | |
""".strip(), | |
""" | |
kubectl apply -f - <<EOF | |
apiVersion: v1 | |
kind: Pod | |
metadata: | |
name: busybox-sleep | |
spec: | |
containers: | |
- name: busybox | |
image: busybox:1.28 | |
args: | |
- sleep | |
- "1000000" | |
EOF | |
""".strip(), | |
""" | |
mkdir -p _modules && \ | |
for month in {01..04}; do | |
for day in {01..05}; do | |
touch "_modules/2024-${month}-${day}-sample.md" | |
done | |
done | |
""".strip(), | |
] | |
joined_cmds = '\n'.join(cmds) | |
split_cmds = split_bash_commands(joined_cmds) | |
for s in split_cmds: | |
print('\nCMD') | |
print(s) | |
for i in range(len(cmds)): | |
assert split_cmds[i].strip() == cmds[i].strip(), ( | |
f'At index {i}: {split_cmds[i]} != {cmds[i]}.' | |
) | |
def test_single_commands(input_command, expected_output): | |
assert split_bash_commands(input_command) == expected_output | |
def test_heredoc(): | |
input_commands = """ | |
cat <<EOF | |
multiline | |
text | |
EOF | |
echo "Done" | |
""" | |
expected_output = ['cat <<EOF\nmultiline\ntext\nEOF', 'echo "Done"'] | |
assert split_bash_commands(input_commands) == expected_output | |
def test_backslash_continuation(): | |
input_commands = """ | |
echo "This is a long \ | |
command that spans \ | |
multiple lines" | |
echo "Next command" | |
""" | |
expected_output = [ | |
'echo "This is a long command that spans multiple lines"', | |
'echo "Next command"', | |
] | |
assert split_bash_commands(input_commands) == expected_output | |
def test_comments(): | |
input_commands = """ | |
echo "Hello" # This is a comment | |
# This is another comment | |
ls -l | |
""" | |
expected_output = [ | |
'echo "Hello" # This is a comment\n# This is another comment', | |
'ls -l', | |
] | |
assert split_bash_commands(input_commands) == expected_output | |
def test_complex_quoting(): | |
input_commands = """ | |
echo "This is a \\"quoted\\" string" | |
echo 'This is a '\''single-quoted'\'' string' | |
echo "Mixed 'quotes' in \\"double quotes\\"" | |
""" | |
expected_output = [ | |
'echo "This is a \\"quoted\\" string"', | |
"echo 'This is a '''single-quoted''' string'", | |
'echo "Mixed \'quotes\' in \\"double quotes\\""', | |
] | |
assert split_bash_commands(input_commands) == expected_output | |
def test_invalid_syntax(): | |
invalid_inputs = [ | |
'echo "Unclosed quote', | |
"echo 'Unclosed quote", | |
'cat <<EOF\nUnclosed heredoc', | |
] | |
for input_command in invalid_inputs: | |
# it will fall back to return the original input | |
assert split_bash_commands(input_command) == [input_command] | |
def test_unclosed_backtick(): | |
# This test reproduces issue #7391 | |
# The issue occurs when parsing a command with an unclosed backtick | |
# which causes a TypeError: ParsingError.__init__() missing 2 required positional arguments: 's' and 'position' | |
command = 'echo `unclosed backtick' | |
# Should not raise TypeError | |
try: | |
result = split_bash_commands(command) | |
# If we get here, the error was handled properly | |
assert result == [command] | |
except TypeError as e: | |
# This is the error we're trying to fix | |
raise e | |
# Also test with the original command from the issue (with placeholder org/repo) | |
curl_command = 'curl -X POST "https://api.github.com/repos/example-org/example-repo/pulls" \\ -H "Authorization: Bearer $GITHUB_TOKEN" \\ -H "Accept: application/vnd.github.v3+json" \\ -d \'{ "title": "XXX", "head": "XXX", "base": "main", "draft": false }\' `echo unclosed' | |
try: | |
result = split_bash_commands(curl_command) | |
assert result == [curl_command] | |
except TypeError as e: | |
raise e | |
def test_over_escaped_command(): | |
# This test reproduces issue #8369 Example 1 | |
# The issue occurs when parsing a command with over-escaped quotes | |
over_escaped_command = r'# 0. Setup directory\\nrm -rf /workspace/repro_sphinx_bug && mkdir -p /workspace/repro_sphinx_bug && cd /workspace/repro_sphinx_bug\\n\\n# 1. Run sphinx-quickstart\\nsphinx-quickstart --no-sep --project myproject --author me -v 0.1.0 --release 0.1.0 --language en . -q\\n\\n# 2. Create index.rst\\necho -e \'Welcome\\\\\\\\n=======\\\\\\\\n\\\\\\\\n.. toctree::\\\\n :maxdepth: 2\\\\\\\\n\\\\\\\\n mypackage_file\\\\\\\\n\' > index.rst' | |
# Should not raise any exception | |
try: | |
result = split_bash_commands(over_escaped_command) | |
# If parsing fails, it should return the original command | |
assert result == [over_escaped_command] | |
except Exception as e: | |
# This is the error we're trying to fix | |
pytest.fail(f'split_bash_commands raised {type(e).__name__} unexpectedly: {e}') | |
def sample_commands(): | |
return [ | |
'ls -l', | |
'echo "Hello, world!"', | |
'cd /tmp && touch test.txt', | |
'echo -e "line1\\nline2\\nline3"', | |
'grep "pattern" file.txt | sort | uniq', | |
'for i in {1..5}; do echo $i; done', | |
'cat <<EOF\nmultiline\ntext\nEOF', | |
'echo "Escaped \\"quotes\\""', | |
"echo 'Single quotes don\\'t escape'", | |
'echo "Command with a trailing backslash \\\n and continuation"', | |
] | |
def test_split_single_commands(sample_commands): | |
for cmd in sample_commands: | |
result = split_bash_commands(cmd) | |
assert len(result) == 1, f'Expected single command, got: {result}' | |
def test_split_commands_with_heredoc(): | |
input_commands = """ | |
cat <<EOF | |
multiline | |
text | |
EOF | |
echo "Done" | |
""" | |
expected_output = ['cat <<EOF\nmultiline\ntext\nEOF', 'echo "Done"'] | |
result = split_bash_commands(input_commands) | |
assert result == expected_output, f'Expected {expected_output}, got {result}' | |
def test_split_commands_with_backslash_continuation(): | |
input_commands = """ | |
echo "This is a long \ | |
command that spans \ | |
multiple lines" | |
echo "Next command" | |
""" | |
expected_output = [ | |
'echo "This is a long command that spans multiple lines"', | |
'echo "Next command"', | |
] | |
result = split_bash_commands(input_commands) | |
assert result == expected_output, f'Expected {expected_output}, got {result}' | |
def test_split_commands_with_empty_lines(): | |
input_commands = """ | |
ls -l | |
echo "Hello" | |
cd /tmp | |
""" | |
expected_output = ['ls -l', 'echo "Hello"', 'cd /tmp'] | |
result = split_bash_commands(input_commands) | |
assert result == expected_output, f'Expected {expected_output}, got {result}' | |
def test_split_commands_with_comments(): | |
input_commands = """ | |
echo "Hello" # This is a comment | |
# This is another comment | |
ls -l | |
""" | |
expected_output = [ | |
'echo "Hello" # This is a comment\n# This is another comment', | |
'ls -l', | |
] | |
result = split_bash_commands(input_commands) | |
assert result == expected_output, f'Expected {expected_output}, got {result}' | |
def test_split_commands_with_complex_quoting(): | |
input_commands = """ | |
echo "This is a \\"quoted\\" string" | |
echo "Mixed 'quotes' in \\"double quotes\\"" | |
""" | |
# echo 'This is a '\''single-quoted'\'' string' | |
expected_output = [ | |
'echo "This is a \\"quoted\\" string"', | |
'echo "Mixed \'quotes\' in \\"double quotes\\""', | |
] | |
# "echo 'This is a '\\''single-quoted'\\'' string'", | |
result = split_bash_commands(input_commands) | |
assert result == expected_output, f'Expected {expected_output}, got {result}' | |
def test_split_commands_with_invalid_input(): | |
invalid_inputs = [ | |
'echo "Unclosed quote', | |
"echo 'Unclosed quote", | |
'cat <<EOF\nUnclosed heredoc', | |
] | |
for input_command in invalid_inputs: | |
# it will fall back to return the original input | |
assert split_bash_commands(input_command) == [input_command] | |
def test_escape_bash_special_chars(): | |
test_cases = [ | |
# Basic cases - use raw strings (r'') to avoid Python escape sequence warnings | |
('echo test \\; ls', 'echo test \\\\; ls'), | |
('grep pattern \\| sort', 'grep pattern \\\\| sort'), | |
('cmd1 \\&\\& cmd2', 'cmd1 \\\\&\\\\& cmd2'), | |
('cat file \\> output.txt', 'cat file \\\\> output.txt'), | |
('cat \\< input.txt', 'cat \\\\< input.txt'), | |
# Quoted strings should remain unchanged | |
('echo "test \\; unchanged"', 'echo "test \\; unchanged"'), | |
("echo 'test \\| unchanged'", "echo 'test \\| unchanged'"), | |
# Mixed quoted and unquoted | |
( | |
'echo "quoted \\;" \\; "more" \\| grep', | |
'echo "quoted \\;" \\\\; "more" \\\\| grep', | |
), | |
# Multiple escapes in sequence | |
('cmd1 \\;\\|\\& cmd2', 'cmd1 \\\\;\\\\|\\\\& cmd2'), | |
# Commands with other backslashes | |
('echo test\\ntest', 'echo test\\ntest'), | |
('echo "test\\ntest"', 'echo "test\\ntest"'), | |
# Edge cases | |
('', ''), # Empty string | |
('\\\\', '\\\\'), # Double backslash | |
('\\"', '\\"'), # Escaped quote | |
] | |
for input_cmd, expected in test_cases: | |
result = escape_bash_special_chars(input_cmd) | |
assert result == expected, ( | |
f'Failed on input "{input_cmd}"\nExpected: "{expected}"\nGot: "{result}"' | |
) | |
def test_escape_bash_special_chars_with_invalid_syntax(): | |
invalid_inputs = [ | |
'echo "unclosed quote', | |
"echo 'unclosed quote", | |
'cat <<EOF\nunclosed heredoc', | |
] | |
for input_cmd in invalid_inputs: | |
# Should return original input when parsing fails | |
result = escape_bash_special_chars(input_cmd) | |
assert result == input_cmd, f'Failed to handle invalid input: {input_cmd}' | |
def test_escape_bash_special_chars_with_heredoc(): | |
input_cmd = r"""cat <<EOF | |
line1 \; not escaped | |
line2 \| not escaped | |
EOF""" | |
# Heredoc content should not be escaped | |
expected = input_cmd | |
result = escape_bash_special_chars(input_cmd) | |
assert result == expected, ( | |
f'Failed to handle heredoc correctly\nExpected: {expected}\nGot: {result}' | |
) | |
def test_escape_bash_special_chars_with_parameter_expansion(): | |
test_cases = [ | |
# Parameter expansion should be preserved | |
('echo $HOME', 'echo $HOME'), | |
('echo ${HOME}', 'echo ${HOME}'), | |
('echo ${HOME:-default}', 'echo ${HOME:-default}'), | |
# Mixed with special chars | |
('echo $HOME \\; ls', 'echo $HOME \\\\; ls'), | |
('echo ${PATH} \\| grep bin', 'echo ${PATH} \\\\| grep bin'), | |
# Quoted parameter expansion | |
('echo "$HOME"', 'echo "$HOME"'), | |
('echo "${HOME}"', 'echo "${HOME}"'), | |
# Complex parameter expansions | |
('echo ${var:=default} \\; ls', 'echo ${var:=default} \\\\; ls'), | |
('echo ${!prefix*} \\| sort', 'echo ${!prefix*} \\\\| sort'), | |
] | |
for input_cmd, expected in test_cases: | |
result = escape_bash_special_chars(input_cmd) | |
assert result == expected, ( | |
f'Failed on input "{input_cmd}"\nExpected: "{expected}"\nGot: "{result}"' | |
) | |
def test_escape_bash_special_chars_with_command_substitution(): | |
test_cases = [ | |
# Basic command substitution | |
('echo $(pwd)', 'echo $(pwd)'), | |
('echo `pwd`', 'echo `pwd`'), | |
# Mixed with special chars | |
('echo $(pwd) \\; ls', 'echo $(pwd) \\\\; ls'), | |
('echo `pwd` \\| grep home', 'echo `pwd` \\\\| grep home'), | |
# Nested command substitution | |
('echo $(echo `pwd`)', 'echo $(echo `pwd`)'), | |
# Complex command substitution | |
('echo $(find . -name "*.txt" \\; ls)', 'echo $(find . -name "*.txt" \\; ls)'), | |
# Mixed with quotes | |
('echo "$(pwd)"', 'echo "$(pwd)"'), | |
('echo "`pwd`"', 'echo "`pwd`"'), | |
] | |
for input_cmd, expected in test_cases: | |
result = escape_bash_special_chars(input_cmd) | |
assert result == expected, ( | |
f'Failed on input "{input_cmd}"\nExpected: "{expected}"\nGot: "{result}"' | |
) | |
def test_escape_bash_special_chars_mixed_nodes(): | |
test_cases = [ | |
# Mix of parameter expansion and command substitution | |
('echo $HOME/$(pwd)', 'echo $HOME/$(pwd)'), | |
# Mix with special chars | |
('echo $HOME/$(pwd) \\; ls', 'echo $HOME/$(pwd) \\\\; ls'), | |
# Complex mixed cases | |
( | |
'echo "${HOME}/$(basename `pwd`) \\; next"', | |
'echo "${HOME}/$(basename `pwd`) \\; next"', | |
), | |
( | |
'VAR=${HOME} \\; echo $(pwd)', | |
'VAR=${HOME} \\\\; echo $(pwd)', | |
), | |
# Real-world examples | |
( | |
'find . -name "*.txt" -exec grep "${PATTERN:-default}" {} \\;', | |
'find . -name "*.txt" -exec grep "${PATTERN:-default}" {} \\\\;', | |
), | |
( | |
'echo "Current path: ${PWD}/$(basename `pwd`)" \\| grep home', | |
'echo "Current path: ${PWD}/$(basename `pwd`)" \\\\| grep home', | |
), | |
] | |
for input_cmd, expected in test_cases: | |
result = escape_bash_special_chars(input_cmd) | |
assert result == expected, ( | |
f'Failed on input "{input_cmd}"\nExpected: "{expected}"\nGot: "{result}"' | |
) | |
def test_escape_bash_special_chars_with_chained_commands(): | |
test_cases = [ | |
# Basic chained commands | |
('ls && pwd', 'ls && pwd'), | |
('echo "hello" && ls', 'echo "hello" && ls'), | |
# Chained commands with special chars | |
('ls \\; pwd && echo test', 'ls \\\\; pwd && echo test'), | |
('echo test && grep pattern \\| sort', 'echo test && grep pattern \\\\| sort'), | |
# Complex chained cases | |
('echo ${HOME} && ls \\; pwd', 'echo ${HOME} && ls \\\\; pwd'), | |
( | |
'echo "$(pwd)" && cat file \\> out.txt', | |
'echo "$(pwd)" && cat file \\\\> out.txt', | |
), | |
# Multiple chains | |
('cmd1 && cmd2 && cmd3', 'cmd1 && cmd2 && cmd3'), | |
( | |
'cmd1 \\; ls && cmd2 \\| grep && cmd3', | |
'cmd1 \\\\; ls && cmd2 \\\\| grep && cmd3', | |
), | |
] | |
for input_cmd, expected in test_cases: | |
result = escape_bash_special_chars(input_cmd) | |
assert result == expected, ( | |
f'Failed on input "{input_cmd}"\nExpected: "{expected}"\nGot: "{result}"' | |
) | |