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