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 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 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}