refactor(cli): move thread pool setup to command execution, use thread::spawn instead...
[ouch.git] / src / cli / args.rs
blob2dafe92074b770060f60946be0ae8057e8b6f765
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.
7 ///
8 /// Supported formats: tar, zip, gz, 7z, xz/lzma, bz/bz2, bz3, lz4, sz (Snappy), zst and rar.
9 ///
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)]
15 pub struct CliArgs {
16     /// Skip [Y/n] questions positively
17     #[arg(short, long, conflicts_with = "no", global = true)]
18     pub yes: bool,
20     /// Skip [Y/n] questions negatively
21     #[arg(short, long, global = true)]
22     pub no: bool,
24     /// Activate accessibility mode, reducing visual noise
25     #[arg(short = 'A', long, env = "ACCESSIBLE", global = true)]
26     pub accessible: bool,
28     /// Ignores hidden files
29     #[arg(short = 'H', long, global = true)]
30     pub hidden: bool,
32     /// Silences output
33     #[arg(short = 'q', long, global = true)]
34     pub quiet: bool,
36     /// Ignores files matched by git's ignore files
37     #[arg(short = 'g', long, global = true)]
38     pub gitignore: bool,
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)]
54     pub cmd: Subcommand,
57 #[derive(Parser, PartialEq, Eq, Debug)]
58 #[allow(rustdoc::bare_urls)]
59 pub enum Subcommand {
60     /// Compress one or more files into one output file
61     #[command(visible_alias = "c")]
62     Compress {
63         /// Files to be compressed
64         #[arg(required = true, value_hint = ValueHint::FilePath)]
65         files: Vec<PathBuf>,
67         /// The resulting file. Its extensions can be used to specify the compression formats
68         #[arg(required = true, value_hint = ValueHint::FilePath)]
69         output: PathBuf,
71         /// Compression level, applied to all formats
72         #[arg(short, long, group = "compression-level")]
73         level: Option<i16>,
75         /// Fastest compression level possible,
76         /// conflicts with --level and --slow
77         #[arg(long, group = "compression-level")]
78         fast: bool,
80         /// Slowest (and best) compression level possible,
81         /// conflicts with --level and --fast
82         #[arg(long, group = "compression-level")]
83         slow: bool,
84     },
85     /// Decompresses one or more files, optionally into another folder
86     #[command(visible_alias = "d")]
87     Decompress {
88         /// Files to be decompressed, or "-" for stdin
89         #[arg(required = true, num_args = 1.., value_hint = ValueHint::FilePath)]
90         files: Vec<PathBuf>,
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)]
98         remove: bool,
99     },
100     /// List contents of an archive
101     #[command(visible_aliases = ["l", "ls"])]
102     List {
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
108         #[arg(short, long)]
109         tree: bool,
110     },
113 #[cfg(test)]
114 mod tests {
115     use super::*;
117     fn args_splitter(input: &str) -> impl Iterator<Item = &str> {
118         input.split_whitespace()
119     }
121     fn to_paths(iter: impl IntoIterator<Item = &'static str>) -> Vec<PathBuf> {
122         iter.into_iter().map(PathBuf::from).collect()
123     }
125     macro_rules! test {
126         ($args:expr, $expected:expr) => {
127             let result = match CliArgs::try_parse_from(args_splitter($args)) {
128                 Ok(result) => result,
129                 Err(err) => panic!(
130                     "CLI result is Err, expected Ok, input: '{}'.\nResult: '{err}'",
131                     $args
132                 ),
133             };
134             assert_eq!(result, $expected, "CLI result mismatched, input: '{}'.", $args);
135         };
136     }
138     fn mock_cli_args() -> CliArgs {
139         CliArgs {
140             yes: false,
141             no: false,
142             accessible: false,
143             hidden: false,
144             quiet: false,
145             gitignore: false,
146             format: None,
147             // This is usually replaced in assertion tests
148             password: None,
149             threads: None,
150             cmd: Subcommand::Decompress {
151                 // Put a crazy value here so no test can assert it unintentionally
152                 files: vec!["\x00\x11\x22".into()],
153                 output_dir: None,
154                 remove: false,
155             },
156         }
157     }
159     #[test]
160     fn test_clap_cli_ok() {
161         test!(
162             "ouch decompress file.tar.gz",
163             CliArgs {
164                 cmd: Subcommand::Decompress {
165                     files: to_paths(["file.tar.gz"]),
166                     output_dir: None,
167                     remove: false,
168                 },
169                 ..mock_cli_args()
170             }
171         );
172         test!(
173             "ouch d file.tar.gz",
174             CliArgs {
175                 cmd: Subcommand::Decompress {
176                     files: to_paths(["file.tar.gz"]),
177                     output_dir: None,
178                     remove: false,
179                 },
180                 ..mock_cli_args()
181             }
182         );
183         test!(
184             "ouch d a b c",
185             CliArgs {
186                 cmd: Subcommand::Decompress {
187                     files: to_paths(["a", "b", "c"]),
188                     output_dir: None,
189                     remove: false,
190                 },
191                 ..mock_cli_args()
192             }
193         );
195         test!(
196             "ouch compress file file.tar.gz",
197             CliArgs {
198                 cmd: Subcommand::Compress {
199                     files: to_paths(["file"]),
200                     output: PathBuf::from("file.tar.gz"),
201                     level: None,
202                     fast: false,
203                     slow: false,
204                 },
205                 ..mock_cli_args()
206             }
207         );
208         test!(
209             "ouch compress a b c archive.tar.gz",
210             CliArgs {
211                 cmd: Subcommand::Compress {
212                     files: to_paths(["a", "b", "c"]),
213                     output: PathBuf::from("archive.tar.gz"),
214                     level: None,
215                     fast: false,
216                     slow: false,
217                 },
218                 ..mock_cli_args()
219             }
220         );
221         test!(
222             "ouch compress a b c archive.tar.gz",
223             CliArgs {
224                 cmd: Subcommand::Compress {
225                     files: to_paths(["a", "b", "c"]),
226                     output: PathBuf::from("archive.tar.gz"),
227                     level: None,
228                     fast: false,
229                     slow: false,
230                 },
231                 ..mock_cli_args()
232             }
233         );
235         let inputs = [
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",
243         ];
244         for input in inputs {
245             test!(
246                 input,
247                 CliArgs {
248                     cmd: Subcommand::Compress {
249                         files: to_paths(["a", "b", "c"]),
250                         output: PathBuf::from("output"),
251                         level: None,
252                         fast: false,
253                         slow: false,
254                     },
255                     format: Some("tar.gz".into()),
256                     ..mock_cli_args()
257                 }
258             );
259         }
260     }
262     #[test]
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());
268     }