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