Skip to content

Commit 0e18bf5

Browse files
fix(shell): handle bashlex parsing errors for bash builtins like 'time' (#799)
* fix(shell): handle bashlex parsing errors for bash builtins like 'time' Adds error handling to split_commands() to gracefully fall back when bashlex can't parse certain bash reserved words and builtins (e.g., 'time'). When bashlex.parse() fails, the function now: - Logs a warning about the parsing failure - Returns the script as a single command This follows the same fallback pattern used for shlex parsing elsewhere in the file. Fixes #798 * test(shell): add test and documentation for bash reserved words fallback Added comprehensive test for split_commands() handling of bash reserved words that bashlex cannot parse (like 'time'). Improved documentation to explain why bashlex fails on reserved words and why fallback behavior is correct. This addresses feedback on PR #799 showing that bashlex (Python port of GNU bash parser) cannot handle bash reserved words and the fallback approach is the correct solution. Co-authored-by: Bob <bob@superuserlabs.org>
1 parent 3c2b3f8 commit 0e18bf5

File tree

2 files changed

+45
-1
lines changed

2 files changed

+45
-1
lines changed

gptme/tools/shell.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1065,7 +1065,25 @@ def split_commands(script: str) -> list[str]:
10651065
# Preprocess script to handle quoted heredoc delimiters that bashlex can't parse
10661066
processed_script = _preprocess_quoted_heredocs(script)
10671067

1068-
parts = bashlex.parse(processed_script)
1068+
try:
1069+
parts = bashlex.parse(processed_script)
1070+
except Exception as e:
1071+
# Fall back to treating script as single command if bashlex can't parse it
1072+
# bashlex (a Python port of GNU bash parser) cannot handle bash reserved words
1073+
# like 'time', 'coproc', etc. These are special keywords in bash that have
1074+
# different parsing rules. When bashlex encounters them, it raises an exception:
1075+
# "type = {time command}, token = {time}"
1076+
#
1077+
# We fall back to treating the entire script as a single command, which is
1078+
# correct behavior since reserved words typically apply to entire command pipelines.
1079+
# This preserves the user's exact command and allows bash to handle the reserved
1080+
# word correctly during execution.
1081+
logger.warning(
1082+
f"bashlex cannot parse bash reserved words (e.g., 'time'). "
1083+
f"Treating script as single command. Error: {e}"
1084+
)
1085+
return [script]
1086+
10691087
commands = []
10701088
for part in parts:
10711089
if part.kind == "command":

tests/test_tools_shell.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,32 @@ def test_split_commands_heredoc_quoted_with_spaces():
247247
assert '<<"EOF"' in commands[0]
248248

249249

250+
def test_split_commands_bash_reserved_words():
251+
"""Test that split_commands handles bash reserved words that bashlex can't parse.
252+
253+
bashlex cannot parse bash reserved words like 'time' and will raise an exception.
254+
In these cases, split_commands should gracefully fall back to treating the
255+
script as a single command.
256+
"""
257+
# Test 'time' reserved word
258+
script_time = "time ls -la"
259+
commands = split_commands(script_time)
260+
assert len(commands) == 1
261+
assert commands[0] == script_time
262+
263+
# Test 'time' with more complex command
264+
script_time_pipeline = "time ls -la | grep test"
265+
commands = split_commands(script_time_pipeline)
266+
assert len(commands) == 1
267+
assert commands[0] == script_time_pipeline
268+
269+
# Test 'time' with redirection
270+
script_time_redirect = "time echo 'test' > output.txt"
271+
commands = split_commands(script_time_redirect)
272+
assert len(commands) == 1
273+
assert commands[0] == script_time_redirect
274+
275+
250276
def test_function(shell):
251277
script = """
252278
function hello() {

0 commit comments

Comments
 (0)