Skip to main content

A Rust implementation of the Parable bash parser

Project description

Rable

CI crates.io docs.rs PyPI License: MIT

A complete GNU Bash 5.3-compatible parser, written in Rust.

Rable is a from-scratch reimplementation of Parable — the excellent Python-based bash parser by @ldayton. It produces identical S-expression output and provides a drop-in replacement Python API via PyO3.

Acknowledgments

This project would not exist without Parable.

Parable is a remarkable piece of work — a complete, well-tested bash parser that produces clean S-expression AST output validated against bash's own internal parser. Its comprehensive test suite (1,604 tests across 36 files) defines the gold standard for bash parsing correctness, and Rable's compatibility is measured entirely against it.

We are deeply grateful to @ldayton for:

  • Building a high-quality, MIT-licensed bash parser that others can learn from and build upon
  • Creating the bash-oracle approach that validates parser output against bash itself
  • Maintaining the extensive .tests corpus that made Rable's development possible
  • Designing the clean S-expression output format that Rable faithfully reproduces

Rable exists because Parable showed the way. Thank you.

Compatibility

Metric Value
Parable test compatibility 1,604 / 1,604 (100%)
Test files at 100% 36 / 36
S-expression output Identical to Parable
Minimum Rust version 1.93
Python version 3.12+

Performance

Rable is approximately 9.5x faster than Parable across all test inputs:

Input Type Parable Rable Speedup
Simple command 41us 5us 8.1x
Pipeline (5 stages) 144us 14us 10.6x
Nested compound 265us 27us 10.0x
Complex real-world script 640us 67us 9.5x
Overall 2.1ms 221us 9.5x

Run just benchmark to reproduce these results on your machine.

Installation

As a Rust library

[dependencies]
rable = "0.1"

As a Python package

pip install rable

Or build from source:

just setup       # creates venv, builds bindings, installs Parable
just develop     # rebuild after code changes

Usage

Rust

use rable::{parse, NodeKind};

fn main() {
    // Parse bash source into AST nodes
    let nodes = parse("echo hello | grep h", false).unwrap();
    for node in &nodes {
        println!("{node}");
    }
    // Output: (pipe (command (word "echo") (word "hello")) (command (word "grep") (word "h")))

    // Inspect the AST via pattern matching
    if let NodeKind::Pipeline { commands, .. } = &nodes[0].kind {
        println!("Pipeline with {} stages", commands.len());
    }

    // Enable extended glob patterns (@(), ?(), *(), +(), !())
    let nodes = parse("echo @(foo|bar)", true).unwrap();
    println!("{}", nodes[0]);
}

Error handling:

match rable::parse("if", false) {
    Ok(nodes) => { /* use nodes */ }
    Err(e) => {
        eprintln!("line {}, pos {}: {}", e.line(), e.pos(), e.message());
    }
}

See examples/basic.rs for a more complete example, or run it with:

cargo run --example basic

Python

from rable import parse, ParseError, MatchedPairError

# Parse bash source into AST nodes
nodes = parse('if [ -f file ]; then cat file; fi')
for node in nodes:
    print(node.to_sexp())
# Output: (if (command (word "[") (word "-f") (word "file") (word "]")) (command (word "cat") (word "file")))

# Errors are raised as exceptions
try:
    parse('if')
except ParseError as e:
    print(f"Syntax error: {e}")

# Enable extended glob patterns
nodes = parse('echo @(foo|bar)', extglob=True)

The Python API is a drop-in replacement for Parable:

# Before (Parable)
from parable import parse, ParseError, MatchedPairError

# After (Rable) — same API, ~10x faster
from rable import parse, ParseError, MatchedPairError

API Reference

parse(source, extglob) -> Vec<Node>

The main entry point. Parses a bash source string into a list of top-level AST nodes.

  • source — the bash source code to parse
  • extglob — set to true to enable extended glob patterns (@(), ?(), *(), +(), !())
  • ReturnsVec<Node>, where each top-level command separated by newlines is a separate node
  • ErrorsRableError::Parse for syntax errors, RableError::MatchedPair for unclosed delimiters

AST Types

The AST is built from Node structs, each containing a NodeKind variant and a source Span:

use rable::{Node, NodeKind, Span};

Key NodeKind variants:

