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