1use crate::driver::HelpListMode;
7use crate::missing::normalize_program_name;
8use crate::schema::{ArgLevelSchema, ArgSchema, Schema, Subcommand};
9use facet_core::Facet;
10use owo_colors::OwoColorize;
11use owo_colors::Stream::Stdout;
12use std::fmt;
13use std::string::String;
14use std::sync::Arc;
15use std::vec::Vec;
16
17pub fn generate_help<T: Facet<'static>>(config: &HelpConfig) -> String {
22 generate_help_for_shape(T::SHAPE, config)
23}
24
25pub fn generate_help_for_shape(shape: &'static facet_core::Shape, config: &HelpConfig) -> String {
30 let schema = match Schema::from_shape(shape) {
31 Ok(s) => s,
32 Err(_) => {
33 let program_name = config
35 .program_name
36 .clone()
37 .or_else(|| {
38 std::env::args()
39 .next()
40 .map(|path| normalize_program_name(&path))
41 })
42 .unwrap_or_else(|| "program".to_string());
43 return format!(
44 "{}\n\n(Schema could not be built for this type)\n",
45 program_name
46 );
47 }
48 };
49
50 generate_help_for_subcommand(&schema, &[], config)
51}
52
53#[derive(Clone)]
55pub struct HelpConfig {
56 pub program_name: Option<String>,
58 pub version: Option<String>,
60 pub description: Option<String>,
62 pub width: usize,
64 pub include_implementation_source_file: bool,
66 pub implementation_url: Option<Arc<dyn Fn(&str) -> String + Send + Sync>>,
68}
69
70impl fmt::Debug for HelpConfig {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 f.debug_struct("HelpConfig")
73 .field("program_name", &self.program_name)
74 .field("version", &self.version)
75 .field("description", &self.description)
76 .field("width", &self.width)
77 .field(
78 "include_implementation_source_file",
79 &self.include_implementation_source_file,
80 )
81 .field(
82 "implementation_url",
83 &self.implementation_url.as_ref().map(|_| "<fn>"),
84 )
85 .finish()
86 }
87}
88
89impl Default for HelpConfig {
90 fn default() -> Self {
91 Self {
92 program_name: None,
93 version: None,
94 description: None,
95 width: 80,
96 include_implementation_source_file: false,
97 implementation_url: None,
98 }
99 }
100}
101
102pub(crate) fn implementation_source_for_subcommand_path(
107 root_shape: &'static facet_core::Shape,
108 subcommand_path: &[String],
109) -> Option<&'static str> {
110 let mut current_shape = root_shape;
111
112 if subcommand_path.is_empty() {
113 return current_shape.source_file;
114 }
115
116 for segment in subcommand_path {
117 let next_shape = next_subcommand_shape(current_shape, segment)?;
118 current_shape = next_shape;
119 }
120
121 current_shape.source_file
122}
123
124fn next_subcommand_shape(
125 shape: &'static facet_core::Shape,
126 target_effective_name: &str,
127) -> Option<&'static facet_core::Shape> {
128 let fields = match shape.ty {
129 facet_core::Type::User(facet_core::UserType::Struct(s)) => s.fields,
130 _ => return None,
131 };
132
133 let subcommand_field = fields
134 .iter()
135 .find(|field| field.has_attr(Some("args"), "subcommand"))?;
136
137 let enum_shape = unwrap_option_shape(subcommand_field.shape());
138 let variants = match enum_shape.ty {
139 facet_core::Type::User(facet_core::UserType::Enum(e)) => e.variants,
140 _ => return None,
141 };
142
143 let variant = variants
144 .iter()
145 .find(|variant| variant.effective_name() == target_effective_name)?;
146
147 if variant.data.fields.is_empty() {
148 return Some(enum_shape);
149 }
150
151 let has_direct_subcommand = variant
152 .data
153 .fields
154 .iter()
155 .any(|field| field.has_attr(Some("args"), "subcommand"));
156
157 if has_direct_subcommand {
158 return Some(enum_shape);
159 }
160
161 if variant.data.fields.len() == 1 {
162 return Some(unwrap_option_shape(variant.data.fields[0].shape()));
163 }
164
165 Some(enum_shape)
166}
167
168fn unwrap_option_shape(mut shape: &'static facet_core::Shape) -> &'static facet_core::Shape {
169 while let facet_core::Def::Option(option_def) = shape.def {
170 shape = option_def.t;
171 }
172 shape
173}
174
175pub fn generate_help_for_subcommand(
180 schema: &Schema,
181 subcommand_path: &[String],
182 config: &HelpConfig,
183) -> String {
184 let program_name = config
185 .program_name
186 .clone()
187 .or_else(|| {
188 std::env::args()
189 .next()
190 .map(|path| normalize_program_name(&path))
191 })
192 .unwrap_or_else(|| "program".to_string());
193
194 if subcommand_path.is_empty() {
195 return generate_help_from_schema(schema, &program_name, config);
196 }
197
198 let mut current_args = schema.args();
200 let mut command_path = vec![program_name.clone()];
201 let mut inherited_flags = Vec::<ArgSchema>::new();
202
203 for name in subcommand_path {
204 merge_named_flags(&mut inherited_flags, current_args);
205
206 let sub = current_args
209 .subcommands()
210 .values()
211 .find(|s| s.effective_name() == name);
212
213 if let Some(sub) = sub {
214 command_path.push(sub.cli_name().to_string());
215 current_args = sub.args();
216 } else {
217 return generate_help_from_schema(schema, &program_name, config);
219 }
220 }
221
222 remove_shadowed_named_flags(&mut inherited_flags, current_args);
223
224 let mut final_sub: Option<&Subcommand> = None;
226 let mut args = schema.args();
227
228 for name in subcommand_path {
229 let sub = args
230 .subcommands()
231 .values()
232 .find(|s| s.effective_name() == name);
233 if let Some(sub) = sub {
234 final_sub = Some(sub);
235 args = sub.args();
236 }
237 }
238
239 generate_help_for_subcommand_level(
240 current_args,
241 final_sub,
242 &command_path.join(" "),
243 &inherited_flags,
244 config,
245 )
246}
247
248pub(crate) fn generate_help_list_for_subcommand(
255 schema: &Schema,
256 subcommand_path: &[String],
257 config: &HelpConfig,
258 mode: HelpListMode,
259) -> String {
260 let program_name = config
261 .program_name
262 .clone()
263 .or_else(|| {
264 std::env::args()
265 .next()
266 .map(|path| normalize_program_name(&path))
267 })
268 .unwrap_or_else(|| "program".to_string());
269
270 let mut current_args = schema.args();
271 let mut resolved_path = Vec::new();
272
273 for name in subcommand_path {
274 let sub = current_args
275 .subcommands()
276 .values()
277 .find(|s| s.effective_name() == name);
278
279 let Some(sub) = sub else {
280 return generate_help_for_subcommand(schema, &[], config);
282 };
283
284 resolved_path.push(sub.effective_name().to_string());
285 current_args = sub.args();
286 }
287
288 if !current_args.has_subcommands() {
289 let command_display = if resolved_path.is_empty() {
290 program_name
291 } else {
292 let cli_chain = resolve_cli_chain(schema, &resolved_path);
293 if cli_chain.is_empty() {
294 program_name
295 } else {
296 format!("{} {}", program_name, cli_chain.join(" "))
297 }
298 };
299 return format!("No subcommands available for {command_display}.");
300 }
301
302 match mode {
303 HelpListMode::Short => {
304 let mut cli_chain = if resolved_path.is_empty() {
305 Vec::new()
306 } else {
307 resolve_cli_chain(schema, &resolved_path)
308 };
309 let mut commands = Vec::new();
310 collect_short_help_commands(
311 &mut commands,
312 program_name.as_str(),
313 &mut cli_chain,
314 current_args,
315 );
316 commands.join("\n")
317 }
318 HelpListMode::Full => {
319 let mut sections = Vec::new();
320 let mut leaf_paths = Vec::new();
321 let mut working_path = resolved_path.clone();
322 collect_leaf_subcommand_paths(&mut leaf_paths, &mut working_path, current_args);
323
324 for child_path in leaf_paths {
325 sections.push(generate_help_for_subcommand(schema, &child_path, config));
326 }
327 sections.join("\n\n")
328 }
329 }
330}
331
332fn collect_leaf_subcommand_paths(
333 leaf_paths: &mut Vec<Vec<String>>,
334 current_path: &mut Vec<String>,
335 args: &ArgLevelSchema,
336) {
337 if !args.has_subcommands() {
338 if !current_path.is_empty() {
339 leaf_paths.push(current_path.clone());
340 }
341 return;
342 }
343
344 for sub in args.subcommands().values() {
345 current_path.push(sub.effective_name().to_string());
346 collect_leaf_subcommand_paths(leaf_paths, current_path, sub.args());
347 current_path.pop();
348 }
349}
350
351fn collect_short_help_commands(
352 commands: &mut Vec<String>,
353 program_name: &str,
354 cli_chain: &mut Vec<String>,
355 args: &ArgLevelSchema,
356) {
357 if args.subcommand_optional() {
358 if cli_chain.is_empty() {
359 commands.push(program_name.to_string());
360 } else {
361 commands.push(format!("{} {}", program_name, cli_chain.join(" ")));
362 }
363 }
364
365 if !args.has_subcommands() {
366 if !cli_chain.is_empty() {
367 commands.push(format!("{} {}", program_name, cli_chain.join(" ")));
368 }
369 return;
370 }
371
372 for sub in args.subcommands().values() {
373 cli_chain.push(sub.cli_name().to_string());
374 collect_short_help_commands(commands, program_name, cli_chain, sub.args());
375 cli_chain.pop();
376 }
377}
378
379fn resolve_cli_chain(schema: &Schema, subcommand_path: &[String]) -> Vec<String> {
380 let mut current_args = schema.args();
381 let mut cli_path = Vec::new();
382
383 for name in subcommand_path {
384 let sub = current_args
385 .subcommands()
386 .values()
387 .find(|s| s.effective_name() == name);
388 let Some(sub) = sub else {
389 break;
390 };
391 cli_path.push(sub.cli_name().to_string());
392 current_args = sub.args();
393 }
394
395 cli_path
396}
397
398fn generate_help_from_schema(schema: &Schema, program_name: &str, config: &HelpConfig) -> String {
400 let mut out = String::new();
401
402 if let Some(version) = &config.version {
404 out.push_str(&format!("{program_name} {version}\n"));
405 } else {
406 out.push_str(&format!("{program_name}\n"));
407 }
408
409 if let Some(summary) = schema.docs().summary() {
411 out.push('\n');
412 out.push_str(summary.trim());
413 out.push('\n');
414 }
415 if let Some(details) = schema.docs().details() {
416 for line in details.lines() {
417 out.push_str(line.trim());
418 out.push('\n');
419 }
420 }
421
422 if let Some(desc) = &config.description {
424 out.push('\n');
425 out.push_str(desc);
426 out.push('\n');
427 }
428
429 out.push('\n');
430
431 generate_arg_level_help(&mut out, schema.args(), program_name, &[]);
432
433 out
434}
435
436fn generate_help_for_subcommand_level(
438 args: &ArgLevelSchema,
439 subcommand: Option<&Subcommand>,
440 full_command: &str,
441 inherited_flags: &[ArgSchema],
442 config: &HelpConfig,
443) -> String {
444 let mut out = String::new();
445
446 out.push_str(&format!("{full_command}\n"));
448
449 if let Some(sub) = subcommand {
451 if let Some(summary) = sub.docs().summary() {
452 out.push('\n');
453 out.push_str(summary.trim());
454 out.push('\n');
455 }
456 if let Some(details) = sub.docs().details() {
457 for line in details.lines() {
458 out.push_str(line.trim());
459 out.push('\n');
460 }
461 }
462 }
463
464 if let Some(desc) = &config.description {
466 out.push('\n');
467 out.push_str(desc);
468 out.push('\n');
469 }
470
471 out.push('\n');
472
473 generate_arg_level_help(&mut out, args, full_command, inherited_flags);
474
475 out
476}
477
478fn generate_arg_level_help(
480 out: &mut String,
481 args: &ArgLevelSchema,
482 program_name: &str,
483 inherited_flags: &[ArgSchema],
484) {
485 let mut positionals: Vec<&ArgSchema> = Vec::new();
487 let mut flags = inherited_flags.to_vec();
488
489 for (_name, arg) in args.args().iter() {
490 if arg.kind().is_positional() {
491 positionals.push(arg);
492 } else {
493 flags.push(arg.clone());
494 }
495 }
496
497 out.push_str(&format!("{}:\n ", "USAGE".yellow().bold()));
499 out.push_str(program_name);
500
501 if !flags.is_empty() {
502 out.push_str(" [OPTIONS]");
503 }
504
505 for pos in &positionals {
506 let name = pos.name().to_uppercase();
507 if pos.required() {
508 out.push_str(&format!(" <{name}>"));
509 } else {
510 out.push_str(&format!(" [{name}]"));
511 }
512 }
513
514 if args.has_subcommands() {
515 if args.subcommand_optional() {
516 out.push_str(" [COMMAND]");
517 } else {
518 out.push_str(" <COMMAND>");
519 }
520 }
521
522 out.push_str("\n\n");
523
524 if !positionals.is_empty() {
526 out.push_str(&format!("{}:\n", "ARGUMENTS".yellow().bold()));
527 for arg in &positionals {
528 write_arg_help(out, arg);
529 }
530 out.push('\n');
531 }
532
533 if !flags.is_empty() {
535 out.push_str(&format!("{}:\n", "OPTIONS".yellow().bold()));
536 for arg in &flags {
537 write_arg_help(out, arg);
538 }
539 out.push('\n');
540 }
541
542 if args.has_subcommands() {
544 out.push_str(&format!("{}:\n", "COMMANDS".yellow().bold()));
545 for sub in args.subcommands().values() {
546 write_subcommand_help(out, sub);
547 }
548 out.push('\n');
549 }
550}
551
552fn merge_named_flags(flags: &mut Vec<ArgSchema>, args: &ArgLevelSchema) {
553 for (_name, arg) in args.args().iter() {
554 if arg.kind().is_positional() {
555 continue;
556 }
557
558 flags.retain(|existing| !arg_schemas_conflict(existing, arg));
559 flags.push(arg.clone());
560 }
561}
562
563fn remove_shadowed_named_flags(flags: &mut Vec<ArgSchema>, args: &ArgLevelSchema) {
564 let local_flags = args
565 .args()
566 .iter()
567 .map(|(_name, arg)| arg)
568 .filter(|arg| !arg.kind().is_positional())
569 .collect::<Vec<_>>();
570
571 flags.retain(|existing| {
572 !local_flags
573 .iter()
574 .any(|local| arg_schemas_conflict(existing, local))
575 });
576}
577
578fn arg_schemas_conflict(left: &ArgSchema, right: &ArgSchema) -> bool {
579 left.name() == right.name()
580 || matches!(
581 (left.kind().short(), right.kind().short()),
582 (Some(left_short), Some(right_short)) if left_short == right_short
583 )
584}
585
586fn write_arg_help(out: &mut String, arg: &ArgSchema) {
588 out.push_str(" ");
589
590 let is_positional = arg.kind().is_positional();
591
592 if let Some(c) = arg.kind().short() {
594 out.push_str(&format!(
595 "{}, ",
596 format!("-{c}").if_supports_color(Stdout, |text| text.green())
597 ));
598 } else {
599 out.push_str(" ");
601 }
602
603 let name = arg.name();
605 let is_counted = arg.kind().is_counted();
606
607 if is_positional {
608 out.push_str(&format!(
609 "{}",
610 format!("<{}>", name.to_uppercase()).if_supports_color(Stdout, |text| text.green())
611 ));
612 } else {
613 out.push_str(&format!(
614 "{}",
615 format!("--{name}").if_supports_color(Stdout, |text| text.green())
616 ));
617
618 if !is_counted && !arg.value().is_bool() {
620 let placeholder = if let Some(desc) = arg.label() {
621 desc.to_uppercase()
622 } else if let Some(variants) = arg.value().inner_if_option().enum_variants() {
623 variants.join(",")
624 } else {
625 arg.value().type_identifier().to_uppercase()
626 };
627 out.push_str(&format!(" <{}>", placeholder));
628 }
629 }
630
631 if let Some(summary) = arg.docs().summary() {
633 out.push_str("\n ");
634 out.push_str(summary.trim());
635 }
636
637 if is_counted {
638 out.push_str("\n ");
639 out.push_str("[can be repeated]");
640 }
641
642 out.push('\n');
643}
644
645fn write_subcommand_help(out: &mut String, sub: &Subcommand) {
647 out.push_str(" ");
648
649 out.push_str(&format!(
650 "{}",
651 sub.cli_name()
652 .if_supports_color(Stdout, |text| text.green())
653 ));
654
655 if let Some(summary) = sub.docs().summary() {
657 out.push_str("\n ");
658 out.push_str(summary.trim());
659 }
660
661 out.push('\n');
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667 use facet::Facet;
668 use figue_attrs as args;
669
670 #[derive(Facet)]
672 struct CommonArgs {
673 #[facet(args::named, crate::short = 'v')]
675 verbose: bool,
676
677 #[facet(args::named, crate::short = 'q')]
679 quiet: bool,
680 }
681
682 #[derive(Facet)]
684 struct ArgsWithFlatten {
685 #[facet(args::positional)]
687 input: String,
688
689 #[facet(flatten)]
691 common: CommonArgs,
692 }
693
694 #[test]
695 fn test_flatten_args_appear_in_help() {
696 let schema = Schema::from_shape(ArgsWithFlatten::SHAPE).unwrap();
697 let help = generate_help_for_subcommand(&schema, &[], &HelpConfig::default());
698
699 assert!(
701 help.contains("--verbose"),
702 "help should contain --verbose from flattened CommonArgs"
703 );
704 assert!(help.contains("-v"), "help should contain -v short flag");
705 assert!(
706 help.contains("--quiet"),
707 "help should contain --quiet from flattened CommonArgs"
708 );
709 assert!(help.contains("-q"), "help should contain -q short flag");
710
711 assert!(
713 !help.contains("--common"),
714 "help should not show --common as a flag"
715 );
716 }
717
718 #[test]
719 fn test_flatten_docs_preserved() {
720 let schema = Schema::from_shape(ArgsWithFlatten::SHAPE).unwrap();
721 let help = generate_help_for_subcommand(&schema, &[], &HelpConfig::default());
722
723 assert!(
725 help.contains("verbose output"),
726 "help should contain verbose field doc"
727 );
728 assert!(
729 help.contains("quiet mode"),
730 "help should contain quiet field doc"
731 );
732 }
733
734 #[derive(Facet)]
736 struct ServeArgs {
737 #[facet(args::named)]
739 port: u16,
740
741 #[facet(args::named)]
743 host: String,
744 }
745
746 #[derive(Facet)]
748 struct TupleVariantArgs {
749 #[facet(args::subcommand)]
751 command: Option<TupleVariantCommand>,
752 }
753
754 #[derive(Facet)]
756 #[repr(u8)]
757 #[allow(dead_code)]
758 enum TupleVariantCommand {
759 Serve(ServeArgs),
761 }
762
763 #[test]
764 fn test_label_overrides_placeholder() {
765 #[derive(Facet)]
766 struct TDArgs {
767 #[facet(args::named, args::label = "PATH")]
769 input: std::path::PathBuf,
770 }
771 let schema = Schema::from_shape(TDArgs::SHAPE).unwrap();
772 let help = generate_help_for_subcommand(&schema, &[], &HelpConfig::default());
773 assert!(
775 help.contains("<PATH>"),
776 "help should use custom label placeholder"
777 );
778 }
779
780 #[test]
781 fn test_tuple_variant_fields_not_shown_as_option() {
782 let schema = Schema::from_shape(TupleVariantArgs::SHAPE).unwrap();
783 let help =
785 generate_help_for_subcommand(&schema, &["Serve".to_string()], &HelpConfig::default());
786
787 assert!(
789 help.contains("--port"),
790 "help should contain --port from ServeArgs"
791 );
792 assert!(
793 help.contains("--host"),
794 "help should contain --host from ServeArgs"
795 );
796
797 assert!(
799 !help.contains("--0"),
800 "help should NOT show --0 for tuple variant wrapper field"
801 );
802 assert!(
803 !help.contains("SERVEARGS"),
804 "help should NOT show SERVEARGS as an option value"
805 );
806 }
807
808 #[derive(Facet)]
809 struct NestedRootArgs {
810 #[facet(args::subcommand)]
811 command: NestedRootCommand,
812 }
813
814 #[derive(Facet)]
815 #[repr(u8)]
816 #[allow(dead_code)]
817 enum NestedRootCommand {
818 Home(NestedHomeArgs),
819 Cache(NestedCacheArgs),
820 }
821
822 #[derive(Facet)]
823 struct NestedHomeArgs {
824 #[facet(args::subcommand)]
825 command: NestedHomeCommand,
826 }
827
828 #[derive(Facet)]
829 #[repr(u8)]
830 #[allow(dead_code)]
831 enum NestedHomeCommand {
832 Open,
833 Show,
834 }
835
836 #[derive(Facet)]
837 struct NestedCacheArgs {
838 #[facet(args::subcommand)]
839 command: NestedCacheCommand,
840 }
841
842 #[derive(Facet)]
843 #[repr(u8)]
844 #[allow(dead_code)]
845 enum NestedCacheCommand {
846 Open,
847 Show,
848 }
849
850 #[test]
851 fn test_help_list_short_is_recursive_with_full_command_paths() {
852 let schema = Schema::from_shape(NestedRootArgs::SHAPE).unwrap();
853 let output = generate_help_list_for_subcommand(
854 &schema,
855 &[],
856 &HelpConfig {
857 program_name: Some("myapp".to_string()),
858 ..HelpConfig::default()
859 },
860 HelpListMode::Short,
861 );
862
863 let lines: Vec<&str> = output.lines().collect();
864 assert_eq!(
865 lines,
866 vec![
867 "myapp home open",
868 "myapp home show",
869 "myapp cache open",
870 "myapp cache show"
871 ]
872 );
873 }
874
875 #[test]
876 fn test_help_list_full_is_recursive_for_leaf_subcommands() {
877 let schema = Schema::from_shape(NestedRootArgs::SHAPE).unwrap();
878 let output = generate_help_list_for_subcommand(
879 &schema,
880 &[],
881 &HelpConfig {
882 program_name: Some("myapp".to_string()),
883 ..HelpConfig::default()
884 },
885 HelpListMode::Full,
886 );
887
888 assert!(output.contains("myapp home open"));
889 assert!(output.contains("myapp home show"));
890 assert!(output.contains("myapp cache open"));
891 assert!(output.contains("myapp cache show"));
892 assert!(!output.contains("myapp home\n\n"));
893 assert!(!output.contains("myapp cache\n\n"));
894 }
895
896 #[derive(Facet)]
897 struct InheritedHelpRootArgs {
898 #[facet(args::named, crate::short = 'd')]
900 debug: bool,
901
902 #[facet(args::subcommand)]
903 command: InheritedHelpRootCommand,
904 }
905
906 #[derive(Facet)]
907 #[repr(u8)]
908 #[allow(dead_code)]
909 enum InheritedHelpRootCommand {
910 Repo(InheritedHelpRepoArgs),
911 }
912
913 #[derive(Facet)]
914 struct InheritedHelpRepoArgs {
915 #[facet(args::named, crate::short = 'q')]
917 quiet: bool,
918
919 #[facet(args::subcommand)]
920 command: InheritedHelpRepoCommand,
921 }
922
923 #[derive(Facet)]
924 #[repr(u8)]
925 #[allow(dead_code)]
926 enum InheritedHelpRepoCommand {
927 Clone {
928 #[facet(args::positional)]
930 url: String,
931 },
932 }
933
934 #[test]
935 fn test_nested_subcommand_help_includes_inherited_parent_flags() {
936 let schema = Schema::from_shape(InheritedHelpRootArgs::SHAPE).unwrap();
937 let help = generate_help_for_subcommand(
938 &schema,
939 &["Repo".to_string(), "Clone".to_string()],
940 &HelpConfig {
941 program_name: Some("myapp".to_string()),
942 ..HelpConfig::default()
943 },
944 );
945
946 assert!(help.contains("myapp repo clone [OPTIONS] <URL>"));
947 assert!(help.contains("--debug"));
948 assert!(help.contains("--quiet"));
949 assert!(help.contains("<URL>"));
950 }
951
952 #[derive(Facet)]
953 struct ShadowedHelpRootArgs {
954 #[facet(args::named, crate::short = 'v')]
956 verbose: bool,
957
958 #[facet(args::subcommand)]
959 command: ShadowedHelpCommand,
960 }
961
962 #[derive(Facet)]
963 #[repr(u8)]
964 #[allow(dead_code)]
965 enum ShadowedHelpCommand {
966 Run(ShadowedHelpRunArgs),
967 }
968
969 #[derive(Facet)]
970 struct ShadowedHelpRunArgs {
971 #[facet(args::named, crate::short = 'v')]
973 verbose: bool,
974 }
975
976 #[test]
977 fn test_subcommand_help_omits_shadowed_parent_flags() {
978 let schema = Schema::from_shape(ShadowedHelpRootArgs::SHAPE).unwrap();
979 let help =
980 generate_help_for_subcommand(&schema, &["Run".to_string()], &HelpConfig::default());
981
982 assert!(help.contains("Local verbose flag"));
983 assert!(!help.contains("Root verbose flag"));
984 assert_eq!(help.matches("--verbose").count(), 1);
985 }
986}