1use super::Command;
20use std::iter::Peekable;
21use thiserror::Error;
22use yash_env::option::FromStrError::*;
23use yash_env::option::State;
24use yash_env::option::canonicalize;
25use yash_env::option::parse_long;
26use yash_env::option::parse_short;
27use yash_env::semantics::Field;
28use yash_env::source::pretty::Snippet;
29use yash_env::source::pretty::{Report, ReportType};
30
31#[derive(Clone, Debug, Eq, Error, PartialEq)]
33pub enum Error {
34 #[error("unknown option {0:?}")]
36 UnknownShortOption(char, Field),
37
38 #[error("unknown option {:?}", .0.value)]
40 UnknownLongOption(Field),
41
42 #[error("ambiguous option name {:?}", .0.value)]
44 AmbiguousLongOption(Field),
45
46 #[error("option {:?} missing an argument", .0.value)]
48 MissingOptionArgument(Field),
49
50 #[error("option {0:?} not modifiable by the set built-in")]
52 UnmodifiableShortOption(char, Field),
53
54 #[error("option {:?} not modifiable by the set built-in", .0.value)]
56 UnmodifiableLongOption(Field),
57}
58
59impl Error {
60 pub fn field(&self) -> &Field {
62 match self {
63 Error::UnknownShortOption(_char, field) => field,
64 Error::UnknownLongOption(field) => field,
65 Error::AmbiguousLongOption(field) => field,
66 Error::MissingOptionArgument(field) => field,
67 Error::UnmodifiableShortOption(_char, field) => field,
68 Error::UnmodifiableLongOption(field) => field,
69 }
70 }
71
72 #[must_use]
74 pub fn to_report(&self) -> Report<'_> {
75 let mut report = Report::new();
76 report.r#type = ReportType::Error;
77 report.title = self.to_string().into();
78
79 let field = self.field();
80 report.snippets = Snippet::with_primary_span(&field.origin, field.value.as_str().into());
81
82 report
83 }
84}
85
86impl<'a> From<&'a Error> for Report<'a> {
87 #[inline]
88 fn from(value: &'a Error) -> Self {
89 value.to_report()
90 }
91}
92
93fn try_parse_short<I: Iterator<Item = Field>>(
98 args: &mut Peekable<I>,
99 option_occurrences: &mut Vec<(yash_env::option::Option, State)>,
100) -> Result<bool, Error> {
101 let field = match args.peek() {
102 Some(field) => field,
103 None => return Ok(false),
104 };
105
106 let mut chars = field.value.chars();
107 let negate = match chars.next() {
108 Some('-') => false,
109 Some('+') => true,
110 _ => return Ok(false),
111 };
112 match chars.next() {
113 Some('-') if !negate => return Ok(false),
114 Some('+') if negate => return Ok(false),
115 None => return Ok(false),
116 _ => (),
117 }
118
119 let mut field = args.next().unwrap();
120 let mut chars = field.value.chars();
121 chars.next().unwrap();
122 while let Some(c) = chars.next() {
123 if c == 'o' {
124 let name = chars.as_str();
125 let name = if !name.is_empty() {
126 canonicalize(name)
127 } else {
128 let prev = field;
129 field = args.next().ok_or(Error::MissingOptionArgument(prev))?;
130 canonicalize(&field.value)
131 };
132 match parse_long(&name) {
133 Ok((option, state)) if option.is_modifiable() => {
134 option_occurrences.push((option, if negate { !state } else { state }));
135 break;
136 }
137 Ok(_) => return Err(Error::UnmodifiableLongOption(field)),
138 Err(NoSuchOption) => return Err(Error::UnknownLongOption(field)),
139 Err(Ambiguous) => return Err(Error::AmbiguousLongOption(field)),
140 }
141 }
142
143 match parse_short(c) {
144 Some((option, state)) if option.is_modifiable() => {
145 option_occurrences.push((option, if negate { !state } else { state }))
146 }
147 Some(_) => return Err(Error::UnmodifiableShortOption(c, field)),
148 None => return Err(Error::UnknownShortOption(c, field)),
149 }
150 }
151 Ok(true)
152}
153
154fn try_parse_long<I: Iterator<Item = Field>>(
156 args: &mut Peekable<I>,
157) -> Result<std::option::Option<(yash_env::option::Option, State)>, Error> {
158 let field = match args.peek() {
159 Some(field) => field,
160 None => return Ok(None),
161 };
162
163 let (name, negate) = if let Some(name) = field.value.strip_prefix("--") {
164 if name.is_empty() {
165 return Ok(None);
166 }
167 (name, false)
168 } else if let Some(name) = field.value.strip_prefix("++") {
169 (name, true)
170 } else {
171 return Ok(None);
172 };
173
174 let name = canonicalize(name);
175 let result = parse_long(&name);
176 let field = args.next().unwrap();
177 match result {
178 Ok((option, state)) if option.is_modifiable() => {
179 Ok(Some((option, if negate { !state } else { state })))
180 }
181 Ok(_) => Err(Error::UnmodifiableLongOption(field)),
182 Err(NoSuchOption) => Err(Error::UnknownLongOption(field)),
183 Err(Ambiguous) => Err(Error::AmbiguousLongOption(field)),
184 }
185}
186
187pub fn parse(args: Vec<Field>) -> Result<Command, Error> {
189 match args.len() {
190 0 => return Ok(Command::PrintVariables),
191 1 => match args[0].value.as_str() {
192 "-o" => return Ok(Command::PrintOptionsHumanReadable),
193 "+o" => return Ok(Command::PrintOptionsMachineReadable),
194 _ => (),
195 },
196 _ => (),
197 }
198
199 let mut args = args.into_iter().peekable();
200 let mut options = Vec::new();
201 loop {
202 if try_parse_short(&mut args, &mut options)? {
203 continue;
204 }
205 if let Some(result) = try_parse_long(&mut args)? {
206 options.push(result);
207 } else {
208 break;
209 }
210 }
211
212 let separated = match args.peek().map(|arg| arg.value.as_str()) {
213 Some("--" | "-") => {
214 drop(args.next());
215 true
216 }
217 _ => false,
218 };
219
220 let positional_params = (separated || args.peek().is_some()).then(|| args.collect());
221
222 Ok(Command::Modify {
223 options,
224 positional_params,
225 })
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use assert_matches::assert_matches;
232 use yash_env::option::Option::*;
233 use yash_env::option::State::*;
234
235 #[test]
236 fn simple_cases() {
237 assert_eq!(parse(vec![]), Ok(Command::PrintVariables));
238 assert_eq!(
239 parse(Field::dummies(["-o"])),
240 Ok(Command::PrintOptionsHumanReadable)
241 );
242 assert_eq!(
243 parse(Field::dummies(["+o"])),
244 Ok(Command::PrintOptionsMachineReadable)
245 );
246 }
247
248 #[test]
249 fn positional_params_only() {
250 assert_matches!(
251 parse(Field::dummies(["foo"])),
252 Ok(Command::Modify {
253 options,
254 positional_params
255 }) => {
256 assert_eq!(options, []);
257 assert_matches!(positional_params.unwrap().as_slice(), [first] => {
258 assert_eq!(first.value, "foo");
259 });
260 }
261 );
262
263 assert_matches!(
264 parse(Field::dummies([""])),
265 Ok(Command::Modify {
266 options,
267 positional_params
268 }) => {
269 assert_eq!(options, []);
270 assert_matches!(positional_params.unwrap().as_slice(), [first] => {
271 assert_eq!(first.value, "");
272 });
273 }
274 );
275
276 assert_matches!(
277 parse(Field::dummies(["a", "b", "c"])),
278 Ok(Command::Modify {
279 options,
280 positional_params
281 }) => {
282 assert_eq!(options, []);
283 assert_matches!(positional_params.unwrap().as_slice(), [first, second, third] => {
284 assert_eq!(first.value, "a");
285 assert_eq!(second.value, "b");
286 assert_eq!(third.value, "c");
287 });
288 }
289 );
290 }
291
292 #[test]
293 fn double_hyphen_separator_and_positional_params() {
294 assert_matches!(
295 parse(Field::dummies(["--"])),
296 Ok(Command::Modify {
297 options,
298 positional_params
299 }) => {
300 assert_eq!(options, []);
301 assert_eq!(positional_params.unwrap().as_slice(), []);
302 }
303 );
304
305 assert_matches!(
306 parse(Field::dummies(["--", "foo", "bar"])),
307 Ok(Command::Modify {
308 options,
309 positional_params
310 }) => {
311 assert_eq!(options, []);
312 assert_matches!(positional_params.unwrap().as_slice(), [first, second] => {
313 assert_eq!(first.value, "foo");
314 assert_eq!(second.value, "bar");
315 });
316 }
317 );
318
319 assert_matches!(
320 parse(Field::dummies(["--", "--"])),
321 Ok(Command::Modify {
322 options,
323 positional_params
324 }) => {
325 assert_eq!(options, []);
326 assert_matches!(positional_params.unwrap().as_slice(), [first] => {
327 assert_eq!(first.value, "--");
328 });
329 }
330 );
331
332 assert_matches!(
333 parse(Field::dummies(["--", "-"])),
334 Ok(Command::Modify {
335 options,
336 positional_params
337 }) => {
338 assert_eq!(options, []);
339 assert_matches!(positional_params.unwrap().as_slice(), [first] => {
340 assert_eq!(first.value, "-");
341 });
342 }
343 );
344
345 assert_matches!(
346 parse(Field::dummies(["--", "-a"])),
347 Ok(Command::Modify {
348 options,
349 positional_params
350 }) => {
351 assert_eq!(options, []);
352 assert_matches!(positional_params.unwrap().as_slice(), [first] => {
353 assert_eq!(first.value, "-a");
354 });
355 }
356 );
357 }
358
359 #[test]
360 fn single_hyphen_separator_and_positional_params() {
361 assert_matches!(
362 parse(Field::dummies(["-"])),
363 Ok(Command::Modify {
364 options,
365 positional_params
366 }) => {
367 assert_eq!(options, []);
368 assert_eq!(positional_params.unwrap().as_slice(), []);
369 }
370 );
371
372 assert_matches!(
373 parse(Field::dummies(["-", "foo", "bar"])),
374 Ok(Command::Modify {
375 options,
376 positional_params
377 }) => {
378 assert_eq!(options, []);
379 assert_matches!(positional_params.unwrap().as_slice(), [first, second] => {
380 assert_eq!(first.value, "foo");
381 assert_eq!(second.value, "bar");
382 });
383 }
384 );
385
386 assert_matches!(
387 parse(Field::dummies(["-", "-"])),
388 Ok(Command::Modify {
389 options,
390 positional_params
391 }) => {
392 assert_eq!(options, []);
393 assert_matches!(positional_params.unwrap().as_slice(), [first] => {
394 assert_eq!(first.value, "-");
395 });
396 }
397 );
398
399 assert_matches!(
400 parse(Field::dummies(["-", "--"])),
401 Ok(Command::Modify {
402 options,
403 positional_params
404 }) => {
405 assert_eq!(options, []);
406 assert_matches!(positional_params.unwrap().as_slice(), [first] => {
407 assert_eq!(first.value, "--");
408 });
409 }
410 );
411
412 assert_matches!(
413 parse(Field::dummies(["-", "-a"])),
414 Ok(Command::Modify {
415 options,
416 positional_params
417 }) => {
418 assert_eq!(options, []);
419 assert_matches!(positional_params.unwrap().as_slice(), [first] => {
420 assert_eq!(first.value, "-a");
421 });
422 }
423 );
424 }
425
426 #[test]
427 fn short_options() {
428 assert_matches!(
429 parse(Field::dummies(["-a"])),
430 Ok(Command::Modify {
431 options,
432 positional_params
433 }) => {
434 assert_eq!(options, [(AllExport, On)]);
435 assert_eq!(positional_params, None);
436 }
437 );
438
439 assert_matches!(
440 parse(Field::dummies(["-uv"])),
441 Ok(Command::Modify {
442 options,
443 positional_params
444 }) => {
445 assert_eq!(options, [(Unset, Off), (Verbose, On)]);
446 assert_eq!(positional_params, None);
447 }
448 );
449
450 assert_matches!(
451 parse(Field::dummies(["-u", "-v"])),
452 Ok(Command::Modify {
453 options,
454 positional_params
455 }) => {
456 assert_eq!(options, [(Unset, Off), (Verbose, On)]);
457 assert_eq!(positional_params, None);
458 }
459 );
460 }
461
462 #[test]
463 fn negated_short_options() {
464 assert_matches!(
465 parse(Field::dummies(["+a"])),
466 Ok(Command::Modify {
467 options,
468 positional_params
469 }) => {
470 assert_eq!(options, [(AllExport, Off)]);
471 assert_eq!(positional_params, None);
472 }
473 );
474
475 assert_matches!(
476 parse(Field::dummies(["+uv"])),
477 Ok(Command::Modify {
478 options,
479 positional_params
480 }) => {
481 assert_eq!(options, [(Unset, On), (Verbose, Off)]);
482 assert_eq!(positional_params, None);
483 }
484 );
485
486 assert_matches!(
487 parse(Field::dummies(["+u", "-v"])),
488 Ok(Command::Modify {
489 options,
490 positional_params
491 }) => {
492 assert_eq!(options, [(Unset, On), (Verbose, On)]);
493 assert_eq!(positional_params, None);
494 }
495 );
496 }
497
498 #[test]
499 fn o_options() {
500 assert_matches!(
501 parse(Field::dummies(["-oallexpo"])),
502 Ok(Command::Modify {
503 options,
504 positional_params
505 }) => {
506 assert_eq!(options, [(AllExport, On)]);
507 assert_eq!(positional_params, None);
508 }
509 );
510
511 assert_matches!(
512 parse(Field::dummies(["-o all-Expo"])),
513 Ok(Command::Modify {
514 options,
515 positional_params
516 }) => {
517 assert_eq!(options, [(AllExport, On)]);
518 assert_eq!(positional_params, None);
519 }
520 );
521
522 assert_matches!(
523 parse(Field::dummies(["-onounset"])),
524 Ok(Command::Modify {
525 options,
526 positional_params
527 }) => {
528 assert_eq!(options, [(Unset, Off)]);
529 assert_eq!(positional_params, None);
530 }
531 );
532
533 assert_matches!(
534 parse(Field::dummies(["-o","NO_unset"])),
535 Ok(Command::Modify {
536 options,
537 positional_params
538 }) => {
539 assert_eq!(options, [(Unset, Off)]);
540 assert_eq!(positional_params, None);
541 }
542 );
543 }
544
545 #[test]
546 fn negated_o_options() {
547 assert_matches!(
548 parse(Field::dummies(["+oallexpo"])),
549 Ok(Command::Modify {
550 options,
551 positional_params
552 }) => {
553 assert_eq!(options, [(AllExport, Off)]);
554 assert_eq!(positional_params, None);
555 }
556 );
557
558 assert_matches!(
559 parse(Field::dummies(["+o all-Expo"])),
560 Ok(Command::Modify {
561 options,
562 positional_params
563 }) => {
564 assert_eq!(options, [(AllExport, Off)]);
565 assert_eq!(positional_params, None);
566 }
567 );
568
569 assert_matches!(
570 parse(Field::dummies(["+onounset"])),
571 Ok(Command::Modify {
572 options,
573 positional_params
574 }) => {
575 assert_eq!(options, [(Unset, On)]);
576 assert_eq!(positional_params, None);
577 }
578 );
579
580 assert_matches!(
581 parse(Field::dummies(["+o","NO+unset"])),
582 Ok(Command::Modify {
583 options,
584 positional_params
585 }) => {
586 assert_eq!(options, [(Unset, On)]);
587 assert_eq!(positional_params, None);
588 }
589 );
590 }
591
592 #[test]
593 fn long_options() {
594 assert_matches!(
595 parse(Field::dummies(["--allexpo"])),
596 Ok(Command::Modify {
597 options,
598 positional_params
599 }) => {
600 assert_eq!(options, [(AllExport, On)]);
601 assert_eq!(positional_params, None);
602 }
603 );
604
605 assert_matches!(
606 parse(Field::dummies(["-- all-Expo"])),
607 Ok(Command::Modify {
608 options,
609 positional_params
610 }) => {
611 assert_eq!(options, [(AllExport, On)]);
612 assert_eq!(positional_params, None);
613 }
614 );
615
616 assert_matches!(
617 parse(Field::dummies(["--nounset"])),
618 Ok(Command::Modify {
619 options,
620 positional_params
621 }) => {
622 assert_eq!(options, [(Unset, Off)]);
623 assert_eq!(positional_params, None);
624 }
625 );
626 }
627
628 #[test]
629 fn negated_long_options() {
630 assert_matches!(
631 parse(Field::dummies(["++allexpo"])),
632 Ok(Command::Modify {
633 options,
634 positional_params
635 }) => {
636 assert_eq!(options, [(AllExport, Off)]);
637 assert_eq!(positional_params, None);
638 }
639 );
640
641 assert_matches!(
642 parse(Field::dummies(["++ all-Expo"])),
643 Ok(Command::Modify {
644 options,
645 positional_params
646 }) => {
647 assert_eq!(options, [(AllExport, Off)]);
648 assert_eq!(positional_params, None);
649 }
650 );
651
652 assert_matches!(
653 parse(Field::dummies(["++nounset"])),
654 Ok(Command::Modify {
655 options,
656 positional_params
657 }) => {
658 assert_eq!(options, [(Unset, On)]);
659 assert_eq!(positional_params, None);
660 }
661 );
662 }
663
664 #[test]
665 fn options_and_separator() {
666 assert_matches!(
667 parse(Field::dummies(["-a", "--"])),
668 Ok(Command::Modify {
669 options,
670 positional_params
671 }) => {
672 assert_eq!(options, [(AllExport, On)]);
673 assert_eq!(positional_params, Some(vec![]));
674 }
675 );
676
677 assert_matches!(
678 parse(Field::dummies(["-uv", "--", "-a"])),
679 Ok(Command::Modify {
680 options,
681 positional_params
682 }) => {
683 assert_eq!(options, [(Unset, Off), (Verbose, On)]);
684 assert_matches!(positional_params.unwrap().as_slice(), [first] => {
685 assert_eq!(first.value, "-a");
686 });
687 }
688 );
689
690 assert_matches!(
691 parse(Field::dummies(["-n", "-", "--"])),
692 Ok(Command::Modify {
693 options,
694 positional_params
695 }) => {
696 assert_eq!(options, [(Exec, Off)]);
697 assert_matches!(positional_params.unwrap().as_slice(), [first] => {
698 assert_eq!(first.value, "--");
699 });
700 }
701 );
702 }
703
704 #[test]
705 fn combinations() {
706 assert_matches!(
707 parse(Field::dummies(["+nononotify", "a", "-a"])),
708 Ok(Command::Modify {
709 options,
710 positional_params
711 }) => {
712 assert_eq!(options, [(Exec, On), (Notify, On)]);
713 assert_matches!(positional_params.unwrap().as_slice(), [first, second] => {
714 assert_eq!(first.value, "a");
715 assert_eq!(second.value, "-a");
716 });
717 }
718 );
719
720 assert_matches!(
721 parse(Field::dummies(["-uno", "-notify", "++log", "--", "foo", "-v"])),
722 Ok(Command::Modify {
723 options,
724 positional_params
725 }) => {
726 assert_eq!(options, [(Unset, Off), (Exec, Off), (Notify, On), (Log, Off)]);
727 assert_matches!(positional_params.unwrap().as_slice(), [first, second] => {
728 assert_eq!(first.value, "foo");
729 assert_eq!(second.value, "-v");
730 });
731 }
732 );
733 }
734
735 #[test]
736 fn parse_errors() {
737 assert_matches!(
738 parse(Field::dummies(["-n-a"])),
739 Err(Error::UnknownShortOption('-', field)) => {
740 assert_eq!(field.value, "-n-a");
741 }
742 );
743
744 assert_matches!(
745 parse(Field::dummies(["--foo"])),
746 Err(Error::UnknownLongOption(field)) => {
747 assert_eq!(field.value, "--foo");
748 }
749 );
750
751 assert_matches!(
752 parse(Field::dummies(["-ofoo"])),
753 Err(Error::UnknownLongOption(field)) => {
754 assert_eq!(field.value, "-ofoo");
755 }
756 );
757
758 assert_matches!(
759 parse(Field::dummies(["-o", "foo"])),
760 Err(Error::UnknownLongOption(field)) => {
761 assert_eq!(field.value, "foo");
762 }
763 );
764
765 assert_matches!(
766 parse(Field::dummies(["--no"])),
767 Err(Error::AmbiguousLongOption(field)) => {
768 assert_eq!(field.value, "--no");
769 }
770 );
771
772 assert_matches!(
773 parse(Field::dummies(["-oe"])),
774 Err(Error::AmbiguousLongOption(field)) => {
775 assert_eq!(field.value, "-oe");
776 }
777 );
778
779 assert_matches!(
780 parse(Field::dummies(["-eo"])),
781 Err(Error::MissingOptionArgument(field)) => {
782 assert_eq!(field.value, "-eo");
783 }
784 );
785 }
786
787 #[test]
788 fn unmodifiable_options() {
789 assert_matches!(
790 parse(Field::dummies(["-c"])),
791 Err(Error::UnmodifiableShortOption('c', field)) => {
792 assert_eq!(field.value, "-c");
793 }
794 );
795
796 assert_matches!(
797 parse(Field::dummies(["-ointeract"])),
798 Err(Error::UnmodifiableLongOption(field)) => {
799 assert_eq!(field.value, "-ointeract");
800 }
801 );
802
803 assert_matches!(
804 parse(Field::dummies(["-o", "interact"])),
805 Err(Error::UnmodifiableLongOption(field)) => {
806 assert_eq!(field.value, "interact");
807 }
808 );
809
810 assert_matches!(
811 parse(Field::dummies(["++stdin"])),
812 Err(Error::UnmodifiableLongOption(field)) => {
813 assert_eq!(field.value, "++stdin");
814 }
815 );
816 }
817}