use crate::driver::HelpListMode;
use crate::missing::normalize_program_name;
use crate::schema::{ArgLevelSchema, ArgSchema, Schema, Subcommand};
use facet_core::Facet;
use owo_colors::OwoColorize;
use owo_colors::Stream::Stdout;
use std::fmt;
use std::string::String;
use std::sync::Arc;
use std::vec::Vec;
pub fn generate_help<T: Facet<'static>>(config: &HelpConfig) -> String {
generate_help_for_shape(T::SHAPE, config)
}
pub fn generate_help_for_shape(shape: &'static facet_core::Shape, config: &HelpConfig) -> String {
let schema = match Schema::from_shape(shape) {
Ok(s) => s,
Err(_) => {
let program_name = config
.program_name
.clone()
.or_else(|| {
std::env::args()
.next()
.map(|path| normalize_program_name(&path))
})
.unwrap_or_else(|| "program".to_string());
return format!(
"{}\n\n(Schema could not be built for this type)\n",
program_name
);
}
};
generate_help_for_subcommand(&schema, &[], config)
}
#[derive(Clone)]
pub struct HelpConfig {
pub program_name: Option<String>,
pub version: Option<String>,
pub description: Option<String>,
pub width: usize,
pub include_implementation_source_file: bool,
pub implementation_url: Option<Arc<dyn Fn(&str) -> String + Send + Sync>>,
}
impl fmt::Debug for HelpConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HelpConfig")
.field("program_name", &self.program_name)
.field("version", &self.version)
.field("description", &self.description)
.field("width", &self.width)
.field(
"include_implementation_source_file",
&self.include_implementation_source_file,
)
.field(
"implementation_url",
&self.implementation_url.as_ref().map(|_| "<fn>"),
)
.finish()
}
}
impl Default for HelpConfig {
fn default() -> Self {
Self {
program_name: None,
version: None,
description: None,
width: 80,
include_implementation_source_file: false,
implementation_url: None,
}
}
}
pub(crate) fn implementation_source_for_subcommand_path(
root_shape: &'static facet_core::Shape,
subcommand_path: &[String],
) -> Option<&'static str> {
let mut current_shape = root_shape;
if subcommand_path.is_empty() {
return current_shape.source_file;
}
for segment in subcommand_path {
let next_shape = next_subcommand_shape(current_shape, segment)?;
current_shape = next_shape;
}
current_shape.source_file
}
fn next_subcommand_shape(
shape: &'static facet_core::Shape,
target_effective_name: &str,
) -> Option<&'static facet_core::Shape> {
let fields = match shape.ty {
facet_core::Type::User(facet_core::UserType::Struct(s)) => s.fields,
_ => return None,
};
let subcommand_field = fields
.iter()
.find(|field| field.has_attr(Some("args"), "subcommand"))?;
let enum_shape = unwrap_option_shape(subcommand_field.shape());
let variants = match enum_shape.ty {
facet_core::Type::User(facet_core::UserType::Enum(e)) => e.variants,
_ => return None,
};
let variant = variants
.iter()
.find(|variant| variant.effective_name() == target_effective_name)?;
if variant.data.fields.is_empty() {
return Some(enum_shape);
}
let has_direct_subcommand = variant
.data
.fields
.iter()
.any(|field| field.has_attr(Some("args"), "subcommand"));
if has_direct_subcommand {
return Some(enum_shape);
}
if variant.data.fields.len() == 1 {
return Some(unwrap_option_shape(variant.data.fields[0].shape()));
}
Some(enum_shape)
}
fn unwrap_option_shape(mut shape: &'static facet_core::Shape) -> &'static facet_core::Shape {
while let facet_core::Def::Option(option_def) = shape.def {
shape = option_def.t;
}
shape
}
pub fn generate_help_for_subcommand(
schema: &Schema,
subcommand_path: &[String],
config: &HelpConfig,
) -> String {
let program_name = config
.program_name
.clone()
.or_else(|| {
std::env::args()
.next()
.map(|path| normalize_program_name(&path))
})
.unwrap_or_else(|| "program".to_string());
if subcommand_path.is_empty() {
return generate_help_from_schema(schema, &program_name, config);
}
let mut current_args = schema.args();
let mut command_path = vec![program_name.clone()];
for name in subcommand_path {
let sub = current_args
.subcommands()
.values()
.find(|s| s.effective_name() == name);
if let Some(sub) = sub {
command_path.push(sub.cli_name().to_string());
current_args = sub.args();
} else {
return generate_help_from_schema(schema, &program_name, config);
}
}
let mut final_sub: Option<&Subcommand> = None;
let mut args = schema.args();
for name in subcommand_path {
let sub = args
.subcommands()
.values()
.find(|s| s.effective_name() == name);
if let Some(sub) = sub {
final_sub = Some(sub);
args = sub.args();
}
}
generate_help_for_subcommand_level(current_args, final_sub, &command_path.join(" "), config)
}
pub(crate) fn generate_help_list_for_subcommand(
schema: &Schema,
subcommand_path: &[String],
config: &HelpConfig,
mode: HelpListMode,
) -> String {
let program_name = config
.program_name
.clone()
.or_else(|| {
std::env::args()
.next()
.map(|path| normalize_program_name(&path))
})
.unwrap_or_else(|| "program".to_string());
let mut current_args = schema.args();
let mut resolved_path = Vec::new();
for name in subcommand_path {
let sub = current_args
.subcommands()
.values()
.find(|s| s.effective_name() == name);
let Some(sub) = sub else {
return generate_help_for_subcommand(schema, &[], config);
};
resolved_path.push(sub.effective_name().to_string());
current_args = sub.args();
}
if !current_args.has_subcommands() {
let command_display = if resolved_path.is_empty() {
program_name
} else {
let cli_chain = resolve_cli_chain(schema, &resolved_path);
if cli_chain.is_empty() {
program_name
} else {
format!("{} {}", program_name, cli_chain.join(" "))
}
};
return format!("No subcommands available for {command_display}.");
}
match mode {
HelpListMode::Short => {
let mut cli_chain = if resolved_path.is_empty() {
Vec::new()
} else {
resolve_cli_chain(schema, &resolved_path)
};
let mut commands = Vec::new();
collect_short_help_commands(
&mut commands,
program_name.as_str(),
&mut cli_chain,
current_args,
);
commands.join("\n")
}
HelpListMode::Full => {
let mut sections = Vec::new();
let mut leaf_paths = Vec::new();
let mut working_path = resolved_path.clone();
collect_leaf_subcommand_paths(&mut leaf_paths, &mut working_path, current_args);
for child_path in leaf_paths {
sections.push(generate_help_for_subcommand(schema, &child_path, config));
}
sections.join("\n\n")
}
}
}
fn collect_leaf_subcommand_paths(
leaf_paths: &mut Vec<Vec<String>>,
current_path: &mut Vec<String>,
args: &ArgLevelSchema,
) {
if !args.has_subcommands() {
if !current_path.is_empty() {
leaf_paths.push(current_path.clone());
}
return;
}
for sub in args.subcommands().values() {
current_path.push(sub.effective_name().to_string());
collect_leaf_subcommand_paths(leaf_paths, current_path, sub.args());
current_path.pop();
}
}
fn collect_short_help_commands(
commands: &mut Vec<String>,
program_name: &str,
cli_chain: &mut Vec<String>,
args: &ArgLevelSchema,
) {
if args.subcommand_optional() {
if cli_chain.is_empty() {
commands.push(program_name.to_string());
} else {
commands.push(format!("{} {}", program_name, cli_chain.join(" ")));
}
}
if !args.has_subcommands() {
if !cli_chain.is_empty() {
commands.push(format!("{} {}", program_name, cli_chain.join(" ")));
}
return;
}
for sub in args.subcommands().values() {
cli_chain.push(sub.cli_name().to_string());
collect_short_help_commands(commands, program_name, cli_chain, sub.args());
cli_chain.pop();
}
}
fn resolve_cli_chain(schema: &Schema, subcommand_path: &[String]) -> Vec<String> {
let mut current_args = schema.args();
let mut cli_path = Vec::new();
for name in subcommand_path {
let sub = current_args
.subcommands()
.values()
.find(|s| s.effective_name() == name);
let Some(sub) = sub else {
break;
};
cli_path.push(sub.cli_name().to_string());
current_args = sub.args();
}
cli_path
}
fn generate_help_from_schema(schema: &Schema, program_name: &str, config: &HelpConfig) -> String {
let mut out = String::new();
if let Some(version) = &config.version {
out.push_str(&format!("{program_name} {version}\n"));
} else {
out.push_str(&format!("{program_name}\n"));
}
if let Some(summary) = schema.docs().summary() {
out.push('\n');
out.push_str(summary.trim());
out.push('\n');
}
if let Some(details) = schema.docs().details() {
for line in details.lines() {
out.push_str(line.trim());
out.push('\n');
}
}
if let Some(desc) = &config.description {
out.push('\n');
out.push_str(desc);
out.push('\n');
}
out.push('\n');
generate_arg_level_help(&mut out, schema.args(), program_name);
out
}
fn generate_help_for_subcommand_level(
args: &ArgLevelSchema,
subcommand: Option<&Subcommand>,
full_command: &str,
config: &HelpConfig,
) -> String {
let mut out = String::new();
out.push_str(&format!("{full_command}\n"));
if let Some(sub) = subcommand {
if let Some(summary) = sub.docs().summary() {
out.push('\n');
out.push_str(summary.trim());
out.push('\n');
}
if let Some(details) = sub.docs().details() {
for line in details.lines() {
out.push_str(line.trim());
out.push('\n');
}
}
}
if let Some(desc) = &config.description {
out.push('\n');
out.push_str(desc);
out.push('\n');
}
out.push('\n');
generate_arg_level_help(&mut out, args, full_command);
out
}
fn generate_arg_level_help(out: &mut String, args: &ArgLevelSchema, program_name: &str) {
let mut positionals: Vec<&ArgSchema> = Vec::new();
let mut flags: Vec<&ArgSchema> = Vec::new();
for (_name, arg) in args.args().iter() {
if arg.kind().is_positional() {
positionals.push(arg);
} else {
flags.push(arg);
}
}
out.push_str(&format!("{}:\n ", "USAGE".yellow().bold()));
out.push_str(program_name);
if !flags.is_empty() {
out.push_str(" [OPTIONS]");
}
for pos in &positionals {
let name = pos.name().to_uppercase();
if pos.required() {
out.push_str(&format!(" <{name}>"));
} else {
out.push_str(&format!(" [{name}]"));
}
}
if args.has_subcommands() {
if args.subcommand_optional() {
out.push_str(" [COMMAND]");
} else {
out.push_str(" <COMMAND>");
}
}
out.push_str("\n\n");
if !positionals.is_empty() {
out.push_str(&format!("{}:\n", "ARGUMENTS".yellow().bold()));
for arg in &positionals {
write_arg_help(out, arg);
}
out.push('\n');
}
if !flags.is_empty() {
out.push_str(&format!("{}:\n", "OPTIONS".yellow().bold()));
for arg in &flags {
write_arg_help(out, arg);
}
out.push('\n');
}
if args.has_subcommands() {
out.push_str(&format!("{}:\n", "COMMANDS".yellow().bold()));
for sub in args.subcommands().values() {
write_subcommand_help(out, sub);
}
out.push('\n');
}
}
fn write_arg_help(out: &mut String, arg: &ArgSchema) {
out.push_str(" ");
let is_positional = arg.kind().is_positional();
if let Some(c) = arg.kind().short() {
out.push_str(&format!(
"{}, ",
format!("-{c}").if_supports_color(Stdout, |text| text.green())
));
} else {
out.push_str(" ");
}
let name = arg.name();
let is_counted = arg.kind().is_counted();
if is_positional {
out.push_str(&format!(
"{}",
format!("<{}>", name.to_uppercase()).if_supports_color(Stdout, |text| text.green())
));
} else {
out.push_str(&format!(
"{}",
format!("--{name}").if_supports_color(Stdout, |text| text.green())
));
if !is_counted && !arg.value().is_bool() {
let placeholder = if let Some(desc) = arg.label() {
desc.to_uppercase()
} else if let Some(variants) = arg.value().inner_if_option().enum_variants() {
variants.join(",")
} else {
arg.value().type_identifier().to_uppercase()
};
out.push_str(&format!(" <{}>", placeholder));
}
}
if let Some(summary) = arg.docs().summary() {
out.push_str("\n ");
out.push_str(summary.trim());
}
if is_counted {
out.push_str("\n ");
out.push_str("[can be repeated]");
}
out.push('\n');
}
fn write_subcommand_help(out: &mut String, sub: &Subcommand) {
out.push_str(" ");
out.push_str(&format!(
"{}",
sub.cli_name()
.if_supports_color(Stdout, |text| text.green())
));
if let Some(summary) = sub.docs().summary() {
out.push_str("\n ");
out.push_str(summary.trim());
}
out.push('\n');
}
#[cfg(test)]
mod tests {
use super::*;
use facet::Facet;
use figue_attrs as args;
#[derive(Facet)]
struct CommonArgs {
#[facet(args::named, crate::short = 'v')]
verbose: bool,
#[facet(args::named, crate::short = 'q')]
quiet: bool,
}
#[derive(Facet)]
struct ArgsWithFlatten {
#[facet(args::positional)]
input: String,
#[facet(flatten)]
common: CommonArgs,
}
#[test]
fn test_flatten_args_appear_in_help() {
let schema = Schema::from_shape(ArgsWithFlatten::SHAPE).unwrap();
let help = generate_help_for_subcommand(&schema, &[], &HelpConfig::default());
assert!(
help.contains("--verbose"),
"help should contain --verbose from flattened CommonArgs"
);
assert!(help.contains("-v"), "help should contain -v short flag");
assert!(
help.contains("--quiet"),
"help should contain --quiet from flattened CommonArgs"
);
assert!(help.contains("-q"), "help should contain -q short flag");
assert!(
!help.contains("--common"),
"help should not show --common as a flag"
);
}
#[test]
fn test_flatten_docs_preserved() {
let schema = Schema::from_shape(ArgsWithFlatten::SHAPE).unwrap();
let help = generate_help_for_subcommand(&schema, &[], &HelpConfig::default());
assert!(
help.contains("verbose output"),
"help should contain verbose field doc"
);
assert!(
help.contains("quiet mode"),
"help should contain quiet field doc"
);
}
#[derive(Facet)]
struct ServeArgs {
#[facet(args::named)]
port: u16,
#[facet(args::named)]
host: String,
}
#[derive(Facet)]
struct TupleVariantArgs {
#[facet(args::subcommand)]
command: Option<TupleVariantCommand>,
}
#[derive(Facet)]
#[repr(u8)]
#[allow(dead_code)]
enum TupleVariantCommand {
Serve(ServeArgs),
}
#[test]
fn test_label_overrides_placeholder() {
#[derive(Facet)]
struct TDArgs {
#[facet(args::named, args::label = "PATH")]
input: std::path::PathBuf,
}
let schema = Schema::from_shape(TDArgs::SHAPE).unwrap();
let help = generate_help_for_subcommand(&schema, &[], &HelpConfig::default());
assert!(
help.contains("<PATH>"),
"help should use custom label placeholder"
);
}
#[test]
fn test_tuple_variant_fields_not_shown_as_option() {
let schema = Schema::from_shape(TupleVariantArgs::SHAPE).unwrap();
let help =
generate_help_for_subcommand(&schema, &["Serve".to_string()], &HelpConfig::default());
assert!(
help.contains("--port"),
"help should contain --port from ServeArgs"
);
assert!(
help.contains("--host"),
"help should contain --host from ServeArgs"
);
assert!(
!help.contains("--0"),
"help should NOT show --0 for tuple variant wrapper field"
);
assert!(
!help.contains("SERVEARGS"),
"help should NOT show SERVEARGS as an option value"
);
}
#[derive(Facet)]
struct NestedRootArgs {
#[facet(args::subcommand)]
command: NestedRootCommand,
}
#[derive(Facet)]
#[repr(u8)]
#[allow(dead_code)]
enum NestedRootCommand {
Home(NestedHomeArgs),
Cache(NestedCacheArgs),
}
#[derive(Facet)]
struct NestedHomeArgs {
#[facet(args::subcommand)]
command: NestedHomeCommand,
}
#[derive(Facet)]
#[repr(u8)]
#[allow(dead_code)]
enum NestedHomeCommand {
Open,
Show,
}
#[derive(Facet)]
struct NestedCacheArgs {
#[facet(args::subcommand)]
command: NestedCacheCommand,
}
#[derive(Facet)]
#[repr(u8)]
#[allow(dead_code)]
enum NestedCacheCommand {
Open,
Show,
}
#[test]
fn test_help_list_short_is_recursive_with_full_command_paths() {
let schema = Schema::from_shape(NestedRootArgs::SHAPE).unwrap();
let output = generate_help_list_for_subcommand(
&schema,
&[],
&HelpConfig {
program_name: Some("myapp".to_string()),
..HelpConfig::default()
},
HelpListMode::Short,
);
let lines: Vec<&str> = output.lines().collect();
assert_eq!(
lines,
vec![
"myapp home open",
"myapp home show",
"myapp cache open",
"myapp cache show"
]
);
}
#[test]
fn test_help_list_full_is_recursive_for_leaf_subcommands() {
let schema = Schema::from_shape(NestedRootArgs::SHAPE).unwrap();
let output = generate_help_list_for_subcommand(
&schema,
&[],
&HelpConfig {
program_name: Some("myapp".to_string()),
..HelpConfig::default()
},
HelpListMode::Full,
);
assert!(output.contains("myapp home open"));
assert!(output.contains("myapp home show"));
assert!(output.contains("myapp cache open"));
assert!(output.contains("myapp cache show"));
assert!(!output.contains("myapp home\n\n"));
assert!(!output.contains("myapp cache\n\n"));
}
}