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