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