From 808885c5a042aaf2e07c8d44b0d8aff38e275e18 Mon Sep 17 00:00:00 2001 From: nasso Date: Thu, 3 Jul 2025 19:40:05 +0200 Subject: [PATCH] Add argument parsing Change-Id: mwqsknkpzksxnpytkzyxmpoqxkkkqmuu --- src/cli.rs | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 17 ++++- 2 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 src/cli.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..b64c375 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,190 @@ +use std::{ + error::Error, + net::{AddrParseError, SocketAddr}, + path::PathBuf, +}; + +#[derive(Debug, PartialEq, Eq)] +pub enum Args { + Help, + Host { boats: PathBuf, addr: SocketAddr }, + Join { boats: PathBuf, addr: SocketAddr }, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ArgsParseError { + NotEnoughArguments, + InvalidMode(String), + InvalidAddr(AddrParseError), +} + +impl Error for ArgsParseError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + ArgsParseError::NotEnoughArguments => None, + ArgsParseError::InvalidMode(_) => None, + ArgsParseError::InvalidAddr(e) => Some(e), + } + } +} + +impl std::fmt::Display for ArgsParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ArgsParseError::NotEnoughArguments => write!(f, "not enough arguments"), + ArgsParseError::InvalidMode(mode) => write!(f, "invalid mode: '{mode}'"), + ArgsParseError::InvalidAddr(e) => write!(f, "error parsing address: {e}"), + } + } +} + +impl Args { + pub fn parse>(iter: impl IntoIterator) -> Result { + let mut iter = iter.into_iter(); + let Some(arg) = iter.next() else { + return Ok(Self::Help); + }; + + if matches!(arg.as_ref(), "-h" | "--help") { + return Ok(Self::Help); + } + + let boats = PathBuf::from(arg.as_ref()); + let arg = iter.next().ok_or(ArgsParseError::NotEnoughArguments)?; + + let addr = iter + .next() + .ok_or(ArgsParseError::NotEnoughArguments)? + .as_ref() + .parse::() + .map_err(ArgsParseError::InvalidAddr)?; + + match arg.as_ref() { + "host" => Ok(Self::Host { boats, addr }), + "join" => Ok(Self::Join { boats, addr }), + a => Err(ArgsParseError::InvalidMode(a.to_string())), + } + } +} + +#[cfg(test)] +mod tests { + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + + use super::*; + + #[test] + fn parse_nothing() { + assert_eq!(Args::parse::([]), Ok(Args::Help)); + } + + #[test] + fn parse_help() { + assert_eq!(Args::parse(["-h"]), Ok(Args::Help)); + assert_eq!(Args::parse(["--help"]), Ok(Args::Help)); + } + + #[test] + fn parse_host() { + assert_eq!( + Args::parse(["./my_boats", "host", "127.0.0.1:8080"]), + Ok(Args::Host { + boats: "./my_boats".into(), + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080), + }) + ); + assert_eq!( + Args::parse(["./my_boats", "host", "0.0.0.0:8080"]), + Ok(Args::Host { + boats: "./my_boats".into(), + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080), + }) + ); + assert_eq!( + Args::parse(["./my_boats", "host", "[::1]:8080"]), + Ok(Args::Host { + boats: "./my_boats".into(), + addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8080), + }) + ); + assert_eq!( + Args::parse(["./my_boats", "host", "[::]:8080"]), + Ok(Args::Host { + boats: "./my_boats".into(), + addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 8080), + }) + ); + } + + #[test] + fn parse_join() { + assert_eq!( + Args::parse(["./my_boats", "join", "127.0.0.1:8080"]), + Ok(Args::Join { + boats: "./my_boats".into(), + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080), + }) + ); + assert_eq!( + Args::parse(["./my_boats", "join", "0.0.0.0:8080"]), + Ok(Args::Join { + boats: "./my_boats".into(), + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080), + }) + ); + assert_eq!( + Args::parse(["./my_boats", "join", "[::1]:8080"]), + Ok(Args::Join { + boats: "./my_boats".into(), + addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8080), + }) + ); + assert_eq!( + Args::parse(["./my_boats", "join", "[::]:8080"]), + Ok(Args::Join { + boats: "./my_boats".into(), + addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 8080), + }) + ); + } + + #[test] + fn parse_errors() { + assert_eq!( + Args::parse(["abcoabcuobacwa"]), + Err(ArgsParseError::NotEnoughArguments) + ); + assert_eq!( + Args::parse(["./my_boats", "foobar"]), + Err(ArgsParseError::NotEnoughArguments) + ); + assert_eq!( + Args::parse(["./my_boats", "foobar", "127.0.0.1:8080"]), + Err(ArgsParseError::InvalidMode("foobar".into())) + ); + assert!(matches!( + Args::parse(["./my_boats", "host", "blablabla"]), + Err(ArgsParseError::InvalidAddr(_)) + )); + assert!(matches!( + Args::parse(["./my_boats", "host", "localhost"]), + Err(ArgsParseError::InvalidAddr(_)) + )); + assert!(matches!( + Args::parse(["./my_boats", "host", "0.0.0.0"]), + Err(ArgsParseError::InvalidAddr(_)) + )); + assert!(matches!( + Args::parse(["./my_boats", "join", "127.0.0.1"]), + Err(ArgsParseError::InvalidAddr(_)) + )); + assert!(matches!( + Args::parse(["./my_boats", "join", "localhost"]), + Err(ArgsParseError::InvalidAddr(_)) + )); + assert!(matches!( + Args::parse(["./my_boats", "join", "localhost:8080"]), + Err(ArgsParseError::InvalidAddr(_)) + )); + } +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..682db50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,18 @@ -fn main() { +use std::process::ExitCode; + +mod cli; + +fn main() -> ExitCode { + let args = match cli::Args::parse(std::env::args().skip(1)) { + Err(e) => { + eprintln!("Invalid arguments: {e}"); + return ExitCode::FAILURE; + } + Ok(args) => args, + }; + println!("Hello, world!"); + println!("{args:#?}"); + + ExitCode::SUCCESS } -- 2.49.0