args.rs (18330B)
1 use core::{ 2 error::Error, 3 fmt::{self, Display, Formatter}, 4 }; 5 use rpz::file::AbsFilePath; 6 use std::env::{self, Args}; 7 /// Error returned when parsing arguments passed to the application. 8 #[expect(clippy::module_name_repetitions, reason = "prefer the name")] 9 #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] 10 pub enum ArgsErr { 11 /// Error when no arguments were passed to the application. 12 NoArgs, 13 /// Error when `-f`/`--file` is not passed or is passed 14 /// without a path to the file. 15 ConfigPathNotPassed, 16 /// Error when an invalid option is passed. The contained [`String`] 17 /// is the value of the invalid option. 18 InvalidOption(String), 19 /// Some options when passed must be the only option passed. 20 /// For such options, this is the error when other options are passed. 21 MoreThanOneOption, 22 /// Error when the passed path to the config file is not `-` nor an absolute file path to a file. 23 InvalidConfigPath, 24 /// Error when an option is passed more than once. 25 DuplicateOption(&'static str), 26 /// Error when the quiet and verbose options were passed. 27 QuietAndVerbose, 28 } 29 impl Display for ArgsErr { 30 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 31 match *self { 32 Self::NoArgs => write!(f, "no arguments were passed, but at least two are required containing the option '-f' and its value which must be an absolute path to the config file"), 33 Self::ConfigPathNotPassed => f.write_str("'-f' followed by '-' or the absolute file path to the config file was not passed"), 34 Self::InvalidOption(ref arg) => write!(f, "{arg} is an invalid option. Only '-f'/'--file' followed by the absolute path to the config file, '-q'/'--quiet', '-h'/'--help', '-v'/'--verbose' and '-V'/'--version' are allowed"), 35 Self::MoreThanOneOption => f.write_str("'-V'/'--version' or '-h'/'--help' was passed with other options; but when those options are passed, they must be the only one"), 36 Self::InvalidConfigPath => write!(f, "an absolute file path to the config file or '-' was not passed"), 37 Self::DuplicateOption(arg) => write!(f, "{arg} was passed more than once"), 38 Self::QuietAndVerbose => f.write_str("'-q'/'--quiet' and '-v'/'--verbose' were both passed, but at most only one of them is allowed to be passed"), 39 } 40 } 41 } 42 impl Error for ArgsErr {} 43 /// The location of the configuration file. 44 #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] 45 pub enum ConfigPath { 46 /// The config file is to be read from `stdin`. 47 Stdin, 48 /// The config file resides on the local file system. 49 Path(AbsFilePath<false>), 50 } 51 /// The options passed to the application. 52 #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] 53 pub enum Opts { 54 /// Variant when no arguments were passed. 55 None, 56 /// Variant when the help argument was passed. 57 Help, 58 /// Variant when the version argument was passed. 59 Version, 60 /// Variant when the quiet argument was passed. 61 Quiet, 62 /// Variant when the verbose argument was passed. 63 Verbose, 64 /// Variant when the file argument with the path to the file was passed. 65 Config(ConfigPath), 66 /// Variant when the quiet argument and the file argument with the path to the file 67 /// were passed. 68 ConfigQuiet(ConfigPath), 69 /// Variant when the verbose argument and the file argument with the path to the file 70 /// were passed. 71 ConfigVerbose(ConfigPath), 72 } 73 impl Opts { 74 /// Returns `Opts` based on arguments passed to the application. 75 #[expect(clippy::too_many_lines, reason = "this is fine")] 76 pub fn from_args() -> Result<Self, ArgsErr> { 77 /// Attempts to parse the next `Arg` into `-` or an absolute 78 /// path to a file. 79 fn get_path(args: &mut Args) -> Result<ConfigPath, ArgsErr> { 80 args.next() 81 .map_or(Err(ArgsErr::ConfigPathNotPassed), |path| { 82 if path == "-" { 83 Ok(ConfigPath::Stdin) 84 } else { 85 AbsFilePath::<false>::from_string(path) 86 .map_or(Err(ArgsErr::InvalidConfigPath), |config| { 87 Ok(ConfigPath::Path(config)) 88 }) 89 } 90 }) 91 } 92 let mut args = env::args(); 93 if args.next().is_some() { 94 let mut opts = Self::None; 95 while let Some(arg) = args.next() { 96 match arg.as_str() { 97 "-h" | "--help" => match opts { 98 Self::None => { 99 opts = Self::Help; 100 } 101 Self::Help => return Err(ArgsErr::DuplicateOption("-h/--help")), 102 Self::Verbose 103 | Self::Quiet 104 | Self::Version 105 | Self::Config(_) 106 | Self::ConfigQuiet(_) 107 | Self::ConfigVerbose(_) => return Err(ArgsErr::MoreThanOneOption), 108 }, 109 "-V" | "--version" => match opts { 110 Self::None => { 111 opts = Self::Version; 112 } 113 Self::Version => return Err(ArgsErr::DuplicateOption("-V/--version")), 114 Self::Verbose 115 | Self::Quiet 116 | Self::Help 117 | Self::Config(_) 118 | Self::ConfigQuiet(_) 119 | Self::ConfigVerbose(_) => return Err(ArgsErr::MoreThanOneOption), 120 }, 121 "-f" | "--file" => match opts { 122 Self::None => { 123 opts = Self::Config(get_path(&mut args)?); 124 } 125 Self::Quiet => { 126 opts = Self::ConfigQuiet(get_path(&mut args)?); 127 } 128 Self::Verbose => { 129 opts = Self::ConfigVerbose(get_path(&mut args)?); 130 } 131 Self::Config(_) | Self::ConfigQuiet(_) | Self::ConfigVerbose(_) => { 132 return Err(ArgsErr::DuplicateOption("-f/--file")); 133 } 134 Self::Help | Self::Version => return Err(ArgsErr::MoreThanOneOption), 135 }, 136 "-q" | "--quiet" => match opts { 137 Self::None => { 138 opts = Self::Quiet; 139 } 140 Self::Config(path) => { 141 opts = Self::ConfigQuiet(path); 142 } 143 Self::Quiet | Self::ConfigQuiet(_) => { 144 return Err(ArgsErr::DuplicateOption("-q/--quiet")); 145 } 146 Self::Verbose | Self::ConfigVerbose(_) => { 147 return Err(ArgsErr::QuietAndVerbose) 148 } 149 Self::Help | Self::Version => return Err(ArgsErr::MoreThanOneOption), 150 }, 151 "-v" | "--verbose" => match opts { 152 Self::None => { 153 opts = Self::Verbose; 154 } 155 Self::Config(path) => { 156 opts = Self::ConfigVerbose(path); 157 } 158 Self::Quiet | Self::ConfigQuiet(_) => return Err(ArgsErr::QuietAndVerbose), 159 Self::Verbose | Self::ConfigVerbose(_) => { 160 return Err(ArgsErr::DuplicateOption("-v/--verbose")); 161 } 162 Self::Help | Self::Version => return Err(ArgsErr::MoreThanOneOption), 163 }, 164 "-fq" | "-qf" => match opts { 165 Self::None => { 166 opts = Self::ConfigQuiet(get_path(&mut args)?); 167 } 168 Self::Config(_) | Self::ConfigQuiet(_) => { 169 return Err(ArgsErr::DuplicateOption("-f/--file")) 170 } 171 Self::Quiet => return Err(ArgsErr::DuplicateOption("-q/--quiet")), 172 Self::Verbose | Self::ConfigVerbose(_) => { 173 return Err(ArgsErr::QuietAndVerbose) 174 } 175 Self::Help | Self::Version => return Err(ArgsErr::MoreThanOneOption), 176 }, 177 "-fv" | "-vf" => match opts { 178 Self::None => { 179 opts = Self::ConfigVerbose(get_path(&mut args)?); 180 } 181 Self::Config(_) | Self::ConfigVerbose(_) => { 182 return Err(ArgsErr::DuplicateOption("-f/--file")) 183 } 184 Self::Verbose => return Err(ArgsErr::DuplicateOption("-v/--verbose")), 185 Self::Quiet | Self::ConfigQuiet(_) => return Err(ArgsErr::QuietAndVerbose), 186 Self::Help | Self::Version => return Err(ArgsErr::MoreThanOneOption), 187 }, 188 _ => return Err(ArgsErr::InvalidOption(arg)), 189 } 190 } 191 Ok(opts) 192 } else { 193 Err(ArgsErr::NoArgs) 194 } 195 } 196 } 197 #[cfg(test)] 198 mod tests { 199 use crate::{test_prog, ArgsErr, E}; 200 use core::convert; 201 use std::io::Write; 202 use std::process::Stdio; 203 use std::thread; 204 #[test] 205 #[ignore] 206 fn test_args() { 207 test_prog::verify_files(); 208 assert!(test_prog::get_command() 209 .stderr(Stdio::piped()) 210 .stdin(Stdio::null()) 211 .stdout(Stdio::null()) 212 .output() 213 .map_or(false, |output| { 214 !output.status.success() 215 && output.stderr 216 == format!("Error: {:?}\n", E::Args(ArgsErr::NoArgs)).into_bytes() 217 })); 218 assert!(test_prog::get_command() 219 .arg("-f") 220 .stderr(Stdio::piped()) 221 .stdin(Stdio::null()) 222 .stdout(Stdio::null()) 223 .output() 224 .map_or(false, |output| { 225 !output.status.success() 226 && output.stderr 227 == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) 228 .into_bytes() 229 })); 230 assert!(test_prog::get_command() 231 .arg("-q") 232 .stderr(Stdio::piped()) 233 .stdin(Stdio::null()) 234 .stdout(Stdio::null()) 235 .output() 236 .map_or(false, |output| { 237 !output.status.success() 238 && output.stderr 239 == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) 240 .into_bytes() 241 })); 242 assert!(test_prog::get_command() 243 .arg("-v") 244 .stderr(Stdio::piped()) 245 .stdin(Stdio::null()) 246 .stdout(Stdio::null()) 247 .output() 248 .map_or(false, |output| { 249 !output.status.success() 250 && output.stderr 251 == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) 252 .into_bytes() 253 })); 254 assert!(test_prog::get_command() 255 .arg("-fq") 256 .stderr(Stdio::piped()) 257 .stdin(Stdio::null()) 258 .stdout(Stdio::null()) 259 .output() 260 .map_or(false, |output| { 261 !output.status.success() 262 && output.stderr 263 == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) 264 .into_bytes() 265 })); 266 assert!(test_prog::get_command() 267 .arg("-qf") 268 .stderr(Stdio::piped()) 269 .stdin(Stdio::null()) 270 .stdout(Stdio::null()) 271 .output() 272 .map_or(false, |output| { 273 !output.status.success() 274 && output.stderr 275 == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) 276 .into_bytes() 277 })); 278 assert!(test_prog::get_command() 279 .arg("-fv") 280 .stderr(Stdio::piped()) 281 .stdin(Stdio::null()) 282 .stdout(Stdio::null()) 283 .output() 284 .map_or(false, |output| { 285 !output.status.success() 286 && output.stderr 287 == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) 288 .into_bytes() 289 })); 290 assert!(test_prog::get_command() 291 .arg("-vf") 292 .stderr(Stdio::piped()) 293 .stdin(Stdio::null()) 294 .stdout(Stdio::null()) 295 .output() 296 .map_or(false, |output| { 297 !output.status.success() 298 && output.stderr 299 == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) 300 .into_bytes() 301 })); 302 assert!(test_prog::get_command() 303 .args(["-h", "-V"]) 304 .stderr(Stdio::piped()) 305 .stdin(Stdio::null()) 306 .stdout(Stdio::null()) 307 .output() 308 .map_or(false, |output| { 309 !output.status.success() 310 && output.stderr 311 == format!("Error: {:?}\n", E::Args(ArgsErr::MoreThanOneOption)) 312 .into_bytes() 313 })); 314 assert!(test_prog::get_command() 315 .args(["-h", "-h"]) 316 .stderr(Stdio::piped()) 317 .stdin(Stdio::null()) 318 .stdout(Stdio::null()) 319 .output() 320 .map_or(false, |output| { 321 !output.status.success() 322 && output.stderr 323 == format!( 324 "Error: {:?}\n", 325 E::Args(ArgsErr::DuplicateOption("-h/--help")) 326 ) 327 .into_bytes() 328 })); 329 assert!(test_prog::get_command() 330 .args(["-f", "/home/zack/foo", "-V"]) 331 .stderr(Stdio::piped()) 332 .stdin(Stdio::null()) 333 .stdout(Stdio::null()) 334 .output() 335 .map_or(false, |output| { 336 !output.status.success() 337 && output.stderr 338 == format!("Error: {:?}\n", E::Args(ArgsErr::MoreThanOneOption)) 339 .into_bytes() 340 })); 341 assert!(test_prog::get_command() 342 .args(["-f", "home/zack/foo"]) 343 .stderr(Stdio::piped()) 344 .stdin(Stdio::null()) 345 .stdout(Stdio::null()) 346 .output() 347 .map_or(false, |output| { 348 !output.status.success() 349 && output.stderr 350 == format!("Error: {:?}\n", E::Args(ArgsErr::InvalidConfigPath)) 351 .into_bytes() 352 })); 353 assert!(test_prog::get_command() 354 .args(["-f", "/home/zack/foo/"]) 355 .stderr(Stdio::piped()) 356 .stdin(Stdio::null()) 357 .stdout(Stdio::null()) 358 .output() 359 .map_or(false, |output| { 360 !output.status.success() 361 && output.stderr 362 == format!("Error: {:?}\n", E::Args(ArgsErr::InvalidConfigPath)) 363 .into_bytes() 364 })); 365 assert!(test_prog::get_command() 366 .arg("-foo") 367 .stderr(Stdio::piped()) 368 .stdin(Stdio::null()) 369 .stdout(Stdio::null()) 370 .output() 371 .map_or(false, |output| { 372 !output.status.success() 373 && output.stderr 374 == format!( 375 "Error: {:?}\n", 376 E::Args(ArgsErr::InvalidOption(String::from("-foo"))) 377 ) 378 .into_bytes() 379 })); 380 assert!(test_prog::get_command() 381 .args(["-f", "-"]) 382 .stderr(Stdio::piped()) 383 .stdin(Stdio::null()) 384 .stdout(Stdio::null()) 385 .output() 386 .map_or(false, |output| !output.status.success() 387 && output.stderr.get(..23) == Some(b"Error: TOML parse error"))); 388 assert!(test_prog::get_command() 389 .args(["-f", "-"]) 390 .stderr(Stdio::piped()) 391 .stdin(Stdio::piped()) 392 .stdout(Stdio::null()) 393 .spawn() 394 .map_or(false, |mut cmd| { 395 cmd.stdin.take().map_or(false, |mut stdin| { 396 thread::spawn(move || { 397 stdin 398 .write_all(b"junk") 399 .map_or(false, |_| stdin.flush().map_or(false, |_| true)) 400 }) 401 .join() 402 .map_or(false, convert::identity) 403 }) && cmd.wait_with_output().map_or(false, |output| { 404 !output.status.success() 405 && output.stderr.get(..23) == Some(b"Error: TOML parse error") 406 }) 407 })); 408 assert!(test_prog::get_command() 409 .arg("-h") 410 .stderr(Stdio::null()) 411 .stdin(Stdio::null()) 412 .stdout(Stdio::piped()) 413 .output() 414 .map_or(false, |output| output.status.success() 415 && output.stdout.get(..crate::HELP.len()) 416 == Some(crate::HELP.as_bytes()))); 417 assert!(test_prog::get_command() 418 .arg("-V") 419 .stderr(Stdio::null()) 420 .stdin(Stdio::null()) 421 .stdout(Stdio::piped()) 422 .output() 423 .map_or(false, |output| output.status.success() 424 && output.stdout.get(..crate::VERSION.len()) 425 == Some(crate::VERSION.as_bytes()))); 426 } 427 }