Parse bash scripts to JSON AST using GNU Bash's actual parser, and convert AST back to bash.
bash-ast is a Rust tool that uses FFI bindings to GNU Bash's parser to convert bash scripts into JSON AST output, and can also convert AST back to executable bash code. This provides 100% compatibility with bash syntax since it uses bash's own parser.
- 100% bash compatibility: Uses the actual GNU Bash parser via FFI
- JSON output: Serializes the AST to JSON for easy consumption
- Round-trip support: Convert AST back to bash with
--to-bash - All bash constructs: Supports all 16 bash command types including:
- Simple commands (
cmd arg1 arg2) - Pipelines (
cmd1 | cmd2) - Lists (
cmd1 && cmd2,cmd1 || cmd2,cmd1 ; cmd2,cmd1 &) - For loops (
for var in list; do ...; done) - While/Until loops
- If statements
- Case statements
- Select statements
- Group commands (
{ ...; }) - Subshells (
( ... )) - Function definitions
- Arithmetic evaluation (
(( expr ))) - C-style for loops (
for ((i=0; i<n; i++))) - Conditional expressions (
[[ expr ]]) - Coprocesses
- Simple commands (
# Install from tap
brew tap cv/taps
brew install bash-ast
# Or install HEAD version directly
brew install --HEAD https://raw.githubusercontent.com/cv/bash-ast/main/Formula/bash-ast.rbDownload the .deb package from the releases page:
# Download (replace VERSION with actual version)
curl -LO https://github.com/cv/bash-ast/releases/download/vVERSION/bash-ast_VERSION-1_amd64.deb
# Install
sudo dpkg -i bash-ast_VERSION-1_amd64.debDownload the .rpm package from the releases page:
# Download (replace VERSION with actual version)
curl -LO https://github.com/cv/bash-ast/releases/download/vVERSION/bash-ast-VERSION-1.x86_64.rpm
# Install
sudo rpm -i bash-ast-VERSION-1.x86_64.rpm
# Or with dnf (Fedora)
sudo dnf install ./bash-ast-VERSION-1.x86_64.rpm- Rust 1.70 or later
- LLVM/Clang (for bindgen)
- A C compiler (gcc or clang)
- ncurses development libraries
Installing Rust:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/envOn macOS:
# Install Xcode command line tools (includes clang)
xcode-select --install
# Install LLVM for bindgen
brew install llvm
# Set LLVM paths for bindgen
export LLVM_CONFIG_PATH="$(brew --prefix llvm)/bin/llvm-config"On Ubuntu/Debian:
sudo apt-get install llvm-dev libclang-dev clang libncurses-dev build-essential# Clone the repository
git clone <repository-url>
cd bash-ast
# Initialize the bash submodule
git submodule update --init
# Build the project
cargo build --release# Parse a script file to JSON AST
./target/release/bash-ast < script.sh
# Parse inline
echo 'for i in a b c; do echo $i; done' | ./target/release/bash-ast
# Pretty print with jq
./target/release/bash-ast < script.sh | jq .
# Convert JSON AST back to bash
./target/release/bash-ast script.sh | ./target/release/bash-ast --to-bash
# Round-trip: parse and regenerate
echo 'for i in a b c; do echo $i; done' | ./target/release/bash-ast | ./target/release/bash-ast -buse bash_ast::{init, parse, to_bash, Command};
fn main() {
// Initialize bash (call once at startup)
init();
// Parse a script
let cmd = parse("echo hello world").unwrap();
// Work with the AST
if let Command::Simple { words, .. } = &cmd {
for word in words {
println!("Word: {}", word.word);
}
}
// Convert AST back to bash
let script = to_bash(&cmd);
println!("Regenerated: {}", script);
// Or get JSON directly
let json = bash_ast::parse_to_json("echo hello", true).unwrap();
println!("{}", json);
}Input:
for i in a b c; do
echo $i
done | grep aOutput:
{
"type": "pipeline",
"commands": [
{
"type": "for",
"line": 1,
"variable": "i",
"words": ["a", "b", "c"],
"body": {
"type": "simple",
"line": 2,
"words": [
{ "word": "echo" },
{ "word": "$i", "flags": 1 }
],
"redirects": []
}
},
{
"type": "simple",
"line": 3,
"words": [
{ "word": "grep" },
{ "word": "a" }
],
"redirects": []
}
]
}Note: The flags field on words indicates special expansion handling (e.g., flags: 1 means the word contains a variable expansion).
Syntax errors are handled gracefully and return ParseError::SyntaxError:
# Invalid syntax returns an error (no crash)
$ echo 'if then fi' | bash-ast
Error: Syntax error in scriptFor detailed error information (line numbers, tokens), use bash -n for pre-validation:
$ bash -n script.sh 2>&1
script.sh: line 1: syntax error near unexpected token `then'bash-ast is not thread-safe. The underlying bash parser uses global state, so all parsing must be done from a single thread.
Tests are automatically configured to run single-threaded via .cargo/config.toml.
┌─────────────────────────────────────────────────────────────┐
│ bash-ast (Rust, GPL v3) │
│ │
│ stdin (script) ──► FFI to bash ──► AST ──► JSON stdout │
│ │
│ stdin (JSON) ──► serde parse ──► AST ──► bash stdout │
│ │
│ - bindgen-generated FFI bindings to bash │
│ - Calls parse_string_to_command() via FFI │
│ - Walks C AST, converts to Rust types │
│ - Serializes to JSON with serde │
│ - Regenerates bash from AST with to_bash() │
└─────────────────────────────────────────────────────────────┘
This project is licensed under the GNU General Public License v3.0 (GPL-3.0) due to its linkage with GNU Bash.
See the LICENSE file for details.
# Run all tests
cargo test
# Run property-based tests only
cargo test prop_
# Run with Makefile
make test # Run all tests
make lint # Run clippy and fmt check
make ci # Full CI pipeline (lint + test)Coverage requires rustup-installed Rust:
rustup component add llvm-tools-preview
cargo llvm-cov --html --output-dir coverageFuzz testing requires nightly Rust:
rustup install nightly
cargo +nightly fuzz run fuzz_parse -- -max_total_time=60See fuzz/README.md for details.
Run benchmarks with criterion:
cargo bench
# Or via Makefile
make benchResults are saved to target/criterion/report/index.html with detailed HTML reports.
Contributions are welcome! Please ensure any contributions are compatible with the GPL-3.0 license.
- GNU Bash project for the parser
- The Rust community for bindgen and serde