examples

A guided tour of typical shell tasks written in ral. Each is a complete, runnable script — `ral <file>.ral` to execute. The comment block at the top explains the bash equivalent and what ral does differently.

hello.ral

Greet a list of people. ral hello.ral alice bob "carol smith" In bash, "carol smith" would split into two words unless you remembered to quote everything in the loop body too. Here it cannot — `$args` is a list of String values, not a re-lexed token soup.
let names = if !{is-empty $args} { return [world] } else { return $args }

for $names { |n| echo "hello, $n" }

wc.ral

Count lines, words, and bytes for each file. ral wc.ral file1 file2 ... `length` is polymorphic — Strings, Lists, Maps, Bytes share one name — so the three counts read like three projections of the same datum, which is what they are. Bytes come from `file-info` because `length` of a String is in Unicode scalar values, not octets, and the distinction is worth keeping honest.
if !{is-empty $args} {
    echo 'usage: wc.ral FILE...' 1>&2
    exit 2
}

let count = { |path|
    let body = !{from-string < $path}
    return [
        path:  $path,
        lines: !{length !{lines $body}},
        words: !{length !{words $body}},
        bytes: !{file-info $path}[size],
    ]
}

for !{map $count $args} { |s|
    echo "$s[lines]\t$s[words]\t$s[bytes]\t$s[path]"
}

dedup.ral

Print stdin lines unique, preserving first-seen order. cat access.log | ral dedup.ral `sort -u` reorders. `awk '!seen[$0]++'` works but is a magic incantation — print-on-falsy-post-increment, only legible if you have the K&R awk paper open. Here the shape is the obvious one: a fold over stdin lines whose accumulator is the set of seen lines.
fold-lines { |seen line|
    if !{has $seen $line} { return $seen } else {
        echo $line
        return [...$seen, $line: true]
    }
} [:]

rename-extension.ral

Rename every *.FROM in a directory to *.TO. Lower-casing a stack of JPEGs: ral rename-extension.ral ~/photos JPEG jpg The bash idiom for f in *.JPEG; do mv "$f" "${f%.JPEG}.jpg"; done silently misbehaves when the glob has zero matches (the literal pattern becomes the filename), and when filenames contain spaces or newlines. Neither failure mode exists here: `glob` returns `[]` when nothing matches, and filenames are values that are never re-split.
let [dir, from, to] = $args

let targets = glob "$dir/*.$from"

if !{is-empty $targets} {
    echo "no *.$from files in $dir"
    exit 0
}

for $targets { |old|
    let new = "!{path-join [!{dir $old}, !{stem $old}]}.$to"
    echo "$old  ->  $new"
    mv $old $new
}

find-large.ral

List the 10 largest files under a directory, biggest first. ral find-large.ral . The bash one-liner find . -type f -printf '%s\t%p\n' | sort -rn | head is one tab character in a filename away from disaster. Here every file-info record is a value of its own; sorting and slicing are operations over typed records, not field-cut strings.
let [root] = $args

let stats = map { |p|
    return [path: $p, size: !{file-info $p}[size]]
} !{filter { |p| is-file $p } !{glob "$root/**/*"}}

let biggest = take 10 !{reverse !{sort-list-by { |s| return $s[size] } $stats}}

for $biggest { |s|
    echo "$s[size]\t$s[path]"
}

du-by-ext.ral

Sum file sizes under a directory, grouped by extension; biggest total first. ral du-by-ext.ral ~/Pictures The bash idiom is find . -type f -printf '%s\t%f\n' \ | awk -F'\t' '{n=split($2,a,".");s[a[n]]+=$1} END {for(k in s) print s[k],k}' \ | sort -rn which works until a filename contains a tab. Here the bucket map is keyed by the actual extension value; the aggregation is a fold; nothing is stringly typed.
let [root] = $args

let files = filter { |p| is-file $p } !{glob "$root/**/*"}

let totals = fold { |acc p|
    let key   = ext $p
    let bytes = !{file-info $p}[size]
    let prev  = !{get $acc $key 0}
    return [...$acc, $key: $[$prev + $bytes]]
} [:] $files

let rows = map { |k| return [ext: $k, size: $totals[$k]] } !{keys $totals}
let sorted = reverse !{sort-list-by { |r| return $r[size] } $rows}

for $sorted { |r|
    let label = if !{equal $r[ext] ''} { return '(none)' } else { return ".$r[ext]" }
    echo "$r[size]\t$label"
}

kv-to-json.ral

Parse a `key=value` config file, emit JSON to stdout. ral kv-to-json.ral app.env Bash habit: `source app.env`. That exposes every key as a global — and a line like `RM='rm -rf /'` is happily executed by the shell on the way in. Configuration is data; reading it should not be running it. Here it stays data.
let [path] = $args

let kv-of = { |line|
    let parts = re-split '=' $line
    let key   = $parts[0]
    let val   = intercalate '=' !{drop 1 $parts}
    return [$key: $val]
}

let result = fold { |acc line|
    let l = re-replace-all '^\s+|\s+$' '' $line
    if $[!{equal $l ''} || !{re-match '^#' $l}] { return $acc } else {
        return !{union $acc !{kv-of $l}}
    }
} [:] !{from-lines-list $path}

echo !{to-json $result | from-string}

csv-summary.ral

Group a CSV by one column, average a numeric column. ral csv-summary.ral sales.csv region revenue The shape is the standard awk associative-array trick — except the records survive the loop as records (so column names are real identifiers), and the average is one more fold instead of a printf trick.
let [path, group_col, value_col] = $args

let [head, ...rows] = !{from-lines-list $path}
let header = re-split ',' $head

