1use crate::schema::tool::canonical::{
22 CanonicalSurfaceRef, canonical_option_type, canonical_positional_type,
23};
24use crate::schema::tool::constraints::validate_tool_constraints;
25use crate::schema::tool::{
26 DuplicateKeyPolicy, FlagShape, FlagSpec, OptionShape, OptionSpec, Positional, Repetition,
27 TailPositional, Tool,
28};
29use crate::schema::{SchemaGraph, SchemaType, SchemaValue, TypedSchemaValue};
30use std::collections::HashMap;
31
32pub enum ParsedToolArguments {
34 Help(String),
36 Invoke {
38 command_path: Vec<String>,
40 input: Box<TypedSchemaValue>,
42 },
43}
44
45#[derive(Clone, Copy)]
46enum Surface<'a> {
47 Option(&'a OptionSpec),
48 Flag(&'a FlagSpec),
49 Positional(usize),
50 Tail,
51}
52
53enum Token<'a> {
54 Help,
55 TailSeparator {
56 verbatim: bool,
57 },
58 EndOptions,
59 LongOption {
60 option: &'a OptionSpec,
61 attached: Option<String>,
62 },
63 LongFlag {
64 flag: &'a FlagSpec,
65 negated: bool,
66 },
67 ShortOptions,
68 Subcommand(usize),
69 Positional,
70 Tail,
71 TailStart,
73}
74
75struct ParseCursor {
76 options_enabled: bool,
77 tail_started: bool,
78 body_started: bool,
79}
80
81impl ParseCursor {
82 fn classify<'a>(
83 &self,
84 tool: &'a Tool,
85 node_index: usize,
86 token: &str,
87 fixed_count: usize,
88 ) -> Result<Token<'a>, String> {
89 let node = &tool.commands.nodes[node_index];
90 if !self.tail_started
93 && let Some(spec) = node
94 .body
95 .as_ref()
96 .filter(|body| {
97 body.positionals
98 .fixed
99 .iter()
100 .skip(fixed_count)
101 .all(|p| !p.required || p.default.is_some())
102 })
103 .and_then(|body| body.positionals.tail.as_ref())
104 .filter(|tail| tail.separator.as_deref() == Some(token))
105 {
106 return Ok(Token::TailSeparator {
107 verbatim: spec.verbatim,
108 });
109 }
110 let implicit_tail = !self.tail_started && implicit_tail(tool, node_index);
111 if self.options_enabled {
112 let surfaces = effective_named(tool, node_index);
113 if let Some(long) = token.strip_prefix("--") {
114 let (name, attached) = long
115 .split_once('=')
116 .map_or((long, None), |(name, value)| (name, Some(value)));
117 if let Some(option) = surfaces.iter().find_map(|(_, surface)| match surface {
118 Surface::Option(option)
119 if option.long == name
120 || option.aliases.iter().any(|alias| alias == name) =>
121 {
122 Some(*option)
123 }
124 _ => None,
125 }) {
126 return Ok(Token::LongOption {
127 option,
128 attached: attached.map(str::to_owned),
129 });
130 }
131 if let Some((_, flag, negated)) = find_long_flag(&surfaces, name) {
132 if attached.is_some() {
133 return Err(format!("flag --{name} does not take a value"));
134 }
135 return Ok(Token::LongFlag { flag, negated });
136 }
137 }
138 if let Some(cluster) = token.strip_prefix('-')
143 && let Some(short) = cluster.chars().next()
144 && if implicit_tail {
145 declared_short_cluster(tool, node_index, &surfaces, cluster)
146 } else {
147 find_short(&surfaces, short).is_some()
148 }
149 {
150 return Ok(Token::ShortOptions);
151 }
152 if !self.body_started
153 && let Some(child) = child_named(tool, node_index, token)
154 {
155 return Ok(Token::Subcommand(child));
156 }
157 if help_available(tool, node_index) && matches!(token, "--help" | "-h") {
158 return Ok(Token::Help);
159 }
160 if token == "--" {
161 return Ok(Token::EndOptions);
162 }
163 if implicit_tail {
164 return Ok(Token::TailStart);
165 }
166 if token.starts_with("--") && token.len() > 2 {
167 let name = token[2..].split('=').next().unwrap();
168 return Err(format!("unknown option --{name}"));
169 }
170 if token.starts_with('-') && token != "-" && !looks_negative_number(token) {
171 return Ok(Token::ShortOptions);
172 }
173 }
174 let body = node
175 .body
176 .as_ref()
177 .ok_or_else(|| format!("expected a subcommand, found {token:?}"))?;
178 if !self.tail_started && fixed_count < body.positionals.fixed.len() {
179 return Ok(Token::Positional);
180 }
181 if let Some(spec) = &body.positionals.tail {
182 if !self.tail_started
183 && let Some(separator) = &spec.separator
184 {
185 return Err(format!("expected tail separator {separator:?}"));
186 }
187 return Ok(Token::Tail);
188 }
189 Err(format!("unexpected argument {token:?}"))
190 }
191}
192
193pub fn parse(tool: &Tool, args: &[String]) -> Result<ParsedToolArguments, String> {
210 parse_with_variables(tool, args, &HashMap::new())
211}
212
213pub fn parse_with_variables(
223 tool: &Tool,
224 args: &[String],
225 variables: &HashMap<String, String>,
226) -> Result<ParsedToolArguments, String> {
227 if tool.commands.nodes.is_empty() {
228 return Err("tool metadata has no root command".into());
229 }
230 let mut node_index = 0;
231 let mut path = Vec::new();
232 let mut supplied: HashMap<String, Vec<String>> = HashMap::new();
233 let mut flags: HashMap<String, (u32, Option<bool>)> = HashMap::new();
234 let mut positionals = Vec::new();
235 let mut tail = Vec::new();
236 let mut cursor = ParseCursor {
237 options_enabled: true,
238 tail_started: false,
239 body_started: false,
240 };
241 let mut i = 0;
242
243 while i < args.len() {
244 let token = &args[i];
245 let node = &tool.commands.nodes[node_index];
246 match cursor.classify(tool, node_index, token, positionals.len())? {
247 Token::Help => {
248 return Ok(ParsedToolArguments::Help(render_help(
249 tool, node_index, &path,
250 )));
251 }
252 Token::TailSeparator { verbatim } => {
253 cursor.options_enabled &= !verbatim;
254 cursor.tail_started = true;
255 cursor.body_started = true;
256 }
257 Token::EndOptions => cursor.options_enabled = false,
258 Token::LongOption { option, attached } => {
259 let value = option_value(option, attached, args, &mut i)?;
260 supplied.entry(option.long.clone()).or_default().push(value);
261 cursor.body_started |= node
262 .body
263 .as_ref()
264 .is_some_and(|body| body.options.iter().any(|o| std::ptr::eq(o, option)));
265 }
266 Token::LongFlag { flag, negated } => {
267 apply_flag(&mut flags, flag, negated)?;
268 cursor.body_started |= node
269 .body
270 .as_ref()
271 .is_some_and(|body| body.flags.iter().any(|f| std::ptr::eq(f, flag)));
272 }
273 Token::ShortOptions => {
274 let surfaces = effective_named(tool, node_index);
275 let chars: Vec<char> = token[1..].chars().collect();
276 let mut c = 0;
277 while c < chars.len() {
278 let short = chars[c];
279 if help_available(tool, node_index) && short == 'h' {
280 return Ok(ParsedToolArguments::Help(render_help(
281 tool, node_index, &path,
282 )));
283 }
284 let Some(surface) = find_short(&surfaces, short) else {
285 return Err(format!("unknown short option -{short}"));
286 };
287 match surface {
288 Surface::Flag(flag) => {
289 apply_flag(&mut flags, flag, false)?;
290 cursor.body_started |= node.body.as_ref().is_some_and(|body| {
291 body.flags.iter().any(|f| std::ptr::eq(f, flag))
292 });
293 }
294 Surface::Option(option) => {
295 cursor.body_started |= node.body.as_ref().is_some_and(|body| {
296 body.options.iter().any(|o| std::ptr::eq(o, option))
297 });
298 let rest: String = chars[c + 1..].iter().collect();
299 let attached = (!rest.is_empty()).then_some(rest);
300 supplied
301 .entry(option.long.clone())
302 .or_default()
303 .push(option_value(option, attached, args, &mut i)?);
304 break;
305 }
306 _ => unreachable!(),
307 }
308 c += 1;
309 }
310 }
311 Token::Subcommand(child) => {
312 node_index = child;
313 path.push(tool.commands.nodes[child].name.clone());
314 }
315 Token::Positional => {
316 cursor.body_started = true;
317 positionals.push(token.clone());
318 }
319 Token::Tail => {
320 cursor.body_started = true;
321 tail.push(token.clone());
322 }
323 Token::TailStart => {
324 cursor.options_enabled = false;
325 cursor.tail_started = true;
326 cursor.body_started = true;
327 tail.push(token.clone());
328 }
329 }
330 i += 1;
331 }
332
333 let node = &tool.commands.nodes[node_index];
334 let body = node
335 .body
336 .as_ref()
337 .ok_or_else(|| "a subcommand is required".to_string())?;
338 let surfaces = tool.canonical_input_surfaces(node_index);
339 let mut from_variable: HashMap<&str, &str> = HashMap::new();
342 for surface_ref in surfaces.iter().copied() {
343 match resolve_surface(tool, node_index, surface_ref)? {
344 Surface::Option(option) if !supplied.contains_key(&option.long) => {
345 if let Some((name, value)) = declared_variable(&option.env_var, variables) {
346 supplied.insert(option.long.clone(), vec![value.to_owned()]);
347 from_variable.insert(&option.long, name);
348 }
349 }
350 Surface::Flag(flag) if !flags.contains_key(&flag.long) => {
351 if let Some((name, value)) = declared_variable(&flag.env_var, variables) {
352 let state = flag_from_variable(flag, value)
353 .map_err(|e| format!("--{} from variable {name}: {e}", flag.long))?;
354 flags.insert(flag.long.clone(), state);
355 }
356 }
357 _ => {}
358 }
359 }
360 let mut values = Vec::with_capacity(surfaces.len());
361 for surface_ref in surfaces.iter().copied() {
362 let surface = resolve_surface(tool, node_index, surface_ref)?;
363 let value = collect_value(
364 tool,
365 node_index,
366 surface,
367 &supplied,
368 &flags,
369 &positionals,
370 &tail,
371 )
372 .map_err(|e| match surface {
373 Surface::Option(option) => match from_variable.get(option.long.as_str()) {
374 Some(name) => format!("--{} from variable {name}: {e}", option.long),
375 None => e,
376 },
377 _ => e,
378 })?;
379 values.push(value);
380 }
381 let model = tool
382 .canonical_input_model(node_index)
383 .map_err(|e| e.to_string())?;
384 let record = SchemaValue::Record { fields: values };
385 let decoded = model
386 .decode_record(record.clone())
387 .map_err(|e| e.to_string())?;
388 validate_tool_constraints(tool, node_index, &body.constraints, &surfaces, &decoded)?;
389 crate::schema::validation::validate_value(
390 &model.record_schema,
391 &model.record_schema.root,
392 &record,
393 )
394 .map_err(|errors| format!("invalid tool arguments: {errors:?}"))?;
395 Ok(ParsedToolArguments::Invoke {
396 command_path: path,
397 input: Box::new(TypedSchemaValue::new(model.record_schema, record)),
398 })
399}
400
401fn effective_named(tool: &Tool, command: usize) -> Vec<((usize, usize), Surface<'_>)> {
402 let mut path = Vec::new();
403 find_node_path(tool, 0, command, &mut path);
404 let mut result = Vec::new();
405 for node_index in path {
406 let node = &tool.commands.nodes[node_index];
407 result.extend(
408 node.globals
409 .options
410 .iter()
411 .enumerate()
412 .map(|(index, option)| ((node_index, index), Surface::Option(option))),
413 );
414 result.extend(
415 node.globals
416 .flags
417 .iter()
418 .enumerate()
419 .map(|(index, flag)| ((node_index, index), Surface::Flag(flag))),
420 );
421 }
422 if let Some(body) = &tool.commands.nodes[command].body {
423 result.extend(
424 body.options
425 .iter()
426 .enumerate()
427 .map(|(index, option)| ((command, index), Surface::Option(option))),
428 );
429 result.extend(
430 body.flags
431 .iter()
432 .enumerate()
433 .map(|(index, flag)| ((command, index), Surface::Flag(flag))),
434 );
435 }
436 result
437}
438
439fn find_node_path(tool: &Tool, current: usize, target: usize, path: &mut Vec<usize>) -> bool {
440 path.push(current);
441 if current == target {
442 return true;
443 }
444 for child in tool.commands.nodes[current]
445 .subcommands
446 .iter()
447 .filter_map(|index| index.as_usize())
448 {
449 if find_node_path(tool, child, target, path) {
450 return true;
451 }
452 }
453 path.pop();
454 false
455}
456
457fn resolve_surface(
458 tool: &Tool,
459 command: usize,
460 r: CanonicalSurfaceRef,
461) -> Result<Surface<'_>, String> {
462 let body = tool.commands.nodes[command]
463 .body
464 .as_ref()
465 .ok_or("command has no body")?;
466 Ok(match r {
467 CanonicalSurfaceRef::GlobalOption { node, index } => {
468 Surface::Option(&tool.commands.nodes[node].globals.options[index])
469 }
470 CanonicalSurfaceRef::GlobalFlag { node, index } => {
471 Surface::Flag(&tool.commands.nodes[node].globals.flags[index])
472 }
473 CanonicalSurfaceRef::BodyPositional { index } => Surface::Positional(index),
474 CanonicalSurfaceRef::BodyTail => Surface::Tail,
475 CanonicalSurfaceRef::BodyOption { index } => Surface::Option(&body.options[index]),
476 CanonicalSurfaceRef::BodyFlag { index } => Surface::Flag(&body.flags[index]),
477 })
478}
479
480fn collect_value(
481 tool: &Tool,
482 command: usize,
483 surface: Surface<'_>,
484 supplied: &HashMap<String, Vec<String>>,
485 flags: &HashMap<String, (u32, Option<bool>)>,
486 positionals: &[String],
487 tail: &[String],
488) -> Result<SchemaValue, String> {
489 let graph = &tool.schema;
490 match surface {
491 Surface::Positional(index) => {
492 let p = &tool.commands.nodes[command]
493 .body
494 .as_ref()
495 .unwrap()
496 .positionals
497 .fixed[index];
498 let type_ = canonical_positional_type(p);
499 match positionals.get(index) {
500 Some(v) => decode(graph, &type_, v),
501 None if p.default.is_some() => Ok(p.default.clone().unwrap()),
502 None if !p.required => omitted(resolve(graph, &type_)?),
503 None => Err(format!("missing required positional {}", p.name)),
504 }
505 }
506 Surface::Tail => {
507 let p = tool.commands.nodes[command]
508 .body
509 .as_ref()
510 .unwrap()
511 .positionals
512 .tail
513 .as_ref()
514 .unwrap();
515 if tail.len() < p.min as usize || p.max.is_some_and(|m| tail.len() > m as usize) {
516 return Err(format!("tail {} has invalid cardinality", p.name));
517 }
518 Ok(SchemaValue::List {
519 elements: tail
520 .iter()
521 .map(|v| decode(graph, &p.item_type, v))
522 .collect::<Result<_, _>>()?,
523 })
524 }
525 Surface::Flag(flag) => {
526 let (count, boolean) = flags.get(&flag.long).copied().unwrap_or_default();
527 match flag.shape {
528 FlagShape::BoolFlag(s) => Ok(SchemaValue::Bool(boolean.unwrap_or(s.default))),
529 FlagShape::CountFlag(_) => Ok(SchemaValue::U32(count)),
530 }
531 }
532 Surface::Option(option) => {
533 let raw = supplied.get(&option.long).cloned();
534 if raw.is_none() {
535 if let Some(default) = &option.default {
536 return Ok(default.clone());
537 }
538 if option.required {
539 return Err(format!("missing required option --{}", option.long));
540 }
541 }
542 collect_option(graph, option, raw.unwrap_or_default())
543 }
544 }
545}
546
547fn collect_option(
548 graph: &SchemaGraph,
549 option: &OptionSpec,
550 raw: Vec<String>,
551) -> Result<SchemaValue, String> {
552 let repeatable = match &option.shape {
553 OptionShape::Scalar(_) | OptionShape::OptionalScalar(_) => false,
554 OptionShape::RepeatableList(shape) => !matches!(shape.repetition, Repetition::Delimited(_)),
555 OptionShape::RepeatableMap(shape) => !matches!(shape.repetition, Repetition::Delimited(_)),
556 };
557 if raw.len() > 1 && !repeatable {
558 return Err(format!("--{} cannot be repeated", option.long));
559 }
560 match &option.shape {
561 OptionShape::Scalar(_) => {
562 let ty = canonical_option_type(option);
563 raw.last()
564 .map(|v| decode(graph, &ty, v))
565 .unwrap_or_else(|| omitted(resolve(graph, &ty)?))
566 }
567 OptionShape::OptionalScalar(_) => {
568 let ty = canonical_option_type(option);
569 match raw.last() {
570 Some(value) if value == "\0" => option
571 .default
572 .clone()
573 .ok_or_else(|| format!("--{} has no bare-value default", option.long)),
574 Some(value) => decode(graph, &ty, value),
575 None => omitted(resolve(graph, &ty)?),
576 }
577 }
578 OptionShape::RepeatableList(shape) => {
579 let parts = expand(&raw, shape.repetition);
580 Ok(SchemaValue::List {
581 elements: parts
582 .iter()
583 .map(|v| decode(graph, &shape.item_type, v))
584 .collect::<Result<_, _>>()?,
585 })
586 }
587 OptionShape::RepeatableMap(shape) => {
588 let SchemaType::Map { key, value, .. } = resolve(graph, &shape.map_type)? else {
589 return Err(format!("--{} does not declare a map type", option.long));
590 };
591 let mut entries: Vec<(SchemaValue, SchemaValue)> = Vec::new();
592 for item in expand(&raw, shape.repetition) {
593 let (k, v) = item
594 .split_once('=')
595 .ok_or_else(|| format!("--{} expects KEY=VALUE", option.long))?;
596 let pair = (decode(graph, key, k)?, decode(graph, value, v)?);
597 if let Some(index) = entries.iter().position(|(old, _)| old == &pair.0) {
598 match shape.duplicate_key_policy {
599 DuplicateKeyPolicy::Reject => {
600 return Err(format!("duplicate key for --{}", option.long));
601 }
602 DuplicateKeyPolicy::LastWins => entries[index] = pair,
603 }
604 } else {
605 entries.push(pair);
606 }
607 }
608 Ok(SchemaValue::Map { entries })
609 }
610 }
611}
612
613fn option_value(
614 option: &OptionSpec,
615 attached: Option<String>,
616 args: &[String],
617 i: &mut usize,
618) -> Result<String, String> {
619 if let Some(value) = attached {
620 return Ok(value);
621 }
622 if matches!(option.shape, OptionShape::OptionalScalar(_)) {
623 return Ok("\0".to_string());
624 }
625 *i += 1;
626 args.get(*i)
627 .cloned()
628 .ok_or_else(|| format!("--{} requires a value", option.long))
629}
630
631fn declared_variable<'a>(
632 declared: &'a Option<String>,
633 variables: &'a HashMap<String, String>,
634) -> Option<(&'a str, &'a str)> {
635 let name = declared.as_deref()?;
636 Some((name, variables.get(name)?))
637}
638
639fn flag_from_variable(flag: &FlagSpec, value: &str) -> Result<(u32, Option<bool>), String> {
640 match flag.shape {
641 FlagShape::BoolFlag(_) => match bool_word(value) {
642 Some(state) => Ok((0, Some(state))),
643 None => Err(format!(
644 "invalid value {value:?}: expected true, yes, on, y, t or 1, or false, no, off, n, f or 0"
645 )),
646 },
647 FlagShape::CountFlag(max) => {
648 let count: u32 = value
649 .parse()
650 .map_err(|_| format!("invalid value {value:?}: expected a count"))?;
651 if max.is_some_and(|max| count > max) {
652 return Err(format!("invalid value {value:?}: exceeds maximum count"));
653 }
654 Ok((count, None))
655 }
656 }
657}
658
659fn bool_word(value: &str) -> Option<bool> {
662 const TRUE: [&str; 6] = ["1", "true", "yes", "on", "y", "t"];
663 const FALSE: [&str; 6] = ["0", "false", "no", "off", "n", "f"];
664 if TRUE.iter().any(|word| value.eq_ignore_ascii_case(word)) {
665 Some(true)
666 } else if FALSE.iter().any(|word| value.eq_ignore_ascii_case(word)) {
667 Some(false)
668 } else {
669 None
670 }
671}
672
673fn apply_flag(
674 store: &mut HashMap<String, (u32, Option<bool>)>,
675 flag: &FlagSpec,
676 negated: bool,
677) -> Result<(), String> {
678 let entry = store.entry(flag.long.clone()).or_default();
679 match flag.shape {
680 FlagShape::BoolFlag(_) => entry.1 = Some(!negated),
681 FlagShape::CountFlag(max) => {
682 entry.0 += 1;
683 if max.is_some_and(|m| entry.0 > m) {
684 return Err(format!("--{} exceeds maximum count", flag.long));
685 }
686 }
687 }
688 Ok(())
689}
690
691fn find_short<'a>(surfaces: &[((usize, usize), Surface<'a>)], short: char) -> Option<Surface<'a>> {
692 surfaces.iter().find_map(|(_, surface)| match surface {
693 Surface::Option(option) if option.short == Some(short) => Some(*surface),
694 Surface::Flag(flag) if flag.short == Some(short) => Some(*surface),
695 _ => None,
696 })
697}
698
699fn find_long_flag<'a>(
700 surfaces: &[((usize, usize), Surface<'a>)],
701 name: &str,
702) -> Option<((usize, usize), &'a FlagSpec, bool)> {
703 surfaces
704 .iter()
705 .find_map(|(key, s)| match s {
706 Surface::Flag(f) if f.long == name || f.aliases.iter().any(|a| a == name) => {
707 Some((*key, *f, false))
708 }
709 _ => None,
710 })
711 .or_else(|| {
712 surfaces.iter().find_map(|(key, s)| match s {
713 Surface::Flag(f)
714 if matches!(f.shape, FlagShape::BoolFlag(s) if s.negatable)
715 && name == format!("no-{}", f.long) =>
716 {
717 Some((*key, *f, true))
718 }
719 _ => None,
720 })
721 })
722}
723
724fn decode(graph: &SchemaGraph, ty: &SchemaType, raw: &str) -> Result<SchemaValue, String> {
725 let resolved = resolve(graph, ty)?;
726 if let SchemaType::Option { inner, .. } = resolved {
727 return Ok(SchemaValue::Option {
728 inner: Some(Box::new(decode(graph, inner, raw)?)),
729 });
730 }
731 let json = match resolved {
732 SchemaType::String { .. }
733 | SchemaType::Char { .. }
734 | SchemaType::Path { .. }
735 | SchemaType::Url { .. }
736 | SchemaType::Uuid { .. }
737 | SchemaType::Datetime { .. }
738 | SchemaType::Duration { .. } => serde_json::Value::String(raw.into()),
739 SchemaType::Enum { cases, .. } => serde_json::Value::String(enum_case(cases, raw).into()),
740 SchemaType::Bool { .. } if raw.eq_ignore_ascii_case("true") => {
741 serde_json::Value::Bool(true)
742 }
743 SchemaType::Bool { .. } if raw.eq_ignore_ascii_case("false") => {
744 serde_json::Value::Bool(false)
745 }
746 SchemaType::U64 { .. } | SchemaType::S64 { .. } if !raw.starts_with('"') => {
749 serde_json::Value::String(raw.into())
750 }
751 _ => serde_json::from_str(raw).map_err(|e| format!("invalid value {raw:?}: {e}"))?,
752 };
753 crate::schema::render::from_untrusted_json_value(graph, ty, &json)
754 .map_err(|e| format!("invalid value {raw:?}: {e}"))
755}
756
757fn enum_case<'a>(cases: &'a [String], raw: &'a str) -> &'a str {
760 if cases.iter().any(|case| case == raw) {
761 return raw;
762 }
763 let mut equal = cases.iter().filter(|case| case.eq_ignore_ascii_case(raw));
764 match (equal.next(), equal.next()) {
765 (Some(case), None) => case,
766 _ => raw,
767 }
768}
769
770fn resolve<'a>(graph: &'a SchemaGraph, ty: &'a SchemaType) -> Result<&'a SchemaType, String> {
771 graph.resolve_ref(ty).map_err(|e| e.to_string())
772}
773
774fn omitted(ty: &SchemaType) -> Result<SchemaValue, String> {
775 match ty {
776 SchemaType::Option { .. } => Ok(SchemaValue::Option { inner: None }),
777 SchemaType::List { .. } => Ok(SchemaValue::List { elements: vec![] }),
778 SchemaType::Map { .. } => Ok(SchemaValue::Map { entries: vec![] }),
779 _ => Err("optional argument must have an option or collection schema, or a default".into()),
780 }
781}
782
783fn expand(values: &[String], repetition: Repetition) -> Vec<String> {
784 match repetition {
785 Repetition::Repeated => values.to_vec(),
786 Repetition::Delimited(c) | Repetition::Either(c) => values
787 .iter()
788 .flat_map(|v| v.split(c).map(str::to_string))
789 .collect(),
790 }
791}
792
793fn child_named(tool: &Tool, node: usize, name: &str) -> Option<usize> {
794 tool.commands.nodes[node]
795 .subcommands
796 .iter()
797 .filter_map(|i| i.as_usize())
798 .find(|i| {
799 tool.commands
800 .nodes
801 .get(*i)
802 .is_some_and(|n| n.name == name || n.aliases.iter().any(|a| a == name))
803 })
804}
805
806fn looks_negative_number(token: &str) -> bool {
807 token[1..].starts_with(|c: char| c.is_ascii_digit() || c == '.')
808}
809
810fn implicit_tail(tool: &Tool, node: usize) -> bool {
816 let command = &tool.commands.nodes[node];
817 command.subcommands.is_empty()
818 && command.body.as_ref().is_some_and(|body| {
819 body.positionals.fixed.is_empty()
820 && body
821 .positionals
822 .tail
823 .as_ref()
824 .is_some_and(|tail| tail.verbatim && tail.separator.as_deref() == Some("--"))
825 })
826}
827
828fn declared_short_cluster(
832 tool: &Tool,
833 node: usize,
834 surfaces: &[((usize, usize), Surface<'_>)],
835 cluster: &str,
836) -> bool {
837 let help = help_available(tool, node);
838 for short in cluster.chars() {
839 if help && short == 'h' {
840 return true;
841 }
842 match find_short(surfaces, short) {
843 Some(Surface::Flag(_)) => {}
844 Some(_) => return true,
845 None => return false,
846 }
847 }
848 !cluster.is_empty()
849}
850
851fn help_available(tool: &Tool, node: usize) -> bool {
852 if tool.commands.nodes[node]
853 .body
854 .as_ref()
855 .and_then(|body| body.positionals.tail.as_ref())
856 .and_then(|tail| tail.separator.as_deref())
857 .is_some_and(|separator| matches!(separator, "--help" | "-h"))
858 {
859 return false;
860 }
861 !effective_named(tool, node).iter().any(|(_, s)| match s {
862 Surface::Option(o) => {
863 o.long == "help" || o.aliases.iter().any(|a| a == "help") || o.short == Some('h')
864 }
865 Surface::Flag(f) => {
866 f.long == "help" || f.aliases.iter().any(|a| a == "help") || f.short == Some('h')
867 }
868 _ => false,
869 })
870}
871
872fn render_help(tool: &Tool, node: usize, path: &[String]) -> String {
873 let command = &tool.commands.nodes[node];
874 let mut out = format!("Usage: {}", tool.name().unwrap_or("tool"));
875 for part in path {
876 out.push(' ');
877 out.push_str(part);
878 }
879 if !effective_named(tool, node).is_empty() {
880 out.push_str(" [OPTIONS]");
881 }
882 if !command.subcommands.is_empty() {
883 out.push_str(" [COMMAND]");
884 }
885 if let Some(body) = &command.body {
886 for p in &body.positionals.fixed {
887 out.push(' ');
888 out.push_str(&positional_label(p));
889 }
890 if let Some(tail) = &body.positionals.tail {
891 if let Some(separator) = &tail.separator {
892 if implicit_tail(tool, node) {
893 out.push_str(&format!(" [{separator}]"));
894 } else {
895 out.push_str(&format!(" {separator}"));
896 }
897 }
898 out.push(' ');
899 out.push_str(&tail_label(tail));
900 }
901 }
902 out.push('\n');
903 if !command.doc.summary.is_empty() {
904 out.push('\n');
905 out.push_str(&command.doc.summary);
906 out.push('\n');
907 }
908 if !command.doc.description.is_empty() {
909 out.push_str(&command.doc.description);
910 out.push('\n');
911 }
912 if !command.subcommands.is_empty() {
913 out.push_str("\nCommands:\n");
914 for child in &command.subcommands {
915 if let Some(c) = child.as_usize().and_then(|i| tool.commands.nodes.get(i)) {
916 out.push_str(&format!(" {:16} {}\n", c.name, c.doc.summary));
917 }
918 }
919 }
920 if let Some(body) = &command.body {
921 let mut arguments: Vec<_> = body
922 .positionals
923 .fixed
924 .iter()
925 .map(|p| (positional_label(p), &p.doc, &p.type_, ""))
926 .collect();
927 if let Some(tail) = &body.positionals.tail {
928 let start = if implicit_tail(tool, node) {
929 " [starts at the first argument that is not an option of this command, or after --]"
930 } else {
931 ""
932 };
933 arguments.push((tail_label(tail), &tail.doc, &tail.item_type, start));
934 }
935 if !arguments.is_empty() {
936 out.push_str("\nArguments:\n");
937 }
938 for (label, doc, type_, start) in arguments {
939 let text = format!(
940 "{}{}{start}",
941 doc.summary,
942 possible_values(&tool.schema, type_)
943 );
944 let line = format!(" {label:16} {}", text.trim_start());
945 out.push_str(line.trim_end());
946 out.push('\n');
947 }
948 }
949 out.push_str("\nOptions:\n");
950 for (_, s) in effective_named(tool, node) {
951 match s {
952 Surface::Option(o) => {
953 let short = o.short.map(|s| format!("-{s}, ")).unwrap_or_default();
954 let value = o.value_name.as_deref().unwrap_or("VALUE");
955 let value = if matches!(o.shape, OptionShape::OptionalScalar(_)) {
956 format!("[={value}]")
957 } else {
958 format!(" <{value}>")
959 };
960 out.push_str(&format!(" {short}--{}{value} {}", o.long, o.doc.summary));
961 if o.required {
962 out.push_str(" [required]");
963 }
964 if o.default.is_some() {
965 out.push_str(" [has default]");
966 }
967 if !o.aliases.is_empty() {
968 out.push_str(&format!(" [aliases: {}]", o.aliases.join(", ")));
969 }
970 match &o.shape {
971 OptionShape::Scalar(type_) | OptionShape::OptionalScalar(type_) => {
972 out.push_str(&possible_values(&tool.schema, type_));
973 }
974 OptionShape::RepeatableList(shape) => {
975 out.push_str(&possible_values(&tool.schema, &shape.item_type));
976 }
977 OptionShape::RepeatableMap(_) => {}
978 }
979 out.push('\n');
980 }
981 Surface::Flag(f) => {
982 let short = f.short.map(|s| format!("-{s}, ")).unwrap_or_default();
983 out.push_str(&format!(" {short}--{} {}", f.long, f.doc.summary));
984 if let FlagShape::BoolFlag(shape) = f.shape
985 && shape.negatable
986 {
987 out.push_str(&format!(" [--no-{}]", f.long));
988 }
989 if !f.aliases.is_empty() {
990 out.push_str(&format!(" [aliases: {}]", f.aliases.join(", ")));
991 }
992 out.push('\n');
993 }
994 _ => {}
995 }
996 }
997 if help_available(tool, node) {
998 out.push_str(" -h, --help Print help (reserved only when undeclared)\n");
999 }
1000 out
1001}
1002
1003fn positional_label(positional: &Positional) -> String {
1005 let name = positional.value_name.as_deref().unwrap_or(&positional.name);
1006 if positional.required && positional.default.is_none() {
1007 format!("<{name}>")
1008 } else {
1009 format!("[{name}]")
1010 }
1011}
1012
1013fn tail_label(tail: &TailPositional) -> String {
1015 format!("[{}]...", tail.value_name.as_deref().unwrap_or(&tail.name))
1016}
1017
1018fn possible_values(graph: &SchemaGraph, type_: &SchemaType) -> String {
1020 match enum_cases(graph, type_) {
1021 Some(cases) => format!(" [possible values: {}]", cases.join(", ")),
1022 None => String::new(),
1023 }
1024}
1025
1026fn enum_cases<'a>(graph: &'a SchemaGraph, type_: &'a SchemaType) -> Option<&'a [String]> {
1028 let mut current = type_;
1029 for _ in 0..8 {
1031 current = match graph.resolve_ref(current).ok()? {
1032 SchemaType::Enum { cases, .. } => return Some(cases),
1033 SchemaType::Option { inner, .. } => inner,
1034 _ => return None,
1035 };
1036 }
1037 None
1038}
1039
1040#[cfg(test)]
1041mod tests;