Rust · 5252 bytes Raw Blame History
1 //! Hand-rolled CLI parser. No clap.
2 //!
3 //! Sprint 0 recognizes a minimal set (`-o`, `-e`, `-arch`, positional inputs)
4 //! and errors on anything else with a precise diagnostic. Sprint 19 grows this
5 //! into the full `ld`-compatible surface described in `.docs/sprints/sprint19.md`.
6
7 use std::path::PathBuf;
8
9 use crate::{LinkOptions, OutputKind};
10
11 #[derive(Debug)]
12 pub enum ArgsError {
13 /// A flag that takes an argument was supplied without one.
14 MissingValue(String),
15 /// An unrecognized flag.
16 UnknownFlag(String),
17 }
18
19 impl std::fmt::Display for ArgsError {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 ArgsError::MissingValue(flag) => {
23 write!(f, "flag `{flag}` requires a value")
24 }
25 ArgsError::UnknownFlag(flag) => {
26 write!(
27 f,
28 "unknown flag `{flag}` (Sprint 19 adds the full `ld` surface)"
29 )
30 }
31 }
32 }
33 }
34
35 pub fn parse(argv: &[String]) -> Result<LinkOptions, ArgsError> {
36 let mut opts = LinkOptions::default();
37 let mut it = argv.iter();
38 while let Some(arg) = it.next() {
39 match arg.as_str() {
40 "-o" => {
41 opts.output = Some(PathBuf::from(
42 it.next()
43 .ok_or_else(|| ArgsError::MissingValue("-o".into()))?,
44 ));
45 }
46 "-e" => {
47 opts.entry = Some(
48 it.next()
49 .ok_or_else(|| ArgsError::MissingValue("-e".into()))?
50 .clone(),
51 );
52 }
53 "-arch" => {
54 opts.arch = Some(
55 it.next()
56 .ok_or_else(|| ArgsError::MissingValue("-arch".into()))?
57 .clone(),
58 );
59 }
60 "-x" => {
61 opts.strip_locals = true;
62 }
63 "-dylib" => {
64 opts.kind = OutputKind::Dylib;
65 }
66 "--dump" => {
67 opts.dump = Some(PathBuf::from(
68 it.next()
69 .ok_or_else(|| ArgsError::MissingValue("--dump".into()))?,
70 ));
71 }
72 "--dump-archive" => {
73 opts.dump_archive =
74 Some(PathBuf::from(it.next().ok_or_else(|| {
75 ArgsError::MissingValue("--dump-archive".into())
76 })?));
77 }
78 "--dump-dylib" => {
79 opts.dump_dylib =
80 Some(PathBuf::from(it.next().ok_or_else(|| {
81 ArgsError::MissingValue("--dump-dylib".into())
82 })?));
83 }
84 "--dump-tbd" => {
85 opts.dump_tbd =
86 Some(PathBuf::from(it.next().ok_or_else(|| {
87 ArgsError::MissingValue("--dump-tbd".into())
88 })?));
89 }
90 s if s.starts_with('-') => {
91 return Err(ArgsError::UnknownFlag(s.to_string()));
92 }
93 _ => {
94 opts.inputs.push(PathBuf::from(arg));
95 }
96 }
97 }
98 Ok(opts)
99 }
100
101 #[cfg(test)]
102 mod tests {
103 use super::*;
104
105 fn argv(words: &[&str]) -> Vec<String> {
106 words.iter().map(|s| s.to_string()).collect()
107 }
108
109 #[test]
110 fn parse_output_and_entry() {
111 let opts = parse(&argv(&["-o", "out", "-e", "_start", "a.o", "b.o"])).unwrap();
112 assert_eq!(opts.output.as_deref(), Some(std::path::Path::new("out")));
113 assert_eq!(opts.entry.as_deref(), Some("_start"));
114 assert_eq!(opts.inputs.len(), 2);
115 }
116
117 #[test]
118 fn dylib_flag_switches_output_kind() {
119 let opts = parse(&argv(&["-dylib", "foo.o"])).unwrap();
120 assert_eq!(opts.kind, OutputKind::Dylib);
121 }
122
123 #[test]
124 fn strip_locals_flag_is_recorded() {
125 let opts = parse(&argv(&["-x", "foo.o"])).unwrap();
126 assert!(opts.strip_locals);
127 }
128
129 #[test]
130 fn missing_output_value_errors() {
131 let err = parse(&argv(&["-o"])).unwrap_err();
132 assert!(matches!(err, ArgsError::MissingValue(ref f) if f == "-o"));
133 }
134
135 #[test]
136 fn unknown_flag_errors() {
137 let err = parse(&argv(&["-nonsense"])).unwrap_err();
138 assert!(matches!(err, ArgsError::UnknownFlag(ref f) if f == "-nonsense"));
139 }
140
141 #[test]
142 fn empty_argv_is_ok_with_no_inputs() {
143 let opts = parse(&[]).unwrap();
144 assert!(opts.inputs.is_empty());
145 }
146
147 #[test]
148 fn dump_flag_captures_path() {
149 let opts = parse(&argv(&["--dump", "some.o"])).unwrap();
150 assert_eq!(opts.dump.as_deref(), Some(std::path::Path::new("some.o")));
151 }
152
153 #[test]
154 fn dump_flag_without_value_errors() {
155 let err = parse(&argv(&["--dump"])).unwrap_err();
156 assert!(matches!(err, ArgsError::MissingValue(ref f) if f == "--dump"));
157 }
158
159 #[test]
160 fn dump_archive_flag_captures_path() {
161 let opts = parse(&argv(&["--dump-archive", "libfoo.a"])).unwrap();
162 assert_eq!(
163 opts.dump_archive.as_deref(),
164 Some(std::path::Path::new("libfoo.a"))
165 );
166 }
167 }
168