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