usage/spec/
mod.rs

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