No tears. Only toolchains.
The bash script compiler no one asked for.
Compile Bash scripts to native binaries. The compiled output runs 5-12x faster on computation-heavy workloads, integrates seamlessly with the rest of your shell environment, and can export compiled functions and variables back into plain Bash sessions.
In other words, if you want to use the wrong tool (bash) for the wrong job (a computationally heavy one), shellraiser lets you do the wrong thing fast.
Primary legitimate usage case: speed up some of your bash completions.
To put it pedantically, shellraiser is actually a bash-to-c transpiler. It takes your bash script and turns it into C code that, along with the included shellraiser C runtime, can be compiled by your installed C compiler into a native machine code binary without any runtime dependencies.
However, shellraiser largely automates the compilation process (assuming you have a natively supported C compiler available, such as gcc or clang), so from a usage standpoint, you just give it bash scripts and shellraiser
gives you compiled executable binaries.
pip install shellraiserOr directly from the repo:
git clone https://github.com/metacollin/shellraiser.git
cd shellraiser
pip install .shellraiser script.sh # -> ./script (native binary)
shellraiser script.sh -o mybin # custom output name
shellraiser script.sh --sourceable # polyglot: source OR execute (see below)
shellraiser script.sh --emit-only # just print the generated CRequires Python 3.10+, GCC or Clang, Linux or macOS. The output binary is self-contained with no runtime dependencies.
When a compiled script calls export -f, the function becomes a real command on $PATH. Any shell, script, or tool can call it by name and it runs as native compiled code, not interpreted Bash.
# utils.sh -- compile with shellraiser
to_upper() { echo "$1" | tr 'a-z' 'A-Z'; }
export -f to_upper# Anywhere else -- plain Bash, xargs, find, parallel:
to_upper "hello" # -> HELLO
echo -e "foo\nbar" | xargs -I {} bash -c 'to_upper "{}"'
find . -name "*.txt" -exec to_upper {} \;Under the hood, export -f creates a shell shim on $PATH that routes back into the compiled binary via --call. The calling code doesn't know the function is compiled. Cleanup is automatic on exit.
This also works in reverse: if a parent Bash session exports a function with export -f, the compiled binary can call it. The runtime detects BASH_FUNC_name%% environment variables and dispatches through Bash.
With --sourceable, the output is a single file that can be both executed and sourced:
shellraiser mylib.sh -o mylib --sourceable./mylib # run as a compiled binary
source ./mylib # load its functions + variables into your shellAfter sourcing, all functions and variables defined in the compiled script are available in your current session. Functions call back into the cached binary, running as native code.
source ./mylib
echo "$VERSION" # variable from the compiled script
process_data "input" # compiled function, runs as native code
type process_data # -> "process_data is a function"The binary is embedded as a base64 payload, extracted and cached at ~/.cache/shellraiser/ on first run. Subsequent runs skip extraction.
Multi-command pipelines can be backgrounded with proper $! tracking. The common pattern of parallel work with a progress-monitoring loop works correctly:
printf "%s\n" "${items[@]}" | xargs -P 4 -I {} bash -c 'do_work "{}"' &
pid=$!
while ps -p $pid > /dev/null 2>&1; do
count=$(find "$tmpdir" -name "*.done" | wc -l)
printf "\rProgress: %d/%d" "$count" "$total"
sleep 0.1
doneThe runtime handles backgrounded pipelines by forking a supervisor process, correctly sets $!, reaps zombie processes between commands (so ps -p detects completion), and guards EXIT traps against firing in child processes.
Full declare -A support backed by a real hash table:
declare -A config
config["host"]="example.com"
config["port"]="8080"
for key in "${!config[@]}"; do
echo "$key = ${config[$key]}"
done
echo "entries: ${#config[@]}"- Indexed arrays with append (
+=), expansion (${arr[@]}), length (${#arr[@]}), and command-substitution init (arr=($(cmd))) - Functions with
localscoping,return, positional params,shift, recursion - All control flow:
if/elif/else,while,until,for-in, C-stylefor((;;)),case/esac,break,continue,&&/||chains, subshells - Pipes and redirects:
|,>,>>,<,2>,2>&1,>&2 - 28 builtins including
echo -e,printf(with format repetition,-v, width specs),[[ ]](with=~,&&,||),declare,trap,kill,command -v - Special variables:
$?,$$,$!,$#,$@,$*,$0-$9 - Extended test:
[[ ]]with regex matching (=~), pattern globbing (==), logical operators - ANSI-C quoting:
$'\n',$'\t',$'\xNN' - String append:
var+="text"(in-place, O(1) amortized) - Glob expansion:
*,?,[...]in command arguments - Trap handling:
trap 'cmd' EXITand signal traps (INT, TERM, HUP, etc.)
bash bench.sh 100000
Typical results at N=100000 (macOS, x86-64):
Test Bash Compiled Speedup
---- ---- -------- -------
Arithmetic (100000 iterations) 1176ms 130ms 9.0x
String append (100000 iterations) 2138ms 1327ms 1.6x
Function calls (100000 calls) 1638ms 161ms 10.1x
Array append (100000 ops) 1107ms 97ms 11.4x
Assoc array set (20000 ops) 294ms 56ms 5.2x
Assoc array get (20000 ops) 346ms 60ms 5.7x
Conditionals (100000 branches) 1670ms 159ms 10.5x
Nested loops (100x1000) 1240ms 164ms 7.5x
echo to /dev/null (100000 calls) 7356ms 5415ms 1.3x
printf to /dev/null (100000 calls) 7330ms 5090ms 1.4x
[[ ]] extended test (100000 evals) 1206ms 122ms 9.8x
C-style for loop (100000 iters) 782ms 158ms 4.9x
case/esac (100000 matches) 1259ms 130ms 9.6x
Subshell fork (1000 forks) 1960ms 1652ms 1.1x
| Variables & Expansion | Status | Notes |
|---|---|---|
var=value, $var, ${var} |
✅ | |
| Single, double, backslash quoting | ✅ | |
$'...' ANSI-C quoting (\n, \t, \xNN, \0NNN) |
✅ | |
$(( arithmetic )) — all C operators |
✅ | |
$(command) substitution |
✅ | |
`backtick` substitution |
✅ | |
var+="append" |
✅ | |
${#var} string length |
✅ | |
${var:-default} default value |
❌ | |
${var:=word}, ${var:+word}, ${var:?msg} |
❌ | |
${var:offset:length} substring |
❌ | |
${var//pat/rep} substitution |
❌ | |
${var#pat}, ${var##pat} prefix strip |
❌ | |
${var%pat}, ${var%%pat} suffix strip |
❌ | |
${var^^}, ${var,,} case conversion |
❌ | |
| Control Flow | ||
if / elif / else / fi |
✅ | |
while, until |
✅ | |
for var in items (with word splitting) |
✅ | |
for (( init; cond; step )) |
✅ | |
case / esac (with *, ?, | patterns) |
✅ | |
break, continue |
✅ | |
&&, || chains |
✅ | |
Subshells ( ... ) |
✅ | |
Brace groups { ...; } |
✅ | |
select |
❌ | |
coproc |
❌ | |
| Functions | ||
name() { body; } and function name { body; } |
✅ | |
local, return, shift |
✅ | |
$1–$9, $@, $*, $# |
✅ | |
export -f (compiled function export via PATH) |
✅ | |
Calling Bash-exported functions (BASH_FUNC_%%) |
✅ | |
--sourceable polyglot (source or execute) |
✅ | |
| Recursive functions | ✅ | |
| Arrays | ||
arr=(a b c), arr[i]=val |
✅ | |
${arr[i]}, ${arr[@]}, ${arr[*]} |
✅ | |
${#arr[@]} element count |
✅ | |
arr+=(val) append |
✅ | |
arr=($(cmd)) from command substitution |
✅ | |
declare -A map (associative) |
✅ | |
map["key"]="val", ${map[$key]} |
✅ | |
${!map[@]} key iteration |
✅ | |
${#map[@]} entry count |
✅ | |
declare -A map=([k]=v ...) inline init |
❌ | |
| Pipes, Redirection & Jobs | ||
cmd1 | cmd2 | cmd3 |
✅ | |
>, >>, < |
✅ | |
2>, 2>>, 2>&1, >&2 |
✅ | |
cmd & (background) |
✅ | |
cmd1 | cmd2 & (background pipeline) |
✅ | |
$! (last background PID) |
✅ | |
$$, $? |
✅ | |
| Here-strings | ✅ | Only <<< and only inside $() |
Here-documents <<EOF |
❌ | |
Process substitution <(cmd), >(cmd) |
❌ | |
Arbitrary fd numbers 3>, exec 3> |
❌ | |
| Builtins | ||
echo |
✅ | -n, -e, -E, full escape sequences |
printf |
✅ | Format repetition, -v, %b, %s, %d, %f, %x, width/precision |
cd |
✅ | |
exit |
✅ | |
read |
✅ | -r, -p |
export |
✅ | -f for function export |
test / [ |
✅ | File, string, arithmetic tests |
[[ |
✅ | =~ regex, == glob, &&, ||, !, ( ) |
true / false |
✅ | |
return |
✅ | |
local |
✅ | -a for arrays |
shift |
✅ | |
wait |
✅ | Optional PID |
unset |
✅ | |
declare / typeset |
✅ | -a, -A, -i, -x, -g, -r |
trap |
✅ | EXIT + signals; PID-guarded against child forks |
kill |
✅ | Signal names and numbers |
command |
✅ | -v for path lookup |
: (noop) |
✅ | |
source / . |
❌ | Use --sourceable instead |
eval |
❌ | |
set / shopt |
❌ | |
getopts |
❌ | |
mapfile / readarray |
❌ | |
pushd / popd |
❌ | |
| Special Variables | ||
$0, $1–$9, ${10}+ |
✅ | |
$@, $*, $# |
✅ | |
$? (exit code), $$ (PID), $! (bg PID) |
✅ | |
$IFS |
✅ | |
$RANDOM, $LINENO, $FUNCNAME, $BASH_SOURCE |
❌ | |
$PIPESTATUS, $BASH_REMATCH |
❌ |