# Shell Scripting
Deft was born to fill a gap: shells are unbeatable for glue but ugly
at scale; Lisps are beautiful but awkward for one-liners. Deft tries
to synthesise the two. This page is a working scriptwriter's tour of
the language: shell execution, files, I/O, strings, JSON/env, kwargs,
and the small set of patterns that cover most day-to-day scripting.
## Hello, World
Save as `hello.dft`:
#!/usr/bin/env deft set a [os/args] echo "Hello, $a(1)!"
Run it:
```
$ deft hello.dft Alice
Hello, Alice!
$ chmod +x hello.dft
$ ./hello.dft Alice
Hello, Alice!
```
## Variables and Interpolation
`def` introduces a binding at module scope; `set` rebinds it within a
function. Strings interpolate four forms (see
[Syntax and Style](syntax-and-style)):
def name "Alice" set age 30 echo "Hello, $name, age $($age + 1)" # => Hello, Alice, age 31 echo "Length: $name(len)" # => Length: 5 echo "Upper: $[upper $name]" # => Upper: ALICE
`$name` and `$name~field` interpolate directly; `$name(args)` runs a
postfix call; `$(expr)` runs an infix expression (a bare identifier also
works as a variable lookup); `$[func args]` runs a prefix call.
## Shell Execution
`os/run` runs a subprocess and returns a result map. `os/shell` runs
a shell-style command line. Both return `:exit`, `:stdout`, `:stderr`.
def out [os/run "ls" "-la" "/tmp"] echo $out~stdout echo $out~exit def lines [os/shell "find . -name '*.dft' | head"]
The `[|cmd]` shorthand captures stdout:
def git-branch git rev-parse --abbrev-ref HEAD echo "On branch: $git-branch" echo "Commit: $[|git rev-parse --short HEAD]" # inline in a string
Postfix chains apply to the captured output — `[|cmd](lines)` gives a
list of lines, `[|cmd](words)` a list of words. A non-zero exit returns
`%Err{:message}`. The old `[!cmd]` spelling was removed.
### Lines and words
set ps-lines ps aux(lines) # list of lines set parts [split $some-line ":"] # list of strings set first-three [range 0 3 of $parts] # first 3 elements
## Reading and Writing Files
def config-text [fs/read "config.json"] fs/write "out.txt" "result: $value" fs/append "log.txt" "newline!\n" for line [fs/read-lines "big.txt"] { echo $line }
`fs/exists?`, `fs/stat`, `fs/mkdir`, `fs/list`, `fs/glob` cover the
rest of the filesystem surface (see [Stdlib](stdlib)).
if [fs/exists? ".git"] { echo "this is a git repo" } for entry [fs/list "."] { echo $entry~name } for path [fs/glob "src/**/*.dft"] { echo $path }
## Channels and Standard Streams
`fs/read`/`fs/write` cover whole files; for line-oriented I/O — logs,
streaming transforms, interactive prompts — use channels. `open` returns
an opaque `%Chan` handle (never a raw file descriptor — those are
process-global, reusable, and un-GC-able, so they never appear as
values):
# Write lines, read them back set out [open "/tmp/app.log" "w"] # modes: "r" "w" "a" "rw" [puts $out "starting up"] [puts $out 42] # non-strings format like echo [close $out] # => true; a second close => false set in [open "/tmp/app.log"] for i [range 2] { echo [gets $in] } # line by line; nil at EOF [close $in]
Dropped channels are closed by the GC, so explicit `close` is optional.
If a file can't be opened you get `%Err{:message}` back — check with
`err?`:
set r [open "/nonexistent/path"] if [err? $r] { echo "can't open: $r~message" }
The standard streams are addressed by keyword, not by number:
[puts "normal output"] # default target is :stdout [puts :stderr "warning: disk almost full"] set answer [gets] # default source is :stdin
`close` refuses the standard streams (you can't close `:stdout`), and
raw numbers are rejected everywhere — `puts 3 "x"` raises instead of
writing to a random descriptor.
## Arguments and Environment
def args [os/args] # list including script name def argv $args(rest) # without script name def first-arg $args(nth 1) def home [os/env "HOME"] # environment variable os/env # full environment map def cwd [os/cwd]
### `--flag value` kwargs
Define a `deftype` whose fields become CLI flags. Primitive-typed
params stay positional. (Full reference: [Functions › Named Optional
Arguments](functions).)
deftype BuildOpts %{ :push %{:type :boolean} :nocache %{:type :boolean} :tag %{:type :string} } def build {|service^string opts^BuildOpts| echo "building $service" if $opts~?push { echo " will push" } if $opts~?nocache { echo " no cache" } } build "myapp" --push --tag latest build "myapp" --nocache
`opts~?field` returns the value of `:field` or `nil` if absent. This
makes optional CLI flags clean to handle.
## JSON, YAML, and Friends
def data [json/parse $config-text] echo $data~database~host def yaml-data [yaml/load "config.yaml"] set encoded [json/stringify $data] set pretty [json/pretty $data]
For environment access without the `[os/env "X"]` verbosity, drop in
a small helper:
def env {|k^string default| set v [os/env $k] if [nil? $v] { return $default } $v } def port [env "PORT" "8080"]
## Pipelines and Transforms
Deft pipelines thread a value through single-arg transformations.
For shell scripting they replace nested calls and intermediate
temp files:
# Read lines, filter, transform, print [fs/read-lines "log.txt"] |> filter { [starts-with? $it "ERROR"] } |> map { [$it(split " ")](nth 4) } |> unique |> each { echo $it }
For reusable pipelines, factor them into transducers (see
[Transducers](transducers)):
def xf [comp [filter { [starts-with? $it "ERROR"] }] [map { [$it(split " ")](nth 4) }] [map upper]] [into @{} xf [fs/read-lines "log.txt"]]
## Functions and Decorators
Use `def` with `|params|` to declare helpers. `@doc` documents them;
any `@key value` is metadata retrievable at runtime (see
[Metaprogramming](metaprogramming)).
def load-json {|path^string| @doc "Load and parse a JSON file, returning %{} on error." try { set raw [fs/read $path] [json/parse $raw] } catch _e { %{} } } set cfg [load-json "config.json"]
### Closures
Anonymous functions use the same `|params| body` shape:
def double {|x| ($x * 2) } set also-double {|x| ($x * 2) } @{ 1 2 3 } |> map { $it * 2 } |> collect @{ 1 2 3 } |> map { |x| ($x * 2) } |> collect # explicit param
## Pattern Matching for Dispatch
`match` is the cleanest way to dispatch on a string or shape (see
[Pattern Matching](pattern-matching)):
def handle {|cmd args| match $cmd { "build" => [do-build $args] "test" => [do-test $args] "deploy" => [do-deploy $args] _ => [echo "unknown command: $cmd"] } }
## Error Handling
`try`/`catch` recovers locally; Result return (`%Ok`/`%Err`) propagates
upward; `throw` signals (see [Error Handling](error-handling)):
# Recover def safe-read {|path| try { [fs/read $path] } catch _e { "" } } # Propagate — caller decides how to handle %Ok vs %Err def load-config {|path| set raw [fs/read $path] if [err? $raw] { return $raw } [json/parse $raw] } # Signal def assert-file {|path| if not([fs/exists? $path]) { [throw "missing: $path"] } }
## A Real Example: Git Log Pretty-Printer
#!/usr/bin/env deft def format-line {|line| # Line shape: "abc123|Alice|2024-05-01|fix typo" set parts [split $line "|"] set hash $parts(nth 0) set who $parts(nth 1) set when $parts(nth 2) set msg $parts(nth 3) echo "$when $($hash(0 .. 7)) $who" echo " $msg" } def raw git log --pretty=format:"%h|%an|%ci|%s" -n 20 set lines [split $raw "\n"] for line $lines { [format-line $line] }
## A Real Example: Backup Script
#!/usr/bin/env deft def src [os/env "HOME"] def dst "$src/backup-[clock/seconds].tgz" @doc "Backup the home directory to $dst." def result [os/shell "tar czf $dst -C $src ."] if ($result~exit == 0) { echo "Backup OK: $dst" } else { echo "Backup failed: $result~stderr" os/exit 1 }
## Imports and Modules
import "deft/stdext/async" as async # deftask bodies must be self-contained (no captured locals) — # see the Concurrency chapter. set f1 [deftask { [http/get "https://api.example.com/a"] }] set f2 [deftask { [http/get "https://api.example.com/b"] }] set results [async/await-all @{$f1 $f2}]
For larger scripts, factor code into a package and import it. See
[Modules and Imports](modules-and-imports) and
[Packages](../07-Tooling/packages).
## HTTP Quick Hits
# GET set r [http/get "https://api.github.com/repos/ziglang/zig"] if [err? $r] { echo "request failed"; return } def body [json/parse $r~body] echo "stars: $body~stargazers_count" # POST with JSON body set payload [json/stringify %{ :title "hello" :body "world" }] http/post "https://example.com/api" $payload
For HTTP servers (web apps, webhooks), see
[HTTP Server](../04-App-Server/http-server).
## SQLite One-Liners
def db [sqlite/open ":memory:"] sqlite/exec $db "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)" sqlite/exec $db "INSERT INTO users (name, age) VALUES (?, ?)" @{ "Alice" 30 } def rows [sqlite/query $db "SELECT * FROM users WHERE age > ?" @{ 25 }] for row $rows { echo "$row~name is $row~age" } sqlite/close $db
See [SQLite](../02-Platform/sqlite) for the full reference.
## Common Patterns Cheat-Sheet
# Capture command output def files ls -la(lines) # Filter and transform $files |> filter { [starts-with? $it "d"] } |> map { $it(words)(nth -1) } |> each { echo $it } # Read JSON config def cfg [json/parse [fs/read "config.json"]] # Conditional one-liner set msg [if ($count > 10) "many" "few"] # Append to a list def acc @{} for x in $input { if ($x > 0) { $acc << $x } } # Line-oriented file I/O via a channel set log [open "/tmp/app.log" "a"] [puts $log "entry: $value"] [close $log] # Warn on stderr without touching stdout [puts :stderr "cache miss for $key"] # Tick counter incr i # Quick guard if (empty? $args) { echo "usage: ..." ; os/exit 1 }
## Where To Go Next
- [One-Liners](one-liners) — a recipe cookbook
- [Stdlib](stdlib) — full standard library catalogue
- [Modules and Imports](modules-and-imports) — splitting scripts into modules
- [HTTP Server](../04-App-Server/http-server) — building web apps
- [Error Handling](error-handling) — full Result story