let row-of = { |line|
    let cells = re-split ',' $line
    fold { |m pair|
        let k = $pair[0]
        let v = $pair[1]
        return [...$m, $k: $v]
    } [:] !{zip $header $cells}
}

let buckets = fold { |acc r|
    let k = $r[$group_col]
    let v = float $r[$value_col]
    let prev = !{get $acc $k [count: 0, total: 0.0]}
    return [...$acc, $k: [count: $[$prev[count] + 1], total: $[$prev[total] + $v]]]
} [:] !{map $row-of $rows}

echo "$group_col\tavg($value_col)\tn"
for !{keys $buckets} { |k|
    let b = $buckets[$k]
    let n = float $b[count]
    let avg = $[$b[total] / $n]
    echo "$k\t$avg\t$b[count]"
}

atomic-update.ral

Read a JSON state file, bump a counter and append a timestamp, write it back. ral atomic-update.ral state.json Bash: jq '.count += 1' state.json > state.json.tmp \ && mv state.json.tmp state.json The tmp-then-rename is what gives you atomicity, and you have to remember it. In ral, `>` *is* rename-on-close: readers see either the old contents or the new, never a partial write. Crash, ^C, power loss — the file is still valid.
let [path] = $args

let state = if !{is-file $path} {
    return !{from-json < $path}
} else {
    return [count: 0, history: []]
}

let stamp = !{date -u +%Y-%m-%dT%H:%M:%SZ | from-string}

let next = [
    ...$state,
    count:   $[$state[count] + 1],
    history: [...$state[history], $stamp],
]

to-json $next > $path

echo "tick $next[count] at $stamp"

hash-tree.ral

Hash every file under a directory in parallel. ral hash-tree.ral . # 8 workers ral hash-tree.ral . 16 # 16 workers Bash: find . -type f -print0 | xargs -0 -P 8 sha256sum The `-print0` / `-0` dance exists only because filenames are shell tokens that get re-split. Here a path is a value; `par` distributes values; the same paths come back out the other side paired with their hashes, in input order.
let [root, ...rest] = $args
let jobs = if !{is-empty $rest} { return 8 } else { return !{int $rest[0]} }

let files = filter { |p| is-file $p } !{glob "$root/**/*"}

let results = par { |p|
    let line  = !{sha256sum $p | from-string}
    let parts = words $line
    return [hash: $parts[0], path: $p]
} $files $jobs

for !{sort-list-by { |r| return $r[path] } $results} { |r|
    echo "$r[hash]  $r[path]"
}

fanout-fetch.ral

Fetch a list of URLs in parallel; write a JSON manifest of outcomes. ral fanout-fetch.ral urls.txt out/ Bash: cat urls.txt | xargs -P 8 -I{} curl -fsSL -o "out/$(basename {})" {} which silently loses per-URL failures and depends on filenames being unique within `basename`. `par` returns a structured result per input — failures land in the manifest alongside successes, so the "what didn't fetch?" answer is one filter call.
let [list_file, out_dir] = $args

let urls = from-lines-list $list_file

mkdir -p $out_dir

let fetch = { |url|
    let dst = "$out_dir/!{re-replace-all '[^a-zA-Z0-9.]+' '_' $url}"
    try {
        curl -fsSL --max-time 15 $url > $dst
        return [url: $url, path: $dst, ok: true, status: 0]
    } { |err|
        return [url: $url, path: '', ok: false, status: $err[status]]
    }
}

let results = par $fetch $urls 8

to-json $results > "$out_dir/manifest.json"

let ok  = filter { |r| return $r[ok] }       $results
let bad = filter { |r| return $[not $r[ok]] } $results
echo "fetched: !{length $ok} ok, !{length $bad} failed"

for $bad { |r|
    echo "!{styled $ansi-red '  fail'} status=$r[status]  $r[url]" 1>&2
}

tail-multi.ral

Multiplex `tail -f` across several log files, with each line prefixed by its source. ^C ends. ral tail-multi.ral access.log error.log audit.log `tail -f a b c` already prints `==> file <==` blocks, but its output garbles when two streams write at once — the prefix lands separated from its lines. Here `watch LABEL { body }` spawns the command and line-frames its stdout: every line emerges atomic, prefixed.
if !{is-empty $args} {
    echo 'usage: tail-multi.ral FILE...' 1>&2
    exit 2
}

let handles = map { |f|
    return !{watch !{base $f} { tail -f $f }}
} $args

for $handles { |h| await $h }

safe-eval.ral

Capability-sandboxed evaluation. ral safe-eval.ral `grant` attenuates authority — it can only narrow, never amplify. Inside the block, the program sees a synthetic universe where one scratch directory is writable, nothing else is, and exec is dead. Bash's analogue is `chroot` + `setuid` + `seccomp`, each at a different layer of the system, all process-scoped, none in-language. Here it is one expression around the offending code.
let sandbox = temp-dir

let try-op = { |label body|
    let outcome = try { !$body ; return 'allowed' } { |_| return 'blocked' }
    echo "  $outcome  $label"
}

echo 'outside the grant block:'
try-op 'write inside  /tmp'      { to-string 'ok' > "$sandbox/scratch" }
try-op 'read  /etc/hosts'        { let _ = !{from-string < /etc/hosts} ; return unit }

echo ''
echo 'inside grant [exec: [:], fs: [read+write only the sandbox]]:'
grant [exec: [:], fs: [read: [$sandbox], write: [$sandbox]]] {
    try-op 'write inside  sandbox'   { to-string 'ok' > "$sandbox/inside"  }
    try-op 'write outside sandbox'   { to-string 'no' > "/tmp/escape"      }
    try-op 'read  /etc/hosts'        { let _ = !{from-string < /etc/hosts} ; return unit }
    try-op 'exec  curl'              { curl http://example.com 2> /dev/null }
}

rm -rf $sandbox