# 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}]
# 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 }