原生 CLI 快速开始#
本快速开始展示了一个简单但规范的 MoonBit CLI 布局:
将参数解析和业务逻辑放在库包中
保持
cmd/main简洁使用
moonbitlang/async处理原生 IO在不接触网络的情况下测试纯逻辑部分
这个示例使用 moonbitlang/async,它目前对 native 后端支持最好。
创建项目#
先从一个普通的 MoonBit 模块开始:
moon new download_cli
cd download_cli
moon add moonbitlang/async
argparse 已经属于标准库,因此这个快速开始只需要添加 moonbitlang/async。
在 moon.mod.json 中把首选目标设置为 native,这样 moon run 和 moon build 默认就会使用 moonbitlang/async 支持最好的后端:
{
"name": "username/download_cli",
"version": "0.1.0",
"deps": {
"moonbitlang/async": "0.16.6"
},
"preferred-target": "native"
}
最终目录结构如下:
download_cli
├── cmd
│ └── main
│ ├── main.mbt
│ └── moon.pkg
├── cli_test.mbt
├── config.mbt
├── download.mbt
├── moon.mod.json
└── moon.pkg
在库包中保留解析逻辑#
根包定义 CLI 契约。它负责命令的形状,并把 argv 转成带类型的配置值:
pub struct Config {
url : String
output : String?
} derive(Show, Eq)
///|
pub fn command() -> @argparse.Command {
@argparse.Command::new(
"moon-fetch",
about="Download a URL to stdout or a file",
options=[
@argparse.OptionArg::new(
"output",
short='o',
about="Write the response body to this file",
),
],
positionals=[
@argparse.PositionArg::new("url", about="HTTP or HTTPS URL to download"),
],
)
}
///|
pub fn parse_config(argv : Array[String]) -> Config raise {
let matches = @argparse.parse(command(), argv~)
let values : Map[String, Array[String]] = matches.values
guard values is { "url": [url, ..], "output"? : output_paths, .. } else {
fail("missing url")
}
let output = match output_paths {
Some([output, ..]) => Some(output)
_ => None
}
{ url, output }
}
包描述文件从 moonbitlang/core 导入 argparse,并导入实现中使用的异步库:
import {
"moonbitlang/core/argparse",
"moonbitlang/async/fs",
"moonbitlang/async/http",
"moonbitlang/async/stdio",
}
把异步 IO 收拢到一个函数中#
run 负责实际下载。传入 -o 时,它会把响应体写入文件;否则直接写到标准输出:
pub async fn run(config : Config) -> Unit {
let (response, body) = @http.get_stream(config.url)
defer body.close()
guard response.code is (200..<300) else {
fail("download failed: \{response.code} \{response.reason}")
}
match config.output {
Some(path) => {
let file = @fs.create(path, permission=0o644)
defer file.close()
file.write_reader(body)
@stdio.stderr.write("saved \{config.url} to \{path}\n")
}
None => @stdio.stdout.write_reader(body)
}
}
保持 main 精简#
cmd/main 通常只负责接线:读取 argv、构造配置并调用库入口点。
import {
"moonbit-community/cli-quickstart-doc" @app,
"moonbitlang/core/env",
"moonbitlang/async",
}
options(
"is-main": true,
)
async fn main {
let config = @app.parse_config(@env.args())
@app.run(config)
}
运行命令#
将响应体写到标准输出:
moon run cmd/main https://example.com/feed.xml
将其写入文件:
moon run cmd/main https://example.com/feed.xml -o feed.xml
构建原生二进制:
moon build --target native
测试纯逻辑部分#
解析器和配置整理逻辑不执行 IO,因此仍然很容易测试:
///|
test "parse config for stdout" {
let config = parse_config(["https://example.com/feed.xml"])
assert_eq(config.url, "https://example.com/feed.xml")
assert_eq(config.output, None)
}
///|
test "parse config for file output" {
let config = parse_config(["https://example.com/feed.xml", "-o", "feed.xml"])
assert_eq(config.url, "https://example.com/feed.xml")
guard config.output is Some(path) else { fail("expected an output path") }
assert_eq(path, "feed.xml")
}
用下面的命令运行测试:
moon test
当 CLI 继续增长时,仍然沿用同样的拆分方式:
在库包中解析并校验输入
把副作用集中在少量 async 函数中
让
cmd/main专注于接线