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