Skip to main content

xyz_rust/cli/
help.rs

1// 帮助渲染:父节点列子命令、叶子列 flags 与用法;default/env/oneof
2// 提示织进描述里。输出文案与 Go 版对齐。
3
4use std::io::Write;
5
6use crate::cli::tree::CmdNode;
7use crate::errors;
8use crate::spec::field::FieldMeta;
9
10pub fn print_help(w: &mut dyn Write, node: &CmdNode, bin: &str) -> errors::Result<()> {
11    // 自定义块:只在叶子命令上生效(中间节点没有 CliHints)。
12    if node.leaf
13        && let Some(entry) = &node.entry
14    {
15        write_help_block(w, &entry.cli.before)?;
16    }
17    let desc = if node.long.is_empty() {
18        node.short.as_str()
19    } else {
20        node.long.as_str()
21    };
22    if !desc.is_empty() {
23        writeln!(w, "{desc}")?;
24        writeln!(w)?;
25    }
26    writeln!(w, "{}", crate::lang::t("help.usage"))?;
27    write!(w, "  {bin}")?;
28    if node.leaf {
29        // 自定义 usage 是相对父路径的收尾段("add <name>"),拼上祖先即可。
30        if !node.usage.is_empty() {
31            let segs: Vec<&str> = node.path.split('.').collect();
32            let prefix = segs[..segs.len().saturating_sub(1)].join(" ");
33            if !prefix.is_empty() {
34                write!(w, " {prefix}")?;
35            }
36            write!(w, " {} [flags]", node.usage)?;
37        } else {
38            write!(w, " {}", node.path.replace('.', " "))?;
39            for f in &node.pos_f {
40                write!(w, " <{}>", f.json_name)?;
41                if !f.required {
42                    write!(w, "?")?;
43                }
44            }
45            if !node.defs.is_empty() || node.pos_f.is_empty() {
46                write!(w, " [flags]")?;
47            }
48        }
49    } else {
50        if !node.path.is_empty() {
51            write!(w, " {}", node.path.replace('.', " "))?;
52        }
53        write!(w, " {}", crate::lang::t("help.commands_placeholder"))?;
54    }
55    writeln!(w)?;
56
57    if !node.aliases.is_empty() {
58        writeln!(w)?;
59        writeln!(w, "{}", crate::lang::t("help.aliases"))?;
60        writeln!(w, "  {}", node.aliases.join(", "))?;
61    }
62    if !node.leaf {
63        writeln!(w)?;
64        writeln!(w, "{}", crate::lang::t("help.commands"))?;
65        let visible: Vec<&CmdNode> = node.children.iter().filter(|c| !c.hidden).collect();
66        let width = visible.iter().map(|c| c.segment.len()).max().unwrap_or(0);
67        for child in visible {
68            writeln!(w, "  {:<width$}  {}", child.segment, child.short)?;
69        }
70    } else if !node.defs.is_empty() {
71        writeln!(w)?;
72        writeln!(w, "{}", crate::lang::t("help.flags"))?;
73        let mut rows: Vec<(String, String)> = Vec::with_capacity(node.defs.len());
74        for d in &node.defs {
75            let mut name = format!("--{}", d.long);
76            if let Some(sh) = d.short {
77                name = format!("-{sh}, {name}");
78            }
79            let typ = flag_type_name(d);
80            rows.push((format!("{name} {typ}"), flag_description(&d.field)));
81        }
82        print_rows(w, &rows)?;
83    }
84    writeln!(w)?;
85    writeln!(w, "{}", crate::lang::t("help.global_flags"))?;
86    print_rows(
87        w,
88        &[
89            ("--json".to_string(), crate::lang::t("help.json_flag")),
90            (
91                "-v, --version".to_string(),
92                crate::lang::t("help.version_flag"),
93            ),
94            ("-h, --help".to_string(), crate::lang::t("help.help_flag")),
95        ],
96    )?;
97    if node.leaf
98        && let Some(entry) = &node.entry
99    {
100        write_help_block(w, &entry.cli.after)?;
101    }
102    Ok(())
103}
104
105/// 原样输出 -h 的自定义文本块:末尾换行归一;空块不输出。
106pub fn write_help_block(w: &mut dyn Write, s: &str) -> errors::Result<()> {
107    if s.is_empty() {
108        return Ok(());
109    }
110    writeln!(w, "{}", s.trim_end_matches('\n')).map_err(errors::Error::from)
111}
112
113/// 按字段类型保真渲染帮助里的 flag 类型;切片同时标注可重复。
114fn flag_type_name(d: &crate::cli::parse::FlagDef) -> &'static str {
115    use crate::spec::field::FieldKind as K;
116    match d.kind {
117        crate::cli::parse::FlagKind::Bool => "bool",
118        crate::cli::parse::FlagKind::Slice => "strings (repeatable)",
119        crate::cli::parse::FlagKind::Str => match d.field.kind {
120            K::I8 | K::I16 | K::I32 | K::I64 | K::U8 | K::U16 | K::U32 | K::U64 => "integer",
121            K::F32 | K::F64 => "number",
122            K::Duration => "duration",
123            K::Time => "time",
124            _ => "string",
125        },
126    }
127}
128
129fn print_rows(w: &mut dyn Write, rows: &[(String, String)]) -> errors::Result<()> {
130    let width = rows.iter().map(|r| r.0.len()).max().unwrap_or(0);
131    for (name, desc) in rows {
132        if desc.is_empty() {
133            writeln!(w, "  {name}")?;
134        } else {
135            writeln!(w, "  {name:<width$}  {desc}")?;
136        }
137    }
138    Ok(())
139}
140
141/// 把默认值、env 回退与枚举一起织进帮助描述里。
142pub fn flag_description(f: &FieldMeta) -> String {
143    let mut desc = f.description.clone();
144    if let Some(def) = &f.cli.default {
145        desc += &format!(" (default {})", display_value(def));
146    } else if let Some(def) = &f.default {
147        desc += &format!(" (default {})", display_value(def));
148    }
149    if let Some(env) = &f.cli.env_var {
150        desc += &format!(" (env {env})");
151    }
152    if !f.enum_values.is_empty() {
153        let vals = f
154            .enum_values
155            .iter()
156            .map(display_value)
157            .collect::<Vec<_>>()
158            .join("|");
159        desc += &format!(" (oneof {vals})");
160    }
161    desc
162}
163
164fn display_value(v: &serde_json::Value) -> String {
165    match v {
166        serde_json::Value::String(s) => s.clone(),
167        other => other.to_string(),
168    }
169}