use std::io::Write;
use clap::CommandFactory;
use s3util_rs::config::args::Cli;
const CATEGORIES: &[(&str, &[&str])] = &[
("Object Operations", &["cp", "mv", "rm"]),
(
"Object Metadata",
&[
"head-object",
"get-object-tagging",
"put-object-tagging",
"delete-object-tagging",
],
),
(
"Bucket Operations",
&["create-bucket", "head-bucket", "delete-bucket"],
),
(
"Bucket Tagging",
&[
"get-bucket-tagging",
"put-bucket-tagging",
"delete-bucket-tagging",
],
),
(
"Bucket Policy",
&[
"get-bucket-policy",
"put-bucket-policy",
"delete-bucket-policy",
],
),
(
"Bucket Versioning",
&["get-bucket-versioning", "put-bucket-versioning"],
),
(
"Bucket Lifecycle Configuration",
&[
"get-bucket-lifecycle-configuration",
"put-bucket-lifecycle-configuration",
"delete-bucket-lifecycle-configuration",
],
),
(
"Bucket Encryption",
&[
"get-bucket-encryption",
"put-bucket-encryption",
"delete-bucket-encryption",
],
),
(
"Bucket CORS",
&["get-bucket-cors", "put-bucket-cors", "delete-bucket-cors"],
),
(
"Bucket Public Access Block",
&[
"get-public-access-block",
"put-public-access-block",
"delete-public-access-block",
],
),
(
"Bucket Website",
&[
"get-bucket-website",
"put-bucket-website",
"delete-bucket-website",
],
),
(
"Bucket Logging",
&["get-bucket-logging", "put-bucket-logging"],
),
(
"Bucket Notification Configuration",
&[
"get-bucket-notification-configuration",
"put-bucket-notification-configuration",
],
),
];
pub fn is_top_level_help_request(args: &[String]) -> bool {
if args.len() < 2 {
return false;
}
let first = args[1].as_str();
first == "-h" || first == "--help" || (first == "help" && args.len() == 2)
}
pub fn print_categorized_help<W: Write>(out: &mut W) -> std::io::Result<()> {
let cmd = Cli::command();
let bin = cmd.get_name();
let about = cmd.get_about().map(|s| s.to_string()).unwrap_or_default();
if !about.is_empty() {
writeln!(out, "{about}")?;
writeln!(out)?;
}
writeln!(out, "Usage: {bin} [OPTIONS] [COMMAND]")?;
writeln!(out)?;
let max_name_len = cmd
.get_subcommands()
.map(|s| s.get_name().len())
.max()
.unwrap_or(0);
let mut printed: std::collections::HashSet<String> = std::collections::HashSet::new();
for (heading, names) in CATEGORIES {
writeln!(out, "{heading}:")?;
for name in *names {
if let Some(sub) = cmd.find_subcommand(name) {
let desc = sub.get_about().map(|s| s.to_string()).unwrap_or_default();
writeln!(out, " {name:<max_name_len$} {desc}")?;
printed.insert(name.to_string());
}
}
writeln!(out)?;
}
let other_subs: Vec<_> = cmd
.get_subcommands()
.filter(|s| !printed.contains(s.get_name()))
.collect();
let mut other_lines: Vec<(String, String)> = other_subs
.iter()
.map(|s| {
(
s.get_name().to_string(),
s.get_about().map(|x| x.to_string()).unwrap_or_default(),
)
})
.collect();
if !other_lines.iter().any(|(n, _)| n == "help") {
other_lines.push((
"help".to_string(),
"Print this message or the help of the given subcommand(s)".to_string(),
));
}
if !other_lines.is_empty() {
writeln!(out, "Other:")?;
for (name, desc) in &other_lines {
writeln!(out, " {name:<max_name_len$} {desc}")?;
}
writeln!(out)?;
}
writeln!(out, "Options:")?;
writeln!(out, " -h, --help Print help")?;
writeln!(out, " -V, --version Print version")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_help_flag() {
assert!(is_top_level_help_request(&[
"s3util".into(),
"--help".into(),
]));
assert!(is_top_level_help_request(&["s3util".into(), "-h".into()]));
assert!(is_top_level_help_request(
&["s3util".into(), "help".into(),]
));
}
#[test]
fn does_not_treat_bare_invocation_as_help() {
assert!(!is_top_level_help_request(&["s3util".into()]));
assert!(!is_top_level_help_request(&[]));
}
#[test]
fn does_not_intercept_help_with_subcommand() {
assert!(!is_top_level_help_request(&[
"s3util".into(),
"help".into(),
"cp".into(),
]));
}
#[test]
fn does_not_intercept_subcommand_help() {
assert!(!is_top_level_help_request(&[
"s3util".into(),
"cp".into(),
"--help".into(),
]));
}
#[test]
fn every_categorized_name_is_a_real_subcommand() {
let cmd = Cli::command();
for (heading, names) in CATEGORIES {
for name in *names {
assert!(
cmd.find_subcommand(name).is_some(),
"category {heading:?} references unknown subcommand {name:?}"
);
}
}
}
#[test]
fn every_subcommand_is_categorized() {
let cmd = Cli::command();
let mut categorized = std::collections::HashSet::new();
for (_, names) in CATEGORIES {
for n in *names {
categorized.insert(n.to_string());
}
}
let allowed_other = ["help"];
for sub in cmd.get_subcommands() {
let name = sub.get_name();
if categorized.contains(name) || allowed_other.contains(&name) {
continue;
}
panic!(
"subcommand {name:?} is not in CATEGORIES — add it to a category in src/bin/s3util/help.rs"
);
}
}
#[test]
fn rendered_help_contains_category_headings() {
let mut buf = Vec::new();
print_categorized_help(&mut buf).unwrap();
let s = String::from_utf8(buf).unwrap();
assert!(s.contains("Object Operations:"));
assert!(s.contains("Bucket Operations:"));
assert!(s.contains("Bucket Lifecycle Configuration:"));
assert!(s.contains("Other:"));
assert!(s.contains("Options:"));
assert!(s.contains("cp"));
assert!(s.contains("get-bucket-cors"));
}
}