# Shell 脚本 Deft 的诞生是为了填补一个空白:shell 在胶水脚本上无可匹敌,但规模一大就丑陋不堪;Lisp 优雅,却不擅长单行命令。Deft 试图将二者融合。本页是写给脚本作者的实用语言导览:shell 执行、文件、I/O、字符串、JSON/env、关键字参数,以及覆盖大多数日常脚本任务的一小组模式。 ## Hello, World 保存为 `hello.dft`:
#!/usr/bin/env deft

set a [os/args]
echo "Hello, $a(1)!"
运行它: ``` $ deft hello.dft Alice Hello, Alice! $ chmod +x hello.dft $ ./hello.dft Alice Hello, Alice! ```` ## 变量与插值 `def` 在模块作用域引入绑定;`set` 在函数内重新绑定。字符串支持四种插值形式(参见 语法与风格):
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` 和 `$name~field` 直接插值;`$name(args)` 运行后缀调用;`$(expr)` 运行中缀表达式(裸标识符也可用作变量查找);`$[func args]` 运行前缀调用。 ## Shell 执行 `os/run` 运行子进程并返回结果映射。`os/shell` 运行 shell 风格命令行。二者都返回 `:status`、`:stdout`、`:stderr`。
def out [os/run "ls" "-la" "/tmp"]
echo $out~stdout
echo $out~status

def lines [os/shell "find . -name '*.dft' | head"]
`[|cmd]` 简写捕获 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
后缀链作用于捕获的输出——`[|cmd](lines)` 给出行的列表,`[|cmd](words)` 给出词的列表。非零退出返回 `%Err{:message}`。旧的 `[!cmd]` 写法已被移除。 ### 行与词
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
## 读写文件
def config-text [unwrap [fs/read "config.json"]]
fs/write "out.txt" "result: $value"
fs/append "log.txt" "newline!\n"

for line [unwrap [fs/read-lines "big.txt"]] {
    echo $line
}
`fs/exists?`、`fs/stat`、`fs/mkdir`、`fs/list`、`fs/glob` 覆盖文件系统的其余接口(参见 标准库)。
if [fs/exists? ".git"] {
    echo "this is a git repo"
}

for entry [unwrap [fs/list "."]] {
    echo $entry~name
}

for path [unwrap [fs/glob "src/**/*.dft"]] {
    echo $path
}
## 通道与标准流 `fs/read`/`fs/write` 覆盖整文件读写;对于面向行的 I/O——日志、流式转换、交互式提示——请使用通道。`open` 返回不透明的 `%Chan` 句柄(绝不是原始文件描述符——它们进程全局、可复用且无法被 GC 管理,因此永远不会作为值出现):
# 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]
被丢弃的通道由 GC 关闭,因此显式的 `close` 是可选的。如果文件无法打开,会得到 `%Err{:message}`——用 `err?` 检查:
set r [open "/nonexistent/path"]
if [err? $r] { echo "can't open: $r~message" }
标准流通过关键字而非编号来寻址:
[puts "normal output"]                # default target is :stdout
[puts :stderr "warning: disk almost full"]
set answer [gets]                     # default source is :stdin
`close` 拒绝标准流(不能关闭 `:stdout`),原始数字在任何地方都会被拒绝——`puts 3 "x"` 会报错,而不是写入随机描述符。 ## 参数与环境变量
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` 关键字参数 定义一个 `deftype`,其字段成为 CLI 标志。原始类型的参数保持位置参数。(完整参考:函数 › 命名可选参数。)
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` 返回 `:field` 的值,若不存在则返回 `nil`。这让可选 CLI 标志的处理变得干净。 ## JSON、YAML 及其他
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]
若想访问环境变量而不必写冗长的 `[os/env "X"]`,可以加入一个小辅助函数:
def env {|k^string default|
    set v [os/env $k]
    if [nil? $v] { return $default }
    $v
}

def port [env "PORT" "8080"]
## 管道与转换 Deft 管道让值依次穿过单参数转换。在 shell 脚本中,它们取代嵌套调用和中间临时文件:
# Read lines, filter, transform, print
[unwrap [fs/read-lines "log.txt"]]
    |> filter { [starts-with? $it "ERROR"] }
    |> map { [$it(split " ")](nth 4) }
    |> unique
    |> each { echo $it }
对于可复用的管道,把它们提取为转换器(transducer)(参见 转换器):
def xf [comp
    [filter { [starts-with? $it "ERROR"] }]
    [map { [$it(split " ")](nth 4) }]
    [map upper]]

[into @{} xf [unwrap [fs/read-lines "log.txt"]]]
## 函数与装饰器 使用带 `|params|` 的 `def` 声明辅助函数。`@doc` 为其添加文档;任何 `@key value` 都是可在运行时检索的元数据(参见 元编程)。
def load-json {|path^string|
    @doc "Load and parse a JSON file, returning %{} on error."
    try {
        set raw [unwrap [fs/read $path]]
        [json/parse $raw]
    } catch _e {
        %{}
    }
}

set cfg [load-json "config.json"]
### 闭包 匿名函数使用相同的 `|params| body` 形式:
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
## 用于分派的模式匹配 `match` 是按字符串或形状进行分派的最干净方式(参见 模式匹配):
def handle {|cmd args|
    match $cmd {
        "build"  => [do-build $args]
        "test"   => [do-test $args]
        "deploy" => [do-deploy $args]
        _        => [echo "unknown command: $cmd"]
    }
}
## 错误处理 `try`/`catch` 在本地恢复;Result 返回(`%Ok`/`%Err`)向上传播;`throw` 发出信号(参见 错误处理):
# Recover
def safe-read {|path|
    [unwrap [fs/read $path] ""]
}

# Propagate — caller decides how to handle %Ok vs %Err
def load-config {|path|
    set raw [ok! [fs/read $path]]
    [json/parse $raw]
}

# Signal
def assert-file {|path|
    if not([fs/exists? $path]) {
        [throw "missing: $path"]
    }
}
## 实战示例:Git 日志美化输出
#!/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]
}
## 实战示例:备份脚本
#!/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~status == 0) {
    echo "Backup OK: $dst"
} else {
    echo "Backup failed: $result~stderr"
    os/exit 1
}
## 导入与模块
import "deft/stdext/async" as async

# deftask bodies must be self-contained (no captured locals) —
# see the Concurrency chapter.
set f1 [deftask { [await [http/get "https://api.example.com/a"]] }]
set f2 [deftask { [await [http/get "https://api.example.com/b"]] }]
set results [async/await-all @{$f1 $f2}]
对于较大的脚本,将代码提取为包并导入。参见 模块与导入 和 包。 ## HTTP 快速上手
# GET
set r [await [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" }]
[await [http/post "https://example.com/api" $payload]]
关于 HTTP 服务器(Web 应用、webhook),参见 HTTP 服务器。 ## SQLite 单行命令
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
完整参考参见 SQLite。 ## 常用模式速查表
# 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 [unwrap [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 }
## 接下来读什么 - 单行命令 — 一份配方手册 - 标准库 — 完整标准库目录 - 模块与导入 — 将脚本拆分为模块 - HTTP 服务器 — 构建 Web 应用 - 错误处理 — Result 的完整故事