1use std::iter::Peekable;
20use thiserror::Error;
21#[cfg(doc)]
22use yash_env::Env;
23use yash_env::option::FromStrError::{Ambiguous, NoSuchOption};
24use yash_env::option::Option as ShellOption;
25use yash_env::option::State;
26use yash_env::option::canonicalize;
27use yash_env::option::parse_long;
28use yash_env::option::parse_short;
29
30#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
32pub enum Source {
33 #[default]
35 Stdin,
36 File { path: String },
38 String(String),
40}
41
42#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
44pub enum InitFile {
45 None,
47 #[default]
49 Default,
50 File { path: String },
52}
53
54#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
56pub struct Work {
57 pub source: Source,
59 pub profile: InitFile,
61 pub rcfile: InitFile,
63}
64
65#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
66pub struct Run {
68 pub work: Work,
70 pub options: Vec<(ShellOption, State)>,
72 pub arg0: String,
74 pub positional_params: Vec<String>,
76}
77
78#[derive(Clone, Debug, Eq, Hash, PartialEq)]
80pub enum Parse {
81 Run(Run),
83 Help,
85 Version,
87}
88
89impl From<Run> for Parse {
90 fn from(run: Run) -> Self {
91 Parse::Run(run)
92 }
93}
94
95#[derive(Clone, Debug, Eq, Error, PartialEq)]
97pub enum Error {
98 #[error("unknown option `{0}`")]
100 UnknownShortOption(char),
101
102 #[error("unknown option `{0}`")]
104 UnknownLongOption(String),
105
106 #[error("ambiguous option name `{0}`")]
108 AmbiguousLongOption(String),
109
110 #[error("option `{0}` missing an argument")]
112 MissingOptionArgument(String),
113
114 #[error("option `{0}` does not take an argument")]
116 UnexpectedOptionArgument(String),
117
118 #[error("cannot specify both `-c` and `-s`")]
120 ConflictingSources,
121
122 #[error("cannot negate option `{0}`")]
124 UnnegatableShortOption(char),
125
126 #[error("cannot negate option `{0}`")]
128 UnnegatableLongOption(String),
129
130 #[error("missing command string for `-c`")]
132 MissingCommandString,
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137enum ShortOption {
138 Shell,
140 Version,
142}
143
144#[derive(Clone, Debug, PartialEq, Eq)]
146enum LongOption {
147 Shell(ShellOption, State),
148 Profile { path: String },
149 NoProfile,
150 Rcfile { path: String },
151 NoRcfile,
152 Help,
153 Version,
154}
155
156#[derive(Clone, Debug)]
158enum NonShellOptionConstructor {
159 WithoutArgument(LongOption),
160 WithArgument(fn(String) -> LongOption),
161}
162
163impl NonShellOptionConstructor {
164 fn from_name(name: &str) -> Option<Self> {
165 if "profile".starts_with(name) {
166 Some(Self::WithArgument(|path| LongOption::Profile { path }))
167 } else if "rcfile".starts_with(name) {
168 Some(Self::WithArgument(|path| LongOption::Rcfile { path }))
169 } else if "noprofile".starts_with(name) {
170 Some(Self::WithoutArgument(LongOption::NoProfile))
171 } else if "norcfile".starts_with(name) {
172 Some(Self::WithoutArgument(LongOption::NoRcfile))
173 } else if "help".starts_with(name) {
174 Some(Self::WithoutArgument(LongOption::Help))
175 } else if "version".starts_with(name) {
176 Some(Self::WithoutArgument(LongOption::Version))
177 } else {
178 None
179 }
180 }
181}
182
183pub fn parse<I, S>(args: I) -> Result<Parse, Error>
185where
186 I: IntoIterator<Item = S>,
187 S: Into<String>,
188{
189 let mut args = args.into_iter().map(Into::into).peekable();
190 let mut result = Run::default();
191
192 if let Some(arg0) = args.next_if(|_| true) {
197 parse_arg0(&arg0, &mut result.options);
198 result.arg0 = arg0;
199 }
200
201 loop {
203 if let Some(option) = try_parse_short(&mut args, &mut result.options)? {
204 match option {
205 ShortOption::Shell => continue,
206 ShortOption::Version => return Ok(Parse::Version),
207 }
208 }
209
210 let Some(option) = try_parse_long(&mut args)? else {
211 break;
212 };
213 match option {
214 LongOption::Shell(option, state) => result.options.push((option, state)),
215 LongOption::Profile { path } => {
216 if result.work.profile != InitFile::None {
217 result.work.profile = InitFile::File { path }
218 }
219 }
220 LongOption::NoProfile => result.work.profile = InitFile::None,
221 LongOption::Rcfile { path } => {
222 if result.work.rcfile != InitFile::None {
223 result.work.rcfile = InitFile::File { path }
224 }
225 }
226 LongOption::NoRcfile => result.work.rcfile = InitFile::None,
227 LongOption::Help => return Ok(Parse::Help),
228 LongOption::Version => return Ok(Parse::Version),
229 }
230 }
231
232 args.next_if(|arg| arg == "-" || arg == "--");
233
234 if result.options.contains(&(ShellOption::CmdLine, State::On)) {
236 if result.options.contains(&(ShellOption::Stdin, State::On)) {
237 return Err(Error::ConflictingSources);
238 }
239
240 let command = args.next_if(|_| true).ok_or(Error::MissingCommandString)?;
241 result.work.source = Source::String(command);
242 if let Some(name) = args.next_if(|_| true) {
243 result.arg0 = name;
244 }
245 } else if result.options.contains(&(ShellOption::Stdin, State::On)) {
246 result.work.source = Source::Stdin;
247 } else {
248 if let Some(operand) = args.next_if(|_| true) {
250 result.arg0.clone_from(&operand);
251 result.work.source = Source::File { path: operand };
252 }
253 }
254 result.positional_params = args.collect();
255
256 Ok(Parse::Run(result))
257}
258
259fn parse_arg0(arg0: &str, options: &mut Vec<(ShellOption, State)>) {
260 if arg0.starts_with('-') {
261 options.push((ShellOption::Login, State::On));
262 }
263 if arg0.rsplit('/').next().unwrap_or("") == "sh" {
264 options.push((ShellOption::PosixlyCorrect, State::On));
265 }
266}
267
268fn try_parse_short<I: Iterator<Item = String>>(
275 args: &mut Peekable<I>,
276 option_occurrences: &mut Vec<(ShellOption, State)>,
277) -> Result<Option<ShortOption>, Error> {
278 let Some(mut arg) = args.next_if(|arg| is_short_option(arg)) else {
279 return Ok(None);
280 };
281
282 let mut chars = arg.chars();
283 let negate = match chars.next() {
284 Some('-') => false,
285 Some('+') => true,
286 _ => unreachable!(),
287 };
288
289 while let Some(c) = chars.next() {
290 if c == 'V' {
291 return if negate {
292 Err(Error::UnnegatableShortOption('V'))
293 } else {
294 Ok(Some(ShortOption::Version))
295 };
296 }
297 if c == 'o' {
298 let name = chars.as_str();
299 let name = if !name.is_empty() {
300 canonicalize(name)
301 } else {
302 let prev = arg;
303 arg = args.next().ok_or(Error::MissingOptionArgument(prev))?;
304 canonicalize(&arg)
305 };
306 match parse_long(&name) {
307 Ok((option, state)) => {
308 option_occurrences.push((option, if negate { !state } else { state }));
309 break;
310 }
311 Err(NoSuchOption) => return Err(Error::UnknownLongOption(name.into_owned())),
312 Err(Ambiguous) => return Err(Error::AmbiguousLongOption(name.into_owned())),
313 }
314 }
315
316 let (option, state) = parse_short(c).ok_or(Error::UnknownShortOption(c))?;
317 option_occurrences.push((option, if negate { !state } else { state }));
318 }
319
320 Ok(Some(ShortOption::Shell))
321}
322
323fn is_short_option(arg: &str) -> bool {
325 let mut chars = arg.chars();
326 let negate = match chars.next() {
327 Some('-') => false,
328 Some('+') => true,
329 _ => return false,
330 };
331 match chars.next() {
332 Some('-') if !negate => false,
333 Some('+') if negate => false,
334 Some(_) => true,
335 None => false,
336 }
337}
338
339fn try_parse_long<I: Iterator<Item = String>>(
341 args: &mut Peekable<I>,
342) -> Result<Option<LongOption>, Error> {
343 let Some(arg) = args.next_if(|arg| is_long_option(arg)) else {
344 return Ok(None);
345 };
346
347 let mut chars = arg.chars();
348 let negate = match chars.next() {
349 Some('-') => false,
350 Some('+') => true,
351 _ => unreachable!(),
352 };
353
354 chars.next();
356
357 let chars = chars.as_str();
358
359 let (name, value) = match chars.split_once('=') {
361 Some((name, value)) => (name, Some(value)),
362 None => (chars, None),
363 };
364 let non_shell_option = NonShellOptionConstructor::from_name(name);
365
366 let shell_option = parse_long(&canonicalize(chars));
368
369 match (non_shell_option, shell_option) {
371 (_, Err(Ambiguous)) | (Some(_), Ok(_)) => Err(Error::AmbiguousLongOption(arg)),
372
373 (None, Err(NoSuchOption)) => Err(Error::UnknownLongOption(arg)),
374
375 (Some(_), Err(NoSuchOption)) if negate => Err(Error::UnnegatableLongOption(arg)),
376
377 (Some(NonShellOptionConstructor::WithoutArgument(option)), Err(NoSuchOption)) => {
378 if value.is_none() {
379 Ok(Some(option))
380 } else {
381 Err(Error::UnexpectedOptionArgument(arg))
382 }
383 }
384
385 (Some(NonShellOptionConstructor::WithArgument(ctor)), Err(NoSuchOption)) => {
386 let value = match value {
387 Some(value) => value.to_owned(),
388 None => match args.next() {
389 Some(next_arg) => next_arg,
390 None => return Err(Error::MissingOptionArgument(arg)),
391 },
392 };
393 Ok(Some(ctor(value)))
394 }
395
396 (None, Ok((option, state))) if negate => Ok(Some(LongOption::Shell(option, !state))),
397 (None, Ok((option, state))) => Ok(Some(LongOption::Shell(option, state))),
398 }
399}
400
401fn is_long_option(arg: &str) -> bool {
403 if let Some(name) = arg.strip_prefix("--") {
404 !name.is_empty()
405 } else {
406 arg.starts_with("++")
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use assert_matches::assert_matches;
414
415 fn parse<I, S>(args: I) -> Result<Parse, Error>
416 where
417 I: IntoIterator<Item = S>,
418 S: Into<String>,
419 {
420 use fuzed_iterator::IteratorExt;
421 super::parse(args.into_iter().fuze())
422 }
423
424 #[test]
425 fn no_arguments() {
426 assert_eq!(parse([] as [&str; 0]), Ok(Parse::Run(Run::default())));
427 }
428
429 #[test]
430 fn arg0_only() {
431 assert_eq!(
432 parse(["yash"]),
433 Ok(Parse::Run(Run {
434 arg0: "yash".to_string(),
435 ..Run::default()
436 })),
437 );
438 }
439
440 #[test]
441 fn run_file() {
442 assert_eq!(
444 parse(["yash", "my-script"]),
445 Ok(Parse::Run(Run {
446 work: Work {
447 source: Source::File {
448 path: "my-script".to_string()
449 },
450 ..Work::default()
451 },
452 arg0: "my-script".to_string(),
453 ..Run::default()
454 })),
455 );
456
457 assert_eq!(
459 parse(["yash", "path/to/script", "-option", "foo", "bar"]),
460 Ok(Parse::Run(Run {
461 work: Work {
462 source: Source::File {
463 path: "path/to/script".to_string()
464 },
465 ..Work::default()
466 },
467 arg0: "path/to/script".to_string(),
468 positional_params: vec![
469 "-option".to_string(),
470 "foo".to_string(),
471 "bar".to_string()
472 ],
473 ..Run::default()
474 })),
475 );
476 }
477
478 #[test]
479 fn run_string() {
480 assert_eq!(
482 parse(["yash", "-c", "echo"]),
483 Ok(Parse::Run(Run {
484 work: Work {
485 source: Source::String("echo".to_string()),
486 ..Work::default()
487 },
488 options: vec![(ShellOption::CmdLine, State::On)],
489 arg0: "yash".to_string(),
490 ..Run::default()
491 })),
492 );
493
494 assert_eq!(
496 parse(["yash", "-c", "echo", "name"]),
497 Ok(Parse::Run(Run {
498 work: Work {
499 source: Source::String("echo".to_string()),
500 ..Work::default()
501 },
502 options: vec![(ShellOption::CmdLine, State::On)],
503 arg0: "name".to_string(),
504 ..Run::default()
505 })),
506 );
507
508 assert_eq!(
510 parse(["yash", "-c", "echo", "name", "foo", "bar"]),
511 Ok(Parse::Run(Run {
512 work: Work {
513 source: Source::String("echo".to_string()),
514 ..Work::default()
515 },
516 options: vec![(ShellOption::CmdLine, State::On)],
517 arg0: "name".to_string(),
518 positional_params: vec!["foo".to_string(), "bar".to_string()],
519 }))
520 );
521
522 assert_eq!(
524 parse(["yash", "--cmd-line", "echo"]),
525 Ok(Parse::Run(Run {
526 work: Work {
527 source: Source::String("echo".to_string()),
528 ..Work::default()
529 },
530 options: vec![(ShellOption::CmdLine, State::On)],
531 arg0: "yash".to_string(),
532 ..Run::default()
533 })),
534 );
535 }
536
537 #[test]
538 fn missing_command_string() {
539 assert_eq!(parse(["yash", "-c"]), Err(Error::MissingCommandString));
540 }
541
542 #[test]
543 fn run_stdin() {
544 assert_eq!(
546 parse(["yash", "-s"]),
547 Ok(Parse::Run(Run {
548 work: Work {
549 source: Source::Stdin,
550 ..Work::default()
551 },
552 options: vec![(ShellOption::Stdin, State::On)],
553 arg0: "yash".to_string(),
554 ..Run::default()
555 })),
556 );
557
558 assert_eq!(
560 parse(["yash", "-s", "foo", "bar", "-baz"]),
561 Ok(Parse::Run(Run {
562 work: Work {
563 source: Source::Stdin,
564 ..Work::default()
565 },
566 options: vec![(ShellOption::Stdin, State::On)],
567 arg0: "yash".to_string(),
568 positional_params: vec!["foo".to_string(), "bar".to_string(), "-baz".to_string()],
569 })),
570 );
571
572 assert_eq!(
574 parse(["yash", "--stdin"]),
575 Ok(Parse::Run(Run {
576 work: Work {
577 source: Source::Stdin,
578 ..Work::default()
579 },
580 options: vec![(ShellOption::Stdin, State::On)],
581 arg0: "yash".to_string(),
582 ..Run::default()
583 })),
584 );
585 }
586
587 #[test]
588 fn conflicting_sources() {
589 assert_eq!(parse(["yash", "-cs"]), Err(Error::ConflictingSources));
590 }
591
592 #[test]
593 fn short_options() {
594 assert_eq!(
596 parse(["yash", "-a"]),
597 Ok(Parse::Run(Run {
598 options: vec![(ShellOption::AllExport, State::On)],
599 arg0: "yash".to_string(),
600 ..Run::default()
601 })),
602 );
603 assert_eq!(
604 parse(["yash", "-n"]),
605 Ok(Parse::Run(Run {
606 options: vec![(ShellOption::Exec, State::Off)],
607 arg0: "yash".to_string(),
608 ..Run::default()
609 })),
610 );
611
612 assert_eq!(
614 parse(["yash", "-an"]),
615 Ok(Parse::Run(Run {
616 options: vec![
617 (ShellOption::AllExport, State::On),
618 (ShellOption::Exec, State::Off)
619 ],
620 arg0: "yash".to_string(),
621 ..Run::default()
622 })),
623 );
624
625 assert_eq!(
627 parse(["yash", "-a", "-nu", "-x"]),
628 Ok(Parse::Run(Run {
629 options: vec![
630 (ShellOption::AllExport, State::On),
631 (ShellOption::Exec, State::Off),
632 (ShellOption::Unset, State::Off),
633 (ShellOption::XTrace, State::On),
634 ],
635 arg0: "yash".to_string(),
636 ..Run::default()
637 })),
638 );
639 }
640
641 #[test]
642 fn negated_short_options() {
643 assert_eq!(
645 parse(["yash", "+a"]),
646 Ok(Parse::Run(Run {
647 options: vec![(ShellOption::AllExport, State::Off)],
648 arg0: "yash".to_string(),
649 ..Run::default()
650 })),
651 );
652 assert_eq!(
653 parse(["yash", "+n"]),
654 Ok(Parse::Run(Run {
655 options: vec![(ShellOption::Exec, State::On)],
656 arg0: "yash".to_string(),
657 ..Run::default()
658 })),
659 );
660
661 assert_eq!(
663 parse(["yash", "+an"]),
664 Ok(Parse::Run(Run {
665 options: vec![
666 (ShellOption::AllExport, State::Off),
667 (ShellOption::Exec, State::On)
668 ],
669 arg0: "yash".to_string(),
670 ..Run::default()
671 })),
672 );
673
674 assert_eq!(
676 parse(["yash", "+a", "+ns", "+x"]),
677 Ok(Parse::Run(Run {
678 options: vec![
679 (ShellOption::AllExport, State::Off),
680 (ShellOption::Exec, State::On),
681 (ShellOption::Stdin, State::Off),
682 (ShellOption::XTrace, State::Off),
683 ],
684 arg0: "yash".to_string(),
685 ..Run::default()
686 })),
687 );
688 }
689
690 #[test]
691 fn o_options() {
692 assert_eq!(
694 parse(["yash", "-oallexport"]),
695 Ok(Parse::Run(Run {
696 options: vec![(ShellOption::AllExport, State::On)],
697 arg0: "yash".to_string(),
698 ..Run::default()
699 })),
700 );
701
702 assert_eq!(
704 parse(["yash", "-o", "allexport"]),
705 Ok(Parse::Run(Run {
706 options: vec![(ShellOption::AllExport, State::On)],
707 arg0: "yash".to_string(),
708 ..Run::default()
709 })),
710 );
711
712 assert_eq!(
714 parse(["yash", "-o", "all-Export", "-o StD+in_"]),
715 Ok(Parse::Run(Run {
716 options: vec![
717 (ShellOption::AllExport, State::On),
718 (ShellOption::Stdin, State::On),
719 ],
720 arg0: "yash".to_string(),
721 ..Run::default()
722 })),
723 );
724 }
725
726 #[test]
727 fn negated_o_options() {
728 assert_eq!(
730 parse(["yash", "+oallexport"]),
731 Ok(Parse::Run(Run {
732 options: vec![(ShellOption::AllExport, State::Off)],
733 arg0: "yash".to_string(),
734 ..Run::default()
735 })),
736 );
737
738 assert_eq!(
740 parse(["yash", "+o", "allexport"]),
741 Ok(Parse::Run(Run {
742 options: vec![(ShellOption::AllExport, State::Off)],
743 arg0: "yash".to_string(),
744 ..Run::default()
745 })),
746 );
747
748 assert_eq!(
750 parse(["yash", "+o", "all-Export", "+o no_exec"]),
751 Ok(Parse::Run(Run {
752 options: vec![
753 (ShellOption::AllExport, State::Off),
754 (ShellOption::Exec, State::On),
755 ],
756 arg0: "yash".to_string(),
757 ..Run::default()
758 })),
759 );
760 }
761
762 #[test]
763 fn long_options() {
764 assert_eq!(
765 parse(["yash", "--all-export"]),
766 Ok(Parse::Run(Run {
767 options: vec![(ShellOption::AllExport, State::On)],
768 arg0: "yash".to_string(),
769 ..Run::default()
770 })),
771 );
772 assert_eq!(
773 parse(["yash", "--all-export", "--no*un=set"]),
774 Ok(Parse::Run(Run {
775 options: vec![
776 (ShellOption::AllExport, State::On),
777 (ShellOption::Unset, State::Off),
778 ],
779 arg0: "yash".to_string(),
780 ..Run::default()
781 })),
782 );
783 }
784
785 #[test]
786 fn negated_long_options() {
787 assert_eq!(
788 parse(["yash", "++all-export"]),
789 Ok(Parse::Run(Run {
790 options: vec![(ShellOption::AllExport, State::Off)],
791 arg0: "yash".to_string(),
792 ..Run::default()
793 })),
794 );
795 assert_eq!(
796 parse(["yash", "++all+export", "++no*un-set"]),
797 Ok(Parse::Run(Run {
798 options: vec![
799 (ShellOption::AllExport, State::Off),
800 (ShellOption::Unset, State::On),
801 ],
802 arg0: "yash".to_string(),
803 ..Run::default()
804 })),
805 );
806 }
807
808 #[test]
809 fn profile_option() {
810 assert_eq!(
812 parse(["yash", "--profile", "my/file"]),
813 Ok(Parse::Run(Run {
814 work: Work {
815 profile: InitFile::File {
816 path: "my/file".to_string(),
817 },
818 ..Work::default()
819 },
820 arg0: "yash".to_string(),
821 ..Run::default()
822 })),
823 );
824
825 assert_eq!(
827 parse(["yash", "--profile=my/file"]),
828 Ok(Parse::Run(Run {
829 work: Work {
830 profile: InitFile::File {
831 path: "my/file".to_string(),
832 },
833 ..Work::default()
834 },
835 arg0: "yash".to_string(),
836 ..Run::default()
837 })),
838 );
839
840 assert_eq!(
842 parse(["yash", "--pr=ofile"]),
843 Ok(Parse::Run(Run {
844 work: Work {
845 profile: InitFile::File {
846 path: "ofile".to_string(),
847 },
848 ..Work::default()
849 },
850 arg0: "yash".to_string(),
851 ..Run::default()
852 })),
853 );
854 }
855
856 #[test]
857 fn missing_profile_option_argument() {
858 assert_eq!(
859 parse(["yash", "--profile"]),
860 Err(Error::MissingOptionArgument("--profile".to_string())),
861 );
862 }
863
864 #[test]
865 fn noprofile_option() {
866 let expected = Ok(Parse::Run(Run {
867 work: Work {
868 profile: InitFile::None,
869 ..Work::default()
870 },
871 arg0: "yash".to_string(),
872 ..Run::default()
873 }));
874 assert_eq!(parse(["yash", "--noprofile"]), expected);
875
876 assert_eq!(parse(["yash", "--profile=file", "--noprofile"]), expected);
878 assert_eq!(parse(["yash", "--noprofile", "--profile=file"]), expected);
879 }
880
881 #[test]
882 fn unexpected_noprofile_option_argument() {
883 assert_eq!(
884 parse(["yash", "--noprofile=x"]),
885 Err(Error::UnexpectedOptionArgument("--noprofile=x".to_string())),
886 );
887 }
888
889 #[test]
890 fn rcfile_option() {
891 assert_eq!(
893 parse(["yash", "--rcfile", "my/file"]),
894 Ok(Parse::Run(Run {
895 work: Work {
896 rcfile: InitFile::File {
897 path: "my/file".to_string(),
898 },
899 ..Work::default()
900 },
901 arg0: "yash".to_string(),
902 ..Run::default()
903 })),
904 );
905
906 assert_eq!(
908 parse(["yash", "--rcfile=my/file"]),
909 Ok(Parse::Run(Run {
910 work: Work {
911 rcfile: InitFile::File {
912 path: "my/file".to_string(),
913 },
914 ..Work::default()
915 },
916 arg0: "yash".to_string(),
917 ..Run::default()
918 })),
919 );
920
921 assert_eq!(
923 parse(["yash", "--rc=file"]),
924 Ok(Parse::Run(Run {
925 work: Work {
926 rcfile: InitFile::File {
927 path: "file".to_string(),
928 },
929 ..Work::default()
930 },
931 arg0: "yash".to_string(),
932 ..Run::default()
933 })),
934 );
935 }
936
937 #[test]
938 fn missing_rcfile_option_argument() {
939 assert_eq!(
940 parse(["yash", "--rcfile"]),
941 Err(Error::MissingOptionArgument("--rcfile".to_string())),
942 );
943 }
944
945 #[test]
946 fn norcfile_option() {
947 let expected = Ok(Parse::Run(Run {
948 work: Work {
949 rcfile: InitFile::None,
950 ..Work::default()
951 },
952 arg0: "yash".to_string(),
953 ..Run::default()
954 }));
955 assert_eq!(parse(["yash", "--norcfile"]), expected);
956
957 assert_eq!(parse(["yash", "--rcfile=file", "--norcfile"]), expected);
959 assert_eq!(parse(["yash", "--norcfile", "--rcfile=file"]), expected);
960 }
961
962 #[test]
963 fn option_combinations() {
964 assert_eq!(
965 parse(["yash", "-a", "--err-exit", "-u"]),
966 Ok(Parse::Run(Run {
967 options: vec![
968 (ShellOption::AllExport, State::On),
969 (ShellOption::ErrExit, State::On),
970 (ShellOption::Unset, State::Off),
971 ],
972 arg0: "yash".to_string(),
973 ..Run::default()
974 })),
975 );
976
977 assert_eq!(
978 parse(["yash", "-xo", "noclobber", "-il"]),
979 Ok(Parse::Run(Run {
980 options: vec![
981 (ShellOption::XTrace, State::On),
982 (ShellOption::Clobber, State::Off),
983 (ShellOption::Interactive, State::On),
984 (ShellOption::Login, State::On),
985 ],
986 arg0: "yash".to_string(),
987 ..Run::default()
988 })),
989 );
990
991 assert_eq!(
992 parse(["yash", "--all", "-f", "--posix"]),
993 Ok(Parse::Run(Run {
994 options: vec![
995 (ShellOption::AllExport, State::On),
996 (ShellOption::Glob, State::Off),
997 (ShellOption::PosixlyCorrect, State::On),
998 ],
999 arg0: "yash".to_string(),
1000 ..Run::default()
1001 })),
1002 );
1003 }
1004
1005 #[test]
1006 fn double_hyphen_separator_and_operands() {
1007 assert_eq!(
1008 parse(["yash", "--"]),
1009 Ok(Parse::Run(Run {
1010 arg0: "yash".to_string(),
1011 ..Default::default()
1012 })),
1013 );
1014
1015 assert_eq!(
1016 parse(["yash", "-a", "--", "file", "arg"]),
1017 Ok(Parse::Run(Run {
1018 work: Work {
1019 source: Source::File {
1020 path: "file".to_string()
1021 },
1022 ..Work::default()
1023 },
1024 options: vec![(ShellOption::AllExport, State::On)],
1025 arg0: "file".to_string(),
1026 positional_params: vec!["arg".to_string()],
1027 })),
1028 );
1029
1030 assert_eq!(
1031 parse(["yash", "-a", "--", "--", "arg"]),
1032 Ok(Parse::Run(Run {
1033 work: Work {
1034 source: Source::File {
1035 path: "--".to_string()
1036 },
1037 ..Work::default()
1038 },
1039 options: vec![(ShellOption::AllExport, State::On)],
1040 arg0: "--".to_string(),
1041 positional_params: vec!["arg".to_string()],
1042 })),
1043 );
1044 }
1045
1046 #[test]
1047 fn single_hyphen_separator_and_operands() {
1048 assert_eq!(
1049 parse(["yash", "-"]),
1050 Ok(Parse::Run(Run {
1051 arg0: "yash".to_string(),
1052 ..Default::default()
1053 })),
1054 );
1055
1056 assert_eq!(
1057 parse(["yash", "-a", "-", "file", "arg"]),
1058 Ok(Parse::Run(Run {
1059 work: Work {
1060 source: Source::File {
1061 path: "file".to_string()
1062 },
1063 ..Work::default()
1064 },
1065 options: vec![(ShellOption::AllExport, State::On)],
1066 arg0: "file".to_string(),
1067 positional_params: vec!["arg".to_string()],
1068 })),
1069 );
1070
1071 assert_eq!(
1072 parse(["yash", "-a", "-", "-", "arg"]),
1073 Ok(Parse::Run(Run {
1074 work: Work {
1075 source: Source::File {
1076 path: "-".to_string()
1077 },
1078 ..Work::default()
1079 },
1080 options: vec![(ShellOption::AllExport, State::On)],
1081 arg0: "-".to_string(),
1082 positional_params: vec!["arg".to_string()],
1083 })),
1084 );
1085 }
1086
1087 #[test]
1088 fn option_after_operand() {
1089 assert_eq!(
1090 parse(["yash", "-a", "file", "-e"]),
1091 Ok(Parse::Run(Run {
1092 work: Work {
1093 source: Source::File {
1094 path: "file".to_string()
1095 },
1096 ..Work::default()
1097 },
1098 options: vec![(ShellOption::AllExport, State::On)],
1099 arg0: "file".to_string(),
1100 positional_params: vec!["-e".to_string()],
1101 })),
1102 );
1103 }
1104
1105 #[test]
1106 fn leading_hyphen_in_arg0_makes_login_shell() {
1107 assert_eq!(
1108 parse(["-yash"]),
1109 Ok(Parse::Run(Run {
1110 options: vec![(ShellOption::Login, State::On)],
1111 arg0: "-yash".to_string(),
1112 ..Run::default()
1113 })),
1114 );
1115
1116 assert_matches!(
1117 parse(["-/bin/sh"]),
1118 Ok(Parse::Run(run)) => {
1119 assert!(run.options.contains(&(ShellOption::Login, State::On)));
1120 }
1121 );
1122 }
1123
1124 #[test]
1125 fn command_name_sh_enables_posix_mode() {
1126 assert_eq!(
1127 parse(["sh"]),
1128 Ok(Parse::Run(Run {
1129 options: vec![(ShellOption::PosixlyCorrect, State::On)],
1130 arg0: "sh".to_string(),
1131 ..Run::default()
1132 })),
1133 );
1134 assert_eq!(
1135 parse(["/usr/bin/sh"]),
1136 Ok(Parse::Run(Run {
1137 options: vec![(ShellOption::PosixlyCorrect, State::On)],
1138 arg0: "/usr/bin/sh".to_string(),
1139 ..Run::default()
1140 })),
1141 );
1142
1143 assert_matches!(
1144 parse(["-/bin/sh"]),
1145 Ok(Parse::Run(run)) => {
1146 assert!(run.options.contains(&(ShellOption::PosixlyCorrect, State::On)));
1147 }
1148 );
1149 }
1150
1151 #[test]
1152 fn help_option() {
1153 assert_eq!(parse(["yash", "--help"]), Ok(Parse::Help));
1154 assert_eq!(parse(["yash", "-a", "--help", "file"]), Ok(Parse::Help));
1155 }
1156
1157 #[test]
1158 fn version_option() {
1159 assert_eq!(parse(["yash", "-V"]), Ok(Parse::Version));
1160 assert_eq!(parse(["yash", "-aV", "x"]), Ok(Parse::Version));
1161
1162 assert_eq!(parse(["yash", "--version"]), Ok(Parse::Version));
1163 assert_eq!(parse(["yash", "-a", "--version", "x"]), Ok(Parse::Version));
1164 }
1165
1166 #[test]
1167 fn ambiguous_long_option() {
1168 assert_eq!(
1169 parse(["yash", "--no"]),
1170 Err(Error::AmbiguousLongOption("--no".to_string())),
1171 );
1172 assert_eq!(
1173 parse(["yash", "--p"]),
1174 Err(Error::AmbiguousLongOption("--p".to_string())),
1175 );
1176 assert_eq!(
1177 parse(["yash", "--ver=bose"]),
1178 Err(Error::AmbiguousLongOption("--ver=bose".to_string())),
1179 );
1180 }
1181
1182 #[test]
1183 fn non_existing_option() {
1184 assert_eq!(
1185 parse(["yash", "-x", "-y"]),
1186 Err(Error::UnknownShortOption('y')),
1187 );
1188 assert_eq!(parse(["yash", "-CDf"]), Err(Error::UnknownShortOption('D')),);
1189
1190 assert_eq!(
1191 parse(["yash", "--unexisting"]),
1192 Err(Error::UnknownLongOption("--unexisting".to_string())),
1193 );
1194 assert_eq!(
1195 parse(["yash", "--no+un=existing"]),
1196 Err(Error::UnknownLongOption("--no+un=existing".to_string())),
1197 );
1198 }
1199
1200 #[test]
1201 fn unnegatable_short_option() {
1202 assert_eq!(
1203 parse(["yash", "+V"]),
1204 Err(Error::UnnegatableShortOption('V')),
1205 );
1206 }
1207
1208 #[test]
1209 fn unnegatable_long_option() {
1210 assert_eq!(
1211 parse(["yash", "++profile"]),
1212 Err(Error::UnnegatableLongOption("++profile".to_string())),
1213 );
1214 assert_eq!(
1215 parse(["yash", "++vers=ion"]),
1216 Err(Error::UnnegatableLongOption("++vers=ion".to_string())),
1217 );
1218 }
1219}