1use crate::formatting::{render_yaml, serialize_json_output};
2use crate::protocol::build_cli_error;
3use crate::redaction::{
4 OutputOptions, PlainStyle, RedactionContext, RedactionPolicy, Redactor, is_secret_flag_name,
5};
6use serde_json::Value;
7
8#[cfg(feature = "cli-help")]
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum HelpScope {
18 OneLevel,
23 Recursive,
25}
26
27#[cfg(feature = "cli-help")]
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum HelpFormat {
33 Plain,
34 Markdown,
35 Json,
36 Yaml,
37}
38
39#[cfg(feature = "cli-help")]
40impl HelpFormat {
41 fn parse(s: &str) -> Option<Self> {
42 match s {
43 "plain" => Some(Self::Plain),
44 "markdown" => Some(Self::Markdown),
45 "json" => Some(Self::Json),
46 "yaml" => Some(Self::Yaml),
47 _ => None,
48 }
49 }
50}
51
52#[cfg(feature = "cli-help")]
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub struct HelpOptions {
58 pub scope: HelpScope,
59 pub format: HelpFormat,
60}
61
62#[cfg(feature = "cli-help")]
63impl HelpOptions {
64 pub const fn one_level_plain() -> Self {
66 Self {
67 scope: HelpScope::OneLevel,
68 format: HelpFormat::Plain,
69 }
70 }
71
72 pub const fn recursive_plain() -> Self {
74 Self {
75 scope: HelpScope::Recursive,
76 format: HelpFormat::Plain,
77 }
78 }
79}
80
81#[cfg(feature = "cli-help")]
89#[derive(Clone, Debug, PartialEq, Eq)]
90pub struct HelpConfig {
91 pub default_scope: HelpScope,
93 pub default_format: HelpFormat,
99}
100
101#[cfg(feature = "cli-help")]
102impl HelpConfig {
103 pub const fn output_aware() -> Self {
111 Self::output_aware_with_fallback(HelpFormat::Plain)
112 }
113
114 pub const fn output_aware_with_fallback(default_format: HelpFormat) -> Self {
121 Self {
122 default_scope: HelpScope::OneLevel,
123 default_format,
124 }
125 }
126
127 pub const fn human_cli_default() -> Self {
132 Self::output_aware()
133 }
134}
135
136#[cfg(feature = "cli-help")]
149pub fn cli_render_help_with_options(
150 cmd: &clap::Command,
151 subcommand_path: &[&str],
152 options: &HelpOptions,
153) -> String {
154 let selected = scoped_help_command(cmd, subcommand_path);
155 let target = &selected.conventional_command;
156 let mut rendered = match options.format {
157 HelpFormat::Plain => match options.scope {
158 HelpScope::OneLevel => render_help_one_level_plain(target),
159 HelpScope::Recursive => {
160 let mut buf = String::new();
161 render_help_recursive_plain(target, &[], &mut buf);
162 buf
163 }
164 },
165 HelpFormat::Markdown => render_help_markdown(
166 target,
167 std::slice::from_ref(&selected.command_path),
168 options.scope,
169 ),
170 HelpFormat::Json => {
171 serialize_json_output(help_result_event(&selected, options.scope).as_value())
172 }
173 HelpFormat::Yaml => render_yaml(
174 help_result_event(&selected, options.scope).as_value(),
175 &OutputOptions {
176 redaction: Redactor::new().policy(RedactionPolicy::Off),
177 style: PlainStyle::Raw,
178 },
179 ),
180 };
181 while rendered.ends_with('\n') {
185 rendered.pop();
186 }
187 rendered.push('\n');
188 rendered
189}
190
191#[cfg(feature = "cli-help")]
198pub fn cli_render_help(cmd: &clap::Command, subcommand_path: &[&str]) -> String {
199 cli_render_help_with_options(cmd, subcommand_path, &HelpOptions::recursive_plain())
200}
201
202#[cfg(feature = "cli-help-markdown")]
209pub fn cli_render_help_markdown(cmd: &clap::Command, subcommand_path: &[&str]) -> String {
210 cli_render_help_with_options(
211 cmd,
212 subcommand_path,
213 &HelpOptions {
214 scope: HelpScope::Recursive,
215 format: HelpFormat::Markdown,
216 },
217 )
218}
219
220#[cfg(feature = "cli-help")]
236pub fn cli_handle_help_or_continue(
237 raw_args: &[String],
238 cmd: &clap::Command,
239 config: &HelpConfig,
240) -> Result<Option<String>, Value> {
241 cli_handle_help_or_continue_inner(raw_args, cmd, config)
242}
243
244#[cfg(feature = "cli-help")]
254#[allow(clippy::too_many_arguments)]
255pub fn cli_handle_version_or_help_or_continue(
256 raw_args: &[String],
257 cmd: &clap::Command,
258 config: &HelpConfig,
259 name: &str,
260 display_name: Option<&str>,
261 version: &str,
262 build: Option<&str>,
263) -> Result<Option<String>, Value> {
264 match crate::cli::cli_handle_version_or_continue(
265 raw_args,
266 cmd,
267 name,
268 display_name,
269 version,
270 build,
271 ) {
272 Ok(Some(rendered)) => return Ok(Some(rendered)),
273 Ok(None) => {}
274 Err(event) => return Err(event.into()),
275 }
276 let cmd = version_aware_help_command(cmd);
283 cli_handle_help_or_continue(raw_args, &cmd, config)
284}
285
286#[cfg(feature = "cli-help")]
292fn version_aware_help_command(cmd: &clap::Command) -> clap::Command {
293 let already_exposed = cmd.get_arguments().any(|arg| {
294 arg.get_long() == Some("version") || arg.get_short().is_some_and(|short| short == 'V')
295 }) || (!cmd.is_disable_version_flag_set()
296 && (cmd.get_version().is_some() || cmd.get_long_version().is_some()));
297 if already_exposed {
298 return cmd.clone();
299 }
300 let version_flag = clap::Arg::new("version")
301 .long("version")
302 .help("Print version")
303 .action(clap::ArgAction::SetTrue);
304 cmd.clone().disable_version_flag(true).arg(version_flag)
305}
306
307#[cfg(feature = "cli-help")]
308fn cli_handle_help_or_continue_inner(
309 raw_args: &[String],
310 cmd: &clap::Command,
311 config: &HelpConfig,
312) -> Result<Option<String>, Value> {
313 let parsed = parse_help_request(raw_args, cmd);
314 if !parsed.help_requested {
315 return Ok(None);
316 }
317 if let Some(error) = parsed.output_error {
318 let event = build_cli_error(
319 &error,
320 Some("valid help output formats: plain, markdown, json, yaml"),
321 );
322 return Err(event.into());
323 }
324
325 let (scope, format) = resolve_help_options(&parsed, cmd, config);
326 let path: Vec<&str> = parsed.subcommand_path.iter().map(String::as_str).collect();
327 if matches!(format, HelpFormat::Json | HelpFormat::Yaml) {
331 let event = help_result_event(&scoped_help_command(cmd, &path), scope);
332 let rendered = match format {
333 HelpFormat::Json => serialize_json_output(event.as_value()),
334 HelpFormat::Yaml => render_yaml(
335 event.as_value(),
336 &OutputOptions {
337 redaction: Redactor::new().policy(RedactionPolicy::Off),
338 style: PlainStyle::Raw,
339 },
340 ),
341 HelpFormat::Plain | HelpFormat::Markdown => unreachable!(),
342 };
343 return Ok(Some(format!("{rendered}\n")));
344 }
345 let options = HelpOptions { scope, format };
346 Ok(Some(cli_render_help_with_options(cmd, &path, &options)))
347}
348
349#[cfg(feature = "cli-help")]
352fn help_result_event(selected: &ScopedHelpCommand, scope: HelpScope) -> crate::protocol::Event {
353 crate::protocol::json_result(serde_json::json!({
354 "code": "help",
355 "help": build_selected_help_schema(selected, scope),
356 }))
357 .trace(serde_json::json!({}))
358 .build()
359}
360
361#[cfg(feature = "cli-help")]
362fn resolve_help_options(
363 parsed: &ParsedHelpRequest,
364 cmd: &clap::Command,
365 config: &HelpConfig,
366) -> (HelpScope, HelpFormat) {
367 let scope = if parsed.recursive_requested {
370 HelpScope::Recursive
371 } else {
372 config.default_scope
373 };
374 let format = parsed
375 .output_format
376 .or_else(|| command_output_default(cmd, &parsed.subcommand_path))
377 .unwrap_or(config.default_format);
378 (scope, format)
379}
380
381#[cfg(feature = "cli-help")]
382fn command_output_default(cmd: &clap::Command, path: &[String]) -> Option<HelpFormat> {
383 let mut current = cmd;
384 let mut format = command_output_default_here(current);
385 for name in path {
386 let Some(next) = current.find_subcommand(name) else {
387 break;
388 };
389 current = next;
390 if let Some(selected) = command_output_default_here(current) {
391 format = Some(selected);
392 }
393 }
394 format
395}
396
397#[cfg(feature = "cli-help")]
398fn command_output_default_here(cmd: &clap::Command) -> Option<HelpFormat> {
399 cmd.get_arguments()
400 .find(|arg| arg.get_long() == Some("output"))
401 .and_then(|arg| arg.get_default_values().first())
402 .and_then(|value| value.to_str())
403 .and_then(HelpFormat::parse)
404}
405
406#[cfg(feature = "cli-help")]
407struct ScopedHelpCommand {
408 local_command: clap::Command,
409 conventional_command: clap::Command,
410 command_path: String,
411 inherited_arguments_from: Vec<String>,
412}
413
414#[cfg(feature = "cli-help")]
415fn scoped_help_command(cmd: &clap::Command, path: &[&str]) -> ScopedHelpCommand {
416 let mut current = cmd;
417 let mut command_names = vec![cmd.get_bin_name().unwrap_or(cmd.get_name()).to_string()];
418 let mut inherited_groups = Vec::new();
419
420 for name in path {
421 let Some(next) = current.find_subcommand(name) else {
422 break;
423 };
424 let globals: Vec<clap::Arg> = current
425 .get_arguments()
426 .filter(|arg| arg.is_global_set() && !arg.is_hide_set())
427 .cloned()
428 .collect();
429 if !globals.is_empty() {
430 inherited_groups.push((command_names.join(" "), globals));
431 }
432 current = next;
433 command_names.push(next.get_name().to_string());
434 }
435
436 let command_path = command_names.join(" ");
437 let local_command = current.clone().bin_name(command_path.clone());
438 let mut conventional_command = local_command.clone();
439 let mut inherited_arguments_from = Vec::new();
440 for (source, arguments) in inherited_groups {
441 let mut inherited_from_source = false;
442 for argument in arguments {
443 let already_declared = conventional_command
444 .get_arguments()
445 .any(|candidate| candidate.get_id() == argument.get_id());
446 if !already_declared {
447 conventional_command = conventional_command.arg(argument);
448 inherited_from_source = true;
449 }
450 }
451 if inherited_from_source {
452 inherited_arguments_from.push(source);
453 }
454 }
455
456 ScopedHelpCommand {
457 local_command,
458 conventional_command,
459 command_path,
460 inherited_arguments_from,
461 }
462}
463
464#[cfg(feature = "cli-help")]
465fn render_help_one_level_plain(cmd: &clap::Command) -> String {
466 enriched_help_command(cmd).render_long_help().to_string()
467}
468
469#[cfg(feature = "cli-help")]
470fn redact_secret_help_defaults(mut cmd: clap::Command) -> clap::Command {
471 let context = RedactionContext::default();
472 let ids: Vec<String> = cmd
473 .get_arguments()
474 .filter(|arg| !arg.get_default_values().is_empty())
475 .filter(|arg| help_arg_is_secret(arg, &context))
476 .map(|arg| arg.get_id().to_string())
477 .collect();
478 for id in ids {
479 cmd = cmd.mut_arg(id, |arg| arg.default_value("***"));
480 }
481 cmd
482}
483
484#[cfg(feature = "cli-help")]
485fn help_arg_is_secret(arg: &clap::Arg, context: &RedactionContext) -> bool {
486 is_secret_flag_name(arg.get_id().as_ref(), context)
487 || arg
488 .get_long()
489 .is_some_and(|long| is_secret_flag_name(long, context))
490}
491
492#[cfg(feature = "cli-help")]
503fn enriched_help_command(cmd: &clap::Command) -> clap::Command {
504 let cmd = redact_secret_help_defaults(cmd.clone()).disable_help_subcommand(true);
507 let description = if visible_subcommands(&cmd).next().is_some() {
508 HELP_FLAG_WITH_SUBCOMMANDS
509 } else {
510 HELP_FLAG_LEAF
511 };
512 let has_explicit_version = cmd.get_arguments().any(|arg| {
513 arg.get_long() == Some("version") || arg.get_short().is_some_and(|short| short == 'V')
514 });
515 let exposes_auto_version = !cmd.is_disable_version_flag_set()
516 && (cmd.get_version().is_some() || cmd.get_long_version().is_some());
517 let help_flag = clap::Arg::new("help")
523 .long("help")
524 .help(description)
525 .long_help(description)
526 .action(clap::ArgAction::Help);
527 let mut enriched = cmd.disable_help_flag(true).arg(help_flag);
531 if exposes_auto_version && !has_explicit_version {
532 enriched = enriched.disable_version_flag(true).arg(
533 clap::Arg::new("version")
534 .long("version")
535 .help("Print version")
536 .action(clap::ArgAction::Version),
537 );
538 }
539 enriched
540}
541
542#[cfg(feature = "cli-help")]
544const HELP_FLAG_WITH_SUBCOMMANDS: &str = "Print help. Add --recursive to expand every nested subcommand; \
545 add --output plain|json|yaml|markdown to choose the format.";
546
547#[cfg(feature = "cli-help")]
549const HELP_FLAG_LEAF: &str =
550 "Print help. Add --output plain|json|yaml|markdown to choose the format.";
551
552#[cfg(feature = "cli-help")]
553fn render_help_recursive_plain(cmd: &clap::Command, parent_path: &[&str], buf: &mut String) {
554 use std::fmt::Write;
555
556 let mut cmd_path = parent_path.to_vec();
557 cmd_path.push(if parent_path.is_empty() {
558 cmd.get_bin_name().unwrap_or(cmd.get_name())
559 } else {
560 cmd.get_name()
561 });
562 let path = cmd_path.join(" ");
563 let usage = compact_usage(cmd, &path);
564 if let Some(about) = cmd.get_about() {
565 let _ = writeln!(buf, "{usage} — {about}");
566 } else {
567 let _ = writeln!(buf, "{usage}");
568 }
569
570 let is_target = parent_path.is_empty();
571 let owned = if is_target {
572 Some(enriched_help_command(cmd))
573 } else {
574 None
575 };
576 let source = owned.as_ref().unwrap_or(cmd);
577 for arg in source.get_arguments().filter(|arg| !arg.is_hide_set()) {
578 let _ = writeln!(buf, " {}", compact_plain_argument(arg));
579 }
580
581 for sub in visible_subcommands(cmd) {
582 if !buf.ends_with('\n') {
583 let _ = writeln!(buf);
584 }
585 render_help_recursive_plain(sub, &cmd_path, buf);
586 }
587}
588
589#[cfg(feature = "cli-help")]
590fn compact_usage(cmd: &clap::Command, path: &str) -> String {
591 let rendered = cmd.clone().render_usage().to_string();
592 let usage = rendered.strip_prefix("Usage: ").unwrap_or(&rendered);
593 let invocation = cmd.get_bin_name().unwrap_or(cmd.get_name());
594 usage.strip_prefix(invocation).map_or_else(
595 || format!("{path} {usage}"),
596 |suffix| format!("{path}{suffix}"),
597 )
598}
599
600#[cfg(feature = "cli-help")]
601fn compact_usage_suffix(cmd: &clap::Command) -> String {
602 let rendered = cmd.clone().render_usage().to_string();
603 let usage = rendered.strip_prefix("Usage: ").unwrap_or(&rendered);
604 usage
605 .strip_prefix(cmd.get_bin_name().unwrap_or(cmd.get_name()))
606 .unwrap_or(usage)
607 .trim()
608 .to_string()
609}
610
611#[cfg(feature = "cli-help")]
612fn compact_plain_argument(arg: &clap::Arg) -> String {
613 use std::fmt::Write;
614
615 let mut rendered = String::new();
616 if let Some(short) = arg.get_short() {
617 let _ = write!(rendered, "-{short}");
618 if arg.get_long().is_some() {
619 rendered.push_str(", ");
620 }
621 }
622 if let Some(long) = arg.get_long() {
623 let _ = write!(rendered, "--{long}");
624 } else if arg.get_short().is_none() {
625 rendered.push_str(
626 &arg.get_value_names()
627 .and_then(|names| names.first())
628 .map_or_else(|| arg.get_id().to_string(), ToString::to_string),
629 );
630 }
631 if argument_takes_values(arg)
632 && let Some(names) = arg.get_value_names()
633 && (arg.get_long().is_some() || arg.get_short().is_some())
634 {
635 for name in names {
636 let _ = write!(rendered, " <{name}>");
637 }
638 }
639 if matches!(
640 arg.get_action(),
641 clap::ArgAction::Append | clap::ArgAction::Count
642 ) {
643 rendered.push_str("...");
644 }
645 let defaults = redacted_default_values(arg);
646 if !defaults.is_empty() {
647 let _ = write!(rendered, " [default={}]", defaults.join(","));
648 }
649 if let Some(help) = arg.get_help() {
650 let _ = write!(rendered, ": {help}");
651 }
652 rendered
653}
654
655#[cfg(feature = "cli-help")]
656fn render_help_markdown(cmd: &clap::Command, names: &[String], scope: HelpScope) -> String {
657 let mut buf = String::new();
658 render_markdown_command(cmd, names, &mut buf, 1, true);
659 if matches!(scope, HelpScope::Recursive) {
660 render_markdown_descendants(cmd, names, &mut buf, 2);
661 }
662 buf
663}
664
665#[cfg(feature = "cli-help")]
666fn render_markdown_descendants(
667 cmd: &clap::Command,
668 parent_names: &[String],
669 buf: &mut String,
670 level: usize,
671) {
672 for sub in cmd.get_subcommands() {
673 if sub.get_name() == "help" || sub.is_hide_set() {
674 continue;
675 }
676 let mut names = parent_names.to_vec();
677 names.push(sub.get_name().to_string());
678 render_markdown_command(sub, &names, buf, level, false);
679 render_markdown_descendants(sub, &names, buf, level.saturating_add(1));
680 }
681}
682
683#[cfg(feature = "cli-help")]
684fn render_markdown_command(
685 cmd: &clap::Command,
686 names: &[String],
687 buf: &mut String,
688 level: usize,
689 enrich: bool,
690) {
691 use std::fmt::Write;
692
693 if !buf.is_empty() {
694 let _ = writeln!(buf);
695 }
696 let heading_level = "#".repeat(level.max(1));
697 let path = names.join(" ");
701 if let Some(about) = cmd.get_about() {
702 let _ = writeln!(buf, "{heading_level} {path} - {about}");
703 } else {
704 let _ = writeln!(buf, "{heading_level} {path}");
705 }
706 if let Some(long_about) = markdown_long_about(cmd) {
707 let _ = writeln!(buf);
708 write_trimmed_help(buf, &long_about);
709 }
710 let _ = writeln!(buf);
711 let _ = writeln!(buf, "```text");
712 let help = markdown_help_block_command(cmd, enrich)
713 .bin_name(path.clone())
714 .render_long_help();
715 write_trimmed_help(buf, &help.to_string());
716 if !buf.ends_with('\n') {
717 let _ = writeln!(buf);
718 }
719 let _ = writeln!(buf, "```");
720}
721
722#[cfg(feature = "cli-help")]
723fn markdown_long_about(cmd: &clap::Command) -> Option<String> {
724 let long_about = cmd.get_long_about()?.to_string();
725 let rendered = match cmd.get_about() {
726 Some(about) => {
727 let about_str = about.to_string();
728 if long_about.trim() == format!("{} - {}", cmd.get_name(), about_str) {
729 return None;
730 }
731 strip_leading_about_paragraph(&long_about, &about_str)
732 }
733 None => long_about.as_str(),
734 };
735 let rendered = rendered.trim_matches(['\r', '\n']);
736 if rendered.is_empty() {
737 None
738 } else {
739 Some(rendered.to_string())
740 }
741}
742
743#[cfg(feature = "cli-help")]
744fn strip_leading_about_paragraph<'a>(long_about: &'a str, about: &str) -> &'a str {
745 let long_about = long_about.trim_start_matches(['\r', '\n']);
746 let Some(rest) = long_about.strip_prefix(about) else {
747 return long_about;
748 };
749 if rest.is_empty() {
750 return "";
751 }
752 rest.strip_prefix("\r\n\r\n")
753 .or_else(|| rest.strip_prefix("\n\n"))
754 .unwrap_or(long_about)
755}
756
757#[cfg(feature = "cli-help")]
758fn markdown_help_block_command(cmd: &clap::Command, enrich: bool) -> clap::Command {
759 let cmd = if enrich {
760 enriched_help_command(cmd)
761 } else {
762 redact_secret_help_defaults(cmd.clone()).disable_help_subcommand(true)
763 };
764 cmd.about(None::<&str>).long_about(None::<&str>)
765}
766
767#[cfg(feature = "cli-help")]
768fn write_trimmed_help(buf: &mut String, help: &str) {
769 use std::fmt::Write;
770
771 for line in help.lines() {
772 let _ = writeln!(buf, "{}", line.trim_end());
773 }
774}
775
776#[cfg(feature = "cli-help")]
777struct ParsedHelpRequest {
778 help_requested: bool,
779 recursive_requested: bool,
780 output_format: Option<HelpFormat>,
781 output_error: Option<String>,
782 subcommand_path: Vec<String>,
783}
784
785#[cfg(feature = "cli-help")]
786fn parse_help_request(raw_args: &[String], cmd: &clap::Command) -> ParsedHelpRequest {
787 let args = crate::cli::strip_argv0(raw_args);
788 let mut help_requested = false;
789 let mut recursive_requested = false;
790 let mut output_format = None;
791 let mut output_error = None;
792 let mut subcommand_path = Vec::new();
793 let mut current = cmd;
794
795 let mut i = 0usize;
796 while i < args.len() {
797 let arg = args[i].as_str();
798 if arg == "--" {
799 break;
800 }
801
802 let (flag_name, inline_value) = split_flag(arg);
803 if arg == "--help" {
809 help_requested = true;
810 i += 1;
811 continue;
812 }
813 if arg == "--recursive" {
818 recursive_requested = true;
819 i += 1;
820 continue;
821 }
822 if flag_name == Some("output") {
823 let value = inline_value.or_else(|| {
824 args.get(i + 1)
825 .map(String::as_str)
826 .filter(|next| !next.starts_with('-'))
827 });
828 if let Some(value) = value {
829 match HelpFormat::parse(value) {
830 Some(format) => set_help_output_format(
831 &mut output_format,
832 format,
833 &format!("--output {value}"),
834 &mut output_error,
835 ),
836 None => {
837 output_error = Some(format!(
838 "invalid --output format '{value}': expected plain, json, yaml, or markdown"
839 ));
840 }
841 }
842 } else {
843 output_error = Some(
844 "missing value for --output: expected plain, json, yaml, or markdown"
845 .to_string(),
846 );
847 }
848 i += if inline_value.is_some() || value.is_none() {
849 1
850 } else {
851 2
852 };
853 continue;
854 }
855 if arg.starts_with('-') {
856 i += if inline_value.is_none() && flag_takes_value(current, arg) {
857 2
858 } else {
859 1
860 };
861 continue;
862 }
863 if let Some(sub) = current.find_subcommand(arg)
864 && sub.get_name() != "help"
865 && !sub.is_hide_set()
866 {
867 subcommand_path.push(sub.get_name().to_string());
868 current = sub;
869 }
870 i += 1;
871 }
872
873 ParsedHelpRequest {
874 help_requested,
875 recursive_requested,
876 output_format,
877 output_error,
878 subcommand_path,
879 }
880}
881
882#[cfg(feature = "cli-help")]
883fn set_help_output_format(
884 current: &mut Option<HelpFormat>,
885 next: HelpFormat,
886 source: &str,
887 output_error: &mut Option<String>,
888) {
889 if let Some(existing) = current
890 && *existing != next
891 {
892 *output_error = Some(format!(
893 "conflicting output formats: {source} conflicts with previous output format"
894 ));
895 return;
896 }
897 *current = Some(next);
898}
899
900fn split_flag(arg: &str) -> (Option<&str>, Option<&str>) {
901 if let Some(stripped) = arg.strip_prefix("--") {
902 if let Some((name, value)) = stripped.split_once('=') {
903 (Some(name), Some(value))
904 } else {
905 (Some(stripped), None)
906 }
907 } else if let Some(stripped) = arg.strip_prefix('-') {
908 (Some(stripped), None)
909 } else {
910 (None, None)
911 }
912}
913
914#[cfg(feature = "cli-help")]
915fn flag_takes_value(cmd: &clap::Command, raw_flag: &str) -> bool {
916 let Some(flag) = raw_flag.strip_prefix('-') else {
917 return false;
918 };
919 let name = flag.trim_start_matches('-');
920 cmd.get_arguments().any(|arg| {
921 let long_matches = arg.get_long().is_some_and(|long| long == name);
922 let short_matches =
923 name.len() == 1 && arg.get_short().is_some_and(|short| name.starts_with(short));
924 (long_matches || short_matches)
925 && matches!(
926 arg.get_action(),
927 clap::ArgAction::Set | clap::ArgAction::Append
928 )
929 })
930}
931
932#[cfg(feature = "cli-help")]
933fn build_selected_help_schema(selected: &ScopedHelpCommand, scope: HelpScope) -> Value {
934 let mut schema = command_schema(
935 &selected.local_command,
936 matches!(scope, HelpScope::Recursive),
937 true,
938 );
939 if let Value::Object(map) = &mut schema {
940 map.insert(
941 "scope".to_string(),
942 Value::String(help_scope_tag(scope).to_string()),
943 );
944 map.insert(
945 "command_path".to_string(),
946 Value::String(selected.command_path.clone()),
947 );
948 if !selected.inherited_arguments_from.is_empty() {
949 map.insert(
950 "inherited_arguments_from".to_string(),
951 Value::Array(
952 selected
953 .inherited_arguments_from
954 .iter()
955 .cloned()
956 .map(Value::String)
957 .collect(),
958 ),
959 );
960 }
961 }
962 schema
963}
964
965#[cfg(feature = "cli-help")]
966fn help_scope_tag(scope: HelpScope) -> &'static str {
967 match scope {
968 HelpScope::OneLevel => "one_level",
969 HelpScope::Recursive => "recursive",
970 }
971}
972
973#[cfg(feature = "cli-help")]
974fn command_schema(cmd: &clap::Command, recursive: bool, enrich: bool) -> Value {
975 let subcommands: Vec<Value> = visible_subcommands(cmd)
976 .map(|sub| {
977 if recursive {
978 command_schema(sub, true, false)
980 } else {
981 command_summary_schema(sub)
982 }
983 })
984 .collect();
985
986 let mut schema = serde_json::Map::new();
987 schema.insert(
988 "name".to_string(),
989 Value::String(cmd.get_name().to_string()),
990 );
991 insert_non_empty(
992 &mut schema,
993 "about",
994 cmd.get_about().map(ToString::to_string).unwrap_or_default(),
995 );
996 let usage = compact_usage_suffix(cmd);
997 if !usage.is_empty() {
998 schema.insert("usage".to_string(), Value::String(usage));
999 }
1000 let arguments = command_arguments_schema(cmd, enrich);
1001 if !arguments.is_empty() {
1002 schema.insert("arguments".to_string(), Value::Array(arguments));
1003 }
1004 if !subcommands.is_empty() {
1005 schema.insert("subcommands".to_string(), Value::Array(subcommands));
1006 }
1007 Value::Object(schema)
1008}
1009
1010#[cfg(feature = "cli-help")]
1011fn command_summary_schema(cmd: &clap::Command) -> Value {
1012 let mut schema = serde_json::Map::new();
1013 schema.insert(
1014 "name".to_string(),
1015 Value::String(cmd.get_name().to_string()),
1016 );
1017 insert_non_empty(
1018 &mut schema,
1019 "about",
1020 cmd.get_about().map(ToString::to_string).unwrap_or_default(),
1021 );
1022 Value::Object(schema)
1023}
1024
1025#[cfg(feature = "cli-help")]
1026fn visible_subcommands(cmd: &clap::Command) -> impl Iterator<Item = &clap::Command> {
1027 cmd.get_subcommands()
1028 .filter(|sub| sub.get_name() != "help" && !sub.is_hide_set())
1029}
1030
1031#[cfg(feature = "cli-help")]
1032fn command_arguments_schema(cmd: &clap::Command, enrich: bool) -> Vec<Value> {
1033 let owned = enrich.then(|| enriched_help_command(cmd));
1039 let source = owned.as_ref().unwrap_or(cmd);
1040 source
1041 .get_arguments()
1042 .filter(|arg| !arg.is_hide_set())
1043 .map(argument_schema)
1044 .collect()
1045}
1046
1047#[cfg(feature = "cli-help")]
1054fn insert_non_empty(schema: &mut serde_json::Map<String, Value>, key: &str, value: String) {
1055 if !value.is_empty() {
1056 schema.insert(key.to_string(), Value::String(value));
1057 }
1058}
1059
1060#[cfg(feature = "cli-help")]
1061fn argument_schema(arg: &clap::Arg) -> Value {
1062 let mut schema = serde_json::Map::new();
1063 if let Some(long) = arg.get_long() {
1064 schema.insert("name".to_string(), Value::String(format!("--{long}")));
1065 } else if let Some(short) = arg.get_short() {
1066 schema.insert("name".to_string(), Value::String(format!("-{short}")));
1067 } else {
1068 schema.insert(
1069 "name".to_string(),
1070 Value::String(
1071 arg.get_value_names()
1072 .and_then(|names| names.first())
1073 .map_or_else(|| arg.get_id().to_string(), ToString::to_string),
1074 ),
1075 );
1076 }
1077 if arg.get_long().is_some()
1078 && let Some(short) = arg.get_short()
1079 {
1080 schema.insert("short".to_string(), Value::String(format!("-{short}")));
1081 }
1082 insert_non_empty(
1083 &mut schema,
1084 "help",
1085 arg.get_help().map(ToString::to_string).unwrap_or_default(),
1086 );
1087 if arg.is_required_set() {
1088 schema.insert("required".to_string(), Value::Bool(true));
1089 }
1090 if arg.is_global_set() {
1091 schema.insert("global".to_string(), Value::Bool(true));
1092 }
1093 if matches!(
1094 arg.get_action(),
1095 clap::ArgAction::Append | clap::ArgAction::Count
1096 ) {
1097 schema.insert("repeatable".to_string(), Value::Bool(true));
1098 }
1099 if arg.get_long().is_none()
1104 && arg.get_short().is_none()
1105 && let Some(names) = arg.get_value_names()
1106 && names.len() > 1
1107 {
1108 schema.insert(
1109 "values".to_string(),
1110 Value::Array(
1111 names
1112 .iter()
1113 .map(ToString::to_string)
1114 .filter(|name| !name.is_empty())
1115 .map(Value::String)
1116 .collect(),
1117 ),
1118 );
1119 }
1120 if (arg.get_long().is_some() || arg.get_short().is_some())
1121 && argument_takes_values(arg)
1122 && let Some(names) = arg.get_value_names()
1123 {
1124 if names.len() == 1 {
1125 insert_non_empty(&mut schema, "value", names[0].to_string());
1126 } else {
1127 let values: Vec<Value> = names
1128 .iter()
1129 .map(ToString::to_string)
1130 .filter(|name| !name.is_empty())
1131 .map(Value::String)
1132 .collect();
1133 if !values.is_empty() {
1134 schema.insert("values".to_string(), Value::Array(values));
1135 }
1136 }
1137 }
1138 let defaults = redacted_default_values(arg);
1139 if !defaults.is_empty() {
1140 if defaults.len() == 1 {
1141 schema.insert("default".to_string(), Value::String(defaults[0].clone()));
1142 } else {
1143 schema.insert(
1144 "defaults".to_string(),
1145 Value::Array(defaults.into_iter().map(Value::String).collect()),
1146 );
1147 }
1148 }
1149 Value::Object(schema)
1150}
1151
1152#[cfg(feature = "cli-help")]
1153fn argument_takes_values(arg: &clap::Arg) -> bool {
1154 matches!(
1155 arg.get_action(),
1156 clap::ArgAction::Set | clap::ArgAction::Append
1157 )
1158}
1159
1160#[cfg(feature = "cli-help")]
1161fn redacted_default_values(arg: &clap::Arg) -> Vec<String> {
1162 if arg.is_hide_default_value_set() {
1165 return Vec::new();
1166 }
1167 arg.get_default_values()
1168 .iter()
1169 .map(|value| {
1170 if help_arg_is_secret(arg, &RedactionContext::default()) {
1171 "***".to_string()
1172 } else {
1173 value.to_string_lossy().to_string()
1174 }
1175 })
1176 .collect()
1177}