Category Variants
Basic Word, Command, Pipeline, List
Compound If, While, Until, For, ForArith, Select, Case, Function, Subshell, BraceGroup, Coproc
Redirections Redirect, HereDoc
Expansions ParamExpansion, ParamLength, ParamIndirect, CommandSubstitution, ProcessSubstitution, ArithmeticExpansion, AnsiCQuote, LocaleString
Arithmetic ArithmeticCommand, ArithNumber, ArithVar, ArithBinaryOp, ArithUnaryOp, ArithTernary, ArithAssign, and more
Conditionals ConditionalExpr, UnaryTest, BinaryTest, CondAnd, CondOr, CondNot
Other Negation, Time, Array, Comment, Empty

Every node implements Display, producing S-expression output identical to Parable.

Error Types

use rable::{RableError, Result};

Both error variants provide .line(), .pos(), and .message() accessors:

  • RableError::Parse — syntax error (e.g., unexpected token, missing keyword)
  • RableError::MatchedPair — unclosed delimiter (parenthesis, brace, bracket, or quote)

Architecture

Rable is a hand-written recursive descent parser with a context-sensitive lexer:

Source string
  -> Lexer (context-sensitive tokenizer)
    -> Parser (recursive descent)
      -> AST (Node tree)
        -> S-expression output (via Display)
Module Responsibility
lexer/ Context-sensitive tokenizer with heredoc, quote, and expansion handling
parser/ Recursive descent parser for all bash constructs
ast.rs 50+ AST node types covering the full bash grammar
token.rs Token types and lexer output
error.rs Error types with line/position information
context.rs Parsing context and state management
sexp/ S-expression output with word segment processing
format/ Canonical bash reformatter (used for command substitution content)
python.rs PyO3 bindings (feature-gated under python)

Design principles

  1. Compatibility is correctness — output matches Parable's S-expressions exactly
  2. If it is not tested, it is not shipped — 1,604 integration tests + unit tests
  3. Simplicity is king — solve problems with least complexity
  4. Correctness over speed — match bash-oracle behavior, optimize later

Development

Prerequisites

  • Rust 1.93+ (pinned in rust-toolchain.toml)
  • Python 3.12+ (for Python bindings)
  • just (task runner)

Quick start

git clone https://github.com/mpecan/rable.git
cd rable
just              # format, lint, test

Available commands

Core development:

Command Description
just Format, lint, and test (default)
just fmt Format all Rust code
just clippy Run clippy with strict settings
just test Run all Rust tests
just test-parable Run Parable compatibility suite
just test-file NAME Run a specific test file (e.g., just test-file 12_command_substitution)
just check Same as just — format, lint, test
just ci Run exactly what CI runs

Python bindings:

Command Description
just setup Full setup: venv + bindings + Parable for comparison
just venv Create Python virtual environment with maturin
just develop Build and install Python bindings in dev mode
just test-python Run Parable's test runner against Rable's Python bindings
just benchmark Performance benchmark vs Parable
just wheel Build a release Python wheel

Fuzzing and oracle testing:

Command Description
just fuzz-mutate [N] Differential fuzzer: mutate existing test inputs (default 10,000 iterations)
just fuzz-generate [N] Differential fuzzer: generate random bash fragments (default 5,000)
just fuzz-minimize INPUT Minimize a failing fuzzer input to its smallest form
just fuzz-generate-tests Regenerate oracle test cases from bash-oracle fuzzing
just test-oracle Run the bash-oracle compatibility test suite
just build-oracle Build bash-oracle from source (requires autotools)

Cleanup:

Command Description
just clean Clean build artifacts and venv

Testing

Test corpus

Tests live in tests/parable/ using Parable's .tests format:

=== test name
bash source code
---
(expected s-expression)
---

There are 36 test files covering words, commands, pipelines, lists, redirects, compound statements, loops, functions, expansions, arithmetic, here-documents, process substitution, conditionals, arrays, and more.

Oracle tests

Additional tests in tests/oracle/ are generated from bash-oracle differential fuzzing. These provide extra coverage beyond Parable's test suite and are run separately:

just test-oracle

Differential fuzzing

The fuzzer (tests/fuzz.py) compares Rable's output against Parable on randomly generated or mutated bash inputs, catching edge-case divergences:

just setup               # one-time: set up Python environment
just fuzz-mutate 50000   # mutate existing test inputs
just fuzz-generate 10000 # generate random bash fragments

