Skip to content

chore(tools): replace bashlex (GPLv3) with tree-sitter-bash (MIT)#3237

Open
VascoSch92 wants to merge 2 commits into
mainfrom
vasco/move-away-from-bashlex
Open

chore(tools): replace bashlex (GPLv3) with tree-sitter-bash (MIT)#3237
VascoSch92 wants to merge 2 commits into
mainfrom
vasco/move-away-from-bashlex

Conversation

@VascoSch92
Copy link
Copy Markdown
Contributor

@VascoSch92 VascoSch92 commented May 13, 2026

  • A human has tested these changes.

Why

bashlex is GPLv3+ while this repo is MIT — a transitive copyleft dependency we want to drop. It's also unmaintained (last meaningful release ~2020) and crashes on a variety of valid shell constructs, which is why the wrapper currently catches four exception types and silently falls back to the raw string.

tree-sitter and tree-sitter-bash are both MIT, actively maintained, and recover gracefully on malformed input (an ERROR node in the tree, not an exception).

Summary

  • openhands-tools/pyproject.toml: bashlex>=0.18 → tree-sitter>=0.24, tree-sitter-bash>=0.23
  • openhands-tools/openhands/tools/terminal/utils/command.py: rewritten against tree-sitter (130 lines, 19 fewer than the bashlex version)
  • uv.lock: regenerated

API & behavior

Public surface unchanged: split_bash_commands(commands: str) -> list[str] and escape_bash_special_chars(command: str) -> str.

Splitting contract preserved: statements separated by a newline split into separate entries; statements joined by ;, &&, ||, |, or & stay together. Comments and whitespace between statements fold into the preceding entry. On parse failure the input is returned unchanged — same silent-passthrough as the bashlex era.

Issue Number

Refs #2721

How to Test

  • tests/tools/terminal/test_terminal_parsing.py: 30/30 pass, unchanged.
  • tests/tools/terminal/test_terminal_session.py integration tests touching split_bash_commands (test_long_running_command_follow_by_execute, test_complex_commands, test_multiple_multiline_commands,
    test_multiline_command_loop) pass on both tmux and subprocess backends.
  • Pre-commit clean: Ruff format/lint, pycodestyle, pyright, import dependency rules.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:bf3b062-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-bf3b062-python \
  ghcr.io/openhands/agent-server:bf3b062-python

All tags pushed for this build

ghcr.io/openhands/agent-server:bf3b062-golang-amd64
ghcr.io/openhands/agent-server:bf3b062753b7a2041cca82ecf3eeb7a552423b92-golang-amd64
ghcr.io/openhands/agent-server:vasco-move-away-from-bashlex-golang-amd64
ghcr.io/openhands/agent-server:bf3b062-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:bf3b062-golang-arm64
ghcr.io/openhands/agent-server:bf3b062753b7a2041cca82ecf3eeb7a552423b92-golang-arm64
ghcr.io/openhands/agent-server:vasco-move-away-from-bashlex-golang-arm64
ghcr.io/openhands/agent-server:bf3b062-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:bf3b062-java-amd64
ghcr.io/openhands/agent-server:bf3b062753b7a2041cca82ecf3eeb7a552423b92-java-amd64
ghcr.io/openhands/agent-server:vasco-move-away-from-bashlex-java-amd64
ghcr.io/openhands/agent-server:bf3b062-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:bf3b062-java-arm64
ghcr.io/openhands/agent-server:bf3b062753b7a2041cca82ecf3eeb7a552423b92-java-arm64
ghcr.io/openhands/agent-server:vasco-move-away-from-bashlex-java-arm64
ghcr.io/openhands/agent-server:bf3b062-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:bf3b062-python-amd64
ghcr.io/openhands/agent-server:bf3b062753b7a2041cca82ecf3eeb7a552423b92-python-amd64
ghcr.io/openhands/agent-server:vasco-move-away-from-bashlex-python-amd64
ghcr.io/openhands/agent-server:bf3b062-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:bf3b062-python-arm64
ghcr.io/openhands/agent-server:bf3b062753b7a2041cca82ecf3eeb7a552423b92-python-arm64
ghcr.io/openhands/agent-server:vasco-move-away-from-bashlex-python-arm64
ghcr.io/openhands/agent-server:bf3b062-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:bf3b062-golang
ghcr.io/openhands/agent-server:bf3b062753b7a2041cca82ecf3eeb7a552423b92-golang
ghcr.io/openhands/agent-server:vasco-move-away-from-bashlex-golang
ghcr.io/openhands/agent-server:bf3b062-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:bf3b062-java
ghcr.io/openhands/agent-server:bf3b062753b7a2041cca82ecf3eeb7a552423b92-java
ghcr.io/openhands/agent-server:vasco-move-away-from-bashlex-java
ghcr.io/openhands/agent-server:bf3b062-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:bf3b062-python
ghcr.io/openhands/agent-server:bf3b062753b7a2041cca82ecf3eeb7a552423b92-python
ghcr.io/openhands/agent-server:vasco-move-away-from-bashlex-python
ghcr.io/openhands/agent-server:bf3b062-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., bf3b062-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., bf3b062-python-amd64) are also available if needed

