# 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