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 if blank_comment_regex.is_match(line) {
277 continue;
278 }
279 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}