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