Contributing

Contributions are welcome! See CONTRIBUTING.md for the full guide. The short version:

  1. All tests pass: just check must succeed
  2. Parable compatibility: just test-parable must show 1604/1604
  3. Code quality: No clippy warnings (just clippy)
  4. Formatting: Code is formatted (just fmt)
  5. Commit style: Conventional Commits (feat, fix, refactor, test, docs, chore)

License

MIT License. See LICENSE for details.

Disclosure

Rable is a complete reimplementation of Parable in Rust. It was built by studying Parable's test suite and output format, not by translating Parable's Python source code. The test corpus (tests/parable/*.tests) originates from the Parable project and is used under its MIT license.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

rable-0.1.15-cp313-cp313-win_amd64.whl (256.8 kB view details)

Uploaded CPython 3.13Windows x86-64

rable-0.1.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (401.4 kB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ x86-64

rable-0.1.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (393.2 kB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ ARM64

rable-0.1.15-cp313-cp313-macosx_11_0_arm64.whl (356.8 kB view details)

Uploaded CPython 3.13macOS 11.0+ ARM64

rable-0.1.15-cp313-cp313-macosx_10_12_x86_64.whl (373.1 kB view details)

Uploaded CPython 3.13macOS 10.12+ x86-64

File details

Details for the file rable-0.1.15-cp313-cp313-win_amd64.whl.

File metadata

  • Download URL: rable-0.1.15-cp313-cp313-win_amd64.whl
  • Upload date:
  • Size: 256.8 kB
  • Tags: CPython 3.13, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for rable-0.1.15-cp313-cp313-win_amd64.whl
Algorithm Hash digest
SHA256 d6a7d65430a7e6dad10e39975336cceb8cb118c91cbfda40e228bce3fa9ba39e
MD5 85bae8c6c359863645f36a2120a6a504
BLAKE2b-256 16f2f3e229f0d36764b3001ec95042b4a02bd34ead0905a176c326a3e8b0a42f

See more details on using hashes here.

Provenance

The following attestation bundles were made for rable-0.1.15-cp313-cp313-win_amd64.whl:

Publisher: publish.yml on mpecan/rable

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rable-0.1.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for rable-0.1.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 6ac8db5c89a9e11b49bee06dee7519ac379cdb7eb3506bea9957c9e47e4c849f
MD5 b75c0c97fe0fb0c1c828e2ca09cee255
BLAKE2b-256 8898821491e1e1e6efa16b047ac15da5dfd09a1c213d83d7b8555404795cd209

See more details on using hashes here.

Provenance

The following attestation bundles were made for rable-0.1.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: publish.yml on mpecan/rable

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rable-0.1.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for rable-0.1.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 b4dda4416cb577f9a6b47a6e7d45a948c847b0880d92960f49be4d79dc16458e
MD5 bb5d3b75b158bd6850713a2caa51f352
BLAKE2b-256 77076e35e18514d2add00201a02b2fb186e60fbb44dc913e70ce5959b3d53598

See more details on using hashes here.

Provenance

The following attestation bundles were made for rable-0.1.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: publish.yml on mpecan/rable

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rable-0.1.15-cp313-cp313-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for rable-0.1.15-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 f5f375d90af7eb3b338e4a103d2b6446b5e2a3960f885471a5a787d444e81d9a
MD5 ff3ecc21f5a7f5e1b5ab865a658b8c28
BLAKE2b-256 e2305508f33ec33e98ecfd69be48e2d4b858224d10fee495f5cd6a092f19781d

See more details on using hashes here.

Provenance

The following attestation bundles were made for rable-0.1.15-cp313-cp313-macosx_11_0_arm64.whl:

Publisher: publish.yml on mpecan/rable

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rable-0.1.15-cp313-cp313-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for rable-0.1.15-cp313-cp313-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 a382665c15d04556187a63ed470212adce34bc6136ff6395bb45c583f2670207
MD5 797ddfc80ff24e272b41021acc01c4dd
BLAKE2b-256 ad73d11b76dff783f764f2dd7fdf44f8f88124d27c75f287682540a14d7c5cdf

See more details on using hashes here.

Provenance

The following attestation bundles were made for rable-0.1.15-cp313-cp313-macosx_10_12_x86_64.whl:

Publisher: publish.yml on mpecan/rable

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page