Skip to main content

xyz_rust/cli/
app.rs

1// CLI 前端:注册表 → 命令树 → flag 解析 → Invoke + 渲染。
2//
3// 输出契约:命令结果去 stdout;错误与诊断去 stderr。--json 生效时结果
4// 以两空格缩进 JSON 输出;否则用人类可读渲染(render.rs)。
5//
6// 与 Go 版差异:进程名取 std::env::args()[0] 的 basename(Go filepath.Base
7// 同);输出流用 Arc<Mutex<_>> 共享(Go 的 io.Writer 接口引用语义在
8// Rust 里需要锁来获得 &mut 写出权)。
9
10use std::io::Write;
11use std::sync::{Arc, Mutex};
12
13use serde_json::{Map, Value};
14
15use crate::cli::completion::print_completion;
16use crate::ctx::Ctx;
17use crate::errors;
18use crate::registry::Registry;
19use crate::spec::Entry;
20
21use super::format::Format;
22use super::tree::{CmdNode, build_tree};
23
24type SharedWriter = Arc<Mutex<Box<dyn Write + Send>>>;
25
26/// CLI 前端 -v/--version 汇报的版本(cli::run 直接嵌入时使用;根派发器
27/// 在到达这里之前用 version 模块应答 -v)。
28static CLI_VERSION: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new();
29
30/// 设置 CLI 前端 -v 输出的版本串。
31pub fn set_cli_version(v: &'static str) {
32    let _ = CLI_VERSION.set(v);
33}
34
35fn cli_version() -> &'static str {
36    CLI_VERSION.get_or_init(crate::version::version)
37}
38
39pub struct App {
40    pub(crate) root: CmdNode,
41    pub(crate) out: SharedWriter,
42    pub(crate) err_out: SharedWriter,
43    pub(crate) mws: Vec<ExecFunc>,
44    /// 代码级全局默认格式(spec §10.7 第四层):""|auto 按 TTY 解析。
45    pub(crate) default_format: String,
46    /// --xyz.format 是否来自命令行(第二层,高于逐命令 hint)。
47    pub(crate) format_from_flag: bool,
48    /// auto 交互式下半(默认 text)。
49    pub(crate) format_interactive: String,
50    /// auto 非交互下半(默认 jsonl)。
51    pub(crate) format_piped: String,
52    /// 强制交互式裁定(测试/嵌入;None=按输出目标探测,§10.7)。
53    pub(crate) interactive_override: Option<bool>,
54    /// 输出是否由嵌入方注入(非默认 stdout):注入 writer 一律非交互。
55    pub(crate) out_injected: bool,
56}
57
58/// 嵌入场景的前端级配置(零值保持 stdout/stderr)。
59#[derive(Default)]
60pub struct Options {
61    pub out: Option<Box<dyn Write + Send>>,
62    pub err_out: Option<Box<dyn Write + Send>>,
63    /// 默认输出格式(--xyz.format / Config.Format;None/空=auto)。
64    pub format: Option<String>,
65    /// format 来自命令行 --xyz.format(命令行层)。
66    pub format_from_flag: bool,
67    /// auto 的交互式下半(空= text)。
68    pub format_interactive: Option<String>,
69    /// auto 的非交互下半(空= jsonl)。
70    pub format_piped: Option<String>,
71    /// 强制交互式裁定(测试/嵌入用;None=按输出目标探测)。
72    pub interactive: Option<bool>,
73}
74
75/// 一次叶子命令执行的只读快照,交给 Execute 中间件(use_mw)。
76pub struct ExecContext {
77    /// 点分注册名,如 user.add。
78    pub path: String,
79    /// 命令元数据(Hints、InputSchema、OutputSchema)。
80    pub entry: Arc<Entry>,
81    /// 生效的输出格式(spec §10.7;未调 next 自行渲染时可参考)。
82    pub format: Format,
83    /// 机器格式(json/jsonl)——保留的兼容字段,等价 format.is_machine()。
84    pub json: bool,
85    /// 结果的输出目标(Arc<Mutex>,lock 后即 &mut dyn Write)。
86    pub out: SharedWriter,
87}
88
89/// Execute 中间件:args 是已解析的入参 map(flag、env 与位置参数已应用);
90/// next(&mut args) 续链到 Invoke + 渲染(可多次调用)。返回值语义与命令
91/// 错误一致(按分类映射退出码)。
92pub type ExecFunc = Box<
93    dyn Fn(
94            &Ctx,
95            &ExecContext,
96            &mut Map<String, Value>,
97            &mut dyn FnMut(&mut Map<String, Value>) -> errors::Result<()>,
98        ) -> errors::Result<()>
99        + Send
100        + Sync,
101>;
102
103impl App {
104    /// 由注册表构建命令树。不可绑定的字段形状(嵌套 struct、元素为
105    /// struct 的切片)与有歧义的位置参数(optional 后面的 required)是
106    /// 配置错误。
107    pub fn new(reg: &Registry) -> errors::Result<App> {
108        let root = build_tree(reg)?;
109        Ok(App {
110            root,
111            out: Arc::new(Mutex::new(Box::new(std::io::stdout()))),
112            err_out: Arc::new(Mutex::new(Box::new(std::io::stderr()))),
113            mws: Vec::new(),
114            default_format: String::new(),
115            format_from_flag: false,
116            format_interactive: String::new(),
117            format_piped: String::new(),
118            interactive_override: None,
119            out_injected: false,
120        })
121    }
122
123    /// 带前端选项的构建;None 选项保持默认。
124    pub fn new_with_options(reg: &Registry, opts: Options) -> errors::Result<App> {
125        // 构建期校验代码级格式配置(对齐 Go NewWithOptions):非法值让 CLI
126        // 构建失败(run_context 打印并 exit 2),而不是静默兜底。
127        for v in [
128            opts.format.as_deref().unwrap_or(""),
129            opts.format_interactive.as_deref().unwrap_or(""),
130            opts.format_piped.as_deref().unwrap_or(""),
131        ] {
132            if Format::parse(v).is_none() {
133                return Err(errors::Error::new(
134                    errors::Kind::Internal,
135                    format!("cli: invalid format {v:?} (want auto|text|json|jsonl|markdown)"),
136                ));
137            }
138        }
139        let mut a = App::new(reg)?;
140        if let Some(o) = opts.out {
141            a.out = Arc::new(Mutex::new(o));
142            a.out_injected = true;
143        }
144        if let Some(e) = opts.err_out {
145            a.err_out = Arc::new(Mutex::new(e));
146        }
147        if let Some(f) = opts.format {
148            a.default_format = f;
149        }
150        a.format_from_flag = opts.format_from_flag;
151        if let Some(v) = opts.format_interactive {
152            a.format_interactive = v;
153        }
154        if let Some(v) = opts.format_piped {
155            a.format_piped = v;
156        }
157        a.interactive_override = opts.interactive;
158        if a.out_injected {
159            // 注入 writer 非交互(spec §10.7):嵌入方可用 interactive 强制。
160        }
161        Ok(a)
162    }
163
164    /// 重定向输出流;None 保持现状。嵌入大程序/测试必备。
165    pub fn set_output(
166        &mut self,
167        out: Option<Box<dyn Write + Send>>,
168        err_out: Option<Box<dyn Write + Send>>,
169    ) {
170        if let Some(o) = out {
171            self.out = Arc::new(Mutex::new(o));
172            self.out_injected = true;
173        }
174        if let Some(e) = err_out {
175            self.err_out = Arc::new(Mutex::new(e));
176        }
177    }
178
179    /// 追加 Execute 中间件(最外层最先)。next() 续链到 Invoke + 渲染;
180    /// 中间件可改写入参、短路(跳过 next 自绘)或包装 next 计时。
181    pub fn use_mw(&mut self, mw: ExecFunc) {
182        self.mws.push(mw);
183    }
184
185    /// App 自身的执行入口。
186    pub fn run(&mut self, args: &[String]) -> i32 {
187        self.run_ctx(&Ctx::new(), args)
188    }
189
190    pub fn run_ctx(&mut self, ctx: &Ctx, args: &[String]) -> i32 {
191        // 内建 completion 子命令:生成 shell 补全脚本(bash/zsh/fish)。
192        if args.first().map(String::as_str) == Some("completion") {
193            let shell = args.get(1).map(String::as_str).unwrap_or("bash");
194            return print_completion(
195                &self.root_collect_top(),
196                &mut *self.out.lock().unwrap(),
197                &mut *self.err_out.lock().unwrap(),
198                &bin_name(),
199                shell,
200            );
201        }
202        let bin = bin_name();
203        for arg in args {
204            if arg == "--" {
205                break; // 之后的 token 全是位置参数,-v 不再算开关
206            }
207            if arg == "-v" || arg == "--version" {
208                let _ = writeln!(
209                    self.out.lock().unwrap(),
210                    "{} version {}",
211                    bin,
212                    cli_version()
213                );
214                return 0;
215            }
216        }
217        // 输出格式(spec §10.7):默认取 --xyz.format(注入 default_format),
218        // 裸 --format/--json 在未被目标命令同名 flag 遮蔽时覆盖之;全称
219        // --xyz.format 已在派发层消费(不受遮蔽影响)。
220        let target = self.resolve_target(args);
221        let format_conflict = target.map(|n| node_has_flag(n, "format")).unwrap_or(false);
222        let json_conflict = target.map(|n| node_has_flag(n, "json")).unwrap_or(false);
223        let mut bare: Option<String> = None;
224        let mut filtered: Vec<String> = Vec::with_capacity(args.len());
225        let mut past_double_dash = false;
226        let mut i = 0;
227        while i < args.len() {
228            let a = &args[i];
229            if past_double_dash {
230                filtered.push(a.clone());
231                i += 1;
232                continue;
233            }
234            match a.as_str() {
235                "--" => {
236                    past_double_dash = true;
237                    filtered.push(a.clone());
238                }
239                "--json" if !json_conflict => bare = Some("json".to_string()),
240                "--format" if !format_conflict => {
241                    if i + 1 >= args.len() {
242                        let _ = writeln!(
243                            self.err_out.lock().unwrap(),
244                            "{bin}: --format needs an argument (text|json|jsonl|markdown)"
245                        );
246                        return 2;
247                    }
248                    i += 1;
249                    bare = Some(args[i].clone());
250                }
251                _ if a.starts_with("--format=") && !format_conflict => {
252                    bare = Some(a["--format=".len()..].to_string());
253                }
254                // 被遮蔽(或未知)的裸标志原样留给命令自己的解析。
255                _ => filtered.push(a.clone()),
256            }
257            i += 1;
258        }
259        if let Some(b) = &bare
260            && Format::parse(b).is_none()
261        {
262            let _ = writeln!(
263                self.err_out.lock().unwrap(),
264                "{bin}: invalid output format {b:?} (want auto|text|json|jsonl|markdown)"
265            );
266            return 2;
267        }
268        let entry = target.and_then(|n| n.entry.clone());
269        let format = self.resolve_format(bare.as_deref(), entry.as_deref());
270        if let Err(e) = self.execute(ctx, self.root.clone(), &filtered, format, &bin) {
271            // 下游早关管道(| head 等):静默按 Go 的 SIGPIPE 惯例退出。
272            if errors::is_broken_pipe(&e) {
273                return 141;
274            }
275            self.render_error(&e, format);
276            return exit_code_of(&e);
277        }
278        0
279    }
280
281    /// 输出目标是否交互式(spec §10.7):强制裁定优先;嵌入注入的 writer
282    /// 一律非交互;默认 stdout 走 termx 探针(与保留的样式轴共用,§10.7a)。
283    pub(crate) fn interactive(&self) -> bool {
284        if let Some(v) = self.interactive_override {
285            return v;
286        }
287        if self.out_injected {
288            return false;
289        }
290        crate::termx::interactive()
291    }
292
293    /// 五级优先级解析本次执行的具体格式(spec §10.7):bare --format/--json
294    /// (未遮蔽)> --xyz.format(命令行)> CliHints.format > Config.Format >
295    /// auto(按 TTY 解析)。任一层可写 auto(或空白沿用下层)。层 1/2 的
296    /// 非法值在调用方用法错误退出;层 3/4 的非法值宽松忽略(视同未设置)。
297    pub(crate) fn resolve_format(&self, bare: Option<&str>, entry: Option<&Entry>) -> Format {
298        let iv = self.interactive();
299        let resolved = |s: &str| match Format::parse(s) {
300            Some(Format::Auto) => {
301                Format::Auto.resolve(iv, &self.format_interactive, &self.format_piped)
302            }
303            Some(f) => f,
304            // 非法代码配置值:按 Go 渲染 default 语义走 text(纵深兜底;
305            // 命令行两层非法在调用方与 builtins 已分别拦下)。
306            None => Format::Text,
307        };
308        if let Some(b) = bare {
309            return resolved(b);
310        }
311        if self.format_from_flag {
312            return resolved(&self.default_format);
313        }
314        // 层 3/4:非空即采用(即使解析失败也停止下落,与 Go resolveFormat
315        // 的"读到值即停"一致,失败值渲染时落 text)。
316        if let Some(e) = entry
317            && !e.cli.format.is_empty()
318        {
319            return resolved(&e.cli.format);
320        }
321        if !self.default_format.is_empty() {
322            return resolved(&self.default_format);
323        }
324        Format::Auto.resolve(iv, &self.format_interactive, &self.format_piped)
325    }
326
327    /// 目标节点解析(§10.7 冲突让位判定用):沿非 flag 段下沉(含默认子
328    /// 命令转发),返回最深可达节点。
329    fn resolve_target(&self, args: &[String]) -> Option<&CmdNode> {
330        let mut node: &CmdNode = &self.root;
331        let mut rest = args;
332        loop {
333            let Some(first) = rest.first() else {
334                return Some(node);
335            };
336            if first == "--" || first.starts_with('-') {
337                rest = &rest[1..];
338                continue;
339            }
340            if let Some(child) = node
341                .children
342                .iter()
343                .find(|c| c.segment == *first || (c.leaf && c.aliases.iter().any(|a| a == first)))
344            {
345                node = child;
346                rest = &rest[1..];
347                continue;
348            }
349            let default_seg = node.default_segment.clone();
350            if let Some(seg) = default_seg
351                && let Some(child) = node.children.iter().find(|c| c.segment == seg)
352            {
353                node = child;
354                continue; // 默认子命令:不消费该段
355            }
356            return Some(node);
357        }
358    }
359
360    /// 命令错误 → stderr(spec §10.7/§8.6):机器格式写共享错误体
361    /// (json pretty / jsonl 紧凑),其余格式写人类可读一行。
362    fn render_error(&self, e: &errors::Error, format: Format) {
363        let mut w = self.err_out.lock().unwrap();
364        match format {
365            Format::Json | Format::JsonL => {
366                let body = errors::error_body(e);
367                let s = if format == Format::Json {
368                    serde_json::to_string_pretty(&body)
369                } else {
370                    serde_json::to_string(&body)
371                }
372                .unwrap_or_else(|_| "{\"error\":\"\"}".to_string());
373                let _ = writeln!(*w, "{s}");
374            }
375            _ => {
376                let _ = writeln!(*w, "{e}");
377            }
378        }
379    }
380
381    fn root_collect_top(&self) -> Vec<String> {
382        self.root
383            .children
384            .iter()
385            .map(|c| c.segment.clone())
386            .collect()
387    }
388
389    pub fn execute(
390        &mut self,
391        ctx: &Ctx,
392        mut node: CmdNode,
393        args: &[String],
394        format: Format,
395        bin: &str,
396    ) -> errors::Result<()> {
397        let mut rest = args;
398        // 逐段下沉子命令树(别名与子命令段等价)
399        while let Some(first) = rest.first() {
400            let hit = node
401                .children
402                .iter()
403                .find(|c| c.segment == *first || (c.leaf && c.aliases.iter().any(|a| a == first)));
404            match hit {
405                Some(child) => {
406                    node = child.clone();
407                    rest = &rest[1..];
408                }
409                None => break,
410            }
411        }
412        // 默认子命令:首段不是已注册命令段、也不是 flag(-h/-v 等)时,
413        // 整串参数不消费地转发给默认子命令(udf img ⇔ udf extract img)。
414        if !node.leaf
415            && !rest.is_empty()
416            && !rest[0].starts_with('-')
417            && let Some(seg) = node.default_segment.clone()
418            && let Some(child) = node.children.iter().find(|c| c.segment == seg)
419        {
420            node = child.clone();
421        }
422        for t in rest {
423            if t == "-h" || t == "--help" {
424                return self.print_help(&node, bin);
425            }
426        }
427        if !node.leaf {
428            return self.print_help(&node, bin);
429        }
430        let (fvals, pos) = super::parse::parse_flags(&node.defs, rest)?;
431        if pos.len() < node.min_pos || pos.len() > node.max_pos {
432            return Err(errors::Error::new(
433                errors::Kind::InvalidInput,
434                crate::lang::tf(
435                    "cli.err_positional_count",
436                    &[
437                        &node.path.replace('.', " "),
438                        &node.min_pos.to_string(),
439                        &node.max_pos.to_string(),
440                        &pos.len().to_string(),
441                    ],
442                ),
443            ));
444        }
445        let mut m = Map::new();
446        for (i, d) in node.defs.iter().enumerate() {
447            let fv = &fvals[i];
448            if fv.seen {
449                match d.kind {
450                    super::parse::FlagKind::Bool => {
451                        m.insert(d.field.json_name.clone(), Value::Bool(fv.boolean));
452                    }
453                    super::parse::FlagKind::Slice => {
454                        m.insert(
455                            d.field.json_name.clone(),
456                            Value::Array(
457                                fv.list.iter().map(|s| Value::String(s.clone())).collect(),
458                            ),
459                        );
460                    }
461                    _ => {
462                        m.insert(d.field.json_name.clone(), Value::String(fv.str.clone()));
463                    }
464                }
465                continue;
466            }
467            if let Some(env) = &d.field.cli.env_var
468                && let Ok(v) = std::env::var(env)
469                && !v.is_empty()
470            {
471                m.insert(d.field.json_name.clone(), Value::String(v));
472                continue;
473            }
474            if let Some(def) = &d.field.cli.default {
475                m.insert(d.field.json_name.clone(), def.clone());
476            }
477        }
478        // json:"-"(#[xyz(skip)])的注入字段:env 值以 Rust 字段名为键送达。
479        for f in &node.env_only {
480            if let Some(env) = &f.cli.env_var
481                && let Ok(v) = std::env::var(env)
482                && !v.is_empty()
483            {
484                m.insert(f.name.clone(), Value::String(v));
485            }
486        }
487        for (i, f) in node.pos_f.iter().enumerate() {
488            if i < pos.len() {
489                m.insert(f.json_name.clone(), Value::String(pos[i].clone()));
490            }
491        }
492        let entry = node
493            .entry
494            .as_ref()
495            .ok_or_else(|| {
496                errors::Error::new(errors::Kind::Internal, "leaf without entry".to_string())
497            })?
498            .clone();
499        let ec = ExecContext {
500            path: node.path.clone(),
501            entry,
502            format,
503            json: format.is_machine(),
504            out: Arc::clone(&self.out),
505        };
506
507        // 中间件洋葱链:自内向外构建(最晚注册的最外层)。
508        let terminal = |ctx: &Ctx, ec: &ExecContext, args: &mut Map<String, Value>| {
509            let out = (ec.entry.invoke)(ctx, args)?;
510            // 长驻命令:ctx 取消即优雅关停,不渲染返回值。
511            if ec.entry.cli.daemon {
512                return Ok(());
513            }
514            let mut w = ec.out.lock().unwrap();
515            match ec.format {
516                // resolve 后不应到达 Auto(兜底走人类渲染)。
517                Format::Auto | Format::Text => super::render::render(&mut **w, &out)?,
518                Format::Json => {
519                    let s = serde_json::to_string_pretty(&out).map_err(|e| {
520                        errors::Error::new(
521                            errors::Kind::Internal,
522                            format!("result serialization: {e}"),
523                        )
524                    })?;
525                    writeln!(*w, "{s}").map_err(io_err)?;
526                }
527                Format::JsonL => super::format::render_jsonl(&mut **w, &out)?,
528                Format::Markdown => super::format::render_markdown(&mut **w, &out)?,
529            }
530            Ok(())
531        };
532        // 中间件洋葱链:入参 map 经 next 的 &mut 参数逐层下传(Go 接口引用
533        // 语义的 Rust 等价物),每层用自己的 RefCell 槽位保存内层链,支撑
534        // next 的多次调用。
535        let ec_ref = &ec;
536        #[allow(clippy::type_complexity)]
537        let mut chain: Box<dyn FnMut(&mut Map<String, Value>) -> errors::Result<()> + '_> =
538            Box::new(|m| terminal(ctx, ec_ref, m));
539        for i in (0..self.mws.len()).rev() {
540            let mw = &self.mws[i];
541            let inner = chain;
542            let cell = std::cell::RefCell::new(Some(inner));
543            chain = Box::new(move |m: &mut Map<String, Value>| {
544                let mut guard = cell.borrow_mut();
545                let mut inner_fn = guard.take().unwrap();
546                let res = {
547                    let mut next_local = |mm: &mut Map<String, Value>| inner_fn(mm);
548                    let mut next_trait: &mut dyn FnMut(
549                        &mut Map<String, Value>,
550                    ) -> errors::Result<()> = &mut next_local;
551                    (mw)(ctx, ec_ref, m, &mut next_trait)
552                };
553                *guard = Some(inner_fn);
554                res
555            });
556        }
557        let mut m = m;
558        chain(&mut m)
559    }
560
561    pub(crate) fn print_help(&mut self, node: &CmdNode, bin: &str) -> errors::Result<()> {
562        super::help::print_help(&mut *self.out.lock().unwrap(), node, bin)
563    }
564}
565
566fn io_err(e: std::io::Error) -> errors::Error {
567    errors::Error::new(errors::Kind::Internal, format!("write error: {e}"))
568}
569
570/// 目标命令是否定义了同名长 flag(spec §10.7 裸标志让位判定)。
571fn node_has_flag(node: &CmdNode, long: &str) -> bool {
572    node.leaf && node.defs.iter().any(|d| d.long == long)
573}
574
575/// 把 handler 错误映射成退出码:有分类的按表;未分类(flag/用法等)给 2。
576pub(crate) fn exit_code_of(e: &errors::Error) -> i32 {
577    match errors::classify(e) {
578        Some(kind) => errors::exit_code(kind),
579        None => 2,
580    }
581}
582
583/// 进程名:std::env::args()[0] 的 basename;异常值兜底 "app"。
584pub fn bin_name() -> String {
585    let arg0 = std::env::args().next().unwrap_or_default();
586    let base = std::path::Path::new(&arg0)
587        .file_name()
588        .and_then(|s| s.to_str())
589        .unwrap_or("app");
590    if base.is_empty() || base == "." || base == "/" {
591        "app".to_string()
592    } else {
593        base.to_string()
594    }
595}
596
597/// 一次调用形态:构建 + 执行。
598pub fn run(reg: &Registry, args: &[String]) -> i32 {
599    run_context(&Ctx::new(), reg, args, Options::default())
600}
601
602/// 带上下文的执行(取消信号流进被调 handler)。
603pub fn run_context(ctx: &Ctx, reg: &Registry, args: &[String], opts: Options) -> i32 {
604    let mut a = match App::new_with_options(reg, opts) {
605        Ok(a) => a,
606        Err(e) => {
607            eprintln!("{e}");
608            return 2;
609        }
610    };
611    a.run_ctx(ctx, args)
612}