Rust · 26200 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::resolve::{levenshtein, UndefinedTreatment};
10 use crate::{FrameworkSpec, IcfMode, LinkOptions, OutputKind, PlatformVersion};
11
12 const KNOWN_FLAGS: &[&str] = &[
13 "-o",
14 "-e",
15 "-arch",
16 "-l",
17 "-L",
18 "-framework",
19 "-weak_framework",
20 "-ObjC",
21 "-syslibroot",
22 "-platform_version",
23 "-r",
24 "-bundle",
25 "-undefined",
26 "-rpath",
27 "-install_name",
28 "-current_version",
29 "-compatibility_version",
30 "-exported_symbols_list",
31 "-unexported_symbols_list",
32 "-exported_symbol",
33 "-unexported_symbol",
34 "-S",
35 "-no_uuid",
36 "-dead_strip",
37 "-icf=safe",
38 "-icf=none",
39 "-fixup_chains",
40 "-no_fixup_chains",
41 "-map",
42 "-why_live",
43 "-t",
44 "-trace",
45 "-v",
46 "--version",
47 "-h",
48 "--help",
49 "-x",
50 "-dylib",
51 "-all_load",
52 "-force_load",
53 "--dump",
54 "--dump-archive",
55 "--dump-dylib",
56 "--dump-tbd",
57 ];
58
59 #[derive(Debug)]
60 pub enum ArgsError {
61 /// A flag that takes an argument was supplied without one.
62 MissingValue(String),
63 /// A recognized flag got a value we do not accept.
64 InvalidValue {
65 flag: String,
66 value: String,
67 expected: String,
68 },
69 /// An unrecognized flag.
70 UnknownFlag {
71 flag: String,
72 suggestion: Option<String>,
73 },
74 }
75
76 impl std::fmt::Display for ArgsError {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 ArgsError::MissingValue(flag) => {
80 write!(f, "flag `{flag}` requires a value")
81 }
82 ArgsError::InvalidValue {
83 flag,
84 value,
85 expected,
86 } => {
87 write!(
88 f,
89 "flag `{flag}` got invalid value `{value}` (expected {expected})"
90 )
91 }
92 ArgsError::UnknownFlag { flag, suggestion } => {
93 write!(f, "unknown flag `{flag}`")?;
94 if let Some(suggestion) = suggestion {
95 write!(f, " (did you mean `{suggestion}`?)")?;
96 }
97 write!(f, " (Sprint 19 adds the full `ld` surface)")
98 }
99 }
100 }
101 }
102
103 fn unknown_flag(flag: &str) -> ArgsError {
104 let suggestion = KNOWN_FLAGS
105 .iter()
106 .map(|candidate| (levenshtein(flag, candidate), *candidate))
107 .filter(|(distance, _)| *distance <= 3)
108 .min_by_key(|(distance, candidate)| (*distance, candidate.len()))
109 .map(|(_, candidate)| candidate.to_string());
110 ArgsError::UnknownFlag {
111 flag: flag.to_string(),
112 suggestion,
113 }
114 }
115
116 fn parse_version_component(flag: &str, value: &str) -> Result<u32, ArgsError> {
117 let mut parts = value.split('.');
118 let parse_part = |piece: Option<&str>| -> Result<u32, ArgsError> {
119 let raw = piece.unwrap_or("0");
120 raw.parse::<u32>().map_err(|_| ArgsError::InvalidValue {
121 flag: flag.to_string(),
122 value: value.to_string(),
123 expected: "version like <major>[.<minor>[.<patch>]]".into(),
124 })
125 };
126 let major = parse_part(parts.next())?;
127 let minor = parse_part(parts.next())?;
128 let patch = parse_part(parts.next())?;
129 if parts.next().is_some() {
130 return Err(ArgsError::InvalidValue {
131 flag: flag.to_string(),
132 value: value.to_string(),
133 expected: "version like <major>[.<minor>[.<patch>]]".into(),
134 });
135 }
136 Ok((major << 16) | ((minor & 0xff) << 8) | (patch & 0xff))
137 }
138
139 pub fn parse(argv: &[String]) -> Result<LinkOptions, ArgsError> {
140 let normalized = normalize_wl(argv);
141 let mut opts = LinkOptions::default();
142 let mut it = normalized.iter();
143 while let Some(arg) = it.next() {
144 match arg.as_str() {
145 "-o" => {
146 opts.output = Some(PathBuf::from(
147 it.next()
148 .ok_or_else(|| ArgsError::MissingValue("-o".into()))?,
149 ));
150 }
151 "-e" => {
152 opts.entry = Some(
153 it.next()
154 .ok_or_else(|| ArgsError::MissingValue("-e".into()))?
155 .clone(),
156 );
157 }
158 "-arch" => {
159 opts.arch = Some(
160 it.next()
161 .ok_or_else(|| ArgsError::MissingValue("-arch".into()))?
162 .clone(),
163 );
164 }
165 "-l" => {
166 opts.library_names.push(
167 it.next()
168 .ok_or_else(|| ArgsError::MissingValue("-l".into()))?
169 .clone(),
170 );
171 }
172 s if s.starts_with("-l") && s.len() > 2 => {
173 opts.library_names.push(s[2..].to_string());
174 }
175 "-L" => {
176 opts.search_paths.push(PathBuf::from(
177 it.next()
178 .ok_or_else(|| ArgsError::MissingValue("-L".into()))?,
179 ));
180 }
181 "-framework" => {
182 opts.frameworks.push(FrameworkSpec {
183 name: it
184 .next()
185 .ok_or_else(|| ArgsError::MissingValue("-framework".into()))?
186 .clone(),
187 weak: false,
188 });
189 }
190 "-weak_framework" => {
191 opts.frameworks.push(FrameworkSpec {
192 name: it
193 .next()
194 .ok_or_else(|| ArgsError::MissingValue("-weak_framework".into()))?
195 .clone(),
196 weak: true,
197 });
198 }
199 "-ObjC" => {
200 opts.objc_force_load = true;
201 }
202 "-syslibroot" => {
203 opts.syslibroot =
204 Some(PathBuf::from(it.next().ok_or_else(|| {
205 ArgsError::MissingValue("-syslibroot".into())
206 })?));
207 }
208 "-platform_version" => {
209 let platform = it
210 .next()
211 .ok_or_else(|| ArgsError::MissingValue("-platform_version".into()))?;
212 if platform != "macos" {
213 return Err(ArgsError::InvalidValue {
214 flag: "-platform_version".into(),
215 value: platform.clone(),
216 expected: "platform `macos`".into(),
217 });
218 }
219 let minos_raw = it
220 .next()
221 .ok_or_else(|| ArgsError::MissingValue("-platform_version".into()))?;
222 let sdk_raw = it
223 .next()
224 .ok_or_else(|| ArgsError::MissingValue("-platform_version".into()))?;
225 opts.platform_version = Some(PlatformVersion {
226 minos: parse_version_component("-platform_version", minos_raw)?,
227 sdk: parse_version_component("-platform_version", sdk_raw)?,
228 });
229 }
230 "-r" => {
231 opts.relocatable = true;
232 }
233 "-bundle" => {
234 opts.bundle = true;
235 }
236 "-undefined" => {
237 let value = it
238 .next()
239 .ok_or_else(|| ArgsError::MissingValue("-undefined".into()))?;
240 opts.undefined_treatment = match value.as_str() {
241 "error" => UndefinedTreatment::Error,
242 "warning" => UndefinedTreatment::Warning,
243 "suppress" => UndefinedTreatment::Suppress,
244 "dynamic_lookup" => UndefinedTreatment::DynamicLookup,
245 _ => {
246 return Err(ArgsError::InvalidValue {
247 flag: "-undefined".into(),
248 value: value.clone(),
249 expected: "`error`, `warning`, `suppress`, or `dynamic_lookup`"
250 .into(),
251 });
252 }
253 };
254 }
255 "-rpath" => {
256 opts.rpaths.push(
257 it.next()
258 .ok_or_else(|| ArgsError::MissingValue("-rpath".into()))?
259 .clone(),
260 );
261 }
262 "-install_name" => {
263 opts.install_name = Some(
264 it.next()
265 .ok_or_else(|| ArgsError::MissingValue("-install_name".into()))?
266 .clone(),
267 );
268 }
269 "-current_version" => {
270 let value = it
271 .next()
272 .ok_or_else(|| ArgsError::MissingValue("-current_version".into()))?;
273 opts.current_version = Some(parse_version_component("-current_version", value)?);
274 }
275 "-compatibility_version" => {
276 let value = it
277 .next()
278 .ok_or_else(|| ArgsError::MissingValue("-compatibility_version".into()))?;
279 opts.compatibility_version =
280 Some(parse_version_component("-compatibility_version", value)?);
281 }
282 "-exported_symbols_list" => {
283 opts.exported_symbols_lists
284 .push(PathBuf::from(it.next().ok_or_else(|| {
285 ArgsError::MissingValue("-exported_symbols_list".into())
286 })?));
287 }
288 "-unexported_symbols_list" => {
289 opts.unexported_symbols_lists.push(PathBuf::from(
290 it.next().ok_or_else(|| {
291 ArgsError::MissingValue("-unexported_symbols_list".into())
292 })?,
293 ));
294 }
295 "-exported_symbol" => {
296 opts.exported_symbols.push(
297 it.next()
298 .ok_or_else(|| ArgsError::MissingValue("-exported_symbol".into()))?
299 .clone(),
300 );
301 }
302 "-unexported_symbol" => {
303 opts.unexported_symbols.push(
304 it.next()
305 .ok_or_else(|| ArgsError::MissingValue("-unexported_symbol".into()))?
306 .clone(),
307 );
308 }
309 "-S" => {
310 opts.strip_debug = true;
311 }
312 "-no_uuid" => {
313 opts.emit_uuid = false;
314 }
315 "-dead_strip" => {
316 opts.dead_strip = true;
317 }
318 s if s.starts_with("-icf=") => {
319 opts.icf_mode = match s {
320 "-icf=none" => IcfMode::None,
321 "-icf=safe" => IcfMode::Safe,
322 _ => {
323 let value = s.trim_start_matches("-icf=").to_string();
324 return Err(ArgsError::InvalidValue {
325 flag: "-icf".into(),
326 value,
327 expected: "`safe` or `none`".into(),
328 });
329 }
330 };
331 }
332 "-fixup_chains" => {
333 opts.fixup_chains = true;
334 }
335 "-no_fixup_chains" => {
336 opts.fixup_chains = false;
337 }
338 "-map" => {
339 opts.map = Some(PathBuf::from(
340 it.next()
341 .ok_or_else(|| ArgsError::MissingValue("-map".into()))?,
342 ));
343 }
344 "-why_live" => {
345 opts.why_live.push(
346 it.next()
347 .ok_or_else(|| ArgsError::MissingValue("-why_live".into()))?
348 .clone(),
349 );
350 }
351 "-t" | "-trace" => {
352 opts.trace_inputs = true;
353 }
354 "-v" | "--version" => {
355 opts.show_version = true;
356 }
357 "-h" | "--help" => {
358 opts.show_help = true;
359 }
360 "-x" => {
361 opts.strip_locals = true;
362 }
363 "-dylib" => {
364 opts.kind = OutputKind::Dylib;
365 }
366 "-all_load" => {
367 opts.all_load = true;
368 }
369 "-force_load" => {
370 opts.force_load_archives
371 .push(PathBuf::from(it.next().ok_or_else(|| {
372 ArgsError::MissingValue("-force_load".into())
373 })?));
374 }
375 "--dump" => {
376 opts.dump = Some(PathBuf::from(
377 it.next()
378 .ok_or_else(|| ArgsError::MissingValue("--dump".into()))?,
379 ));
380 }
381 "--dump-archive" => {
382 opts.dump_archive =
383 Some(PathBuf::from(it.next().ok_or_else(|| {
384 ArgsError::MissingValue("--dump-archive".into())
385 })?));
386 }
387 "--dump-dylib" => {
388 opts.dump_dylib =
389 Some(PathBuf::from(it.next().ok_or_else(|| {
390 ArgsError::MissingValue("--dump-dylib".into())
391 })?));
392 }
393 "--dump-tbd" => {
394 opts.dump_tbd =
395 Some(PathBuf::from(it.next().ok_or_else(|| {
396 ArgsError::MissingValue("--dump-tbd".into())
397 })?));
398 }
399 s if s.starts_with('-') => {
400 return Err(unknown_flag(s));
401 }
402 _ => {
403 opts.inputs.push(PathBuf::from(arg));
404 }
405 }
406 }
407 Ok(opts)
408 }
409
410 fn normalize_wl(argv: &[String]) -> Vec<String> {
411 let mut out = Vec::with_capacity(argv.len());
412 for arg in argv {
413 if let Some(rest) = arg.strip_prefix("-Wl,") {
414 out.extend(
415 rest.split(',')
416 .filter(|piece| !piece.is_empty())
417 .map(ToString::to_string),
418 );
419 } else {
420 out.push(arg.clone());
421 }
422 }
423 out
424 }
425
426 #[cfg(test)]
427 mod tests {
428 use super::*;
429
430 fn argv(words: &[&str]) -> Vec<String> {
431 words.iter().map(|s| s.to_string()).collect()
432 }
433
434 #[test]
435 fn parse_output_and_entry() {
436 let opts = parse(&argv(&["-o", "out", "-e", "_start", "a.o", "b.o"])).unwrap();
437 assert_eq!(opts.output.as_deref(), Some(std::path::Path::new("out")));
438 assert_eq!(opts.entry.as_deref(), Some("_start"));
439 assert_eq!(opts.inputs.len(), 2);
440 }
441
442 #[test]
443 fn dylib_flag_switches_output_kind() {
444 let opts = parse(&argv(&["-dylib", "foo.o"])).unwrap();
445 assert_eq!(opts.kind, OutputKind::Dylib);
446 }
447
448 #[test]
449 fn deferred_output_flags_are_recorded() {
450 let opts = parse(&argv(&["-r", "-bundle", "foo.o"])).unwrap();
451 assert!(opts.relocatable);
452 assert!(opts.bundle);
453 }
454
455 #[test]
456 fn strip_locals_flag_is_recorded() {
457 let opts = parse(&argv(&["-x", "foo.o"])).unwrap();
458 assert!(opts.strip_locals);
459 }
460
461 #[test]
462 fn strip_debug_and_uuid_flags_are_recorded() {
463 let opts = parse(&argv(&["-S", "-no_uuid", "foo.o"])).unwrap();
464 assert!(opts.strip_debug);
465 assert!(!opts.emit_uuid);
466 }
467
468 #[test]
469 fn dead_strip_icf_and_fixup_chain_flags_are_recorded() {
470 let opts = parse(&argv(&[
471 "-dead_strip",
472 "-icf=safe",
473 "-fixup_chains",
474 "-no_fixup_chains",
475 "-icf=none",
476 "foo.o",
477 ]))
478 .unwrap();
479 assert!(opts.dead_strip);
480 assert_eq!(opts.icf_mode, IcfMode::None);
481 assert!(!opts.fixup_chains);
482 }
483
484 #[test]
485 fn icf_flag_rejects_unknown_modes() {
486 let err = parse(&argv(&["-icf=aggressive", "main.o"])).unwrap_err();
487 assert!(matches!(
488 err,
489 ArgsError::InvalidValue {
490 ref flag,
491 ref value,
492 ..
493 } if flag == "-icf" && value == "aggressive"
494 ));
495 }
496
497 #[test]
498 fn l_flag_accepts_separate_value() {
499 let opts = parse(&argv(&["-l", "System", "main.o"])).unwrap();
500 assert_eq!(opts.library_names, vec!["System".to_string()]);
501 assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
502 }
503
504 #[test]
505 fn l_flag_accepts_joined_value() {
506 let opts = parse(&argv(&["-lSystem", "main.o"])).unwrap();
507 assert_eq!(opts.library_names, vec!["System".to_string()]);
508 assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
509 }
510
511 #[test]
512 fn search_path_and_syslibroot_flags_are_recorded() {
513 let opts = parse(&argv(&["-L", "/tmp/lib", "-syslibroot", "/sdk", "main.o"])).unwrap();
514 assert_eq!(opts.search_paths, vec![PathBuf::from("/tmp/lib")]);
515 assert_eq!(opts.syslibroot, Some(PathBuf::from("/sdk")));
516 }
517
518 #[test]
519 fn framework_flags_are_recorded_in_order() {
520 let opts = parse(&argv(&[
521 "-framework",
522 "Foundation",
523 "-weak_framework",
524 "Metal",
525 "main.o",
526 ]))
527 .unwrap();
528 assert_eq!(
529 opts.frameworks,
530 vec![
531 FrameworkSpec {
532 name: "Foundation".into(),
533 weak: false,
534 },
535 FrameworkSpec {
536 name: "Metal".into(),
537 weak: true,
538 }
539 ]
540 );
541 assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
542 }
543
544 #[test]
545 fn objc_flag_is_recorded() {
546 let opts = parse(&argv(&["-ObjC", "main.o"])).unwrap();
547 assert!(opts.objc_force_load);
548 }
549
550 #[test]
551 fn platform_version_flag_is_recorded() {
552 let opts = parse(&argv(&[
553 "-platform_version",
554 "macos",
555 "13.2.1",
556 "14.5",
557 "main.o",
558 ]))
559 .unwrap();
560 let platform = opts.platform_version.expect("platform version");
561 assert_eq!(platform.minos, (13 << 16) | (2 << 8) | 1);
562 assert_eq!(platform.sdk, (14 << 16) | (5 << 8));
563 }
564
565 #[test]
566 fn platform_version_rejects_non_macos_platform() {
567 let err = parse(&argv(&["-platform_version", "ios", "13.0", "13.0"])).unwrap_err();
568 assert!(matches!(
569 err,
570 ArgsError::InvalidValue {
571 ref flag,
572 ref value,
573 ..
574 } if flag == "-platform_version" && value == "ios"
575 ));
576 }
577
578 #[test]
579 fn platform_version_rejects_bad_version() {
580 let err = parse(&argv(&["-platform_version", "macos", "13.bad", "14.0"])).unwrap_err();
581 assert!(matches!(
582 err,
583 ArgsError::InvalidValue {
584 ref flag,
585 ref value,
586 ..
587 } if flag == "-platform_version" && value == "13.bad"
588 ));
589 }
590
591 #[test]
592 fn undefined_flag_records_dynamic_lookup() {
593 let opts = parse(&argv(&["-undefined", "dynamic_lookup", "main.o"])).unwrap();
594 assert_eq!(opts.undefined_treatment, UndefinedTreatment::DynamicLookup);
595 }
596
597 #[test]
598 fn undefined_flag_records_warning_and_suppress() {
599 let warning = parse(&argv(&["-undefined", "warning", "main.o"])).unwrap();
600 assert_eq!(warning.undefined_treatment, UndefinedTreatment::Warning);
601
602 let suppress = parse(&argv(&["-undefined", "suppress", "main.o"])).unwrap();
603 assert_eq!(suppress.undefined_treatment, UndefinedTreatment::Suppress);
604 }
605
606 #[test]
607 fn undefined_flag_rejects_unknown_modes() {
608 let err = parse(&argv(&["-undefined", "bogus", "main.o"])).unwrap_err();
609 assert!(matches!(
610 err,
611 ArgsError::InvalidValue {
612 ref flag,
613 ref value,
614 ..
615 } if flag == "-undefined" && value == "bogus"
616 ));
617 }
618
619 #[test]
620 fn dylib_metadata_flags_are_recorded() {
621 let opts = parse(&argv(&[
622 "-rpath",
623 "@loader_path/../lib",
624 "-install_name",
625 "@rpath/libdemo.dylib",
626 "-current_version",
627 "2.3.4",
628 "-compatibility_version",
629 "1.2",
630 "main.o",
631 ]))
632 .unwrap();
633 assert_eq!(opts.rpaths, vec!["@loader_path/../lib".to_string()]);
634 assert_eq!(opts.install_name.as_deref(), Some("@rpath/libdemo.dylib"));
635 assert_eq!(opts.current_version, Some((2 << 16) | (3 << 8) | 4));
636 assert_eq!(opts.compatibility_version, Some((1 << 16) | (2 << 8)));
637 }
638
639 #[test]
640 fn export_visibility_flags_are_recorded() {
641 let opts = parse(&argv(&[
642 "-exported_symbols_list",
643 "exports.txt",
644 "-unexported_symbols_list",
645 "hidden.txt",
646 "-exported_symbol",
647 "_keep",
648 "-unexported_symbol",
649 "_drop",
650 "main.o",
651 ]))
652 .unwrap();
653 assert_eq!(
654 opts.exported_symbols_lists,
655 vec![PathBuf::from("exports.txt")]
656 );
657 assert_eq!(
658 opts.unexported_symbols_lists,
659 vec![PathBuf::from("hidden.txt")]
660 );
661 assert_eq!(opts.exported_symbols, vec!["_keep".to_string()]);
662 assert_eq!(opts.unexported_symbols, vec!["_drop".to_string()]);
663 }
664
665 #[test]
666 fn map_flag_is_recorded() {
667 let opts = parse(&argv(&["-map", "link.map", "main.o"])).unwrap();
668 assert_eq!(opts.map.as_deref(), Some(std::path::Path::new("link.map")));
669 }
670
671 #[test]
672 fn wl_normalizes_map_like_direct_flag() {
673 let opts = parse(&argv(&["-Wl,-map,link.map", "main.o"])).unwrap();
674 assert_eq!(opts.map.as_deref(), Some(std::path::Path::new("link.map")));
675 assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
676 }
677
678 #[test]
679 fn trace_flags_are_recorded() {
680 let opts = parse(&argv(&["-trace", "main.o"])).unwrap();
681 assert!(opts.trace_inputs);
682 let opts = parse(&argv(&["-t", "main.o"])).unwrap();
683 assert!(opts.trace_inputs);
684 }
685
686 #[test]
687 fn why_live_flag_accumulates_symbols() {
688 let opts = parse(&argv(&[
689 "-why_live",
690 "_helper",
691 "-why_live",
692 "_leaf",
693 "main.o",
694 ]))
695 .unwrap();
696 assert_eq!(
697 opts.why_live,
698 vec!["_helper".to_string(), "_leaf".to_string()]
699 );
700 }
701
702 #[test]
703 fn help_and_version_flags_are_recorded() {
704 let opts = parse(&argv(&["--help"])).unwrap();
705 assert!(opts.show_help);
706 let opts = parse(&argv(&["-h"])).unwrap();
707 assert!(opts.show_help);
708 let opts = parse(&argv(&["--version"])).unwrap();
709 assert!(opts.show_version);
710 let opts = parse(&argv(&["-v"])).unwrap();
711 assert!(opts.show_version);
712 }
713
714 #[test]
715 fn all_load_flag_is_recorded() {
716 let opts = parse(&argv(&["-all_load", "libfoo.a"])).unwrap();
717 assert!(opts.all_load);
718 assert_eq!(opts.inputs, vec![PathBuf::from("libfoo.a")]);
719 }
720
721 #[test]
722 fn force_load_flag_accumulates_archive_paths() {
723 let opts = parse(&argv(&[
724 "-force_load",
725 "liba.a",
726 "-force_load",
727 "libb.a",
728 "main.o",
729 ]))
730 .unwrap();
731 assert_eq!(
732 opts.force_load_archives,
733 vec![PathBuf::from("liba.a"), PathBuf::from("libb.a")]
734 );
735 assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
736 }
737
738 #[test]
739 fn missing_force_load_value_errors() {
740 let err = parse(&argv(&["-force_load"])).unwrap_err();
741 assert!(matches!(err, ArgsError::MissingValue(ref f) if f == "-force_load"));
742 }
743
744 #[test]
745 fn missing_l_value_errors() {
746 let err = parse(&argv(&["-l"])).unwrap_err();
747 assert!(matches!(err, ArgsError::MissingValue(ref f) if f == "-l"));
748 }
749
750 #[test]
751 fn missing_output_value_errors() {
752 let err = parse(&argv(&["-o"])).unwrap_err();
753 assert!(matches!(err, ArgsError::MissingValue(ref f) if f == "-o"));
754 }
755
756 #[test]
757 fn unknown_flag_errors() {
758 let err = parse(&argv(&["-nonsense"])).unwrap_err();
759 assert!(matches!(
760 err,
761 ArgsError::UnknownFlag {
762 ref flag,
763 suggestion: None
764 } if flag == "-nonsense"
765 ));
766 }
767
768 #[test]
769 fn unknown_flag_suggests_nearby_match() {
770 let err = parse(&argv(&["-all_lod"])).unwrap_err();
771 assert!(matches!(
772 err,
773 ArgsError::UnknownFlag {
774 ref flag,
775 suggestion: Some(ref suggestion)
776 } if flag == "-all_lod" && suggestion == "-all_load"
777 ));
778 }
779
780 #[test]
781 fn empty_argv_is_ok_with_no_inputs() {
782 let opts = parse(&[]).unwrap();
783 assert!(opts.inputs.is_empty());
784 }
785
786 #[test]
787 fn dump_flag_captures_path() {
788 let opts = parse(&argv(&["--dump", "some.o"])).unwrap();
789 assert_eq!(opts.dump.as_deref(), Some(std::path::Path::new("some.o")));
790 }
791
792 #[test]
793 fn dump_flag_without_value_errors() {
794 let err = parse(&argv(&["--dump"])).unwrap_err();
795 assert!(matches!(err, ArgsError::MissingValue(ref f) if f == "--dump"));
796 }
797
798 #[test]
799 fn dump_archive_flag_captures_path() {
800 let opts = parse(&argv(&["--dump-archive", "libfoo.a"])).unwrap();
801 assert_eq!(
802 opts.dump_archive.as_deref(),
803 Some(std::path::Path::new("libfoo.a"))
804 );
805 }
806 }
807