@VascoSch92 VascoSch92 requested a review from all-hands-bot May 13, 2026 10:14
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 2026

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 2026

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

Copy link
Copy Markdown
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Acceptable - Well-executed dependency migration that solves real license and maintenance problems.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM

This PR changes terminal command parsing, which technically falls under the eval-risk category (terminal/stdin/stdout handling). However, the comprehensive test coverage (30 parsing tests + integration tests all passing) provides strong evidence that the behavior contract is preserved. The license incompatibility (GPLv3 bashlex in MIT repo) is a real blocker that justifies this migration. The new tree-sitter implementation is cleaner (19 fewer lines, eliminates 4 exception types, better abstractions with _PRESERVE_TYPES) and more maintainable (active project vs unmaintained since 2020).

Dependency age verification: tree-sitter-bash 0.25.1 uploaded 2025-12-02, well past the 7-day guardrail. ✅

VERDICT:
Worth merging - License blocker resolved, comprehensive test coverage confirms behavior preservation, cleaner implementation.

KEY INSIGHT:
Replacing a GPL-licensed dependency with an MIT-licensed alternative while simultaneously improving code quality and maintainability - this is the right kind of technical debt paydown.

@VascoSch92 VascoSch92 marked this pull request as ready for review May 13, 2026 10:17
@VascoSch92 VascoSch92 requested review from enyst and neubig May 13, 2026 10:19
Copy link
Copy Markdown
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Acceptable - Well-executed dependency migration with excellent test coverage.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM

This changes terminal command parsing (eval-risk category per repo guidelines). While the comprehensive test coverage (30 parsing tests + integration tests all passing) provides strong evidence the behavior contract is preserved, changes to terminal/stdin/stdout handling require eval validation before merging.

VERDICT:
⏸️ Needs eval validation - Code quality is excellent and solves real license/maintenance problems, but please run a lightweight eval (e.g., SWE-bench Lite) to confirm no benchmark impact before merging.

KEY INSIGHT:
Clean refactor that eliminates license incompatibility (GPLv3→MIT) and improves error handling (graceful recovery vs exception catching), but falls under eval-risk category due to terminal parsing changes.

@VascoSch92 VascoSch92 added the run-eval-50 Runs evaluation on 50 SWE-bench instances label May 13, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Evaluation Triggered

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 2026

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-tools/openhands/tools/terminal/utils
   command.py52982%52, 59, 64, 68, 72–73, 88, 93, 98
TOTAL27400608977% 

Copy link
Copy Markdown
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ QA Report: PASS

Successfully verified license swap from bashlex (GPLv3+) to tree-sitter-bash (MIT) while preserving all functionality.

Does this PR achieve its stated goal?

Yes. The PR successfully removes the GPLv3+ bashlex dependency and replaces it with MIT-licensed tree-sitter-bash. I verified:

  1. License compliance achieved: Confirmed bashlex is removed and tree-sitter-bash (MIT) is installed
  2. API preserved: Both split_bash_commands() and escape_bash_special_chars() maintain identical signatures
  3. Behavior maintained: All 30 existing tests pass without modification
  4. Robustness improved: Graceful error recovery on malformed input (no crashes on unclosed quotes, backticks, etc.)
Phase Result
Environment Setup ✅ Dependencies installed, PR branch built successfully
CI Status ✅ All core checks passing (sdk-tests, workspace-tests, cross-tests, pre-commit)
Functional Verification ✅ All tests pass, real-world scenarios work correctly, license change verified
Functional Verification

Test 1: Existing Test Suite

Baseline established (no before/after needed — tests define expected behavior):

Ran the repository's 30 existing bash parsing tests:

uv run pytest tests/tools/terminal/test_terminal_parsing.py -v

Result:

30 passed, 5 warnings in 0.30s

All tests pass without modification, confirming backward compatibility:

  • test_split_commands_util: Multi-line commands split correctly
  • test_heredoc: Heredoc parsing preserved
  • test_backslash_continuation: Line continuation handled
  • test_comments: Comments folded correctly
  • test_escape_bash_special_chars: Escape doubling works for \;, \|, \&, \<, \>
  • test_escape_bash_special_chars_with_heredoc: Heredoc content untouched
  • test_escape_bash_special_chars_with_command_substitution: Command substitutions preserved
  • test_invalid_syntax: Parse errors return input unchanged (graceful fallback)

Test 2: Real-World Command Patterns

Tested 7 realistic scenarios an AI agent might execute:

Scenario 1 — Multi-line script setup:

