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