Add command-line arguments parsing #1

merged
opened by nasso.dev targeting main from nasso/push-mwqsknkpzksx
Changed files
+206 -1
src
+190
src/cli.rs
··· 1 + use std::{ 2 + error::Error, 3 + net::{AddrParseError, SocketAddr}, 4 + path::PathBuf, 5 + }; 6 + 7 + #[derive(Debug, PartialEq, Eq)] 8 + pub enum Args { 9 + Help, 10 + Host { boats: PathBuf, addr: SocketAddr }, 11 + Join { boats: PathBuf, addr: SocketAddr }, 12 + } 13 + 14 + #[derive(Debug, PartialEq, Eq)] 15 + pub enum ArgsParseError { 16 + NotEnoughArguments, 17 + InvalidMode(String), 18 + InvalidAddr(AddrParseError), 19 + } 20 + 21 + impl Error for ArgsParseError { 22 + fn source(&self) -> Option<&(dyn Error + 'static)> { 23 + match self { 24 + ArgsParseError::NotEnoughArguments => None, 25 + ArgsParseError::InvalidMode(_) => None, 26 + ArgsParseError::InvalidAddr(e) => Some(e), 27 + } 28 + } 29 + } 30 + 31 + impl std::fmt::Display for ArgsParseError { 32 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 33 + match self { 34 + ArgsParseError::NotEnoughArguments => write!(f, "not enough arguments"), 35 + ArgsParseError::InvalidMode(mode) => write!(f, "invalid mode: '{mode}'"), 36 + ArgsParseError::InvalidAddr(e) => write!(f, "error parsing address: {e}"), 37 + } 38 + } 39 + } 40 + 41 + impl Args { 42 + pub fn parse<S: AsRef<str>>(iter: impl IntoIterator<Item = S>) -> Result<Self, ArgsParseError> { 43 + let mut iter = iter.into_iter(); 44 + let Some(arg) = iter.next() else { 45 + return Ok(Self::Help); 46 + }; 47 + 48 + if matches!(arg.as_ref(), "-h" | "--help") { 49 + return Ok(Self::Help); 50 + } 51 + 52 + let boats = PathBuf::from(arg.as_ref()); 53 + let arg = iter.next().ok_or(ArgsParseError::NotEnoughArguments)?; 54 + 55 + let addr = iter 56 + .next() 57 + .ok_or(ArgsParseError::NotEnoughArguments)? 58 + .as_ref() 59 + .parse::<SocketAddr>() 60 + .map_err(ArgsParseError::InvalidAddr)?; 61 + 62 + match arg.as_ref() { 63 + "host" => Ok(Self::Host { boats, addr }), 64 + "join" => Ok(Self::Join { boats, addr }), 65 + a => Err(ArgsParseError::InvalidMode(a.to_string())), 66 + } 67 + } 68 + } 69 + 70 + #[cfg(test)] 71 + mod tests { 72 + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 73 + 74 + use super::*; 75 + 76 + #[test] 77 + fn parse_nothing() { 78 + assert_eq!(Args::parse::<String>([]), Ok(Args::Help)); 79 + } 80 + 81 + #[test] 82 + fn parse_help() { 83 + assert_eq!(Args::parse(["-h"]), Ok(Args::Help)); 84 + assert_eq!(Args::parse(["--help"]), Ok(Args::Help)); 85 + } 86 + 87 + #[test] 88 + fn parse_host() { 89 + assert_eq!( 90 + Args::parse(["./my_boats", "host", "127.0.0.1:8080"]), 91 + Ok(Args::Host { 92 + boats: "./my_boats".into(), 93 + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080), 94 + }) 95 + ); 96 + assert_eq!( 97 + Args::parse(["./my_boats", "host", "0.0.0.0:8080"]), 98 + Ok(Args::Host { 99 + boats: "./my_boats".into(), 100 + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080), 101 + }) 102 + ); 103 + assert_eq!( 104 + Args::parse(["./my_boats", "host", "[::1]:8080"]), 105 + Ok(Args::Host { 106 + boats: "./my_boats".into(), 107 + addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8080), 108 + }) 109 + ); 110 + assert_eq!( 111 + Args::parse(["./my_boats", "host", "[::]:8080"]), 112 + Ok(Args::Host { 113 + boats: "./my_boats".into(), 114 + addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 8080), 115 + }) 116 + ); 117 + } 118 + 119 + #[test] 120 + fn parse_join() { 121 + assert_eq!( 122 + Args::parse(["./my_boats", "join", "127.0.0.1:8080"]), 123 + Ok(Args::Join { 124 + boats: "./my_boats".into(), 125 + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080), 126 + }) 127 + ); 128 + assert_eq!( 129 + Args::parse(["./my_boats", "join", "0.0.0.0:8080"]), 130 + Ok(Args::Join { 131 + boats: "./my_boats".into(), 132 + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080), 133 + }) 134 + ); 135 + assert_eq!( 136 + Args::parse(["./my_boats", "join", "[::1]:8080"]), 137 + Ok(Args::Join { 138 + boats: "./my_boats".into(), 139 + addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8080), 140 + }) 141 + ); 142 + assert_eq!( 143 + Args::parse(["./my_boats", "join", "[::]:8080"]), 144 + Ok(Args::Join { 145 + boats: "./my_boats".into(), 146 + addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 8080), 147 + }) 148 + ); 149 + } 150 + 151 + #[test] 152 + fn parse_errors() { 153 + assert_eq!( 154 + Args::parse(["abcoabcuobacwa"]), 155 + Err(ArgsParseError::NotEnoughArguments) 156 + ); 157 + assert_eq!( 158 + Args::parse(["./my_boats", "foobar"]), 159 + Err(ArgsParseError::NotEnoughArguments) 160 + ); 161 + assert_eq!( 162 + Args::parse(["./my_boats", "foobar", "127.0.0.1:8080"]), 163 + Err(ArgsParseError::InvalidMode("foobar".into())) 164 + ); 165 + assert!(matches!( 166 + Args::parse(["./my_boats", "host", "blablabla"]), 167 + Err(ArgsParseError::InvalidAddr(_)) 168 + )); 169 + assert!(matches!( 170 + Args::parse(["./my_boats", "host", "localhost"]), 171 + Err(ArgsParseError::InvalidAddr(_)) 172 + )); 173 + assert!(matches!( 174 + Args::parse(["./my_boats", "host", "0.0.0.0"]), 175 + Err(ArgsParseError::InvalidAddr(_)) 176 + )); 177 + assert!(matches!( 178 + Args::parse(["./my_boats", "join", "127.0.0.1"]), 179 + Err(ArgsParseError::InvalidAddr(_)) 180 + )); 181 + assert!(matches!( 182 + Args::parse(["./my_boats", "join", "localhost"]), 183 + Err(ArgsParseError::InvalidAddr(_)) 184 + )); 185 + assert!(matches!( 186 + Args::parse(["./my_boats", "join", "localhost:8080"]), 187 + Err(ArgsParseError::InvalidAddr(_)) 188 + )); 189 + } 190 + }
+16 -1
src/main.rs
··· 1 - fn main() { 1 + use std::process::ExitCode; 2 + 3 + mod cli; 4 + 5 + fn main() -> ExitCode { 6 + let args = match cli::Args::parse(std::env::args().skip(1)) { 7 + Err(e) => { 8 + eprintln!("Invalid arguments: {e}"); 9 + return ExitCode::FAILURE; 10 + } 11 + Ok(args) => args, 12 + }; 13 + 2 14 println!("Hello, world!"); 15 + println!("{args:#?}"); 16 + 17 + ExitCode::SUCCESS 3 18 }