//! Hand-rolled CLI parser. No clap. //! //! Sprint 0 recognizes a minimal set (`-o`, `-e`, `-arch`, positional inputs) //! and errors on anything else with a precise diagnostic. Sprint 19 grows this //! into the full `ld`-compatible surface described in `.docs/sprints/sprint19.md`. use std::path::PathBuf; use crate::{LinkOptions, OutputKind}; #[derive(Debug)] pub enum ArgsError { /// A flag that takes an argument was supplied without one. MissingValue(String), /// An unrecognized flag. UnknownFlag(String), } impl std::fmt::Display for ArgsError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ArgsError::MissingValue(flag) => { write!(f, "flag `{flag}` requires a value") } ArgsError::UnknownFlag(flag) => { write!( f, "unknown flag `{flag}` (Sprint 19 adds the full `ld` surface)" ) } } } } pub fn parse(argv: &[String]) -> Result { let mut opts = LinkOptions::default(); let mut it = argv.iter(); while let Some(arg) = it.next() { match arg.as_str() { "-o" => { opts.output = Some(PathBuf::from( it.next() .ok_or_else(|| ArgsError::MissingValue("-o".into()))?, )); } "-e" => { opts.entry = Some( it.next() .ok_or_else(|| ArgsError::MissingValue("-e".into()))? .clone(), ); } "-arch" => { opts.arch = Some( it.next() .ok_or_else(|| ArgsError::MissingValue("-arch".into()))? .clone(), ); } "-x" => { opts.strip_locals = true; } "-dylib" => { opts.kind = OutputKind::Dylib; } "--dump" => { opts.dump = Some(PathBuf::from( it.next() .ok_or_else(|| ArgsError::MissingValue("--dump".into()))?, )); } "--dump-archive" => { opts.dump_archive = Some(PathBuf::from(it.next().ok_or_else(|| { ArgsError::MissingValue("--dump-archive".into()) })?)); } "--dump-dylib" => { opts.dump_dylib = Some(PathBuf::from(it.next().ok_or_else(|| { ArgsError::MissingValue("--dump-dylib".into()) })?)); } "--dump-tbd" => { opts.dump_tbd = Some(PathBuf::from(it.next().ok_or_else(|| { ArgsError::MissingValue("--dump-tbd".into()) })?)); } s if s.starts_with('-') => { return Err(ArgsError::UnknownFlag(s.to_string())); } _ => { opts.inputs.push(PathBuf::from(arg)); } } } Ok(opts) } #[cfg(test)] mod tests { use super::*; fn argv(words: &[&str]) -> Vec { words.iter().map(|s| s.to_string()).collect() } #[test] fn parse_output_and_entry() { let opts = parse(&argv(&["-o", "out", "-e", "_start", "a.o", "b.o"])).unwrap(); assert_eq!(opts.output.as_deref(), Some(std::path::Path::new("out"))); assert_eq!(opts.entry.as_deref(), Some("_start")); assert_eq!(opts.inputs.len(), 2); } #[test] fn dylib_flag_switches_output_kind() { let opts = parse(&argv(&["-dylib", "foo.o"])).unwrap(); assert_eq!(opts.kind, OutputKind::Dylib); } #[test] fn strip_locals_flag_is_recorded() { let opts = parse(&argv(&["-x", "foo.o"])).unwrap(); assert!(opts.strip_locals); } #[test] fn missing_output_value_errors() { let err = parse(&argv(&["-o"])).unwrap_err(); assert!(matches!(err, ArgsError::MissingValue(ref f) if f == "-o")); } #[test] fn unknown_flag_errors() { let err = parse(&argv(&["-nonsense"])).unwrap_err(); assert!(matches!(err, ArgsError::UnknownFlag(ref f) if f == "-nonsense")); } #[test] fn empty_argv_is_ok_with_no_inputs() { let opts = parse(&[]).unwrap(); assert!(opts.inputs.is_empty()); } #[test] fn dump_flag_captures_path() { let opts = parse(&argv(&["--dump", "some.o"])).unwrap(); assert_eq!(opts.dump.as_deref(), Some(std::path::Path::new("some.o"))); } #[test] fn dump_flag_without_value_errors() { let err = parse(&argv(&["--dump"])).unwrap_err(); assert!(matches!(err, ArgsError::MissingValue(ref f) if f == "--dump")); } #[test] fn dump_archive_flag_captures_path() { let opts = parse(&argv(&["--dump-archive", "libfoo.a"])).unwrap(); assert_eq!( opts.dump_archive.as_deref(), Some(std::path::Path::new("libfoo.a")) ); } }