input = """mkdir -p test_dir
cd test_dir
echo "Hello" > file.txt
cat file.txt"""
result = split_bash_commands(input)
# Expected: 4 commands (split on newlines)
# Actual: 4 commands ✓

Scenario 2 — Conditional execution:

input = "ls /nonexistent || echo 'Directory not found'"
result = split_bash_commands(input)
# Expected: 1 command (OR operator keeps together)
# Actual: 1 command ✓

Scenario 3 — Chained commands:

input = "mkdir -p build && cd build && cmake .."
result = split_bash_commands(input)
# Expected: 1 command (AND chain stays together)
# Actual: 1 command ✓

Scenario 4 — Complex heredoc:

input = """cat > config.yaml <<EOF
database:
  host: localhost
  port: 5432
EOF
echo "Config created" """
result = split_bash_commands(input)
# Expected: 2 commands (heredoc + echo)
# Actual: 2 commands ✓

Scenario 5 — Background task:

input = "python server.py > server.log 2>&1 &"
result = split_bash_commands(input)
# Expected: 1 command (background operator keeps together)
# Actual: 1 command ✓

Scenario 6 — Escaped special chars:

input = r"find . -name '*.txt' -exec grep 'pattern' {} \;"
result = escape_bash_special_chars(input)
# Expected: \; doubled to \\;
# Actual: Changed as expected ✓

Scenario 7 — Git workflow:

input = """git add .
git commit -m "Update files"
git push origin main"""
result = split_bash_commands(input)
# Expected: 3 commands (split on newlines)
# Actual: 3 commands ✓

All scenarios behaved correctly — splitting on newlines, preserving operators (&&, ||, |, &, ;), handling heredocs, and doubling escapes outside quotes.

Test 3: Crash Resistance

Tested inputs known to crash bashlex (from issue #7391, #8369):

problematic_inputs = [
    ("Unclosed backtick", "echo `unclosed"),
    ("Unclosed quote", 'echo "unclosed'),
    ("Deeply nested", "echo $((((1+1))))"),
    ("Mixed unclosed", 'echo "test `nested'),
    ("Unicode characters", "echo 'Hello 世界'"),
    ("Very long line", "echo " + "x" * 10000),
]

Result: All handled gracefully (no exceptions), returning the input unchanged when parse errors occur. This matches the PR's claim that tree-sitter "recovers gracefully on malformed input" instead of crashing.

Test 4: License Compliance

Before (main branch):

uv pip list | grep bashlex
# bashlex==0.18 (GPLv3+)

After (PR branch):

uv pip list | grep bashlex
# (no output — bashlex removed)

uv run python -c "import importlib.metadata; print(importlib.metadata.metadata('tree-sitter-bash')['License'])"
# MIT

Verified:

  • bashlex dependency removed from openhands-tools/pyproject.toml
  • tree-sitter (>=0.24) and tree-sitter-bash (>=0.23) added
  • tree-sitter-bash is MIT-licensed
  • No bashlex modules loaded at runtime

License goal achieved: GPLv3+ dependency removed, replaced with MIT.

Issues Found

None.

Copy link
Copy Markdown
Collaborator

@enyst enyst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! As a side effect, the code really looks cleaner.

One tiny question we may want to look into is if the dependencies are fairly heavy? But it’s good anyway to start with.

@VascoSch92
Copy link
Copy Markdown
Contributor Author

VascoSch92 commented May 13, 2026

Thank you! As a side effect, the code really looks cleaner.

One tiny question we may want to look into is if the dependencies are fairly heavy? But it’s good anyway to start with.

It would be nice to change every regex related bash command with tree-sitter :-)

To answer your question

  ┌─────────────────────────────────┬───────────────────────┬─────────────────────┬──────────────────────┬─────────────────┐
  │             Package             │    Installed size     │ Wheel (macOS arm64) │ Wheel (Linux x86_64) │ Transitive deps │
  ├─────────────────────────────────┼───────────────────────┼─────────────────────┼──────────────────────┼─────────────────┤
  │ bashlex 0.18 (removed)          │ ~285 KB (pure Python) │               68 KB │                68 KB │ none            │
  ├─────────────────────────────────┼───────────────────────┼─────────────────────┼──────────────────────┼─────────────────┤
  │ tree-sitter 0.25.2 (added)      │                341 KB │              135 KB │               620 KB │ none            │
  ├─────────────────────────────────┼───────────────────────┼─────────────────────┼──────────────────────┼─────────────────┤
  │ tree-sitter-bash 0.25.1 (added) │              1,407 KB │              198 KB │               227 KB │ none            │
  └─────────────────────────────────┴───────────────────────┴─────────────────────┴──────────────────────┴─────────────────┘

I will wait that the eval is done (just for safety) and then I will merge :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-eval-50 Runs evaluation on 50 SWE-bench instances

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants