Skip to main content

fb2_clean/cli/
parser.rs

1use crate::{Config, Input, InputFile, InputFileType, Lang, Msg, Output, Tags, msg};
2use clap::{
3    Arg, ArgAction, ArgMatches, Command, CommandFactory, Error, FromArgMatches, Parser,
4    builder::{TypedValueParser, ValueParser},
5    error::{ContextKind, ContextValue, ErrorKind},
6    value_parser,
7};
8use std::{ffi::OsStr, fs, path::Path, str::FromStr};
9
10type Result<T> = std::result::Result<T, clap::Error>;
11
12impl Parser for Config {}
13
14impl FromArgMatches for Config {
15    fn from_arg_matches(m: &ArgMatches) -> Result<Config> {
16        Self::from_arg_matches_mut(&mut m.clone())
17    }
18
19    fn update_from_arg_matches(&mut self, m: &ArgMatches) -> Result<()> {
20        self.update_from_arg_matches_mut(&mut m.clone())
21    }
22
23    fn from_arg_matches_mut(m: &mut ArgMatches) -> Result<Config> {
24        if let Some(lang) = m.remove_one::<Lang>("lang") {
25            let _ = Msg::set_lang(lang);
26        }
27
28        if m.get_flag("help") {
29            let mut cmd = Config::command();
30            if let Err(_) = cmd.print_help() {
31                println!("{}", cmd.render_help());
32            }
33            std::process::exit(0);
34        }
35
36        let input = match m.remove_many::<InputPath>("input") {
37            None => Input::default(),
38            Some(xs) if xs.len() == 0 => Input::default(),
39            Some(xs) => {
40                let mut paths: Vec<_> = xs.into_iter().collect();
41
42                if paths.len() > 1 && paths.iter().any(|p| p.is_dir()) {
43                    return Err(Error::raw(
44                        ErrorKind::TooManyValues,
45                        "directory must be only 1",
46                    ));
47                }
48
49                if paths[0].is_dir() {
50                    Input::Dir(paths.pop().unwrap().into_boxed_path())
51                } else {
52                    Input::Files(paths.into_iter().map(|p| p.into_input_file()).collect())
53                }
54            }
55        };
56
57        let output = match m.remove_one::<Output>("output") {
58            Some(o) => o,
59            None => Output::try_from_input(&input).unwrap_or_default(),
60        };
61
62        Ok(Config {
63            input,
64            output,
65            recursive: *m
66                .get_one::<u8>("recursive")
67                .unwrap_or(&Self::default_recursive()),
68            tags: m
69                .remove_one::<Tags>("tags")
70                .unwrap_or_else(|| Tags::default()),
71            zip: m.get_flag("zip"),
72            unzip: m.get_flag("unzip"),
73            overwrite: m.get_flag("overwrite"),
74            exit_on_err: m.get_flag("exit-on-err"),
75            jobs: *m.get_one::<u64>("jobs").unwrap_or(&Self::default_jobs()),
76        })
77    }
78
79    fn update_from_arg_matches_mut(&mut self, m: &mut ArgMatches) -> Result<()> {
80        *self = Self::from_arg_matches_mut(m)?;
81        Ok(())
82    }
83}
84
85impl CommandFactory for Config {
86    fn command() -> Command {
87        Command::new(env!("CARGO_PKG_NAME"))
88            .version(concat!("v", env!("CARGO_PKG_VERSION")))
89            .disable_help_flag(true)
90            .disable_version_flag(true)
91            .override_usage(concat!(env!("CARGO_PKG_NAME"), " [options]"))
92            .arg(
93                Arg::new("input")
94                    .short('i')
95                    .long("input")
96                    .value_name("path")
97                    .help(msg!(HelpInput))
98                    .action(ArgAction::Append)
99                    .value_parser(ValueParser::new(InputPathParser)),
100            )
101            .arg(
102                Arg::new("output")
103                    .short('o')
104                    .long("output")
105                    .value_name("dir")
106                    .help(msg!(HelpOutput))
107                    .value_parser(ValueParser::new(OutputParser)),
108            )
109            .arg(
110                Arg::new("recursive")
111                    .short('r')
112                    .long("recursive")
113                    .value_name("n")
114                    .help(msg!(HelpRecursive))
115                    .value_parser(value_parser!(u8)),
116            )
117            .arg(
118                Arg::new("tags")
119                    .short('t')
120                    .long("tags")
121                    .value_name("n[,m...]")
122                    .help(msg!(HelpTags))
123                    .value_parser(ValueParser::new(TagsParser)),
124            )
125            .arg(
126                Arg::new("zip")
127                    .short('z')
128                    .long("zip")
129                    .help(msg!(HelpZip))
130                    .action(ArgAction::SetTrue),
131            )
132            .arg(
133                Arg::new("unzip")
134                    .short('Z')
135                    .long("unzip")
136                    .alias("no-zip")
137                    .help(msg!(HelpUnzip))
138                    .conflicts_with("zip")
139                    .action(ArgAction::SetTrue),
140            )
141            .arg(
142                Arg::new("overwrite")
143                    .short('w')
144                    .long("overwrite")
145                    .help(msg!(HelpOverwrite))
146                    .action(ArgAction::SetTrue),
147            )
148            .arg(
149                Arg::new("exit-on-err")
150                    .short('e')
151                    .long("exit-on-err")
152                    .alias("exit-on-error")
153                    .help(msg!(HelpExitOnError))
154                    .action(ArgAction::SetTrue),
155            )
156            .arg(
157                Arg::new("jobs")
158                    .short('j')
159                    .long("jobs")
160                    .value_name("n")
161                    .help(msg!(HelpJobs))
162                    .value_parser(value_parser!(u64).range(1..)),
163            )
164            .next_help_heading("Other")
165            .arg(
166                Arg::new("lang")
167                    .short('l')
168                    .long("lang")
169                    .alias("language")
170                    .value_name("lng")
171                    .help(msg!(HelpLang))
172                    .value_parser(ValueParser::new(Lang::from_str)),
173            )
174            .arg(
175                Arg::new("version")
176                    .short('V')
177                    .long("version")
178                    .help(msg!(HelpVersion))
179                    .action(ArgAction::Version),
180            )
181            .arg(
182                Arg::new("help")
183                    .short('h')
184                    .long("help")
185                    .help(msg!(HelpHelp))
186                    .action(ArgAction::SetTrue),
187            )
188    }
189
190    fn command_for_update() -> Command {
191        Self::command()
192    }
193}
194
195macro_rules! ty_parser {
196    ($parser:ident, $ty:ty, $try_from_os_str:path) => {
197        #[derive(Clone)]
198        pub(super) struct $parser;
199
200        impl TypedValueParser for $parser {
201            type Value = $ty;
202
203            fn parse_ref(
204                &self,
205                cmd: &Command,
206                arg: Option<&Arg>,
207                value: &OsStr,
208            ) -> Result<Self::Value> {
209                $try_from_os_str(value).map_err(|e| {
210                    let mut err = Error::new(ErrorKind::InvalidValue).with_cmd(&cmd);
211
212                    if let Some(arg) = arg {
213                        err.insert(
214                            ContextKind::InvalidArg,
215                            ContextValue::String(arg.to_string()),
216                        );
217                    }
218                    err.insert(
219                        ContextKind::InvalidValue,
220                        ContextValue::String(format!(
221                            "'{}' (reason: {})",
222                            value.to_string_lossy(),
223                            e
224                        )),
225                    );
226
227                    err
228                })
229            }
230        }
231    };
232}
233
234ty_parser!(InputPathParser, InputPath, InputPath::new);
235ty_parser!(OutputParser, Output, Output::new);
236ty_parser!(TagsParser, Tags, Tags::fallible_new);
237
238impl Tags {
239    /// Always returns `Ok`.
240    fn fallible_new(os: impl AsRef<OsStr>) -> std::result::Result<Tags, String> {
241        Ok(Self::new(os))
242    }
243}
244
245#[derive(Clone, Debug, PartialEq)]
246pub enum InputPath {
247    Dir(Box<Path>),
248    File(InputFile),
249}
250
251impl InputPath {
252    pub(crate) fn new(path: impl AsRef<Path>) -> crate::Result<InputPath> {
253        let path = fs::canonicalize(path)?.into_boxed_path();
254
255        if path.is_dir() {
256            Ok(Self::Dir(path))
257        } else if let Some(ty) = InputFileType::get_new(&path) {
258            Ok(Self::File(InputFile { ty, path }))
259        } else {
260            Err("file has unsupported extension".into())
261        }
262    }
263
264    pub(crate) fn is_dir(&self) -> bool {
265        matches!(self, Self::Dir(_))
266    }
267
268    pub(crate) fn into_boxed_path(self) -> Box<Path> {
269        match self {
270            Self::Dir(p) => p,
271            Self::File(InputFile { path, .. }) => path,
272        }
273    }
274
275    // panic on Self::Dir
276    pub(crate) fn into_input_file(self) -> InputFile {
277        match self {
278            Self::Dir(_) => panic!("must be InputPath::File"),
279            Self::File(f) => f,
280        }
281    }
282}