argot_cmd/model/command.rs
1use std::collections::HashMap;
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5
6use super::{Argument, BuildError, Example, Flag};
7
8/// A handler function that can be registered on a [`Command`].
9///
10/// The function is stored in an [`Arc`] so that [`Command::clone`] only
11/// increments a reference count rather than copying the closure. The
12/// higher-ranked trait bound (`for<'a>`) allows the handler to be called with
13/// a [`ParsedCommand`] of any lifetime, which is required because the parsed
14/// command borrows from the command tree at call time.
15///
16/// # Examples
17///
18/// ```
19/// # use std::sync::Arc;
20/// # use argot_cmd::HandlerFn;
21/// let handler: HandlerFn = Arc::new(|parsed| {
22/// println!("running command: {}", parsed.command.canonical);
23/// Ok(())
24/// });
25/// ```
26pub type HandlerFn =
27 Arc<dyn for<'a> Fn(&ParsedCommand<'a>) -> Result<(), Box<dyn std::error::Error>> + Send + Sync>;
28
29/// An async handler function that can be registered on a [`Command`].
30///
31/// The function receives a [`ParsedCommand`] reference and returns a
32/// pinned, boxed future. Use [`Command::builder`] → [`CommandBuilder::async_handler`]
33/// to register one.
34///
35/// # Feature
36///
37/// Requires the `async` feature flag.
38///
39/// # Examples
40///
41/// ```
42/// # #[cfg(feature = "async")] {
43/// use std::sync::Arc;
44/// use argot_cmd::AsyncHandlerFn;
45///
46/// let handler: AsyncHandlerFn = Arc::new(|parsed| Box::pin(async move {
47/// println!("async command: {}", parsed.command.canonical);
48/// Ok(())
49/// }));
50/// # }
51/// ```
52#[cfg(feature = "async")]
53pub type AsyncHandlerFn = std::sync::Arc<
54 dyn for<'a> Fn(
55 &'a ParsedCommand<'a>,
56 ) -> std::pin::Pin<
57 Box<
58 dyn std::future::Future<
59 Output = Result<(), Box<dyn std::error::Error + Send + Sync>>,
60 > + Send
61 + 'a,
62 >,
63 > + Send
64 + Sync,
65>;
66
67/// The result of successfully parsing an invocation against a [`Command`].
68///
69/// `ParsedCommand` borrows the matched [`Command`] from the registry (lifetime
70/// `'a`) and owns the resolved argument and flag value maps. Keys in both maps
71/// are the canonical names of the argument/flag definitions.
72///
73/// # Examples
74///
75/// ```
76/// # use argot_cmd::{Command, Argument, Parser};
77/// let cmd = Command::builder("get")
78/// .argument(
79/// Argument::builder("id")
80/// .required()
81/// .build()
82/// .unwrap(),
83/// )
84/// .build()
85/// .unwrap();
86///
87/// let cmds = vec![cmd];
88/// let parser = Parser::new(&cmds);
89/// let parsed = parser.parse(&["get", "42"]).unwrap();
90///
91/// assert_eq!(parsed.command.canonical, "get");
92/// assert_eq!(parsed.args["id"], "42");
93/// ```
94#[derive(Debug)]
95pub struct ParsedCommand<'a> {
96 /// The matched [`Command`] definition, borrowed from the registry.
97 pub command: &'a Command,
98 /// Resolved positional argument values, keyed by argument name.
99 pub args: HashMap<String, String>,
100 /// Resolved flag values, keyed by flag name.
101 ///
102 /// Boolean flags (those without `takes_value`) are stored as `"true"`.
103 /// Flags that were not provided but have a `default` value are included
104 /// with that default.
105 pub flags: HashMap<String, String>,
106}
107
108impl<'a> ParsedCommand<'a> {
109 /// Return a positional argument value by name, or `None` if absent.
110 ///
111 /// # Examples
112 ///
113 /// ```
114 /// # use argot_cmd::{Argument, Command, Parser};
115 /// let cmd = Command::builder("get")
116 /// .argument(Argument::builder("id").required().build().unwrap())
117 /// .build().unwrap();
118 /// let cmds = vec![cmd];
119 /// let parsed = Parser::new(&cmds).parse(&["get", "42"]).unwrap();
120 /// assert_eq!(parsed.arg("id"), Some("42"));
121 /// assert_eq!(parsed.arg("missing"), None);
122 /// ```
123 pub fn arg(&self, name: &str) -> Option<&str> {
124 self.args.get(name).map(String::as_str)
125 }
126
127 /// Return a flag value by name, or `None` if absent.
128 ///
129 /// # Examples
130 ///
131 /// ```
132 /// # use argot_cmd::{Command, Flag, Parser};
133 /// let cmd = Command::builder("run")
134 /// .flag(Flag::builder("output").takes_value().default_value("text").build().unwrap())
135 /// .build().unwrap();
136 /// let cmds = vec![cmd];
137 /// let parsed = Parser::new(&cmds).parse(&["run", "--output=json"]).unwrap();
138 /// assert_eq!(parsed.flag("output"), Some("json"));
139 /// ```
140 pub fn flag(&self, name: &str) -> Option<&str> {
141 self.flags.get(name).map(String::as_str)
142 }
143
144 /// Return `true` if a boolean flag is present and set to `"true"`, `false` otherwise.
145 ///
146 /// # Examples
147 ///
148 /// ```
149 /// # use argot_cmd::{Command, Flag, Parser};
150 /// let cmd = Command::builder("build")
151 /// .flag(Flag::builder("release").build().unwrap())
152 /// .build().unwrap();
153 /// let cmds = vec![cmd];
154 /// let parsed = Parser::new(&cmds).parse(&["build", "--release"]).unwrap();
155 /// assert!(parsed.flag_bool("release"));
156 /// assert!(!parsed.flag_bool("missing"));
157 /// ```
158 pub fn flag_bool(&self, name: &str) -> bool {
159 self.flags.get(name).map(|v| v == "true").unwrap_or(false)
160 }
161
162 /// Return the occurrence count for a repeatable boolean flag (stored as a numeric string).
163 /// Returns `0` if the flag was not provided.
164 ///
165 /// For flags stored as `"true"` (non-repeatable), returns `1`.
166 ///
167 /// # Examples
168 ///
169 /// ```
170 /// # use argot_cmd::{Command, Flag, Parser};
171 /// // With a repeatable flag (see Flag::repeatable), -v -v -v → flag_count("verbose") == 3
172 /// // With a normal flag, --verbose → flag_count("verbose") == 1 (stored as "true")
173 /// let cmd = Command::builder("run")
174 /// .flag(Flag::builder("verbose").short('v').build().unwrap())
175 /// .build().unwrap();
176 /// let cmds = vec![cmd];
177 /// let parsed = Parser::new(&cmds).parse(&["run", "-v"]).unwrap();
178 /// // Non-repeatable flag stores "true"; flag_count returns 1
179 /// assert_eq!(parsed.flag_count("verbose"), 1);
180 /// assert_eq!(parsed.flag_count("missing"), 0);
181 /// ```
182 pub fn flag_count(&self, name: &str) -> u64 {
183 match self.flags.get(name) {
184 None => 0,
185 Some(v) if v == "true" => 1,
186 Some(v) if v == "false" => 0,
187 Some(v) => v.parse().unwrap_or(0),
188 }
189 }
190
191 /// Return all values for a repeatable value-taking flag as a `Vec<String>`.
192 ///
193 /// - If the flag was provided multiple times (repeatable), the stored JSON array is
194 /// deserialized into a `Vec`.
195 /// - If the flag was provided once (non-repeatable), returns a single-element `Vec`.
196 /// - If the flag was not provided, returns an empty `Vec`.
197 ///
198 /// # Examples
199 ///
200 /// ```
201 /// # use argot_cmd::{Command, Flag, Parser};
202 /// let cmd = Command::builder("run")
203 /// .flag(Flag::builder("output").takes_value().default_value("text").build().unwrap())
204 /// .build().unwrap();
205 /// let cmds = vec![cmd];
206 /// let parsed = Parser::new(&cmds).parse(&["run", "--output=json"]).unwrap();
207 /// assert_eq!(parsed.flag_values("output"), vec!["json"]);
208 /// assert!(parsed.flag_values("missing").is_empty());
209 /// ```
210 pub fn flag_values(&self, name: &str) -> Vec<String> {
211 match self.flags.get(name) {
212 None => vec![],
213 Some(v) => serde_json::from_str::<Vec<String>>(v).unwrap_or_else(|_| vec![v.clone()]),
214 }
215 }
216
217 /// Return `true` if `name` is present in the parsed flag map.
218 ///
219 /// A flag is present when it was explicitly provided on the command line
220 /// **or** when the parser inserted a default or env-var value.
221 /// To distinguish explicit from default, use [`ParsedCommand::flag`] and
222 /// compare with the flag definition's default.
223 ///
224 /// # Examples
225 ///
226 /// ```
227 /// # use argot_cmd::{Command, Flag, Parser};
228 /// let cmd = Command::builder("run")
229 /// .flag(Flag::builder("verbose").build().unwrap())
230 /// .flag(Flag::builder("output").takes_value().default_value("text").build().unwrap())
231 /// .build().unwrap();
232 /// let cmds = vec![cmd];
233 /// let parsed = Parser::new(&cmds).parse(&["run", "--verbose"]).unwrap();
234 /// assert!(parsed.has_flag("verbose"));
235 /// assert!(parsed.has_flag("output")); // present via default
236 /// assert!(!parsed.has_flag("missing"));
237 /// ```
238 pub fn has_flag(&self, name: &str) -> bool {
239 self.flags.contains_key(name)
240 }
241
242 /// Parse a positional argument as type `T` using [`std::str::FromStr`].
243 ///
244 /// Returns:
245 /// - `None` if the argument was not provided (absent from the map)
246 /// - `Some(Ok(value))` if parsing succeeded
247 /// - `Some(Err(e))` if the string was present but could not be parsed
248 ///
249 /// # Examples
250 ///
251 /// ```
252 /// # use argot_cmd::{Argument, Command, Parser};
253 /// let cmd = Command::builder("resize")
254 /// .argument(Argument::builder("width").required().build().unwrap())
255 /// .build().unwrap();
256 /// let cmds = vec![cmd];
257 /// let parsed = Parser::new(&cmds).parse(&["resize", "1920"]).unwrap();
258 ///
259 /// let width: u32 = parsed.arg_as("width").unwrap().unwrap();
260 /// assert_eq!(width, 1920u32);
261 ///
262 /// assert!(parsed.arg_as::<u32>("missing").is_none());
263 /// ```
264 pub fn arg_as<T: std::str::FromStr>(&self, name: &str) -> Option<Result<T, T::Err>> {
265 self.args.get(name).map(|v| v.parse())
266 }
267
268 /// Parse a flag value as type `T` using [`std::str::FromStr`].
269 ///
270 /// Returns:
271 /// - `None` if the flag was not set (and has no default)
272 /// - `Some(Ok(value))` if parsing succeeded
273 /// - `Some(Err(e))` if the string was present but could not be parsed
274 ///
275 /// # Examples
276 ///
277 /// ```
278 /// # use argot_cmd::{Command, Flag, Parser};
279 /// let cmd = Command::builder("serve")
280 /// .flag(Flag::builder("port").takes_value().default_value("8080").build().unwrap())
281 /// .build().unwrap();
282 /// let cmds = vec![cmd];
283 /// let parsed = Parser::new(&cmds).parse(&["serve"]).unwrap();
284 ///
285 /// let port: u16 = parsed.flag_as("port").unwrap().unwrap();
286 /// assert_eq!(port, 8080u16);
287 /// ```
288 pub fn flag_as<T: std::str::FromStr>(&self, name: &str) -> Option<Result<T, T::Err>> {
289 self.flags.get(name).map(|v| v.parse())
290 }
291
292 /// Parse a positional argument as type `T`, returning a default if absent or unparseable.
293 ///
294 /// # Examples
295 ///
296 /// ```
297 /// # use argot_cmd::{Argument, Command, Parser};
298 /// let cmd = Command::builder("run")
299 /// .argument(Argument::builder("count").build().unwrap())
300 /// .build().unwrap();
301 /// let cmds = vec![cmd];
302 /// let parsed = Parser::new(&cmds).parse(&["run"]).unwrap();
303 ///
304 /// assert_eq!(parsed.arg_as_or("count", 1u32), 1u32);
305 /// ```
306 pub fn arg_as_or<T: std::str::FromStr>(&self, name: &str, default: T) -> T {
307 self.arg_as(name).and_then(|r| r.ok()).unwrap_or(default)
308 }
309
310 /// Parse a flag value as type `T`, returning a default if absent or unparseable.
311 ///
312 /// # Examples
313 ///
314 /// ```
315 /// # use argot_cmd::{Command, Flag, Parser};
316 /// let cmd = Command::builder("serve")
317 /// .flag(Flag::builder("workers").takes_value().build().unwrap())
318 /// .build().unwrap();
319 /// let cmds = vec![cmd];
320 /// let parsed = Parser::new(&cmds).parse(&["serve"]).unwrap();
321 ///
322 /// assert_eq!(parsed.flag_as_or("workers", 4u32), 4u32);
323 /// ```
324 pub fn flag_as_or<T: std::str::FromStr>(&self, name: &str, default: T) -> T {
325 self.flag_as(name).and_then(|r| r.ok()).unwrap_or(default)
326 }
327}
328
329/// A command in the registry, potentially with subcommands.
330///
331/// Commands are the central unit of argot. Each command has a canonical name,
332/// optional aliases and alternate spellings, human-readable documentation,
333/// typed positional arguments, named flags, usage examples, and an optional
334/// handler closure. Commands can be nested arbitrarily deep via
335/// [`Command::subcommands`].
336///
337/// Use [`Command::builder`] to construct instances — direct struct
338/// construction is intentionally not exposed.
339///
340/// # Serialization
341///
342/// `Command` implements `serde::Serialize` / `Deserialize`. The [`handler`]
343/// field is skipped during serialization (it cannot be represented in JSON)
344/// and will be `None` after deserialization.
345///
346/// # Examples
347///
348/// ```
349/// # use argot_cmd::{Command, Argument, Flag, Example};
350/// let cmd = Command::builder("deploy")
351/// .alias("d")
352/// .summary("Deploy the app")
353/// .description("Deploys to the specified environment.")
354/// .argument(
355/// Argument::builder("env")
356/// .description("Target environment")
357/// .required()
358/// .build()
359/// .unwrap(),
360/// )
361/// .flag(
362/// Flag::builder("dry-run")
363/// .short('n')
364/// .description("Simulate only")
365/// .build()
366/// .unwrap(),
367/// )
368/// .example(Example::new("deploy to prod", "myapp deploy production"))
369/// .build()
370/// .unwrap();
371///
372/// assert_eq!(cmd.canonical, "deploy");
373/// assert_eq!(cmd.aliases, vec!["d"]);
374/// ```
375///
376/// [`handler`]: Command::handler
377#[derive(Clone, Serialize, Deserialize)]
378pub struct Command {
379 /// The primary, canonical name used to invoke this command.
380 pub canonical: String,
381 /// Alternative names that resolve to this command (e.g. `"ls"` for `"list"`).
382 pub aliases: Vec<String>,
383 /// Alternate capitalizations or spellings (e.g. `"LIST"` for `"list"`).
384 ///
385 /// Spellings differ from aliases semantically: they represent the same
386 /// word written differently rather than a short-form abbreviation.
387 pub spellings: Vec<String>,
388 /// One-line description shown in command listings.
389 pub summary: String,
390 /// Full prose description shown in detailed help output.
391 pub description: String,
392 /// Ordered list of positional arguments accepted by this command.
393 pub arguments: Vec<Argument>,
394 /// Named flags (long and/or short) accepted by this command.
395 pub flags: Vec<Flag>,
396 /// Usage examples shown in help and Markdown documentation.
397 pub examples: Vec<Example>,
398 /// Nested sub-commands (e.g. `remote add`, `remote remove`).
399 pub subcommands: Vec<Command>,
400 /// Prose tips about correct usage, surfaced to AI agents.
401 pub best_practices: Vec<String>,
402 /// Prose warnings about incorrect usage, surfaced to AI agents.
403 pub anti_patterns: Vec<String>,
404 /// Natural-language phrases describing what this command does.
405 ///
406 /// Used for intent-based discovery (e.g. [`crate::query::Registry::match_intent`])
407 /// but intentionally excluded from normal help output.
408 pub semantic_aliases: Vec<String>,
409 /// Optional runtime handler invoked by [`crate::cli::Cli::run`].
410 ///
411 /// Skipped during JSON serialization/deserialization.
412 #[serde(skip)]
413 pub handler: Option<HandlerFn>,
414 /// Optional async runtime handler (feature: `async`).
415 ///
416 /// Skipped during JSON serialization.
417 #[cfg(feature = "async")]
418 #[serde(skip)]
419 pub async_handler: Option<AsyncHandlerFn>,
420 /// Arbitrary application-defined metadata.
421 ///
422 /// Use this to attach structured data that is not covered by the built-in
423 /// fields (e.g., permission requirements, category tags, telemetry labels).
424 ///
425 /// Serialized to JSON as an object; absent from output when empty.
426 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
427 pub extra: HashMap<String, serde_json::Value>,
428 /// Groups of mutually exclusive flag names.
429 ///
430 /// At most one flag in each group may be provided in a single invocation.
431 /// Validated at build time (flags must exist) and enforced at parse time.
432 #[serde(default, skip_serializing_if = "Vec::is_empty")]
433 pub exclusive_groups: Vec<Vec<String>>,
434}
435
436impl std::fmt::Debug for Command {
437 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438 let mut ds = f.debug_struct("Command");
439 ds.field("canonical", &self.canonical)
440 .field("aliases", &self.aliases)
441 .field("spellings", &self.spellings)
442 .field("summary", &self.summary)
443 .field("description", &self.description)
444 .field("arguments", &self.arguments)
445 .field("flags", &self.flags)
446 .field("examples", &self.examples)
447 .field("subcommands", &self.subcommands)
448 .field("best_practices", &self.best_practices)
449 .field("anti_patterns", &self.anti_patterns)
450 .field("semantic_aliases", &self.semantic_aliases)
451 .field("handler", &self.handler.as_ref().map(|_| "<handler>"));
452 #[cfg(feature = "async")]
453 ds.field(
454 "async_handler",
455 &self.async_handler.as_ref().map(|_| "<async_handler>"),
456 );
457 ds.field("extra", &self.extra)
458 .field("exclusive_groups", &self.exclusive_groups)
459 .finish()
460 }
461}
462
463impl PartialEq for Command {
464 fn eq(&self, other: &Self) -> bool {
465 self.canonical == other.canonical
466 && self.aliases == other.aliases
467 && self.spellings == other.spellings
468 && self.summary == other.summary
469 && self.description == other.description
470 && self.arguments == other.arguments
471 && self.flags == other.flags
472 && self.examples == other.examples
473 && self.subcommands == other.subcommands
474 && self.best_practices == other.best_practices
475 && self.anti_patterns == other.anti_patterns
476 && self.semantic_aliases == other.semantic_aliases
477 && self.extra == other.extra
478 && self.exclusive_groups == other.exclusive_groups
479 }
480}
481
482impl Eq for Command {}
483
484impl std::hash::Hash for Command {
485 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
486 self.canonical.hash(state);
487 self.aliases.hash(state);
488 self.spellings.hash(state);
489 self.summary.hash(state);
490 self.description.hash(state);
491 self.arguments.hash(state);
492 self.flags.hash(state);
493 self.examples.hash(state);
494 self.subcommands.hash(state);
495 self.best_practices.hash(state);
496 self.anti_patterns.hash(state);
497 self.semantic_aliases.hash(state);
498 // handler is intentionally excluded (not hashable)
499 // extra: hash keys in sorted order, with their JSON string representation
500 {
501 let mut keys: Vec<&String> = self.extra.keys().collect();
502 keys.sort();
503 for k in keys {
504 k.hash(state);
505 self.extra[k].to_string().hash(state);
506 }
507 }
508 self.exclusive_groups.hash(state);
509 }
510}
511
512impl PartialOrd for Command {
513 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
514 Some(self.cmp(other))
515 }
516}
517
518/// Commands are ordered by canonical name, then by their full field contents.
519impl Ord for Command {
520 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
521 self.canonical
522 .cmp(&other.canonical)
523 .then_with(|| self.summary.cmp(&other.summary))
524 .then_with(|| self.aliases.cmp(&other.aliases))
525 }
526}
527
528impl Command {
529 /// Create a new [`CommandBuilder`] with the given canonical name.
530 ///
531 /// # Arguments
532 ///
533 /// - `canonical` — The primary command name. Must be non-empty after
534 /// trimming (enforced by [`CommandBuilder::build`]).
535 ///
536 /// # Examples
537 ///
538 /// ```
539 /// # use argot_cmd::Command;
540 /// let cmd = Command::builder("list").build().unwrap();
541 /// assert_eq!(cmd.canonical, "list");
542 /// ```
543 pub fn builder(canonical: impl Into<String>) -> CommandBuilder {
544 CommandBuilder {
545 canonical: canonical.into(),
546 aliases: Vec::new(),
547 spellings: Vec::new(),
548 summary: String::new(),
549 description: String::new(),
550 arguments: Vec::new(),
551 flags: Vec::new(),
552 examples: Vec::new(),
553 subcommands: Vec::new(),
554 best_practices: Vec::new(),
555 anti_patterns: Vec::new(),
556 semantic_aliases: Vec::new(),
557 handler: None,
558 #[cfg(feature = "async")]
559 async_handler: None,
560 extra: HashMap::new(),
561 exclusive_groups: Vec::new(),
562 }
563 }
564
565 /// All strings this command can be matched by for exact lookup
566 /// (canonical + aliases + spellings), lowercased.
567 pub(crate) fn matchable_strings(&self) -> Vec<String> {
568 let mut v = vec![self.canonical.to_lowercase()];
569 v.extend(self.aliases.iter().map(|s| s.to_lowercase()));
570 v.extend(self.spellings.iter().map(|s| s.to_lowercase()));
571 v
572 }
573
574 /// Strings eligible for prefix matching (canonical + aliases only), lowercased.
575 ///
576 /// Spellings are intentionally excluded: they are exact-match-only so they
577 /// do not contribute to prefix ambiguity resolution and never appear in
578 /// "did you mean?" / ambiguous-candidate output.
579 pub(crate) fn prefix_matchable_strings(&self) -> Vec<String> {
580 let mut v = vec![self.canonical.to_lowercase()];
581 v.extend(self.aliases.iter().map(|s| s.to_lowercase()));
582 v
583 }
584}
585
586/// Consuming builder for [`Command`].
587///
588/// Obtain an instance via [`Command::builder`]. All setter methods consume and
589/// return `self` to allow method chaining. Call [`CommandBuilder::build`] to
590/// produce a [`Command`].
591///
592/// # Examples
593///
594/// ```
595/// # use argot_cmd::{Command, Flag};
596/// let cmd = Command::builder("run")
597/// .alias("r")
598/// .summary("Run the pipeline")
599/// .flag(Flag::builder("verbose").short('v').build().unwrap())
600/// .build()
601/// .unwrap();
602///
603/// assert_eq!(cmd.canonical, "run");
604/// assert_eq!(cmd.aliases, vec!["r"]);
605/// ```
606pub struct CommandBuilder {
607 canonical: String,
608 aliases: Vec<String>,
609 spellings: Vec<String>,
610 summary: String,
611 description: String,
612 arguments: Vec<Argument>,
613 flags: Vec<Flag>,
614 examples: Vec<Example>,
615 subcommands: Vec<Command>,
616 best_practices: Vec<String>,
617 anti_patterns: Vec<String>,
618 semantic_aliases: Vec<String>,
619 handler: Option<HandlerFn>,
620 #[cfg(feature = "async")]
621 async_handler: Option<AsyncHandlerFn>,
622 extra: HashMap<String, serde_json::Value>,
623 exclusive_groups: Vec<Vec<String>>,
624}
625
626impl CommandBuilder {
627 /// Replace the entire alias list with the given collection.
628 ///
629 /// To add a single alias use [`CommandBuilder::alias`].
630 pub fn aliases(mut self, aliases: impl IntoIterator<Item = impl Into<String>>) -> Self {
631 self.aliases = aliases.into_iter().map(Into::into).collect();
632 self
633 }
634
635 /// Append a single alias.
636 pub fn alias(mut self, alias: impl Into<String>) -> Self {
637 self.aliases.push(alias.into());
638 self
639 }
640
641 /// Replace the entire spelling list with the given collection.
642 ///
643 /// Spellings are silent typo-corrections or abbreviated forms that the
644 /// resolver accepts but does **not** advertise in help text. They differ
645 /// from aliases in that they will never appear in the `--help` aliases
646 /// line, and they are excluded from prefix-match ambiguity candidates.
647 ///
648 /// To add a single spelling use [`CommandBuilder::spelling`].
649 pub fn spellings(mut self, spellings: impl IntoIterator<Item = impl Into<String>>) -> Self {
650 self.spellings = spellings.into_iter().map(Into::into).collect();
651 self
652 }
653
654 /// Register a common misspelling or abbreviated form that resolves to this command.
655 ///
656 /// Unlike [`CommandBuilder::alias`], spellings are **not shown in help text**.
657 /// They serve as silent typo-correction or shorthand that resolves
658 /// to the canonical command name without advertising the alternative.
659 ///
660 /// # Difference from `alias`
661 ///
662 /// | Feature | `alias` | `spelling` |
663 /// |---------|---------|------------|
664 /// | Shown in `--help` | Yes | No |
665 /// | Resolver exact match | Yes | Yes |
666 /// | Resolver prefix match | Yes | No |
667 /// | Appears in `aliases` field | Yes | No (goes in `spellings`) |
668 ///
669 /// # Examples
670 ///
671 /// ```
672 /// # use argot_cmd::Command;
673 /// let cmd = Command::builder("deploy")
674 /// .alias("release") // shown in help: "deploy (release)"
675 /// .alias("ship") // shown in help
676 /// .spelling("deply") // silent typo correction
677 /// .spelling("delpoy") // silent typo correction
678 /// .build().unwrap();
679 ///
680 /// assert!(cmd.aliases.contains(&"release".to_string()));
681 /// assert!(cmd.spellings.contains(&"deply".to_string()));
682 /// ```
683 pub fn spelling(mut self, s: impl Into<String>) -> Self {
684 self.spellings.push(s.into());
685 self
686 }
687
688 /// Set the one-line summary shown in command listings.
689 pub fn summary(mut self, s: impl Into<String>) -> Self {
690 self.summary = s.into();
691 self
692 }
693
694 /// Set the full prose description shown in detailed help output.
695 pub fn description(mut self, d: impl Into<String>) -> Self {
696 self.description = d.into();
697 self
698 }
699
700 /// Append a positional [`Argument`] definition.
701 ///
702 /// Arguments are bound in declaration order when the command is parsed.
703 pub fn argument(mut self, arg: Argument) -> Self {
704 self.arguments.push(arg);
705 self
706 }
707
708 /// Append a [`Flag`] definition.
709 pub fn flag(mut self, flag: Flag) -> Self {
710 self.flags.push(flag);
711 self
712 }
713
714 /// Append a usage [`Example`].
715 pub fn example(mut self, example: Example) -> Self {
716 self.examples.push(example);
717 self
718 }
719
720 /// Append a subcommand.
721 ///
722 /// Subcommands are resolved before positional arguments during parsing, so
723 /// a token that matches a subcommand name is consumed as the subcommand
724 /// selector, not as a positional value.
725 pub fn subcommand(mut self, cmd: Command) -> Self {
726 self.subcommands.push(cmd);
727 self
728 }
729
730 /// Append a best-practice tip surfaced to AI agents.
731 pub fn best_practice(mut self, bp: impl Into<String>) -> Self {
732 self.best_practices.push(bp.into());
733 self
734 }
735
736 /// Append an anti-pattern warning surfaced to AI agents.
737 pub fn anti_pattern(mut self, ap: impl Into<String>) -> Self {
738 self.anti_patterns.push(ap.into());
739 self
740 }
741
742 /// Replace the entire semantic alias list with the given collection.
743 ///
744 /// Semantic aliases are natural-language phrases used for intent-based
745 /// discovery but are **not shown in help text**.
746 ///
747 /// To add a single semantic alias use [`CommandBuilder::semantic_alias`].
748 pub fn semantic_aliases(
749 mut self,
750 aliases: impl IntoIterator<Item = impl Into<String>>,
751 ) -> Self {
752 self.semantic_aliases = aliases.into_iter().map(Into::into).collect();
753 self
754 }
755
756 /// Append a single natural-language phrase for intent-based discovery.
757 ///
758 /// Unlike [`CommandBuilder::alias`], semantic aliases are natural-language
759 /// phrases describing what the command does (e.g. `"release to production"`,
760 /// `"push to environment"`). They are used by
761 /// [`crate::query::Registry::match_intent`] but are **not shown in help text**.
762 ///
763 /// # Examples
764 ///
765 /// ```
766 /// # use argot_cmd::Command;
767 /// let cmd = Command::builder("deploy")
768 /// .semantic_alias("release to production")
769 /// .semantic_alias("push to environment")
770 /// .build().unwrap();
771 ///
772 /// assert_eq!(cmd.semantic_aliases, vec!["release to production", "push to environment"]);
773 /// ```
774 pub fn semantic_alias(mut self, s: impl Into<String>) -> Self {
775 self.semantic_aliases.push(s.into());
776 self
777 }
778
779 /// Set the runtime handler invoked when this command is dispatched.
780 ///
781 /// The handler receives a [`ParsedCommand`] and should return `Ok(())`
782 /// on success or a boxed error on failure.
783 pub fn handler(mut self, h: HandlerFn) -> Self {
784 self.handler = Some(h);
785 self
786 }
787
788 /// Register an async handler for this command (feature: `async`).
789 ///
790 /// The handler receives a [`ParsedCommand`] and returns a boxed future.
791 /// Use [`crate::Cli::run_async`] to dispatch async handlers.
792 ///
793 /// # Examples
794 ///
795 /// ```
796 /// # #[cfg(feature = "async")] {
797 /// use std::sync::Arc;
798 /// use argot_cmd::Command;
799 ///
800 /// let cmd = Command::builder("deploy")
801 /// .async_handler(Arc::new(|parsed| Box::pin(async move {
802 /// println!("deployed!");
803 /// Ok(())
804 /// })))
805 /// .build()
806 /// .unwrap();
807 /// # }
808 /// ```
809 #[cfg(feature = "async")]
810 pub fn async_handler(mut self, h: AsyncHandlerFn) -> Self {
811 self.async_handler = Some(h);
812 self
813 }
814
815 /// Set an arbitrary metadata entry on this command.
816 ///
817 /// # Examples
818 ///
819 /// ```
820 /// # use argot_cmd::Command;
821 /// # use serde_json::json;
822 /// let cmd = Command::builder("deploy")
823 /// .meta("category", json!("infrastructure"))
824 /// .meta("min_role", json!("ops"))
825 /// .build()
826 /// .unwrap();
827 ///
828 /// assert_eq!(cmd.extra["category"], json!("infrastructure"));
829 /// ```
830 pub fn meta(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
831 self.extra.insert(key.into(), value);
832 self
833 }
834
835 /// Declare a group of mutually exclusive flags.
836 ///
837 /// At most one flag from `flags` may be provided in a single invocation.
838 /// The parser returns [`crate::ParseError::MutuallyExclusive`] if two or
839 /// more flags from the same group are present.
840 ///
841 /// # Panics (at build time via [`BuildError`])
842 ///
843 /// [`CommandBuilder::build`] returns an error if:
844 /// - The group has fewer than 2 flags.
845 /// - Any flag name in the group is not defined on this command.
846 ///
847 /// # Examples
848 ///
849 /// ```
850 /// # use argot_cmd::{Command, Flag};
851 /// let cmd = Command::builder("export")
852 /// .flag(Flag::builder("json").build().unwrap())
853 /// .flag(Flag::builder("yaml").build().unwrap())
854 /// .flag(Flag::builder("csv").build().unwrap())
855 /// .exclusive(["json", "yaml", "csv"])
856 /// .build()
857 /// .unwrap();
858 /// assert_eq!(cmd.exclusive_groups.len(), 1);
859 /// ```
860 pub fn exclusive(mut self, flags: impl IntoIterator<Item = impl Into<String>>) -> Self {
861 self.exclusive_groups
862 .push(flags.into_iter().map(Into::into).collect());
863 self
864 }
865
866 /// Consume the builder and return a [`Command`].
867 ///
868 /// # Errors
869 ///
870 /// - [`BuildError::EmptyCanonical`] — canonical name is empty or whitespace.
871 /// - [`BuildError::AliasEqualsCanonical`] — an alias is identical to the
872 /// canonical name (case-insensitive).
873 /// - [`BuildError::DuplicateAlias`] — two aliases share the same string
874 /// (case-insensitive comparison).
875 /// - [`BuildError::DuplicateFlagName`] — two flags share the same long name.
876 /// - [`BuildError::DuplicateShortFlag`] — two flags share the same short
877 /// character.
878 /// - [`BuildError::DuplicateArgumentName`] — two positional arguments share
879 /// the same name.
880 /// - [`BuildError::DuplicateSubcommandName`] — two subcommands share the
881 /// same canonical name.
882 /// - [`BuildError::VariadicNotLast`] — a variadic argument is not the last
883 /// argument in the list.
884 ///
885 /// # Examples
886 ///
887 /// ```
888 /// # use argot_cmd::{Command, BuildError};
889 /// assert!(Command::builder("ok").build().is_ok());
890 /// assert_eq!(Command::builder("").build().unwrap_err(), BuildError::EmptyCanonical);
891 /// ```
892 pub fn build(self) -> Result<Command, BuildError> {
893 if self.canonical.trim().is_empty() {
894 return Err(BuildError::EmptyCanonical);
895 }
896
897 // 1. Alias equals canonical
898 let canonical_lower = self.canonical.to_lowercase();
899 for alias in &self.aliases {
900 if alias.to_lowercase() == canonical_lower {
901 return Err(BuildError::AliasEqualsCanonical(alias.clone()));
902 }
903 }
904
905 // 2. Duplicate aliases
906 let mut seen_aliases = std::collections::HashSet::new();
907 for alias in &self.aliases {
908 let key = alias.to_lowercase();
909 if !seen_aliases.insert(key) {
910 return Err(BuildError::DuplicateAlias(alias.clone()));
911 }
912 }
913
914 // 3. Duplicate flag names (long)
915 let mut seen_flag_names = std::collections::HashSet::new();
916 for flag in &self.flags {
917 if !seen_flag_names.insert(flag.name.clone()) {
918 return Err(BuildError::DuplicateFlagName(flag.name.clone()));
919 }
920 }
921
922 // 4. Duplicate short flags
923 let mut seen_short_flags = std::collections::HashSet::new();
924 for flag in &self.flags {
925 if let Some(c) = flag.short {
926 if !seen_short_flags.insert(c) {
927 return Err(BuildError::DuplicateShortFlag(c));
928 }
929 }
930 }
931
932 // 5. Duplicate argument names
933 let mut seen_arg_names = std::collections::HashSet::new();
934 for arg in &self.arguments {
935 if !seen_arg_names.insert(arg.name.clone()) {
936 return Err(BuildError::DuplicateArgumentName(arg.name.clone()));
937 }
938 }
939
940 // 6. Duplicate subcommand canonical names
941 let mut seen_sub_names = std::collections::HashSet::new();
942 for sub in &self.subcommands {
943 if !seen_sub_names.insert(sub.canonical.clone()) {
944 return Err(BuildError::DuplicateSubcommandName(sub.canonical.clone()));
945 }
946 }
947
948 // 7. Variadic argument must be last
949 for (i, arg) in self.arguments.iter().enumerate() {
950 if arg.variadic && i != self.arguments.len() - 1 {
951 return Err(BuildError::VariadicNotLast(arg.name.clone()));
952 }
953 }
954
955 // 8. Flags with choices must have a non-empty choices list
956 for flag in &self.flags {
957 if let Some(choices) = &flag.choices {
958 if choices.is_empty() {
959 return Err(BuildError::EmptyChoices(flag.name.clone()));
960 }
961 }
962 }
963
964 // 9. Mutual exclusivity group validation
965 for group in &self.exclusive_groups {
966 if group.len() < 2 {
967 return Err(BuildError::ExclusiveGroupTooSmall);
968 }
969 for flag_name in group {
970 if !self.flags.iter().any(|f| &f.name == flag_name) {
971 return Err(BuildError::ExclusiveGroupUnknownFlag(flag_name.clone()));
972 }
973 }
974 }
975
976 Ok(Command {
977 canonical: self.canonical,
978 aliases: self.aliases,
979 spellings: self.spellings,
980 summary: self.summary,
981 description: self.description,
982 arguments: self.arguments,
983 flags: self.flags,
984 examples: self.examples,
985 subcommands: self.subcommands,
986 best_practices: self.best_practices,
987 anti_patterns: self.anti_patterns,
988 semantic_aliases: self.semantic_aliases,
989 handler: self.handler,
990 #[cfg(feature = "async")]
991 async_handler: self.async_handler,
992 extra: self.extra,
993 exclusive_groups: self.exclusive_groups,
994 })
995 }
996}
997
998#[cfg(test)]
999mod tests {
1000 use super::*;
1001 use crate::model::{Argument, Flag};
1002
1003 fn make_simple_cmd() -> Command {
1004 Command::builder("run")
1005 .alias("r")
1006 .spelling("RUN")
1007 .summary("Run something")
1008 .description("Runs the thing.")
1009 .build()
1010 .unwrap()
1011 }
1012
1013 #[test]
1014 fn test_builder_happy_path() {
1015 let cmd = make_simple_cmd();
1016 assert_eq!(cmd.canonical, "run");
1017 assert_eq!(cmd.aliases, vec!["r"]);
1018 assert_eq!(cmd.spellings, vec!["RUN"]);
1019 }
1020
1021 #[test]
1022 fn test_builder_empty_canonical() {
1023 assert_eq!(
1024 Command::builder("").build().unwrap_err(),
1025 BuildError::EmptyCanonical
1026 );
1027 assert_eq!(
1028 Command::builder(" ").build().unwrap_err(),
1029 BuildError::EmptyCanonical
1030 );
1031 }
1032
1033 #[test]
1034 fn test_partial_eq_ignores_handler() {
1035 let cmd1 = Command::builder("run").build().unwrap();
1036 let mut cmd2 = cmd1.clone();
1037 cmd2.handler = Some(Arc::new(|_| Ok(())));
1038 assert_eq!(cmd1, cmd2);
1039 }
1040
1041 #[test]
1042 fn test_serde_round_trip_skips_handler() {
1043 let cmd = Command::builder("deploy")
1044 .summary("Deploy the app")
1045 .argument(
1046 Argument::builder("env")
1047 .description("target env")
1048 .required()
1049 .build()
1050 .unwrap(),
1051 )
1052 .flag(
1053 Flag::builder("dry-run")
1054 .description("dry run mode")
1055 .build()
1056 .unwrap(),
1057 )
1058 .handler(Arc::new(|_| Ok(())))
1059 .build()
1060 .unwrap();
1061
1062 let json = serde_json::to_string(&cmd).unwrap();
1063 let de: Command = serde_json::from_str(&json).unwrap();
1064 assert_eq!(cmd, de);
1065 assert!(de.handler.is_none());
1066 }
1067
1068 #[test]
1069 fn test_matchable_strings() {
1070 let cmd = Command::builder("Git")
1071 .alias("g")
1072 .spelling("GIT")
1073 .build()
1074 .unwrap();
1075 let matchables = cmd.matchable_strings();
1076 assert!(matchables.contains(&"git".to_string()));
1077 assert!(matchables.contains(&"g".to_string()));
1078 assert!(matchables.contains(&"git".to_string())); // spelling lowercased
1079 }
1080
1081 #[test]
1082 fn test_clone_shares_handler() {
1083 let called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1084 let called_clone = called.clone();
1085 let cmd = Command::builder("x")
1086 .handler(Arc::new(move |_| {
1087 called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
1088 Ok(())
1089 }))
1090 .build()
1091 .unwrap();
1092 let cmd2 = cmd.clone();
1093 assert!(std::sync::Arc::ptr_eq(
1094 cmd.handler.as_ref().unwrap(),
1095 cmd2.handler.as_ref().unwrap()
1096 ));
1097 }
1098
1099 #[test]
1100 fn test_duplicate_alias_rejected() {
1101 let err = Command::builder("cmd")
1102 .alias("a")
1103 .alias("a")
1104 .build()
1105 .unwrap_err();
1106 assert!(matches!(err, BuildError::DuplicateAlias(_)));
1107 }
1108
1109 #[test]
1110 fn test_alias_equals_canonical_rejected() {
1111 let err = Command::builder("deploy")
1112 .alias("deploy")
1113 .build()
1114 .unwrap_err();
1115 assert!(matches!(err, BuildError::AliasEqualsCanonical(_)));
1116 }
1117
1118 #[test]
1119 fn test_duplicate_flag_name_rejected() {
1120 let flag = Flag::builder("verbose").build().unwrap();
1121 let err = Command::builder("cmd")
1122 .flag(flag.clone())
1123 .flag(flag)
1124 .build()
1125 .unwrap_err();
1126 assert!(matches!(err, BuildError::DuplicateFlagName(_)));
1127 }
1128
1129 #[test]
1130 fn test_duplicate_short_flag_rejected() {
1131 let f1 = Flag::builder("verbose").short('v').build().unwrap();
1132 let f2 = Flag::builder("version").short('v').build().unwrap();
1133 let err = Command::builder("cmd")
1134 .flag(f1)
1135 .flag(f2)
1136 .build()
1137 .unwrap_err();
1138 assert!(matches!(err, BuildError::DuplicateShortFlag('v')));
1139 }
1140
1141 #[test]
1142 fn test_duplicate_argument_name_rejected() {
1143 let arg = Argument::builder("env").build().unwrap();
1144 let err = Command::builder("cmd")
1145 .argument(arg.clone())
1146 .argument(arg)
1147 .build()
1148 .unwrap_err();
1149 assert!(matches!(err, BuildError::DuplicateArgumentName(_)));
1150 }
1151
1152 #[test]
1153 fn test_duplicate_subcommand_name_rejected() {
1154 let sub = Command::builder("add").build().unwrap();
1155 let err = Command::builder("remote")
1156 .subcommand(sub.clone())
1157 .subcommand(sub)
1158 .build()
1159 .unwrap_err();
1160 assert!(matches!(err, BuildError::DuplicateSubcommandName(_)));
1161 }
1162
1163 #[test]
1164 fn test_variadic_must_be_last() {
1165 let variadic = Argument::builder("files").variadic().build().unwrap();
1166 let after = Argument::builder("extra").build().unwrap();
1167 let err = Command::builder("cmd")
1168 .argument(variadic)
1169 .argument(after)
1170 .build()
1171 .unwrap_err();
1172 assert!(matches!(err, BuildError::VariadicNotLast(_)));
1173 }
1174
1175 #[test]
1176 fn test_meta_field_serde() {
1177 let cmd = Command::builder("x")
1178 .meta("role", serde_json::json!("admin"))
1179 .build()
1180 .unwrap();
1181 let json = serde_json::to_string(&cmd).unwrap();
1182 assert!(json.contains("admin"));
1183 let de: Command = serde_json::from_str(&json).unwrap();
1184 assert_eq!(de.extra["role"], serde_json::json!("admin"));
1185 }
1186
1187 #[test]
1188 fn test_meta_empty_not_serialized() {
1189 let cmd = Command::builder("x").build().unwrap();
1190 let json = serde_json::to_string(&cmd).unwrap();
1191 assert!(!json.contains("extra"));
1192 }
1193
1194 #[test]
1195 fn test_semantic_alias_builder() {
1196 let cmd = Command::builder("deploy")
1197 .semantic_alias("release to production")
1198 .semantic_alias("push to environment")
1199 .build()
1200 .unwrap();
1201 assert_eq!(
1202 cmd.semantic_aliases,
1203 vec!["release to production", "push to environment"]
1204 );
1205 }
1206
1207 #[test]
1208 fn test_semantic_aliases_bulk_builder() {
1209 let cmd = Command::builder("deploy")
1210 .semantic_aliases(["release to production", "push to environment"])
1211 .build()
1212 .unwrap();
1213 assert_eq!(
1214 cmd.semantic_aliases,
1215 vec!["release to production", "push to environment"]
1216 );
1217 }
1218
1219 #[test]
1220 fn test_semantic_alias_not_in_canonical_aliases() {
1221 let cmd = Command::builder("deploy")
1222 .alias("d")
1223 .semantic_alias("release to production")
1224 .build()
1225 .unwrap();
1226 // semantic_aliases and aliases are distinct fields
1227 assert_eq!(cmd.aliases, vec!["d"]);
1228 assert_eq!(cmd.semantic_aliases, vec!["release to production"]);
1229 assert!(!cmd.aliases.contains(&"release to production".to_string()));
1230 assert!(!cmd.semantic_aliases.contains(&"d".to_string()));
1231 }
1232
1233 #[test]
1234 fn test_debug_impl_includes_fields() {
1235 let cmd = Command::builder("debug-test")
1236 .alias("dt")
1237 .spelling("DEBUGTEST")
1238 .summary("Test debug")
1239 .description("A debug test command")
1240 .handler(Arc::new(|_| Ok(())))
1241 .build()
1242 .unwrap();
1243 let s = format!("{:?}", cmd);
1244 assert!(s.contains("debug-test"));
1245 assert!(s.contains("dt"));
1246 assert!(s.contains("Test debug"));
1247 // Handler should appear as "<handler>" not a raw pointer
1248 assert!(s.contains("<handler>"));
1249 // spellings should also be shown
1250 assert!(s.contains("DEBUGTEST"));
1251 }
1252
1253 #[test]
1254 fn test_hash_deterministic() {
1255 use std::collections::hash_map::DefaultHasher;
1256 use std::hash::{Hash, Hasher};
1257
1258 let cmd1 = Command::builder("hash-test")
1259 .summary("Hash")
1260 .meta("key", serde_json::json!("value"))
1261 .build()
1262 .unwrap();
1263 let cmd2 = Command::builder("hash-test")
1264 .summary("Hash")
1265 .meta("key", serde_json::json!("value"))
1266 .build()
1267 .unwrap();
1268
1269 let mut h1 = DefaultHasher::new();
1270 cmd1.hash(&mut h1);
1271 let mut h2 = DefaultHasher::new();
1272 cmd2.hash(&mut h2);
1273 assert_eq!(h1.finish(), h2.finish());
1274 }
1275
1276 #[test]
1277 fn test_ord_by_canonical() {
1278 let a = Command::builder("alpha").build().unwrap();
1279 let b = Command::builder("beta").build().unwrap();
1280 assert!(a < b);
1281 assert!(b > a);
1282 assert_eq!(a.partial_cmp(&a), Some(std::cmp::Ordering::Equal));
1283 }
1284
1285 #[test]
1286 fn test_ord_same_canonical_by_summary() {
1287 let a = Command::builder("cmd").summary("Alpha").build().unwrap();
1288 let b = Command::builder("cmd").summary("Beta").build().unwrap();
1289 assert!(a < b);
1290 }
1291
1292 #[test]
1293 fn test_aliases_builder_method() {
1294 let cmd = Command::builder("cmd")
1295 .aliases(["a", "b", "c"])
1296 .build()
1297 .unwrap();
1298 assert_eq!(cmd.aliases, vec!["a", "b", "c"]);
1299 }
1300
1301 #[test]
1302 fn test_spellings_builder_method() {
1303 let cmd = Command::builder("deploy")
1304 .spellings(["DEPLOY", "dploy"])
1305 .build()
1306 .unwrap();
1307 assert_eq!(cmd.spellings, vec!["DEPLOY", "dploy"]);
1308 }
1309
1310 #[test]
1311 fn test_best_practice_and_anti_pattern_builder() {
1312 let cmd = Command::builder("cmd")
1313 .best_practice("Always dry-run first")
1314 .anti_pattern("Deploy on Fridays")
1315 .build()
1316 .unwrap();
1317 assert_eq!(cmd.best_practices, vec!["Always dry-run first"]);
1318 assert_eq!(cmd.anti_patterns, vec!["Deploy on Fridays"]);
1319 }
1320
1321 #[test]
1322 fn test_exclusive_group_too_small() {
1323 use crate::model::BuildError;
1324 let result = Command::builder("cmd")
1325 .flag(Flag::builder("json").build().unwrap())
1326 .exclusive(["json"])
1327 .build();
1328 assert!(matches!(result, Err(BuildError::ExclusiveGroupTooSmall)));
1329 }
1330
1331 #[test]
1332 fn test_exclusive_group_unknown_flag() {
1333 use crate::model::BuildError;
1334 let result = Command::builder("cmd")
1335 .flag(Flag::builder("json").build().unwrap())
1336 .exclusive(["json", "nonexistent"])
1337 .build();
1338 assert!(matches!(
1339 result,
1340 Err(BuildError::ExclusiveGroupUnknownFlag(_))
1341 ));
1342 }
1343
1344 #[test]
1345 fn test_parsed_command_helpers() {
1346 use crate::{Argument, Flag, Parser};
1347 let cmd = Command::builder("serve")
1348 .argument(Argument::builder("host").required().build().unwrap())
1349 .flag(
1350 Flag::builder("port")
1351 .takes_value()
1352 .default_value("8080")
1353 .build()
1354 .unwrap(),
1355 )
1356 .flag(Flag::builder("verbose").build().unwrap())
1357 .build()
1358 .unwrap();
1359 let cmds = vec![cmd];
1360 let parser = Parser::new(&cmds);
1361 let parsed = parser.parse(&["serve", "localhost", "--verbose"]).unwrap();
1362
1363 assert_eq!(parsed.arg("host"), Some("localhost"));
1364 assert_eq!(parsed.arg("missing"), None);
1365 assert_eq!(parsed.flag("port"), Some("8080"));
1366 assert_eq!(parsed.flag("missing"), None);
1367 assert!(parsed.flag_bool("verbose"));
1368 assert!(!parsed.flag_bool("missing"));
1369 assert_eq!(parsed.flag_count("verbose"), 1);
1370 assert_eq!(parsed.flag_count("missing"), 0);
1371 assert_eq!(parsed.flag_values("port"), vec!["8080"]);
1372 assert!(parsed.flag_values("missing").is_empty());
1373 assert!(parsed.has_flag("port"));
1374 assert!(!parsed.has_flag("missing"));
1375
1376 let port: u16 = parsed.flag_as("port").unwrap().unwrap();
1377 assert_eq!(port, 8080);
1378
1379 let port_or: u16 = parsed.flag_as_or("port", 9000);
1380 assert_eq!(port_or, 8080);
1381
1382 let missing_or: u16 = parsed.flag_as_or("missing", 9000);
1383 assert_eq!(missing_or, 9000);
1384
1385 let host: String = parsed.arg_as("host").unwrap().unwrap();
1386 assert_eq!(host, "localhost");
1387
1388 let missing_as: Option<Result<u32, _>> = parsed.arg_as("missing");
1389 assert!(missing_as.is_none());
1390
1391 let arg_or: String = parsed.arg_as_or("host", "default".to_string());
1392 assert_eq!(arg_or, "localhost");
1393
1394 let missing_arg_or: String = parsed.arg_as_or("missing", "default".to_string());
1395 assert_eq!(missing_arg_or, "default");
1396 }
1397
1398 #[test]
1399 fn test_flag_count_false_returns_zero() {
1400 use crate::{Flag, Parser};
1401 let cmd = Command::builder("cmd")
1402 .flag(Flag::builder("verbose").build().unwrap())
1403 .build()
1404 .unwrap();
1405 let cmds = vec![cmd];
1406 let parser = Parser::new(&cmds);
1407 let parsed = parser.parse(&["cmd", "--no-verbose"]).unwrap();
1408 assert_eq!(parsed.flag_count("verbose"), 0);
1409 }
1410
1411 #[test]
1412 fn test_flag_values_json_array() {
1413 use crate::{Flag, Parser};
1414 let cmd = Command::builder("cmd")
1415 .flag(
1416 Flag::builder("tag")
1417 .takes_value()
1418 .repeatable()
1419 .build()
1420 .unwrap(),
1421 )
1422 .build()
1423 .unwrap();
1424 let cmds = vec![cmd];
1425 let parser = Parser::new(&cmds);
1426 let parsed = parser.parse(&["cmd", "--tag=alpha", "--tag=beta"]).unwrap();
1427 let values = parsed.flag_values("tag");
1428 assert_eq!(values, vec!["alpha", "beta"]);
1429 }
1430}