1 use std::{ffi::OsString, path::PathBuf};
3 use clap::{Parser, ValueHint};
5 // Ouch command line options (docstrings below are part of --help)
6 /// A command-line utility for easily compressing and decompressing files and directories.
8 /// Supported formats: tar, zip, gz, 7z, xz/lzma, bz/bz2, bz3, lz4, sz (Snappy), zst and rar.
10 /// Repository: https://github.com/ouch-org/ouch
11 #[derive(Parser, Debug, PartialEq)]
12 #[command(about, version)]
13 // Disable rustdoc::bare_urls because rustdoc parses URLs differently than Clap
14 #[allow(rustdoc::bare_urls)]
16 /// Skip [Y/n] questions positively
17 #[arg(short, long, conflicts_with = "no", global = true)]
20 /// Skip [Y/n] questions negatively
21 #[arg(short, long, global = true)]
24 /// Activate accessibility mode, reducing visual noise
25 #[arg(short = 'A', long, env = "ACCESSIBLE", global = true)]
28 /// Ignores hidden files
29 #[arg(short = 'H', long, global = true)]
33 #[arg(short = 'q', long, global = true)]
36 /// Ignores files matched by git's ignore files
37 #[arg(short = 'g', long, global = true)]
40 /// Specify the format of the archive
41 #[arg(short, long, global = true)]
42 pub format: Option<OsString>,
44 /// decompress or list with password
45 #[arg(short = 'p', long = "password", global = true)]
46 pub password: Option<OsString>,
48 /// cocurrent working threads
49 #[arg(short = 't', long, global = true)]
50 pub threads: Option<usize>,
52 // Ouch and claps subcommands
53 #[command(subcommand)]
57 #[derive(Parser, PartialEq, Eq, Debug)]
58 #[allow(rustdoc::bare_urls)]
60 /// Compress one or more files into one output file
61 #[command(visible_alias = "c")]
63 /// Files to be compressed
64 #[arg(required = true, value_hint = ValueHint::FilePath)]
67 /// The resulting file. Its extensions can be used to specify the compression formats
68 #[arg(required = true, value_hint = ValueHint::FilePath)]
71 /// Compression level, applied to all formats
72 #[arg(short, long, group = "compression-level")]
75 /// Fastest compression level possible,
76 /// conflicts with --level and --slow
77 #[arg(long, group = "compression-level")]
80 /// Slowest (and best) compression level possible,
81 /// conflicts with --level and --fast
82 #[arg(long, group = "compression-level")]
85 /// Decompresses one or more files, optionally into another folder
86 #[command(visible_alias = "d")]
88 /// Files to be decompressed, or "-" for stdin
89 #[arg(required = true, num_args = 1.., value_hint = ValueHint::FilePath)]
92 /// Place results in a directory other than the current one
93 #[arg(short = 'd', long = "dir", value_hint = ValueHint::FilePath)]
94 output_dir: Option<PathBuf>,
96 /// Remove the source file after successful decompression
97 #[arg(short = 'r', long)]
100 /// List contents of an archive
101 #[command(visible_aliases = ["l", "ls"])]
103 /// Archives whose contents should be listed
104 #[arg(required = true, num_args = 1.., value_hint = ValueHint::FilePath)]
105 archives: Vec<PathBuf>,
107 /// Show archive contents as a tree
117 fn args_splitter(input: &str) -> impl Iterator<Item = &str> {
118 input.split_whitespace()
121 fn to_paths(iter: impl IntoIterator<Item = &'static str>) -> Vec<PathBuf> {
122 iter.into_iter().map(PathBuf::from).collect()
126 ($args:expr, $expected:expr) => {
127 let result = match CliArgs::try_parse_from(args_splitter($args)) {
128 Ok(result) => result,
130 "CLI result is Err, expected Ok, input: '{}'.\nResult: '{err}'",
134 assert_eq!(result, $expected, "CLI result mismatched, input: '{}'.", $args);
138 fn mock_cli_args() -> CliArgs {
147 // This is usually replaced in assertion tests
150 cmd: Subcommand::Decompress {
151 // Put a crazy value here so no test can assert it unintentionally
152 files: vec!["\x00\x11\x22".into()],
160 fn test_clap_cli_ok() {
162 "ouch decompress file.tar.gz",
164 cmd: Subcommand::Decompress {
165 files: to_paths(["file.tar.gz"]),
173 "ouch d file.tar.gz",
175 cmd: Subcommand::Decompress {
176 files: to_paths(["file.tar.gz"]),
186 cmd: Subcommand::Decompress {
187 files: to_paths(["a", "b", "c"]),
196 "ouch compress file file.tar.gz",
198 cmd: Subcommand::Compress {
199 files: to_paths(["file"]),
200 output: PathBuf::from("file.tar.gz"),
209 "ouch compress a b c archive.tar.gz",
211 cmd: Subcommand::Compress {
212 files: to_paths(["a", "b", "c"]),
213 output: PathBuf::from("archive.tar.gz"),
222 "ouch compress a b c archive.tar.gz",
224 cmd: Subcommand::Compress {
225 files: to_paths(["a", "b", "c"]),
226 output: PathBuf::from("archive.tar.gz"),
236 "ouch compress a b c output --format tar.gz",
237 // https://github.com/clap-rs/clap/issues/5115
238 // "ouch compress a b c --format tar.gz output",
239 // "ouch compress a b --format tar.gz c output",
240 // "ouch compress a --format tar.gz b c output",
241 "ouch compress --format tar.gz a b c output",
242 "ouch --format tar.gz compress a b c output",
244 for input in inputs {
248 cmd: Subcommand::Compress {
249 files: to_paths(["a", "b", "c"]),
250 output: PathBuf::from("output"),
255 format: Some("tar.gz".into()),
263 fn test_clap_cli_err() {
264 assert!(CliArgs::try_parse_from(args_splitter("ouch c")).is_err());
265 assert!(CliArgs::try_parse_from(args_splitter("ouch c input")).is_err());
266 assert!(CliArgs::try_parse_from(args_splitter("ouch d")).is_err());
267 assert!(CliArgs::try_parse_from(args_splitter("ouch l")).is_err());