Skip to main content

usage/spec/
mod.rs

1pub mod arg;
2pub mod builder;
3pub mod choices;
4pub mod cmd;
5pub mod complete;
6pub mod config;
7mod context;
8pub mod data_types;
9pub mod effect;
10pub mod flag;
11pub mod helpers;
12pub mod mount;
13
14use indexmap::IndexMap;
15use kdl::{KdlDocument, KdlEntry, KdlNode};
16use log::{info, warn};
17use serde::Serialize;
18use std::fmt::{Display, Formatter};
19use std::iter::once;
20use std::path::Path;
21use std::str::FromStr;
22use xx::file;
23
24use crate::error::UsageErr;
25use crate::spec::cmd::{SpecCommand, SpecExample};
26use crate::spec::config::SpecConfig;
27use crate::spec::context::ParsingContext;
28use crate::spec::helpers::{string_entry, NodeHelper};
29use crate::{SpecArg, SpecComplete, SpecFlag};
30
31#[derive(Debug, Default, Clone, Serialize)]
32#[non_exhaustive]
33pub struct Spec {
34    pub name: String,
35    pub bin: String,
36    pub cmd: SpecCommand,
37    pub config: SpecConfig,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub version: Option<String>,
40    pub usage: String,
41    pub complete: IndexMap<String, SpecComplete>,
42
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub source_code_link_template: Option<String>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub author: Option<String>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub about: Option<String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub about_long: Option<String>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub about_md: Option<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub license: Option<String>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub before_help: Option<String>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub after_help: Option<String>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub before_help_long: Option<String>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub after_help_long: Option<String>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub disable_help: Option<bool>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub min_usage_version: Option<String>,
67    #[serde(skip_serializing_if = "Vec::is_empty")]
68    pub examples: Vec<SpecExample>,
69    /// Default subcommand to use when first non-flag argument is not a known subcommand.
70    /// This enables "naked" command syntax like `mise foo` instead of `mise run foo`.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub default_subcommand: Option<String>,
73}
74
75impl Spec {
76    /// Parse a spec from a file.
77    ///
78    /// Automatically detects whether the file is:
79    /// - A `.kdl` or `.usage.kdl` file containing a raw spec
80    /// - A script file with embedded `# USAGE:` comments
81    ///
82    /// If `bin` is not specified in the spec, it defaults to the filename.
83    #[must_use = "parsing result should be used"]
84    pub fn parse_file(file: &Path) -> Result<Spec, UsageErr> {
85        let spec = split_script(file)?;
86        let ctx = ParsingContext::new(file, &spec);
87        let mut schema = Self::parse(&ctx, &spec)?;
88        if schema.bin.is_empty() {
89            schema.bin = file
90                .file_name()
91                .and_then(|n| n.to_str())
92                .ok_or_else(|| UsageErr::InvalidPath(file.display().to_string()))?
93                .to_string();
94        }
95        if schema.name.is_empty() {
96            schema.name.clone_from(&schema.bin);
97        }
98        Ok(schema)
99    }
100    /// Parse a spec from a script file's embedded USAGE comments.
101    ///
102    /// Extracts the spec from comment lines starting with `# USAGE:` or `// USAGE:`.
103    /// If `bin` is not specified in the spec, it defaults to the filename.
104    #[must_use = "parsing result should be used"]
105    pub fn parse_script(file: &Path) -> Result<Spec, UsageErr> {
106        let raw = extract_usage_from_comments(&file::read_to_string(file)?);
107        let ctx = ParsingContext::new(file, &raw);
108        let mut spec = Self::parse(&ctx, &raw)?;
109        if spec.bin.is_empty() {
110            spec.bin = file
111                .file_name()
112                .and_then(|n| n.to_str())
113                .ok_or_else(|| UsageErr::InvalidPath(file.display().to_string()))?
114                .to_string();
115        }
116        if spec.name.is_empty() {
117            spec.name.clone_from(&spec.bin);
118        }
119        Ok(spec)
120    }
121
122    #[deprecated]
123    pub fn parse_spec(input: &str) -> Result<Spec, UsageErr> {
124        Self::parse(&Default::default(), input)
125    }
126
127    pub fn is_empty(&self) -> bool {
128        self.name.is_empty()
129            && self.bin.is_empty()
130            && self.usage.is_empty()
131            && self.cmd.is_empty()
132            && self.config.is_empty()
133            && self.complete.is_empty()
134            && self.examples.is_empty()
135    }
136
137    pub(crate) fn parse(ctx: &ParsingContext, input: &str) -> Result<Spec, UsageErr> {
138        let kdl: KdlDocument = input
139            .parse()
140            .map_err(|err: kdl::KdlError| UsageErr::KdlError(err))?;
141        let mut schema = Self {
142            ..Default::default()
143        };
144        for node in kdl.nodes().iter().map(|n| NodeHelper::new(ctx, n)) {
145            match node.name() {
146                "name" => schema.name = node.arg(0)?.ensure_string()?,
147                "bin" => {
148                    schema.bin = node.arg(0)?.ensure_string()?;
149                    if schema.name.is_empty() {
150                        schema.name.clone_from(&schema.bin);
151                    }
152                }
153                "version" => schema.version = Some(node.arg(0)?.ensure_string()?),
154                "author" => schema.author = Some(node.arg(0)?.ensure_string()?),
155                "source_code_link_template" => {
156                    schema.source_code_link_template = Some(node.arg(0)?.ensure_string()?)
157                }
158                "about" => schema.about = Some(node.arg(0)?.ensure_string()?),
159                "long_about" => schema.about_long = Some(node.arg(0)?.ensure_string()?),
160                "about_long" => schema.about_long = Some(node.arg(0)?.ensure_string()?),
161                "about_md" => schema.about_md = Some(node.arg(0)?.ensure_string()?),
162                "license" => schema.license = Some(node.arg(0)?.ensure_string()?),
163                "before_help" => schema.before_help = Some(node.arg(0)?.ensure_string()?),
164                "after_help" => schema.after_help = Some(node.arg(0)?.ensure_string()?),
165                "before_long_help" | "before_help_long" => {
166                    schema.before_help_long = Some(node.arg(0)?.ensure_string()?)
167                }
168                "after_long_help" | "after_help_long" => {
169                    schema.after_help_long = Some(node.arg(0)?.ensure_string()?)
170                }
171                "usage" => schema.usage = node.arg(0)?.ensure_string()?,
172                "arg" => schema.cmd.args.push(SpecArg::parse(ctx, &node)?),
173                "flag" => schema.cmd.flags.push(SpecFlag::parse(ctx, &node)?),
174                "cmd" => {
175                    let node: SpecCommand = SpecCommand::parse(ctx, &node)?;
176                    schema.cmd.subcommands.insert(node.name.to_string(), node);
177                }
178                "config" => schema.config = SpecConfig::parse(ctx, &node)?,
179                "complete" => {
180                    let complete = SpecComplete::parse(ctx, &node)?;
181                    schema.complete.insert(complete.name.clone(), complete);
182                }
183                "disable_help" => schema.disable_help = Some(node.arg(0)?.ensure_bool()?),
184                "min_usage_version" => {
185                    let v = node.arg(0)?.ensure_string()?;
186                    check_usage_version(&v);
187                    schema.min_usage_version = Some(v);
188                }
189                "default_subcommand" => {
190                    schema.default_subcommand = Some(node.arg(0)?.ensure_string()?)
191                }
192                "example" => {
193                    let code = node.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?;
194                    let mut example = SpecExample::new(code.trim().to_string());
195                    for (k, v) in node.props() {
196                        match k {
197                            "header" => example.header = Some(v.ensure_string()?),
198                            "help" => example.help = Some(v.ensure_string()?),
199                            "lang" => example.lang = v.ensure_string()?,
200                            k => bail_parse!(ctx, v.entry.span(), "unsupported example key {k}"),
201                        }
202                    }
203                    schema.examples.push(example);
204                }
205                "include" => {
206                    let file = node
207                        .props()
208                        .get("file")
209                        .map(|v| v.ensure_string())
210                        .transpose()?
211                        .ok_or_else(|| ctx.build_err("missing file".into(), node.span()))?;
212                    let file = Path::new(&file);
213                    let file = match file.is_relative() {
214                        true => ctx
215                            .file
216                            .parent()
217                            .ok_or_else(|| {
218                                ctx.build_err(
219                                    format!("cannot get parent of {}", ctx.file.display()),
220                                    node.span(),
221                                )
222                            })?
223                            .join(file),
224                        false => file.to_path_buf(),
225                    };
226                    info!("include: {}", file.display());
227                    let other = Self::parse_file(&file)?;
228                    schema.merge(other);
229                }
230                k => bail_parse!(ctx, node.node.name().span(), "unsupported spec key {k}"),
231            }
232        }
233        schema.cmd.name = if schema.bin.is_empty() {
234            schema.name.clone()
235        } else {
236            schema.bin.clone()
237        };
238        set_subcommand_ancestors(&mut schema.cmd, &[]);
239        Ok(schema)
240    }
241
242    pub fn merge(&mut self, other: Spec) {
243        macro_rules! merge_str {
244            ($field:ident) => {
245                if !other.$field.is_empty() {
246                    self.$field = other.$field;
247                }
248            };
249        }
250        macro_rules! merge_opt {
251            ($field:ident) => {
252                if other.$field.is_some() {
253                    self.$field = other.$field;
254                }
255            };
256        }
257        macro_rules! merge_extend {
258            ($field:ident) => {
259                if !other.$field.is_empty() {
260                    self.$field.extend(other.$field);
261                }
262            };
263        }
264
265        merge_str!(name);
266        merge_str!(bin);
267        merge_str!(usage);
268        merge_opt!(about);
269        merge_opt!(source_code_link_template);
270        merge_opt!(version);
271        merge_opt!(author);
272        merge_opt!(about_long);
273        merge_opt!(about_md);
274        merge_opt!(license);
275        merge_opt!(before_help);
276        merge_opt!(after_help);
277        merge_opt!(before_help_long);
278        merge_opt!(after_help_long);
279        merge_opt!(disable_help);
280        merge_opt!(min_usage_version);
281        merge_opt!(default_subcommand);
282        merge_extend!(complete);
283        merge_extend!(examples);
284
285        if !other.config.is_empty() {
286            self.config.merge(&other.config);
287        }
288        self.cmd.merge(other.cmd);
289    }
290}
291
292fn check_usage_version(version: &str) {
293    let cur = versions::Versioning::new(env!("CARGO_PKG_VERSION")).unwrap();
294    match versions::Versioning::new(version) {
295        Some(v) => {
296            if cur < v {
297                warn!(
298                    "This usage spec requires at least version {version}, but you are using version {cur} of usage"
299                );
300            }
301        }
302        _ => warn!("Invalid version: {version}"),
303    }
304}
305
306fn split_script(file: &Path) -> Result<String, UsageErr> {
307    let full = file::read_to_string(file)?;
308    // If file has a shebang and USAGE comments, extract the spec from comments
309    if full.starts_with("#!") {
310        let usage_regex = xx::regex!(r"^(?:#|//|::)(?:USAGE| ?\[USAGE\])");
311        if full.lines().any(|l| usage_regex.is_match(l)) {
312            return Ok(extract_usage_from_comments(&full));
313        }
314    }
315    // Otherwise treat the whole file as a KDL spec (e.g., .usage.kdl files)
316    Ok(full)
317}
318
319fn extract_usage_from_comments(full: &str) -> String {
320    let usage_regex = xx::regex!(r"^(?:#|//|::)(?:USAGE| ?\[USAGE\])(.*)$");
321    let blank_comment_regex = xx::regex!(r"^(?:#|//|::)\s*$");
322    let mut usage = vec![];
323    let mut found = false;
324    for line in full.lines() {
325        if let Some(captures) = usage_regex.captures(line) {
326            found = true;
327            let content = captures.get(1).map_or("", |m| m.as_str());
328            usage.push(content.trim());
329        } else if found {
330            // Allow blank comment lines to continue parsing
331            if blank_comment_regex.is_match(line) {
332                continue;
333            }
334            // if there is a non-blank non-USAGE line, stop reading
335            break;
336        }
337    }
338    usage.join("\n")
339}
340
341fn set_subcommand_ancestors(cmd: &mut SpecCommand, ancestors: &[String]) {
342    for subcmd in cmd.subcommands.values_mut() {
343        subcmd.full_cmd = ancestors
344            .iter()
345            .cloned()
346            .chain(once(subcmd.name.clone()))
347            .collect();
348        let child_ancestors = subcmd.full_cmd.clone();
349        set_subcommand_ancestors(subcmd, &child_ancestors);
350    }
351    if cmd.usage.is_empty() {
352        cmd.usage = cmd.usage();
353    }
354}
355
356impl Display for Spec {
357    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
358        let mut doc = KdlDocument::new();
359        let nodes = &mut doc.nodes_mut();
360        if !self.name.is_empty() {
361            let mut node = KdlNode::new("name");
362            node.push(string_entry(None, &self.name));
363            nodes.push(node);
364        }
365        if !self.bin.is_empty() {
366            let mut node = KdlNode::new("bin");
367            node.push(string_entry(None, &self.bin));
368            nodes.push(node);
369        }
370        if let Some(version) = &self.version {
371            let mut node = KdlNode::new("version");
372            node.push(string_entry(None, version));
373            nodes.push(node);
374        }
375        if let Some(author) = &self.author {
376            let mut node = KdlNode::new("author");
377            node.push(string_entry(None, author));
378            nodes.push(node);
379        }
380        if let Some(about) = &self.about {
381            let mut node = KdlNode::new("about");
382            node.push(string_entry(None, about));
383            nodes.push(node);
384        }
385        if let Some(source_code_link_template) = &self.source_code_link_template {
386            let mut node = KdlNode::new("source_code_link_template");
387            node.push(string_entry(None, source_code_link_template));
388            nodes.push(node);
389        }
390        if let Some(about_md) = &self.about_md {
391            let mut node = KdlNode::new("about_md");
392            node.push(string_entry(None, about_md));
393            nodes.push(node);
394        }
395        if let Some(long_about) = &self.about_long {
396            let mut node = KdlNode::new("long_about");
397            node.push(string_entry(None, long_about));
398            nodes.push(node);
399        }
400        if let Some(license) = &self.license {
401            let mut node = KdlNode::new("license");
402            node.push(string_entry(None, license));
403            nodes.push(node);
404        }
405        if let Some(before_help) = &self.before_help {
406            let mut node = KdlNode::new("before_help");
407            node.push(string_entry(None, before_help));
408            nodes.push(node);
409        }
410        if let Some(after_help) = &self.after_help {
411            let mut node = KdlNode::new("after_help");
412            node.push(string_entry(None, after_help));
413            nodes.push(node);
414        }
415        if let Some(before_help_long) = &self.before_help_long {
416            let mut node = KdlNode::new("before_long_help");
417            node.push(string_entry(None, before_help_long));
418            nodes.push(node);
419        }
420        if let Some(after_help_long) = &self.after_help_long {
421            let mut node = KdlNode::new("after_long_help");
422            node.push(string_entry(None, after_help_long));
423            nodes.push(node);
424        }
425        if let Some(disable_help) = self.disable_help {
426            let mut node = KdlNode::new("disable_help");
427            node.push(KdlEntry::new(disable_help));
428            nodes.push(node);
429        }
430        if let Some(min_usage_version) = &self.min_usage_version {
431            let mut node = KdlNode::new("min_usage_version");
432            node.push(string_entry(None, min_usage_version));
433            nodes.push(node);
434        }
435        if let Some(default_subcommand) = &self.default_subcommand {
436            let mut node = KdlNode::new("default_subcommand");
437            node.push(string_entry(None, default_subcommand));
438            nodes.push(node);
439        }
440        if !self.usage.is_empty() {
441            let mut node = KdlNode::new("usage");
442            node.push(string_entry(None, &self.usage));
443            nodes.push(node);
444        }
445        for flag in self.cmd.flags.iter() {
446            nodes.push(flag.into());
447        }
448        for arg in self.cmd.args.iter() {
449            nodes.push(arg.into());
450        }
451        for example in self.examples.iter() {
452            nodes.push(example.into());
453        }
454        for complete in self.complete.values() {
455            nodes.push(complete.into());
456        }
457        for complete in self.cmd.complete.values() {
458            nodes.push(complete.into());
459        }
460        for cmd in self.cmd.subcommands.values() {
461            nodes.push(cmd.into())
462        }
463        if !self.config.is_empty() {
464            nodes.push((&self.config).into());
465        }
466        doc.autoformat_config(&kdl::FormatConfigBuilder::new().build());
467        write!(f, "{doc}")
468    }
469}
470
471impl FromStr for Spec {
472    type Err = UsageErr;
473
474    fn from_str(s: &str) -> Result<Self, Self::Err> {
475        Self::parse(&Default::default(), s)
476    }
477}
478
479#[cfg(feature = "clap")]
480impl From<&clap::Command> for Spec {
481    fn from(cmd: &clap::Command) -> Self {
482        Spec {
483            name: cmd.get_name().to_string(),
484            bin: cmd.get_bin_name().unwrap_or(cmd.get_name()).to_string(),
485            cmd: cmd.into(),
486            version: cmd.get_version().map(|v| v.to_string()),
487            about: cmd.get_about().map(|a| a.to_string()),
488            about_long: cmd.get_long_about().map(|a| a.to_string()),
489            usage: cmd.clone().render_usage().to_string(),
490            ..Default::default()
491        }
492    }
493}
494
495#[inline]
496pub fn is_true(b: &bool) -> bool {
497    *b
498}
499
500#[inline]
501pub fn is_false(b: &bool) -> bool {
502    !is_true(b)
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508    use insta::assert_snapshot;
509
510    #[test]
511    fn test_display() {
512        let spec = Spec::parse(
513            &Default::default(),
514            r#"
515name "Usage CLI"
516bin "usage"
517arg "arg1"
518flag "-f --force" global=#true
519cmd "config" {
520  cmd "set" {
521    arg "key" help="Key to set"
522    arg "value"
523  }
524}
525complete "file" run="ls" descriptions=#true
526        "#,
527        )
528        .unwrap();
529        assert_snapshot!(spec, @r#"
530        name "Usage CLI"
531        bin usage
532        flag "-f --force" global=#true
533        arg <arg1>
534        complete file run=ls descriptions=#true
535        cmd config {
536            cmd set {
537                arg <key> help="Key to set"
538                arg <value>
539            }
540        }
541        "#);
542    }
543
544    #[test]
545    #[cfg(feature = "clap")]
546    fn test_clap() {
547        let cmd = clap::Command::new("test");
548        assert_snapshot!(Spec::from(&cmd), @r#"
549        name test
550        bin test
551        usage "Usage: test"
552        "#);
553    }
554
555    macro_rules! extract_usage_tests {
556        ($($name:ident: $input:expr, $expected:expr,)*) => {
557        $(
558            #[test]
559            fn $name() {
560                let result = extract_usage_from_comments($input);
561                let expected = $expected.trim_start_matches('\n').trim_end();
562                assert_eq!(result, expected);
563            }
564        )*
565        }
566    }
567
568    extract_usage_tests! {
569        test_extract_usage_from_comments_original_hash:
570            r#"
571#!/bin/bash
572#USAGE bin "test"
573#USAGE flag "--foo" help="test"
574echo "hello"
575            "#,
576            r#"
577bin "test"
578flag "--foo" help="test"
579            "#,
580
581        test_extract_usage_from_comments_original_double_slash:
582            r#"
583#!/usr/bin/env node
584//USAGE bin "test"
585//USAGE flag "--foo" help="test"
586console.log("hello");
587            "#,
588            r#"
589bin "test"
590flag "--foo" help="test"
591            "#,
592
593        test_extract_usage_from_comments_bracket_with_space:
594            r#"
595#!/bin/bash
596# [USAGE] bin "test"
597# [USAGE] flag "--foo" help="test"
598echo "hello"
599            "#,
600            r#"
601bin "test"
602flag "--foo" help="test"
603            "#,
604
605        test_extract_usage_from_comments_bracket_no_space:
606            r#"
607#!/bin/bash
608#[USAGE] bin "test"
609#[USAGE] flag "--foo" help="test"
610echo "hello"
611            "#,
612            r#"
613bin "test"
614flag "--foo" help="test"
615            "#,
616
617        test_extract_usage_from_comments_double_slash_bracket_with_space:
618            r#"
619#!/usr/bin/env node
620// [USAGE] bin "test"
621// [USAGE] flag "--foo" help="test"
622console.log("hello");
623            "#,
624            r#"
625bin "test"
626flag "--foo" help="test"
627            "#,
628
629        test_extract_usage_from_comments_double_slash_bracket_no_space:
630            r#"
631#!/usr/bin/env node
632//[USAGE] bin "test"
633//[USAGE] flag "--foo" help="test"
634console.log("hello");
635            "#,
636            r#"
637bin "test"
638flag "--foo" help="test"
639            "#,
640
641        test_extract_usage_from_comments_stops_at_gap:
642            r#"
643#!/bin/bash
644#USAGE bin "test"
645#USAGE flag "--foo" help="test"
646
647#USAGE flag "--bar" help="should not be included"
648echo "hello"
649            "#,
650            r#"
651bin "test"
652flag "--foo" help="test"
653            "#,
654
655        test_extract_usage_from_comments_with_content_after_marker:
656            r#"
657#!/bin/bash
658# [USAGE] bin "test"
659# [USAGE] flag "--verbose" help="verbose mode"
660# [USAGE] arg "input" help="input file"
661echo "hello"
662            "#,
663            r#"
664bin "test"
665flag "--verbose" help="verbose mode"
666arg "input" help="input file"
667            "#,
668
669        test_extract_usage_from_comments_double_colon_original:
670            r#"
671::USAGE bin "test"
672::USAGE flag "--foo" help="test"
673echo "hello"
674            "#,
675            r#"
676bin "test"
677flag "--foo" help="test"
678            "#,
679
680        test_extract_usage_from_comments_double_colon_bracket_with_space:
681            r#"
682:: [USAGE] bin "test"
683:: [USAGE] flag "--foo" help="test"
684echo "hello"
685            "#,
686            r#"
687bin "test"
688flag "--foo" help="test"
689            "#,
690
691        test_extract_usage_from_comments_double_colon_bracket_no_space:
692            r#"
693::[USAGE] bin "test"
694::[USAGE] flag "--foo" help="test"
695echo "hello"
696            "#,
697            r#"
698bin "test"
699flag "--foo" help="test"
700            "#,
701
702        test_extract_usage_from_comments_double_colon_stops_at_gap:
703            r#"
704::USAGE bin "test"
705::USAGE flag "--foo" help="test"
706
707::USAGE flag "--bar" help="should not be included"
708echo "hello"
709            "#,
710            r#"
711bin "test"
712flag "--foo" help="test"
713            "#,
714
715        test_extract_usage_from_comments_double_colon_with_content_after_marker:
716            r#"
717::USAGE bin "test"
718::USAGE flag "--verbose" help="verbose mode"
719::USAGE arg "input" help="input file"
720echo "hello"
721            "#,
722            r#"
723bin "test"
724flag "--verbose" help="verbose mode"
725arg "input" help="input file"
726            "#,
727
728        test_extract_usage_from_comments_double_colon_bracket_with_space_multiple_lines:
729            r#"
730:: [USAGE] bin "myapp"
731:: [USAGE] flag "--config <file>" help="config file"
732:: [USAGE] flag "--verbose" help="verbose output"
733:: [USAGE] arg "input" help="input file"
734:: [USAGE] arg "[output]" help="output file" required=#false
735echo "done"
736            "#,
737            r#"
738bin "myapp"
739flag "--config <file>" help="config file"
740flag "--verbose" help="verbose output"
741arg "input" help="input file"
742arg "[output]" help="output file" required=#false
743            "#,
744
745        test_extract_usage_from_comments_empty:
746            r#"
747#!/bin/bash
748echo "hello"
749            "#,
750            "",
751
752        test_extract_usage_from_comments_lowercase_usage:
753            r#"
754#!/bin/bash
755#usage bin "test"
756#usage flag "--foo" help="test"
757echo "hello"
758            "#,
759            "",
760
761        test_extract_usage_from_comments_mixed_case_usage:
762            r#"
763#!/bin/bash
764#Usage bin "test"
765#Usage flag "--foo" help="test"
766echo "hello"
767            "#,
768            "",
769
770        test_extract_usage_from_comments_space_before_usage:
771            r#"
772#!/bin/bash
773# USAGE bin "test"
774# USAGE flag "--foo" help="test"
775echo "hello"
776            "#,
777            "",
778
779        test_extract_usage_from_comments_double_slash_lowercase:
780            r#"
781#!/usr/bin/env node
782//usage bin "test"
783//usage flag "--foo" help="test"
784console.log("hello");
785            "#,
786            "",
787
788        test_extract_usage_from_comments_double_slash_mixed_case:
789            r#"
790#!/usr/bin/env node
791//Usage bin "test"
792//Usage flag "--foo" help="test"
793console.log("hello");
794            "#,
795            "",
796
797        test_extract_usage_from_comments_double_slash_space_before_usage:
798            r#"
799#!/usr/bin/env node
800// USAGE bin "test"
801// USAGE flag "--foo" help="test"
802console.log("hello");
803            "#,
804            "",
805
806        test_extract_usage_from_comments_bracket_lowercase:
807            r#"
808#!/bin/bash
809#[usage] bin "test"
810#[usage] flag "--foo" help="test"
811echo "hello"
812            "#,
813            "",
814
815        test_extract_usage_from_comments_bracket_mixed_case:
816            r#"
817#!/bin/bash
818#[Usage] bin "test"
819#[Usage] flag "--foo" help="test"
820echo "hello"
821            "#,
822            "",
823
824        test_extract_usage_from_comments_bracket_space_lowercase:
825            r#"
826#!/bin/bash
827# [usage] bin "test"
828# [usage] flag "--foo" help="test"
829echo "hello"
830            "#,
831            "",
832
833        test_extract_usage_from_comments_double_colon_lowercase:
834            r#"
835::usage bin "test"
836::usage flag "--foo" help="test"
837echo "hello"
838            "#,
839            "",
840
841        test_extract_usage_from_comments_double_colon_mixed_case:
842            r#"
843::Usage bin "test"
844::Usage flag "--foo" help="test"
845echo "hello"
846            "#,
847            "",
848
849        test_extract_usage_from_comments_double_colon_space_before_usage:
850            r#"
851:: USAGE bin "test"
852:: USAGE flag "--foo" help="test"
853echo "hello"
854            "#,
855            "",
856
857        test_extract_usage_from_comments_double_colon_bracket_lowercase:
858            r#"
859::[usage] bin "test"
860::[usage] flag "--foo" help="test"
861echo "hello"
862            "#,
863            "",
864
865        test_extract_usage_from_comments_double_colon_bracket_mixed_case:
866            r#"
867::[Usage] bin "test"
868::[Usage] flag "--foo" help="test"
869echo "hello"
870            "#,
871            "",
872
873        test_extract_usage_from_comments_double_colon_bracket_space_lowercase:
874            r#"
875:: [usage] bin "test"
876:: [usage] flag "--foo" help="test"
877echo "hello"
878            "#,
879            "",
880    }
881
882    #[test]
883    fn test_spec_with_examples() {
884        let spec = Spec::parse(
885            &Default::default(),
886            r#"
887name "demo"
888bin "demo"
889example "demo --help" header="Getting help" help="Display help information"
890example "demo --version" header="Check version"
891        "#,
892        )
893        .unwrap();
894
895        assert_eq!(spec.examples.len(), 2);
896
897        assert_eq!(spec.examples[0].code, "demo --help");
898        assert_eq!(spec.examples[0].header, Some("Getting help".to_string()));
899        assert_eq!(
900            spec.examples[0].help,
901            Some("Display help information".to_string())
902        );
903
904        assert_eq!(spec.examples[1].code, "demo --version");
905        assert_eq!(spec.examples[1].header, Some("Check version".to_string()));
906        assert_eq!(spec.examples[1].help, None);
907    }
908
909    #[test]
910    fn test_spec_examples_display() {
911        let spec = Spec::parse(
912            &Default::default(),
913            r#"
914name "demo"
915bin "demo"
916example "demo --help" header="Getting help" help="Show help"
917example "demo --version"
918        "#,
919        )
920        .unwrap();
921
922        let output = format!("{}", spec);
923        assert!(
924            output.contains("example \"demo --help\" header=\"Getting help\" help=\"Show help\"")
925        );
926        assert!(output.contains("example \"demo --version\""));
927    }
928}