1use super::Command;
25use std::borrow::Cow;
26use thiserror::Error;
27use yash_env::Env;
28use yash_env::semantics::Field;
29use yash_env::signal::{Number, RawNumber};
30use yash_env::source::Location;
31use yash_env::source::pretty::{Report, ReportType, Snippet, Span, SpanRole, add_span};
32use yash_env::system::Signals;
33
34#[derive(Clone, Debug, Error, PartialEq, Eq)]
36#[non_exhaustive]
37pub enum Error {
38 #[error("unknown option")]
40 UnknownOption(Field),
41
42 #[error("invalid option combination")]
45 ConflictingOptions {
46 signal_arg: Field,
48 list_option_name: char,
50 list_option_location: Location,
52 },
53
54 #[error("missing signal name or number")]
56 MissingSignal {
57 signal_option_name: char,
59 signal_option_location: Location,
61 },
62
63 #[error("multiple signals specified")]
65 MultipleSignals(Field, Field),
66
67 #[error("invalid signal")]
74 InvalidSignal(Field),
75
76 #[error("no target process specified")]
78 MissingTarget,
79}
80
81impl Error {
82 #[must_use]
84 pub fn to_report(&self) -> Report<'_> {
85 let mut report = Report::new();
86 report.r#type = ReportType::Error;
87 report.title = self.to_string().into();
88 report.snippets = match self {
89 Self::UnknownOption(field) => Snippet::with_primary_span(
90 &field.origin,
91 format!("{:?} is not a valid option", field.value).into(),
92 ),
93
94 Self::ConflictingOptions {
95 signal_arg,
96 list_option_name,
97 list_option_location,
98 } => {
99 let mut snippets = Snippet::with_primary_span(
100 &signal_arg.origin,
101 "signal to send is specified here".into(),
102 );
103 add_span(
104 &list_option_location.code,
105 Span {
106 range: list_option_location.byte_range(),
107 role: SpanRole::Primary {
108 label: format!("option `{list_option_name}` is incompatible").into(),
109 },
110 },
111 &mut snippets,
112 );
113 snippets
114 }
115
116 Self::MissingSignal {
117 signal_option_name,
118 signal_option_location,
119 } => Snippet::with_primary_span(
120 signal_option_location,
121 format!("option `{signal_option_name}` requires a signal name or number").into(),
122 ),
123
124 Self::MultipleSignals(field1, field2) => {
125 let mut snippets = Snippet::with_primary_span(
126 &field1.origin,
127 format!("first signal {:?}", field1.value).into(),
128 );
129 add_span(
130 &field2.origin.code,
131 Span {
132 range: field2.origin.byte_range(),
133 role: SpanRole::Primary {
134 label: format!("second signal {:?}", field2.value).into(),
135 },
136 },
137 &mut snippets,
138 );
139 snippets
140 }
141
142 Self::InvalidSignal(field) => Snippet::with_primary_span(
143 &field.origin,
144 format!("{:?} is not a valid signal name or number", field.value).into(),
145 ),
146
147 Self::MissingTarget => vec![],
148 };
149 report
150 }
151}
152
153impl<'a> From<&'a Error> for Report<'a> {
154 #[inline]
155 fn from(error: &'a Error) -> Self {
156 error.to_report()
157 }
158}
159
160#[must_use]
175pub fn parse_signal<S: Signals>(
176 system: &S,
177 signal_spec: &str,
178 allow_sig_prefix: bool,
179) -> Option<RawNumber> {
180 if let Ok(number) = signal_spec.parse() {
182 return Some(number);
183 }
184
185 let mut signal_spec = Cow::Borrowed(signal_spec);
187 if signal_spec.contains(|c: char| c.is_ascii_lowercase()) {
188 signal_spec.to_mut().make_ascii_uppercase();
189 }
190
191 let signal_name = allow_sig_prefix
193 .then(|| signal_spec.strip_prefix("SIG"))
194 .flatten()
195 .unwrap_or(&signal_spec);
196
197 system.str2sig(signal_name).map(Number::as_raw)
199}
200
201fn set_signal(
212 signal: &mut RawNumber,
213 signal_origin: &mut Option<Field>,
214 new_signal: Option<RawNumber>,
215 new_signal_origin: Field,
216) -> Result<(), Error> {
217 let Some(new_signal) = new_signal else {
218 return Err(Error::InvalidSignal(new_signal_origin));
219 };
220 if let Some(prev) = signal_origin.take() {
221 return Err(Error::MultipleSignals(prev, new_signal_origin));
222 }
223 *signal = new_signal;
224 *signal_origin = Some(new_signal_origin);
225 Ok(())
226}
227
228#[must_use]
230fn invalid_signal_to_unknown_option(error: Error) -> Error {
231 match error {
232 Error::InvalidSignal(field) => Error::UnknownOption(field),
233 error => error,
234 }
235}
236
237fn parse_list_case<I: Iterator<Item = Field>>(
239 operands: I,
240 signal_origin: Option<Field>,
241 list_option_name: char,
242 list_option_location: Location,
243 verbose: bool,
244) -> Result<Command, Error> {
245 if let Some(signal_arg) = signal_origin {
246 Err(Error::ConflictingOptions {
247 signal_arg,
248 list_option_name,
249 list_option_location,
250 })
251 } else {
252 let signals = operands.collect();
253 Ok(Command::Print { signals, verbose })
254 }
255}
256
257pub fn parse<S: Signals>(env: &Env<S>, args: Vec<Field>) -> Result<Command, Error> {
259 let allow_sig_prefix = false; let mut args = args.into_iter().peekable();
261 let mut signal = S::SIGTERM.as_raw();
262 let mut signal_origin = None;
263 let mut list = None;
264 let mut verbose = None;
265
266 while let Some(arg) =
268 args.next_if(|arg| arg.value.strip_prefix('-').is_some_and(|s| !s.is_empty()))
269 {
270 let options = &arg.value[1..];
271 if options == "-" {
272 debug_assert_eq!(arg.value, "--");
273 break;
274 }
275
276 let mut chars = options.chars();
277 while let Some(option) = chars.next() {
278 match option {
279 's' | 'n' => {
280 let remainder = chars.as_str();
281 if remainder.is_empty() {
282 let Some(current_signal_arg) = args.next() else {
283 return Err(Error::MissingSignal {
284 signal_option_name: option,
285 signal_option_location: arg.origin,
286 });
287 };
288 set_signal(
289 &mut signal,
290 &mut signal_origin,
291 parse_signal(&env.system, ¤t_signal_arg.value, allow_sig_prefix),
292 current_signal_arg,
293 )?;
294 } else {
295 set_signal(
296 &mut signal,
297 &mut signal_origin,
298 parse_signal(&env.system, remainder, allow_sig_prefix)
299 .or_else(|| parse_signal(&env.system, options, allow_sig_prefix)),
300 arg,
301 )?;
302 }
303 break;
304 }
305 'l' => {
306 list = Some(arg.origin.clone());
307 }
308 'v' => {
309 verbose = Some(arg.origin.clone());
310 }
311 _ => {
312 set_signal(
313 &mut signal,
314 &mut signal_origin,
315 parse_signal(&env.system, options, allow_sig_prefix),
316 arg,
317 )
318 .map_err(invalid_signal_to_unknown_option)?;
319 break;
320 }
321 }
322 }
323 }
324
325 if let Some(option_location) = verbose {
327 parse_list_case(args, signal_origin, 'v', option_location, true)
328 } else if let Some(option_location) = list {
329 parse_list_case(args, signal_origin, 'l', option_location, false)
330 } else {
331 if args.peek().is_none() {
333 Err(Error::MissingTarget)
334 } else {
335 let targets = args.collect();
336 Ok(Command::Send {
337 signal,
338 signal_origin,
339 targets,
340 })
341 }
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348 use yash_env::system::r#virtual::VirtualSystem;
349
350 #[test]
351 fn parse_signal_names_without_sig_prefix() {
352 let system = VirtualSystem::new();
353 assert_eq!(
354 parse_signal(&system, "INT", false),
355 Some(VirtualSystem::SIGINT.as_raw())
356 );
357 assert_eq!(
358 parse_signal(&system, "RtMin+5", false),
359 Some(system.sigrt_range().unwrap().start().as_raw() + 5)
360 );
361 assert_eq!(parse_signal(&system, "SigRtMin+5", false), None);
362 }
363
364 #[test]
365 fn parse_signal_names_with_sig_prefix() {
366 let system = VirtualSystem::new();
367 assert_eq!(
368 parse_signal(&system, "INT", true),
369 Some(VirtualSystem::SIGINT.as_raw())
370 );
371 assert_eq!(
372 parse_signal(&system, "RtMin+5", true),
373 Some(system.sigrt_range().unwrap().start().as_raw() + 5)
374 );
375 assert_eq!(
376 parse_signal(&system, "SigRtMin+5", true),
377 Some(system.sigrt_range().unwrap().start().as_raw() + 5)
378 );
379 }
380
381 #[test]
382 fn parse_signal_numbers() {
383 let system = VirtualSystem::new();
384 assert_eq!(parse_signal(&system, "0", false), Some(0));
385 assert_eq!(parse_signal(&system, "1", false), Some(1));
386 assert_eq!(parse_signal(&system, "3", true), Some(3));
387 assert_eq!(parse_signal(&system, "6", false), Some(6));
388 assert_eq!(parse_signal(&system, "9", true), Some(9));
389 assert_eq!(parse_signal(&system, "14", true), Some(14));
390 }
391
392 #[test]
393 fn parse_signal_errors() {
394 let system = VirtualSystem::new();
395 assert_eq!(parse_signal(&system, "", false), None);
396 assert_eq!(parse_signal(&system, "TERM1", false), None);
397 assert_eq!(parse_signal(&system, "1TERM", false), None);
398 }
399
400 #[test]
401 fn empty_operand() {
402 let env = Env::new_virtual();
403 let result = parse(&env, Field::dummies([""]));
404 assert_eq!(
405 result,
406 Ok(Command::Send {
407 signal: VirtualSystem::SIGTERM.as_raw(),
408 signal_origin: None,
409 targets: Field::dummies([""]),
410 })
411 )
412 }
413
414 #[test]
415 fn single_hyphen_operand() {
416 let env = Env::new_virtual();
417 let result = parse(&env, Field::dummies(["-"]));
418 assert_eq!(
419 result,
420 Ok(Command::Send {
421 signal: VirtualSystem::SIGTERM.as_raw(),
422 signal_origin: None,
423 targets: Field::dummies(["-"]),
424 })
425 );
426 }
427
428 #[test]
429 fn double_hyphen_separator() {
430 let env = Env::new_virtual();
431
432 let result = parse(&env, Field::dummies(["-s", "INT", "--", "0"]));
433 assert_eq!(
434 result,
435 Ok(Command::Send {
436 signal: VirtualSystem::SIGINT.as_raw(),
437 signal_origin: Some(Field::dummy("INT")),
438 targets: Field::dummies(["0"]),
439 })
440 );
441
442 let result = parse(&env, Field::dummies(["-l", "--", "9"]));
443 assert_eq!(
444 result,
445 Ok(Command::Print {
446 signals: Field::dummies(["9"]),
447 verbose: false,
448 })
449 );
450 }
451
452 #[test]
453 fn option_s_with_separate_signal_name_argument() {
454 let env = Env::new_virtual();
455 let result = parse(&env, Field::dummies(["-s", "QuIt", "1"]));
456 assert_eq!(
457 result,
458 Ok(Command::Send {
459 signal: VirtualSystem::SIGQUIT.as_raw(),
460 signal_origin: Some(Field::dummy("QuIt")),
461 targets: Field::dummies(["1"]),
462 })
463 );
464 }
465
466 #[test]
467 fn option_s_with_adjacent_signal_name_argument() {
468 let env = Env::new_virtual();
469 let result = parse(&env, Field::dummies(["-sQuIt", "1"]));
470 assert_eq!(
471 result,
472 Ok(Command::Send {
473 signal: VirtualSystem::SIGQUIT.as_raw(),
474 signal_origin: Some(Field::dummy("-sQuIt")),
475 targets: Field::dummies(["1"]),
476 })
477 );
478 }
479
480 #[test]
481 fn option_s_with_separate_signal_number_argument() {
482 let env = Env::new_virtual();
483 let result = parse(&env, Field::dummies(["-s", "9", "1"]));
484 assert_eq!(
485 result,
486 Ok(Command::Send {
487 signal: 9,
488 signal_origin: Some(Field::dummy("9")),
489 targets: Field::dummies(["1"]),
490 })
491 );
492 }
493
494 #[test]
495 fn option_n_with_separate_signal_name_argument() {
496 let env = Env::new_virtual();
497 let result = parse(&env, Field::dummies(["-n", "QuIt", "1"]));
498 assert_eq!(
499 result,
500 Ok(Command::Send {
501 signal: VirtualSystem::SIGQUIT.as_raw(),
502 signal_origin: Some(Field::dummy("QuIt")),
503 targets: Field::dummies(["1"]),
504 })
505 );
506 }
507
508 #[test]
509 fn bare_signal_name_in_uppercase() {
510 let env = Env::new_virtual();
511 let result = parse(&env, Field::dummies(["-KILL", "1"]));
512 assert_eq!(
513 result,
514 Ok(Command::Send {
515 signal: VirtualSystem::SIGKILL.as_raw(),
516 signal_origin: Some(Field::dummy("-KILL")),
517 targets: Field::dummies(["1"]),
518 })
519 );
520 }
521
522 #[test]
523 fn bare_signal_name_starting_with_s() {
524 let env = Env::new_virtual();
525 let result = parse(&env, Field::dummies(["-stop", "1"]));
526 assert_eq!(
527 result,
528 Ok(Command::Send {
529 signal: VirtualSystem::SIGSTOP.as_raw(),
530 signal_origin: Some(Field::dummy("-stop")),
531 targets: Field::dummies(["1"]),
532 })
533 );
534 }
535
536 #[test]
537 fn base_signal_number() {
538 let env = Env::new_virtual();
539 let result = parse(&env, Field::dummies(["-9", "1"]));
540 assert_eq!(
541 result,
542 Ok(Command::Send {
543 signal: 9,
544 signal_origin: Some(Field::dummy("-9")),
545 targets: Field::dummies(["1"]),
546 })
547 );
548 }
549
550 #[test]
551 fn option_l_without_operands() {
552 let env = Env::new_virtual();
553 let result = parse(&env, Field::dummies(["-l"]));
554 assert_eq!(
555 result,
556 Ok(Command::Print {
557 signals: vec![],
558 verbose: false,
559 })
560 );
561 }
562
563 #[test]
564 fn option_v_without_operands() {
565 let env = Env::new_virtual();
566 let result = parse(&env, Field::dummies(["-v"]));
567 assert_eq!(
568 result,
569 Ok(Command::Print {
570 signals: vec![],
571 verbose: true,
572 })
573 );
574 }
575
576 #[test]
577 fn option_l_and_v_combined() {
578 let env = Env::new_virtual();
579 let expected_result = Ok(Command::Print {
580 signals: vec![],
581 verbose: true,
582 });
583
584 assert_eq!(parse(&env, Field::dummies(["-lv"])), expected_result);
585 assert_eq!(parse(&env, Field::dummies(["-vl"])), expected_result);
586 assert_eq!(parse(&env, Field::dummies(["-l", "-v"])), expected_result);
587 assert_eq!(parse(&env, Field::dummies(["-v", "-l"])), expected_result);
588 }
589
590 #[test]
591 fn option_l_with_operands() {
592 let env = Env::new_virtual();
593 let result = parse(&env, Field::dummies(["-l", "Term", "1"]));
594 assert_eq!(
595 result,
596 Ok(Command::Print {
597 signals: Field::dummies(["Term", "1"]),
598 verbose: false,
599 })
600 );
601 }
602
603 #[test]
604 fn unknown_option() {
605 let env = Env::new_virtual();
606 let result = parse(&env, Field::dummies(["-x"]));
607 assert_eq!(result, Err(Error::UnknownOption(Field::dummy("-x"))));
608 }
609
610 #[test]
611 fn option_s_conflicts_with_option_l() {
612 let env = Env::new_virtual();
613
614 let result = parse(&env, Field::dummies(["-s", "TERM", "-l"]));
615 assert_eq!(
616 result,
617 Err(Error::ConflictingOptions {
618 signal_arg: Field::dummy("TERM"),
619 list_option_name: 'l',
620 list_option_location: Location::dummy("-l"),
621 })
622 );
623
624 let result = parse(&env, Field::dummies(["-ls", "TERM"]));
625 assert_eq!(
626 result,
627 Err(Error::ConflictingOptions {
628 signal_arg: Field::dummy("TERM"),
629 list_option_name: 'l',
630 list_option_location: Location::dummy("-ls"),
631 })
632 );
633 }
634
635 #[test]
636 fn option_n_conflicts_with_option_l() {
637 let env = Env::new_virtual();
638
639 let result = parse(&env, Field::dummies(["-n", "9", "-l"]));
640 assert_eq!(
641 result,
642 Err(Error::ConflictingOptions {
643 signal_arg: Field::dummy("9"),
644 list_option_name: 'l',
645 list_option_location: Location::dummy("-l"),
646 })
647 );
648
649 let result = parse(&env, Field::dummies(["-ln", "9"]));
650 assert_eq!(
651 result,
652 Err(Error::ConflictingOptions {
653 signal_arg: Field::dummy("9"),
654 list_option_name: 'l',
655 list_option_location: Location::dummy("-ln"),
656 })
657 );
658 }
659
660 #[test]
661 fn option_s_conflicts_with_option_v() {
662 let env = Env::new_virtual();
663
664 let result = parse(&env, Field::dummies(["-s", "TERM", "-v"]));
665 assert_eq!(
666 result,
667 Err(Error::ConflictingOptions {
668 signal_arg: Field::dummy("TERM"),
669 list_option_name: 'v',
670 list_option_location: Location::dummy("-v"),
671 })
672 );
673
674 let result = parse(&env, Field::dummies(["-lvls", "TERM"]));
675 assert_eq!(
676 result,
677 Err(Error::ConflictingOptions {
678 signal_arg: Field::dummy("TERM"),
679 list_option_name: 'v',
680 list_option_location: Location::dummy("-lvls"),
681 })
682 );
683 }
684
685 #[test]
686 fn option_n_conflicts_with_option_v() {
687 let env = Env::new_virtual();
688
689 let result = parse(&env, Field::dummies(["-n", "9", "-v"]));
690 assert_eq!(
691 result,
692 Err(Error::ConflictingOptions {
693 signal_arg: Field::dummy("9"),
694 list_option_name: 'v',
695 list_option_location: Location::dummy("-v"),
696 })
697 );
698
699 let result = parse(&env, Field::dummies(["-lvln", "9"]));
700 assert_eq!(
701 result,
702 Err(Error::ConflictingOptions {
703 signal_arg: Field::dummy("9"),
704 list_option_name: 'v',
705 list_option_location: Location::dummy("-lvln"),
706 })
707 );
708 }
709
710 #[test]
711 fn option_s_without_signal() {
712 let env = Env::new_virtual();
713 let result = parse(&env, Field::dummies(["-s"]));
714 assert_eq!(
715 result,
716 Err(Error::MissingSignal {
717 signal_option_name: 's',
718 signal_option_location: Location::dummy("-s"),
719 })
720 );
721 }
722
723 #[test]
724 fn option_n_without_signal() {
725 let env = Env::new_virtual();
726 let result = parse(&env, Field::dummies(["-n"]));
727 assert_eq!(
728 result,
729 Err(Error::MissingSignal {
730 signal_option_name: 'n',
731 signal_option_location: Location::dummy("-n"),
732 })
733 );
734 }
735
736 #[test]
737 fn multiple_signals_error_on_option_s() {
738 let env = Env::new_virtual();
739 let result = parse(&env, Field::dummies(["-INT", "-s", "TERM"]));
740 assert_eq!(
741 result,
742 Err(Error::MultipleSignals(
743 Field::dummy("-INT"),
744 Field::dummy("TERM")
745 ))
746 );
747 }
748
749 #[test]
750 fn multiple_signals_error_on_option_n() {
751 let env = Env::new_virtual();
752 let result = parse(&env, Field::dummies(["-s", "TERM", "-nINT"]));
753 assert_eq!(
754 result,
755 Err(Error::MultipleSignals(
756 Field::dummy("TERM"),
757 Field::dummy("-nINT")
758 ))
759 );
760 }
761
762 #[test]
763 fn multiple_signals_error_on_bare_signal_name() {
764 let env = Env::new_virtual();
765 let result = parse(&env, Field::dummies(["-n", "TERM", "-QUIT"]));
766 assert_eq!(
767 result,
768 Err(Error::MultipleSignals(
769 Field::dummy("TERM"),
770 Field::dummy("-QUIT")
771 ))
772 );
773 }
774
775 #[test]
776 fn invalid_separate_signal_argument_to_option_s() {
777 let env = Env::new_virtual();
778 let result = parse(&env, Field::dummies(["-s", "TERM1", "123"]));
779 assert_eq!(result, Err(Error::InvalidSignal(Field::dummy("TERM1"))));
780 }
781
782 #[test]
783 fn invalid_separate_signal_argument_to_option_n() {
784 let env = Env::new_virtual();
785 let result = parse(&env, Field::dummies(["-n", "TERM1", "123"]));
786 assert_eq!(result, Err(Error::InvalidSignal(Field::dummy("TERM1"))));
787 }
788
789 #[test]
790 fn invalid_adjoined_signal_argument_to_option_s() {
791 let env = Env::new_virtual();
792 let result = parse(&env, Field::dummies(["-sTERM1", "123"]));
793 assert_eq!(result, Err(Error::InvalidSignal(Field::dummy("-sTERM1"))));
794 }
795
796 #[test]
797 fn missing_target() {
798 let env = Env::new_virtual();
799 let result = parse(&env, vec![]);
800 assert_eq!(result, Err(Error::MissingTarget));
801 }
802}