This file provides guidance to Claude Code when working with code in this repository.
just-bash-py is a Python port of the just-bash TypeScript library - a pure Python bash interpreter with an in-memory virtual filesystem. Designed for AI agents needing a secure, sandboxed bash environment.
# Install dependencies
pip install -e ".[dev]"
# Run tests (excludes spec_tests - use this by default)
pytest tests/ --ignore=tests/spec_tests/ -v
# Run specific test file
pytest tests/test_commands/test_commands.py -v
# Run tests matching pattern
pytest tests/ --ignore=tests/spec_tests/ -v -k "grep"
# Type checking
mypy src/
# Format code
ruff format src/ tests/
ruff check src/ tests/ --fixIMPORTANT: Do NOT run spec_tests unless explicitly asked. The spec_tests suite takes ~2 minutes to run and contains many expected failures. Always use --ignore=tests/spec_tests/ when running the full test suite.
CRITICAL: Always write tests FIRST, before implementing features.
- Write a failing test first - Define the expected behavior before writing any implementation code
- Run the test and watch it fail - Verify the test fails for the right reason
- Write minimal code to pass - Implement just enough to make the test pass
- Refactor - Clean up while keeping tests green
- Repeat - Add more tests for edge cases and additional features
import pytest
from just_bash import Bash
class TestNewCommand:
"""Test new_command implementation."""
@pytest.mark.asyncio
async def test_basic_usage(self):
bash = Bash(files={"/input.txt": "content"})
result = await bash.exec("new_command /input.txt")
assert result.stdout == "expected output\n"
assert result.exit_code == 0
@pytest.mark.asyncio
async def test_with_flag(self):
bash = Bash(files={"/input.txt": "content"})
result = await bash.exec("new_command -f /input.txt")
assert result.stdout == "expected with flag\n"
@pytest.mark.asyncio
async def test_error_case(self):
bash = Bash()
result = await bash.exec("new_command /nonexistent")
assert "No such file" in result.stderr
assert result.exit_code == 1
@pytest.mark.asyncio
async def test_stdin(self):
bash = Bash()
result = await bash.exec("echo input | new_command")
assert result.stdout == "processed input\n"- Assert full stdout/stderr - Don't use partial matches when exact output is known
- Test error cases - Always test what happens with invalid input
- Test stdin - If the command reads from stdin, test it with pipes
- Test flags - Each flag should have at least one test
- Use descriptive test names -
test_grep_ignore_case_flagnottest_grep_2
Input Script → Parser (src/just_bash/parser/) → AST → Interpreter (src/just_bash/interpreter/) → ExecResult
- Parser (
src/just_bash/parser/): Lexer and recursive descent parser - Interpreter (
src/just_bash/interpreter/): AST execution, expansion, builtins - Commands (
src/just_bash/commands/): External command implementations - Filesystem (
src/just_bash/fs/): In-memory virtual filesystem
- Write tests first in
tests/test_commands/test_commands.py - Run tests to see them fail
- Create command in
src/just_bash/commands/<name>/<name>.py - Add to registry in
src/just_bash/commands/registry.py - Run tests to see them pass
Command implementation pattern:
from ...types import CommandContext, ExecResult
class NewCommand:
"""The new_command."""
name = "new_command"
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
# Parse arguments
# Execute logic using ctx.fs, ctx.stdin, ctx.cwd, ctx.env
return ExecResult(stdout=output, stderr=errors, exit_code=code)- Commands:
src/just_bash/commands/<name>/<name>.py - Command registry:
src/just_bash/commands/registry.py - Tests:
tests/test_commands/test_commands.py - Parser:
src/just_bash/parser/ - Interpreter:
src/just_bash/interpreter/ - Types:
src/just_bash/types.py
- TDD first - Write tests before implementation
- Match real bash behavior, not convenience
- Keep implementations simple and focused
- Use
ctx.fsfor all filesystem operations - Handle errors gracefully with appropriate exit codes
- Verify with
pytest tests/ --ignore=tests/spec_tests/ -vbefore finishing - Do not opt for simplified or incomplete implementations unless explicitly granted permission to do so.
IMPORTANT: Whenever committing to GitHub, update the Test Results section in README.md.
Before committing, run pytest tests/ --ignore=tests/spec_tests/ -q and add a new row to the stacked bar graph in the ## Test Results section of README.md. Use the commit hash (short SHA), current date, and test counts. The graph bar is 50 characters wide using:
█for passed (proportional, minimum 1 if non-zero)▒for failed (minimum 1 if non-zero)░for skipped (minimum 1 if non-zero)
Scale: divide total tests by 50 to get the "tests per block" value. Adjust the passed count to keep the total bar width at 50 characters after applying minimums for failed/skipped. Update the "Each █ ≈ N tests" note in the section header if the scale changes.
- Single-quote variable expansion - Variables like
$0are expanded even in single quotes. Workaround: use-fto load scripts from files, or escape with\$.