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.10-cp313-cp313-win_amd64.whl (242.8 kB view details)

Uploaded CPython 3.13Windows x86-64

rable-0.1.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (386.5 kB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ x86-64

rable-0.1.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (380.2 kB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ ARM64

rable-0.1.10-cp313-cp313-macosx_11_0_arm64.whl (340.4 kB view details)

Uploaded CPython 3.13macOS 11.0+ ARM64

rable-0.1.10-cp313-cp313-macosx_10_12_x86_64.whl (355.0 kB view details)

Uploaded CPython 3.13macOS 10.12+ x86-64

File details

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

File metadata

  • Download URL: rable-0.1.10-cp313-cp313-win_amd64.whl
  • Upload date:
  • Size: 242.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.10-cp313-cp313-win_amd64.whl
Algorithm Hash digest
SHA256 9ac8c182a7dbf999b4f6d9a3c6441a5d89855844aa52982f3afd362afb19fa04
MD5 48a09ee051777e2875b3bda220be8cd3
BLAKE2b-256 7f82cd58e029587483c1c3463ec50ab1b5c92be68b74deb16e59b4daed3e229d

See more details on using hashes here.

Provenance

The following attestation bundles were made for rable-0.1.10-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.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for rable-0.1.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 8a0bfeb8a1e8b64eb7ee3fd1442032834daf61a5a3f9ac2fb2b9c2d16d44b566
MD5 0c7297c1dcac38471da9ff6e3c7be09e
BLAKE2b-256 c9d9b820502fd14473b6ecd0d6a74cb3d00380a4f4beb48ab696f05764e874d4

See more details on using hashes here.

Provenance

The following attestation bundles were made for rable-0.1.10-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.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for rable-0.1.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 c6d464f14fd05c69272a759e8b7198b7b95954c82e48d3f1e0984b41a5f6d018
MD5 47ce6a5395fcd908a15ea342af8df05b
BLAKE2b-256 1093aa1d92278cac75b6e55ee91372be27e977436100d9c7305b6c8130f9d6e2

See more details on using hashes here.

Provenance

The following attestation bundles were made for rable-0.1.10-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.10-cp313-cp313-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for rable-0.1.10-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 0d9f4273cd4b52228da204bd92e67234fc4d9093fe2d32ced8e1ee649db7072c
MD5 ae3dd35999c083bdb6cb86397854b5c6
BLAKE2b-256 27392afc7160259a014f9d7a111e36adbd194c6e435d8df66584894374993dbb

See more details on using hashes here.

Provenance

The following attestation bundles were made for rable-0.1.10-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.10-cp313-cp313-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for rable-0.1.10-cp313-cp313-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 0170e866eb53c9e94c478af4de0d4ff1dd02328618567b022276e5e2e935ed96
MD5 b785516606ad349fe5670761b3dcedc7
BLAKE2b-256 a6ae2ff38bd890fdca083d96cb567e622313a08b91a90336d23dd5594b5dedcc

See more details on using hashes here.

Provenance

The following attestation bundles were made for rable-0.1.10-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