1use 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
26static CLI_VERSION: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new();
29
30pub 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 pub(crate) default_format: String,
46 pub(crate) format_from_flag: bool,
48 pub(crate) format_interactive: String,
50 pub(crate) format_piped: String,
52 pub(crate) interactive_override: Option<bool>,
54 pub(crate) out_injected: bool,
56}
57
58#[derive(Default)]
60pub struct Options {
61 pub out: Option<Box<dyn Write + Send>>,
62 pub err_out: Option<Box<dyn Write + Send>>,
63 pub format: Option<String>,
65 pub format_from_flag: bool,
67 pub format_interactive: Option<String>,
69 pub format_piped: Option<String>,
71 pub interactive: Option<bool>,
73}
74
75pub struct ExecContext {
77 pub path: String,
79 pub entry: Arc<Entry>,
81 pub format: Format,
83 pub json: bool,
85 pub out: SharedWriter,
87}
88
89pub 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 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 pub fn new_with_options(reg: &Registry, opts: Options) -> errors::Result<App> {
125 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 }
161 Ok(a)
162 }
163
164 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 pub fn use_mw(&mut self, mw: ExecFunc) {
182 self.mws.push(mw);
183 }
184
185 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 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; }
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 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 _ => 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 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 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 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 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 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 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; }
356 return Some(node);
357 }
358 }
359
360 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 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 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 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 let terminal = |ctx: &Ctx, ec: &ExecContext, args: &mut Map<String, Value>| {
509 let out = (ec.entry.invoke)(ctx, args)?;
510 if ec.entry.cli.daemon {
512 return Ok(());
513 }
514 let mut w = ec.out.lock().unwrap();
515 match ec.format {
516 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 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
570fn node_has_flag(node: &CmdNode, long: &str) -> bool {
572 node.leaf && node.defs.iter().any(|d| d.long == long)
573}
574
575pub(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
583pub 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
597pub fn run(reg: &Registry, args: &[String]) -> i32 {
599 run_context(&Ctx::new(), reg, args, Options::default())
600}
601
602pub 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}