/// 命令行层:解析参数、取输入、交给解析器、按模式输出、决定退出码。
///
/// 退出码约定和大多数 Unix 工具一致:
/// 0 正常(--check 时表示这份 diff 干净)
/// 1 --check 下发现了改动,或者 --review 没拿到模型的意见
/// 2 用法错误、读不到输入、或者 diff 本身解析失败
///|
enum Mode {
Stat
Detail
Check
Json
Prompt
Review
} derive(Eq, Debug)
///|
struct Flags {
mode : Mode
paths : Array[String]
} derive(Eq, Debug)
///|
enum Parsed {
ShowHelp
ShowVersion
Run(Flags)
UsageError(String)
} derive(Eq, Debug)
///|
let version : String = "0.1.0"
///|
fn print_help() -> Unit {
[
"moonreview \{version} —— 解析 unified diff",
"",
"用法: moonreview [选项] [文件 ...]",
"",
"不给文件、或者给 `-` 时从标准输入读。多个文件的内容会按顺序拼起来。",
"",
"选项:",
" --stat 每个文件一行变更量(默认)",
" --detail, -d 逐文件打印状态、hunk 头和每一行",
" --json 输出结构化 JSON,给别的程序读",
" --review-prompt 输出给大模型评审用的提示词(不调模型,只生成文本)",
" --review 把提示词发给模型并打印它的意见(要 curl 和外网)",
" --check 只校验能不能解析,不输出内容",
" -h, --help 显示这份帮助",
" -V, --version 显示版本",
"",
"--review 读这些环境变量:",
" MOONREVIEW_API_KEY 必填,等同于 OPENAI_API_KEY",
" MOONREVIEW_BASE_URL 接口地址,默认商汤 SenseNova 的 OpenAI 兼容端点",
" MOONREVIEW_MODEL 模型名,默认 sensenova-6.7-flash-lite",
"",
"退出码: 0 成功 / 1 --check 发现有改动 / 2 用法或解析错误",
].join("\n")
|> println
}
///|
fn parse_args(argv : Array[String]) -> Parsed {
let mut mode : Mode = Stat
let paths : Array[String] = []
let mut i = 0
while i < argv.length() {
let a = argv[i]
i = i + 1
if a == "--" {
while i < argv.length() {
paths.push(argv[i])
i = i + 1
}
} else if a == "-h" || a == "--help" {
return ShowHelp
} else if a == "-V" || a == "--version" {
return ShowVersion
} else if a == "--stat" {
mode = Stat
} else if a == "-d" || a == "--detail" {
mode = Detail
} else if a == "--json" {
mode = Json
} else if a == "--review-prompt" {
mode = Prompt
} else if a == "--review" {
mode = Review
} else if a == "--check" {
mode = Check
} else if a == "-" {
paths.push("-")
} else if a.has_prefix("-") {
return UsageError("unknown option \{a}")
} else {
paths.push(a)
}
}
Run({ mode, paths, })
}
///|
/// `-` 表示 stdin,其余当文件路径。空列表等价于只读 stdin。
fn read_all(paths : Array[String]) -> String raise IoError {
if paths.is_empty() {
return read_stdin()
}
let out : Array[String] = []
for p in paths {
out.push(if p == "-" { read_stdin() } else { read_file(p) })
}
out.join("\n")
}
///|
/// 打印并给出这个模式的退出码。`--review` 是唯一会联网的模式,
/// 它失败也算 1,和 `--check` 发现改动一样表示「有问题要看」。
fn render(d : @diff.Diff, mode : Mode) -> Int raise IoError {
match mode {
Stat => {
println(d.format_stat())
0
}
Detail => {
println(d.format_detail())
0
}
Json => {
println(d.format_json())
0
}
Prompt => {
println(d.review_prompt())
0
}
Review => {
let (ok, text) = ask_for_review(d)
println(text)
if ok {
0
} else {
1
}
}
Check => if d.is_empty() { 0 } else { 1 }
}
}
///|
/// 解析器报的行号错误换成 IoError::BadLine,好让上层的 catch 一次接住。
fn parse_text(text : String) -> @diff.Diff raise IoError {
@diff.parse_diff(text) catch {
@diff.BadDiff(no, msg) => raise BadLine(no, msg)
}
}
///|
fn run_flags(flags : Flags) -> Int raise IoError {
let text = read_all(flags.paths)
let d = parse_text(text)
render(d, flags.mode)
}
///|
/// 返回进程退出码。所有 raise 都在这里收敛成 2。
fn run(argv : Array[String]) -> Int {
match parse_args(argv) {
ShowHelp => {
print_help()
0
}
ShowVersion => {
println("moonreview \{version}")
0
}
UsageError(msg) => {
eprintln("moonreview: \{msg}")
2
}
Run(flags) =>
run_flags(flags) catch {
err => {
eprintln("moonreview: \{err.message()}")
2
}
}
}
}
///|
fn argv_without_program() -> Array[String] {
let argv = @env.args()
let out : Array[String] = []
for i in 1..