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