1use 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 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 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
105pub 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
113fn 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
141pub 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}