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