use std::{ffi::OsString, path::PathBuf};
use clap::{Parser, ValueHint};
#[allow(clippy::doc_markdown)]
#[derive(Parser, Debug, PartialEq)]
#[command(about, version)]
#[allow(rustdoc::bare_urls)]
pub struct CliArgs {
#[arg(short, long, conflicts_with = "no", global = true)]
pub yes: bool,
#[arg(short, long, global = true)]
pub no: bool,
#[arg(short = 'A', long, env = "ACCESSIBLE", global = true)]
pub accessible: bool,
#[arg(short = 'H', long, global = true)]
pub hidden: bool,
#[arg(short, long, global = true)]
pub quiet: bool,
#[arg(short, long, global = true)]
pub gitignore: bool,
#[arg(short, long, global = true)]
pub format: Option<String>,
#[arg(short, long = "password", aliases = ["pass", "pw"], global = true)]
pub password: Option<OsString>,
#[arg(short = 'c', long, global = true)]
pub threads: Option<usize>,
#[command(subcommand)]
pub cmd: Subcommand,
}
#[derive(Parser, PartialEq, Eq, Debug)]
#[allow(rustdoc::bare_urls, clippy::doc_markdown)]
pub enum Subcommand {
#[command(visible_alias = "c")]
Compress {
#[arg(required = true, value_hint = ValueHint::FilePath)]
files: Vec<PathBuf>,
#[arg(required = true, value_hint = ValueHint::FilePath)]
output: PathBuf,
#[arg(short, long, group = "compression-level")]
level: Option<i16>,
#[arg(long, group = "compression-level")]
fast: bool,
#[arg(long, group = "compression-level")]
slow: bool,
#[arg(long, short = 'S')]
follow_symlinks: bool,
},
#[command(visible_alias = "d")]
Decompress {
#[arg(required = true, num_args = 1.., value_hint = ValueHint::FilePath)]
files: Vec<PathBuf>,
#[arg(short = 'd', long = "dir", value_hint = ValueHint::FilePath)]
output_dir: Option<PathBuf>,
#[arg(short = 'r', long)]
remove: bool,
},
#[command(visible_aliases = ["l", "ls"])]
List {
#[arg(required = true, num_args = 1.., value_hint = ValueHint::FilePath)]
archives: Vec<PathBuf>,
#[arg(short, long)]
tree: bool,
},
}
#[cfg(test)]
mod tests {
use super::*;
fn args_splitter(input: &str) -> impl Iterator<Item = &str> {
input.split_whitespace()
}
fn to_paths(iter: impl IntoIterator<Item = &'static str>) -> Vec<PathBuf> {
iter.into_iter().map(PathBuf::from).collect()
}
macro_rules! test {
($args:expr, $expected:expr) => {
let result = match CliArgs::try_parse_from(args_splitter($args)) {
Ok(result) => result,
Err(err) => panic!(
"CLI result is Err, expected Ok, input: '{}'.\nResult: '{err}'",
$args
),
};
assert_eq!(result, $expected, "CLI result mismatched, input: '{}'.", $args);
};
}
fn mock_cli_args() -> CliArgs {
CliArgs {
yes: false,
no: false,
accessible: false,
hidden: false,
quiet: false,
gitignore: false,
format: None,
password: None,
threads: None,
cmd: Subcommand::Decompress {
files: vec!["\x00\x11\x22".into()],
output_dir: None,
remove: false,
},
}
}
#[test]
fn test_clap_cli_ok() {
test!(
"ouch decompress file.tar.gz",
CliArgs {
cmd: Subcommand::Decompress {
files: to_paths(["file.tar.gz"]),
output_dir: None,
remove: false,
},
..mock_cli_args()
}
);
test!(
"ouch d file.tar.gz",
CliArgs {
cmd: Subcommand::Decompress {
files: to_paths(["file.tar.gz"]),
output_dir: None,
remove: false,
},
..mock_cli_args()
}
);
test!(
"ouch d a b c",
CliArgs {
cmd: Subcommand::Decompress {
files: to_paths(["a", "b", "c"]),
output_dir: None,
remove: false,
},
..mock_cli_args()
}
);
test!(
"ouch compress file file.tar.gz",
CliArgs {
cmd: Subcommand::Compress {
files: to_paths(["file"]),
output: PathBuf::from("file.tar.gz"),
level: None,
fast: false,
slow: false,
follow_symlinks: false,
},
..mock_cli_args()
}
);
test!(
"ouch compress a b c archive.tar.gz",
CliArgs {
cmd: Subcommand::Compress {
files: to_paths(["a", "b", "c"]),
output: PathBuf::from("archive.tar.gz"),
level: None,
fast: false,
slow: false,
follow_symlinks: false,
},
..mock_cli_args()
}
);
test!(
"ouch compress a b c archive.tar.gz",
CliArgs {
cmd: Subcommand::Compress {
files: to_paths(["a", "b", "c"]),
output: PathBuf::from("archive.tar.gz"),
level: None,
fast: false,
slow: false,
follow_symlinks: false,
},
..mock_cli_args()
}
);
let inputs = [
"ouch compress a b c output --format tar.gz",
"ouch compress --format tar.gz a b c output",
"ouch --format tar.gz compress a b c output",
];
for input in inputs {
test!(
input,
CliArgs {
cmd: Subcommand::Compress {
files: to_paths(["a", "b", "c"]),
output: PathBuf::from("output"),
level: None,
fast: false,
slow: false,
follow_symlinks: false,
},
format: Some("tar.gz".into()),
..mock_cli_args()
}
);
}
}
#[test]
fn test_clap_cli_err() {
assert!(CliArgs::try_parse_from(args_splitter("ouch c")).is_err());
assert!(CliArgs::try_parse_from(args_splitter("ouch c input")).is_err());
assert!(CliArgs::try_parse_from(args_splitter("ouch d")).is_err());
assert!(CliArgs::try_parse_from(args_splitter("ouch l")).is_err());
}
}