Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Shell (Bash/Zsh) Cookbook

Practical, copy-paste-ready recipes for Bash and Zsh script generation. Covers sigil_quote! with shell-aware control flow, $V verbatim strings for preserving shell interpolation, and the builder API.

Basic function with sigil_quote!

extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let body = sigil_quote!(Bash {
    local name=$$1
    echo $V("\"Hello, ${name}!\"")
}).unwrap();

let fun = FunSpec::builder("greet")
    .body(body)
    .build()
    .unwrap();

let output = FileSpec::builder("greet.bash")
    .add_function(fun)
    .build()
    .unwrap()
    .render(80)
    .unwrap();
}
function greet() {
    local name=$1
    echo "Hello, ${name}!"
}

Control flow (if/then/fi, for/do/done)

Use { } blocks in sigil_quote! — the backend maps them to the correct shell delimiters.

extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let body = sigil_quote!(Bash {
    if [ -z $$1 ]; {
        echo $S("Error: no argument")
        return 1
    }

    for file in $$@; {
        echo $V("\"Processing: ${file}\"")
    }
}).unwrap();

let fun = FunSpec::builder("process_files")
    .body(body)
    .build()
    .unwrap();

let output = FileSpec::builder("process.bash")
    .add_function(fun)
    .build()
    .unwrap()
    .render(80)
    .unwrap();
}
function process_files() {
    if [ -z $1 ]; then
        echo "Error: no argument"
        return 1
    fi

    for file in $@; do
        echo "Processing: ${file}"
    done
}

$V vs $S — when to use which

$S escapes everything and wraps in quotes (safe for static strings). $V is pure passthrough — no quoting, no escaping. Use $V when you want shell to expand variables, command substitutions, or arithmetic at runtime. Include your own quotes in the $V content when shell quoting is needed.

extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let block = sigil_quote!(Bash {
    echo $S("$HOME")
    echo $V("$HOME")
    echo $V("\"$HOME\"")
}).unwrap();

let output = FileSpec::builder("test.bash")
    .add_code(block)
    .build()
    .unwrap()
    .render(80)
    .unwrap();
// Line 1: echo "\$HOME"       ← $S escapes the dollar sign, wraps in quotes
// Line 2: echo $HOME           ← $V passthrough, no quotes (word-splitting possible)
// Line 3: echo "$HOME"         ← $V passthrough with user-provided quotes (safe)
}

Complex shell interpolation with $V

$V handles all shell expansion patterns — braced defaults, command substitution, arithmetic, arrays, special variables. Since $V is passthrough, include quotes in the content when the generated shell code should have them:

extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let body = sigil_quote!(Bash {
    local config_dir=$V("\"${XDG_CONFIG_HOME:-$HOME/.config}\"")
    local version=$V("\"$(git describe --tags 2>/dev/null || echo dev)\"")
    local port=$V("\"$((BASE_PORT + WORKER_ID))\"")

    echo $V("\"Deploying ${APP_NAME} v${version}\"")
    echo $V("\"Config: ${config_dir}/${APP_NAME}.conf\"")
    echo $V("\"Status: exit=$? pid=$$\"")
    echo $V("\"Args: count=$# all=$@\"")
    echo $V("\"Array: ${services[@]}\"")
}).unwrap();

let fun = FunSpec::builder("setup")
    .body(body)
    .build()
    .unwrap();

let output = FileSpec::builder("setup.bash")
    .add_function(fun)
    .build()
    .unwrap()
    .render(80)
    .unwrap();
}
function setup() {
    local config_dir="${XDG_CONFIG_HOME:-$HOME/.config}"
    local version="$(git describe --tags 2>/dev/null || echo dev)"
    local port="$((BASE_PORT + WORKER_ID))"

    echo "Deploying ${APP_NAME} v${version}"
    echo "Config: ${config_dir}/${APP_NAME}.conf"
    echo "Status: exit=$? pid=$$"
    echo "Args: count=$# all=$@"
    echo "Array: ${services[@]}"
}

@{expr} interpolation in $V

When you need to mix Rust compile-time values with shell runtime variables, use @{expr} inside $V strings. The @{...} parts are evaluated at compile time; everything else passes through verbatim for shell to interpret at runtime:

extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let registry = "ghcr.io/myorg";
let app = "api-server";
let services = ["web", "worker", "scheduler"];

let block = sigil_quote!(Bash {
    docker push $V("@{registry}/@{app}:${TAG}")
    echo $V("Deploying @{services.len()} services to ${ENVIRONMENT}")
    echo $V("Contact: admin@@@{app}.internal")
}).unwrap();
}
docker push ghcr.io/myorg/api-server:${TAG}
echo Deploying 3 services to ${ENVIRONMENT}
echo Contact: admin@api-server.internal

Use @@ to emit a literal @ in the output. Bare @ not followed by { passes through unchanged.

Shebang and header

Use FileSpec::header() for the shebang and preamble:

extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let header = CodeBlock::of("#!/usr/bin/env bash\nset -euo pipefail", ()).unwrap();

let body = sigil_quote!(Bash {
    echo $S("Starting...")
}).unwrap();

let main_fn = FunSpec::builder("main")
    .body(body)
    .build()
    .unwrap();

let output = FileSpec::builder_with("script.bash", Bash::new())
    .header(header)
    .add_function(main_fn)
    .build()
    .unwrap()
    .render(80)
    .unwrap();
}
#!/usr/bin/env bash
set -euo pipefail

function main() {
    echo "Starting..."
}

Imports (source)

extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let utils = TypeName::importable("./lib/utils.sh", "");
let config = TypeName::importable("./lib/config.sh", "");

let body = CodeBlock::of("# uses %T and %T", (utils, config)).unwrap();

let output = FileSpec::builder_with("app.bash", Bash::new())
    .add_code(body)
    .build()
    .unwrap()
    .render(80)
    .unwrap();
// Generates:
//   source "./lib/config.sh"
//   source "./lib/utils.sh"
}

Zsh-specific features

Zsh works identically to Bash for control flow. Use $V for Zsh-specific parameter expansion:

extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::zsh::Zsh;
fn main() {
let body = sigil_quote!(Zsh {
    local lower=$V("\"${(L)USERNAME}\"")
    local joined=$V("\"${(j:,:)array}\"")
    local sliced=$V("\"${array[2,-1]}\"")
    local replaced=$V("\"${input//old/new}\"")
}).unwrap();

let fun = FunSpec::builder("zsh_features")
    .body(body)
    .build()
    .unwrap();

let output = FileSpec::builder_with("demo.zsh", Zsh::new())
    .add_function(fun)
    .build()
    .unwrap()
    .render(80)
    .unwrap();
}
function zsh_features() {
    local lower="${(L)USERNAME}"
    local joined="${(j:,:)array}"
    local sliced="${array[2,-1]}"
    local replaced="${input//old/new}"
}

Double-bracket tests with [[ ]]

extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let body = sigil_quote!(Bash {
    if [[ $$1 == $$2 ]]; {
        echo $S("match")
    }
}).unwrap();

let fun = FunSpec::builder("check_equal")
    .body(body)
    .build()
    .unwrap();

let output = FileSpec::builder("check.bash")
    .add_function(fun)
    .build()
    .unwrap()
    .render(80)
    .unwrap();
}
function check_equal() {
    if [[ $1 == $2 ]]; then
        echo "match"
    fi
}

Combining $V with runtime Rust values

Mix $V (shell-expanded at runtime) with $L/$S (Rust values baked in at generation time):

extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let app_name = "myapp";
let log_dir = "/var/log";

// Build the whole string in Rust and pass via $V (most ergonomic):
let log_pattern = format!("\"${{LOG_DIR:-{log_dir}}}/{app_name}.log\"");
let body = sigil_quote!(Bash {
    local log_file=$V(log_pattern)
    echo $V("\"Writing to ${log_file}\"")
}).unwrap();
}

File extension

Use .with_extension("sh") for POSIX-compatible scripts:

extern crate sigil_stitch;
use sigil_stitch::prelude::*;
use sigil_stitch::lang::bash::Bash;
fn main() {
let bash = Bash::new().with_extension("sh");
let output = FileSpec::builder_with("script.sh", bash)
    .build()
    .unwrap()
    .render(80)
    .unwrap();
}