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