cli: Only suggest the typo correction and exit
[ouch.git] / src / cli.rs
blob1fdcaa3a57fdfd781514610b84755d4395f8f501
1 use std::{
2     env,
3     ffi::OsString,
4     path::{Path, PathBuf},
5     vec::Vec,
6 };
8 use strsim::normalized_damerau_levenshtein;
9 use oof::{arg_flag, flag};
12 #[derive(PartialEq, Eq, Debug)]
13 pub enum Command {
14     /// Files to be compressed
15     Compress {
16         files: Vec<PathBuf>,
17         compressed_output_path: PathBuf,
18     },
19     /// Files to be decompressed and their extensions
20     Decompress {
21         files: Vec<PathBuf>,
22         output_folder: Option<PathBuf>,
23     },
24     ShowHelp,
25     ShowVersion,
28 /// Calls parse_args_and_flags_from using std::env::args_os ( argv )
29 pub fn parse_args() -> crate::Result<ParsedArgs> {
30     let args = env::args_os().skip(1).collect();
31     parse_args_from(args)
34 pub struct ParsedArgs {
35     pub command: Command,
36     pub flags: oof::Flags,
40 /// check_for_typo checks if the first argument is 
41 /// a typo for the compress subcommand. 
42 /// Returns true if the arg is probably a typo or false otherwise.
43 fn is_typo<'a, P>(path: P) -> bool 
44 where
45     P: AsRef<Path> + 'a,
47     if path.as_ref().exists() {
48         // If the file exists then we won't check for a typo
49         return false;
50     }
52     let path = path.as_ref().to_string_lossy();
53     // We'll consider it a typo if the word is somewhat 'close' to "compress"
54     normalized_damerau_levenshtein("compress", &path) > 0.625
57 fn canonicalize<'a, P>(path: P) -> crate::Result<PathBuf>
58 where
59     P: AsRef<Path> + 'a,
61     match std::fs::canonicalize(&path.as_ref()) {
62         Ok(abs_path) => Ok(abs_path),
63         Err(io_err) => {
64             if !path.as_ref().exists() {
65                 Err(crate::Error::FileNotFound(PathBuf::from(path.as_ref())))
66             } else {
67                 Err(crate::Error::IoError(io_err))
68             }
69         }
70     }
75 fn canonicalize_files<'a, P>(files: Vec<P>) -> crate::Result<Vec<PathBuf>>
76 where
77     P: AsRef<Path> + 'a,
79     files.into_iter().map(canonicalize).collect()
82 pub fn parse_args_from(mut args: Vec<OsString>) -> crate::Result<ParsedArgs> {
83     if oof::matches_any_arg(&args, &["--help", "-h"]) || args.is_empty() {
84         return Ok(ParsedArgs {
85             command: Command::ShowHelp,
86             flags: oof::Flags::default(),
87         });
88     }
90     if oof::matches_any_arg(&args, &["--version"]) {
91         return Ok(ParsedArgs {
92             command: Command::ShowVersion,
93             flags: oof::Flags::default(),
94         });
95     }
97     let subcommands = &["compress"];
99     let mut flags_info = vec![flag!('y', "yes"), flag!('n', "no")];
101     let parsed_args = match oof::pop_subcommand(&mut args, subcommands) {
102         Some(&"compress") => {
103             // `ouch compress` subcommand
104             let (args, flags) = oof::filter_flags(args, &flags_info)?;
105             let mut files: Vec<PathBuf> = args.into_iter().map(PathBuf::from).collect();
107             if files.len() < 2 {
108                 return Err(crate::Error::MissingArgumentsForCompression);
109             }
111             // Safety: we checked that args.len() >= 2
112             let compressed_output_path = files.pop().unwrap();
114             let files = canonicalize_files(files)?;
116             let command = Command::Compress {
117                 files,
118                 compressed_output_path,
119             };
120             ParsedArgs { command, flags }
121         }
122         // Defaults to decompression when there is no subcommand
123         None => {
124             flags_info.push(arg_flag!('o', "output"));
125             {
126                 let first_arg = args.first().unwrap();
127                 if is_typo(first_arg) {
128                     return Err(crate::Error::CompressionTypo);
129                 }
130             }
131             
133             // Parse flags
134             let (args, mut flags) = oof::filter_flags(args, &flags_info)?;
136             let files = args.into_iter().map(canonicalize);
137             for file in files.clone() {
138                 if let Err(err) = file {
139                     return Err(err);
140                 }
141             }
142             let files = files.map(Result::unwrap).collect();
144             let output_folder = flags.take_arg("output").map(PathBuf::from);
146             // TODO: ensure all files are decompressible
148             let command = Command::Decompress {
149                 files,
150                 output_folder,
151             };
152             ParsedArgs { command, flags }
153         }
154         _ => unreachable!("You should match each subcommand passed."),
155     };
157     Ok(parsed_args)