Skip to main content

yash_builtin/kill/
syntax.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2024 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Command line parsing
18//!
19//! This module parses command line arguments to the kill built-in.
20//! The parser is implemented without using the utilities in the
21//! [`crate::common::syntax`] crate because of the special syntax of the
22//! signal-specifying option.
23
24use 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/// Error that may occur during parsing
35#[derive(Clone, Debug, Error, PartialEq, Eq)]
36#[non_exhaustive]
37pub enum Error {
38    /// An argument starts with a hyphen (`-`) but is not a valid option.
39    #[error("unknown option")]
40    UnknownOption(Field),
41
42    /// The signal to send is specified and the `-l` or `-v` option is also
43    /// specified.
44    #[error("invalid option combination")]
45    ConflictingOptions {
46        /// Command line argument that specifies the signal to send
47        signal_arg: Field,
48        /// Name of the option that requests a list (`l` or `v`)
49        list_option_name: char,
50        /// Location of the `-l` or `-v` option
51        list_option_location: Location,
52    },
53
54    /// The `-s` or `-n` option is not followed by a signal name or number.
55    #[error("missing signal name or number")]
56    MissingSignal {
57        /// Name of the option for specifying a signal (`s` or `n`)
58        signal_option_name: char,
59        /// Location of the `-s` or `-n` option
60        signal_option_location: Location,
61    },
62
63    /// More than one signal to send is specified.
64    #[error("multiple signals specified")]
65    MultipleSignals(Field, Field),
66
67    /// A specified signal is not a valid signal name or number.
68    ///
69    /// This error is returned when the argument to the `-s` or `-n` option is
70    /// not a valid signal name or number. This error also occurs when an
71    /// operand given with the `-l` or `-v` option is not a valid signal name,
72    /// signal number, or exit status.
73    #[error("invalid signal")]
74    InvalidSignal(Field),
75
76    /// No target is specified and the `-l` or `-v` option is not specified.
77    #[error("no target process specified")]
78    MissingTarget,
79}
80
81impl Error {
82    /// Converts this error to a report
83    #[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/// Converts a string to a signal.
161///
162/// The string may be a signal name or a number.
163///
164/// If the string is a valid signal name as per [`Signals::str2sig`], this
165/// function returns the corresponding signal number. If the string is a decimal
166/// integer, this function returns its value as a signal number regardless of
167/// whether it corresponds to a valid signal. Otherwise, this function returns
168/// `None`.
169///
170/// The signal name is parsed case-insensitively.
171///
172/// If `allow_sig_prefix` is `true`, the `SIG` prefix is optional for signal
173/// names. Otherwise, the `SIG` prefix must **not** be present.
174#[must_use]
175pub fn parse_signal<S: Signals>(
176    system: &S,
177    signal_spec: &str,
178    allow_sig_prefix: bool,
179) -> Option<RawNumber> {
180    // Try parsing as a number first
181    if let Ok(number) = signal_spec.parse() {
182        return Some(number);
183    }
184
185    // Make the string uppercase for case-insensitive comparison
186    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    // Remove the SIG prefix if allowed
192    let signal_name = allow_sig_prefix
193        .then(|| signal_spec.strip_prefix("SIG"))
194        .flatten()
195        .unwrap_or(&signal_spec);
196
197    // Parse as a signal name
198    system.str2sig(signal_name).map(Number::as_raw)
199}
200
201/// Updates a signal and its origin.
202///
203/// `new_signal` is the new value of `signal`. It should be the result of
204/// [`parse_signal`]. If it is `None`, this function returns
205/// `Error::InvalidSignal(new_signal_origin)`.
206///
207/// `new_signal_origin` should be the field containing the string that was
208/// parsed to obtain `new_signal`. It is used to update `signal_origin`.
209/// However, if `signal_origin` already contains a field, this function returns
210/// `Error::MultipleSignals(signal_origin.take().unwrap(), new_signal_origin)`.
211fn 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/// Converts an invalid signal error to an unknown option error.
229#[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
237/// Parses operands after the `-l` or `-v` option, returning the final command.
238fn 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
257/// Parses command line arguments.
258pub fn parse<S: Signals>(env: &Env<S>, args: Vec<Field>) -> Result<Command, Error> {
259    let allow_sig_prefix = false; // TODO true depending on the shell option
260    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    // Parse options
267    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, &current_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    // Parse operands and compute the result
326    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        // Command::Send case
332        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}