feat: add password support for decompress and list
[ouch.git] / src / cli / args.rs
blobd0cdc7337af8d6f3943eddb35f8266caa9eb670f
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, 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<String>,
48     // Ouch and claps subcommands
49     #[command(subcommand)]
50     pub cmd: Subcommand,
53 #[derive(Parser, PartialEq, Eq, Debug)]
54 #[allow(rustdoc::bare_urls)]
55 pub enum Subcommand {
56     /// Compress one or more files into one output file
57     #[command(visible_alias = "c")]
58     Compress {
59         /// Files to be compressed
60         #[arg(required = true, value_hint = ValueHint::FilePath)]
61         files: Vec<PathBuf>,
63         /// The resulting file. Its extensions can be used to specify the compression formats
64         #[arg(required = true, value_hint = ValueHint::FilePath)]
65         output: PathBuf,
67         /// Compression level, applied to all formats
68         #[arg(short, long, group = "compression-level")]
69         level: Option<i16>,
71         /// Fastest compression level possible,
72         /// conflicts with --level and --slow
73         #[arg(long, group = "compression-level")]
74         fast: bool,
76         /// Slowest (and best) compression level possible,
77         /// conflicts with --level and --fast
78         #[arg(long, group = "compression-level")]
79         slow: bool,
80     },
81     /// Decompresses one or more files, optionally into another folder
82     #[command(visible_alias = "d")]
83     Decompress {
84         /// Files to be decompressed, or "-" for stdin
85         #[arg(required = true, num_args = 1.., value_hint = ValueHint::FilePath)]
86         files: Vec<PathBuf>,
88         /// Place results in a directory other than the current one
89         #[arg(short = 'd', long = "dir", value_hint = ValueHint::FilePath)]
90         output_dir: Option<PathBuf>,
91     },
92     /// List contents of an archive
93     #[command(visible_aliases = ["l", "ls"])]
94     List {
95         /// Archives whose contents should be listed
96         #[arg(required = true, num_args = 1.., value_hint = ValueHint::FilePath)]
97         archives: Vec<PathBuf>,
99         /// Show archive contents as a tree
100         #[arg(short, long)]
101         tree: bool,
102     },
105 #[cfg(test)]
106 mod tests {
107     use super::*;
109     fn args_splitter(input: &str) -> impl Iterator<Item = &str> {
110         input.split_whitespace()
111     }
113     fn to_paths(iter: impl IntoIterator<Item = &'static str>) -> Vec<PathBuf> {
114         iter.into_iter().map(PathBuf::from).collect()
115     }
117     macro_rules! test {
118         ($args:expr, $expected:expr) => {
119             let result = match CliArgs::try_parse_from(args_splitter($args)) {
120                 Ok(result) => result,
121                 Err(err) => panic!(
122                     "CLI result is Err, expected Ok, input: '{}'.\nResult: '{err}'",
123                     $args
124                 ),
125             };
126             assert_eq!(result, $expected, "CLI result mismatched, input: '{}'.", $args);
127         };
128     }
130     fn mock_cli_args() -> CliArgs {
131         CliArgs {
132             yes: false,
133             no: false,
134             accessible: false,
135             hidden: false,
136             quiet: false,
137             gitignore: false,
138             format: None,
139             // This is usually replaced in assertion tests
140             cmd: Subcommand::Decompress {
141                 // Put a crazy value here so no test can assert it unintentionally
142                 files: vec!["\x00\x11\x22".into()],
143                 output_dir: None,
144             },
145         }
146     }
148     #[test]
149     fn test_clap_cli_ok() {
150         test!(
151             "ouch decompress file.tar.gz",
152             CliArgs {
153                 cmd: Subcommand::Decompress {
154                     files: to_paths(["file.tar.gz"]),
155                     output_dir: None,
156                 },
157                 ..mock_cli_args()
158             }
159         );
160         test!(
161             "ouch d file.tar.gz",
162             CliArgs {
163                 cmd: Subcommand::Decompress {
164                     files: to_paths(["file.tar.gz"]),
165                     output_dir: None,
166                 },
167                 ..mock_cli_args()
168             }
169         );
170         test!(
171             "ouch d a b c",
172             CliArgs {
173                 cmd: Subcommand::Decompress {
174                     files: to_paths(["a", "b", "c"]),
175                     output_dir: None,
176                 },
177                 ..mock_cli_args()
178             }
179         );
181         test!(
182             "ouch compress file file.tar.gz",
183             CliArgs {
184                 cmd: Subcommand::Compress {
185                     files: to_paths(["file"]),
186                     output: PathBuf::from("file.tar.gz"),
187                     level: None,
188                     fast: false,
189                     slow: false,
190                 },
191                 ..mock_cli_args()
192             }
193         );
194         test!(
195             "ouch compress a b c archive.tar.gz",
196             CliArgs {
197                 cmd: Subcommand::Compress {
198                     files: to_paths(["a", "b", "c"]),
199                     output: PathBuf::from("archive.tar.gz"),
200                     level: None,
201                     fast: false,
202                     slow: false,
203                 },
204                 ..mock_cli_args()
205             }
206         );
207         test!(
208             "ouch compress a b c archive.tar.gz",
209             CliArgs {
210                 cmd: Subcommand::Compress {
211                     files: to_paths(["a", "b", "c"]),
212                     output: PathBuf::from("archive.tar.gz"),
213                     level: None,
214                     fast: false,
215                     slow: false,
216                 },
217                 ..mock_cli_args()
218             }
219         );
221         let inputs = [
222             "ouch compress a b c output --format tar.gz",
223             // https://github.com/clap-rs/clap/issues/5115
224             // "ouch compress a b c --format tar.gz output",
225             // "ouch compress a b --format tar.gz c output",
226             // "ouch compress a --format tar.gz b c output",
227             "ouch compress --format tar.gz a b c output",
228             "ouch --format tar.gz compress a b c output",
229         ];
230         for input in inputs {
231             test!(
232                 input,
233                 CliArgs {
234                     cmd: Subcommand::Compress {
235                         files: to_paths(["a", "b", "c"]),
236                         output: PathBuf::from("output"),
237                         level: None,
238                         fast: false,
239                         slow: false,
240                     },
241                     format: Some("tar.gz".into()),
242                     ..mock_cli_args()
243                 }
244             );
245         }
246     }
248     #[test]
249     fn test_clap_cli_err() {
250         assert!(CliArgs::try_parse_from(args_splitter("ouch c")).is_err());
251         assert!(CliArgs::try_parse_from(args_splitter("ouch c input")).is_err());
252         assert!(CliArgs::try_parse_from(args_splitter("ouch d")).is_err());
253         assert!(CliArgs::try_parse_from(args_splitter("ouch l")).is_err());
254     }