Skip to main content

yash_builtin/typeset/
syntax.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2023 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 argument parser for the typeset built-in
18//!
19//! There are two main functions in this module: [`parse`] and [`interpret`].
20//! The former parses command line arguments into [`OptionOccurrence`]s and
21//! operands, and the latter interprets them into a [`Command`].
22
23use super::*;
24use std::iter::Peekable;
25use thiserror::Error;
26use yash_env::option::State;
27use yash_env::semantics::Field;
28use yash_env::source::Location;
29
30/// Attribute that can be set on a variable or function
31#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
32pub enum Attr {
33    ReadOnly,
34    Export,
35}
36
37/// Dummy error returned when an `Attr` cannot be converted to a `FunctionAttr`
38#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
39pub struct UnsupportedAttr;
40
41impl TryFrom<Attr> for VariableAttr {
42    // An attribute that cannot be converted to a variable attribute may be
43    // added in the future, so we don't use `Infallible` here.
44    // type Error = Infallible;
45    type Error = UnsupportedAttr;
46
47    fn try_from(attr: Attr) -> Result<Self, Self::Error> {
48        match attr {
49            Attr::ReadOnly => Ok(Self::ReadOnly),
50            Attr::Export => Ok(Self::Export),
51        }
52    }
53}
54
55impl TryFrom<Attr> for FunctionAttr {
56    type Error = UnsupportedAttr;
57
58    fn try_from(attr: Attr) -> Result<Self, Self::Error> {
59        match attr {
60            Attr::ReadOnly => Ok(Self::ReadOnly),
61            Attr::Export => Err(UnsupportedAttr),
62        }
63    }
64}
65
66/// Specification of an option
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub struct OptionSpec<'a> {
69    /// Short option name
70    pub short: char,
71    /// Long option name (not including the leading `--`)
72    pub long: &'a str,
73    /// Attribute specified by this option
74    pub attr: Option<Attr>,
75}
76
77impl std::fmt::Display for OptionSpec<'_> {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        write!(f, "-{}/--{}", self.short, self.long)
80    }
81}
82
83/// Specification of the `-f`/`--functions` option
84pub const FUNCTIONS_OPTION: OptionSpec<'static> = OptionSpec {
85    short: 'f',
86    long: "functions",
87    attr: None,
88};
89/// Specification of the `-g`/`--global` option
90pub const GLOBAL_OPTION: OptionSpec<'static> = OptionSpec {
91    short: 'g',
92    long: "global",
93    attr: None,
94};
95/// Specification of the `-p`/`--print` option
96pub const PRINT_OPTION: OptionSpec<'static> = OptionSpec {
97    short: 'p',
98    long: "print",
99    attr: None,
100};
101/// Specification of the `-r`/`--readonly` option
102pub const READONLY_OPTION: OptionSpec<'static> = OptionSpec {
103    short: 'r',
104    long: "readonly",
105    attr: Some(Attr::ReadOnly),
106};
107/// Specification of the `-x`/`--export` option
108pub const EXPORT_OPTION: OptionSpec<'static> = OptionSpec {
109    short: 'x',
110    long: "export",
111    attr: Some(Attr::Export),
112};
113/// Specification of the `-X`/`--unexport` option
114///
115/// This option is deprecated.
116pub const UNEXPORT_OPTION: OptionSpec<'static> = OptionSpec {
117    short: 'X',
118    long: "unexport",
119    attr: None,
120};
121
122/// List of all option specifications applicable to the typeset built-in
123pub const ALL_OPTIONS: &[OptionSpec<'static>] = &[
124    FUNCTIONS_OPTION,
125    GLOBAL_OPTION,
126    PRINT_OPTION,
127    READONLY_OPTION,
128    EXPORT_OPTION,
129    UNEXPORT_OPTION,
130];
131
132/// Occurrence of an option
133#[derive(Clone, Debug, Eq, PartialEq)]
134pub struct OptionOccurrence<'a> {
135    /// Specification for this option
136    pub spec: &'a OptionSpec<'a>,
137    /// Whether this option is negated
138    pub state: State,
139    /// Location of the field containing this option
140    pub location: Location,
141}
142
143/// Error in command line parsing
144#[derive(Clone, Debug, Eq, Error, PartialEq)]
145#[non_exhaustive]
146pub enum ParseError {
147    /// Short option that is not defined in the option specs
148    #[error("unknown option {0:?}")]
149    UnknownShortOption(char, Field),
150
151    /// Long option that is not defined in the option specs
152    #[error("unknown option {:?}", .0.value)]
153    UnknownLongOption(Field),
154
155    /// Long option that matches the prefix of more than one option name.
156    #[error("ambiguous option name {:?}", .0.value)]
157    AmbiguousLongOption(Field),
158
159    /// Negated short option that is not an attribute
160    #[error("option {0:?} cannot be canceled with '+'")]
161    UncancelableShortOption(char, Field),
162
163    /// Negated long option that is not an attribute
164    #[error("option {:?} cannot be canceled with '++'", .0.value)]
165    UncancelableLongOption(Field),
166}
167
168impl ParseError {
169    /// Returns the field containing the option that caused this error.
170    #[must_use]
171    pub fn field(&self) -> &Field {
172        match self {
173            ParseError::UnknownShortOption(_, field)
174            | ParseError::UnknownLongOption(field)
175            | ParseError::AmbiguousLongOption(field)
176            | ParseError::UncancelableShortOption(_, field)
177            | ParseError::UncancelableLongOption(field) => field,
178        }
179    }
180
181    /// Converts this error to a [`Report`].
182    #[must_use]
183    pub fn to_report(&self) -> Report<'_> {
184        let mut report = Report::new();
185        report.r#type = ReportType::Error;
186        report.title = self.to_string().into();
187        report.snippets =
188            Snippet::with_primary_span(&self.field().origin, self.field().value.as_str().into());
189        report
190    }
191}
192
193impl<'a> From<&'a ParseError> for Report<'a> {
194    #[inline]
195    fn from(error: &'a ParseError) -> Self {
196        error.to_report()
197    }
198}
199
200/// Tries to parse the next field in `args`.
201///
202/// Returns `Ok(true)` if the next field contained a short option, in which case
203/// the parsed field is consumed from the iterator.
204fn try_parse_short<'a, I: Iterator<Item = Field>>(
205    option_specs: &'a [OptionSpec<'a>],
206    args: &mut Peekable<I>,
207    option_occurrences: &mut Vec<OptionOccurrence<'a>>,
208) -> Result<bool, ParseError> {
209    let field = match args.peek() {
210        Some(field) => field,
211        None => return Ok(false),
212    };
213    let mut chars = field.value.chars();
214    let negate = match chars.next() {
215        Some('-') => false,
216        Some('+') => true,
217        _ => return Ok(false),
218    };
219    match chars.next() {
220        Some('-') if !negate => return Ok(false),
221        Some('+') if negate => return Ok(false),
222        None => return Ok(false),
223        _ => (),
224    }
225
226    let field = args.next().unwrap();
227    for c in field.value.chars().skip(1) {
228        let spec = match option_specs.iter().find(|spec| spec.short == c) {
229            Some(spec) => spec,
230            None => return Err(ParseError::UnknownShortOption(c, field)),
231        };
232        if negate && spec.attr.is_none() {
233            return Err(ParseError::UncancelableShortOption(c, field));
234        }
235        option_occurrences.push(OptionOccurrence {
236            spec,
237            state: if negate { State::Off } else { State::On },
238            location: field.origin.clone(),
239        });
240    }
241    Ok(true)
242}
243
244/// Tries to parse and consume the next field in `args`.
245fn try_parse_long<'a, I: Iterator<Item = Field>>(
246    option_specs: &'a [OptionSpec<'a>],
247    args: &mut Peekable<I>,
248) -> Result<Option<OptionOccurrence<'a>>, ParseError> {
249    let field = match args.peek() {
250        Some(field) => field,
251        None => return Ok(None),
252    };
253
254    let (name, negate) = if let Some(name) = field.value.strip_prefix("--") {
255        (name, false)
256    } else if let Some(name) = field.value.strip_prefix("++") {
257        (name, true)
258    } else {
259        return Ok(None);
260    };
261
262    let mut option_specs = option_specs
263        .iter()
264        .filter(|spec| spec.long.starts_with(name));
265    let spec = option_specs.next();
266    let spec2 = option_specs.next();
267    let field = args.next().unwrap();
268    match spec {
269        None => Err(ParseError::UnknownLongOption(field)),
270        Some(_spec) if spec2.is_some() => Err(ParseError::AmbiguousLongOption(field)),
271        Some(spec) if negate && spec.attr.is_none() => {
272            Err(ParseError::UncancelableLongOption(field))
273        }
274        Some(spec) => Ok(Some(OptionOccurrence {
275            spec,
276            state: if negate { State::Off } else { State::On },
277            location: field.origin,
278        })),
279    }
280}
281
282/// Parses command line arguments.
283///
284/// The first argument is a list of option specifications that should be
285/// recognized by the parser. The second argument is a list of command line
286/// arguments to be parsed.
287///
288/// Returns a pair of option occurrences and operands, which can be passed to
289/// [`interpret`] to get a [`Command`].
290pub fn parse<'a>(
291    option_specs: &'a [OptionSpec<'a>],
292    // TODO: mode: Mode, (disabling long options & options after operands)
293    args: Vec<Field>,
294) -> Result<(Vec<OptionOccurrence<'a>>, Vec<Field>), ParseError> {
295    let mut args = args.into_iter().peekable();
296    let mut options = Vec::new();
297    loop {
298        if args.next_if(|arg| arg.value == "--").is_some() {
299            break;
300        }
301        if try_parse_short(option_specs, &mut args, &mut options)? {
302            continue;
303        }
304        if let Some(result) = try_parse_long(option_specs, &mut args)? {
305            options.push(result);
306        } else {
307            break; // TODO option after operand
308        }
309    }
310    let operands = args.collect();
311    Ok((options, operands))
312}
313
314/// Error in interpreting command line arguments
315#[derive(Clone, Debug, Eq, Error, PartialEq)]
316#[non_exhaustive]
317pub enum InterpretError<'a> {
318    /// Short option that cannot be used with the `-f` option
319    #[error("option {} is inapplicable for function", .clashing.spec)]
320    OptionInapplicableForFunction {
321        /// Occurrence of the option that conflicts with the `-f` option
322        clashing: OptionOccurrence<'a>,
323        /// Occurrence of the `-f` option
324        function: OptionOccurrence<'a>,
325    },
326}
327
328impl InterpretError<'_> {
329    /// Converts the error to a report.
330    #[must_use]
331    pub fn to_report(&self) -> Report<'_> {
332        let Self::OptionInapplicableForFunction { clashing, function } = self;
333        let mut report = Report::new();
334        report.r#type = ReportType::Error;
335        report.title = self.to_string().into();
336        report.snippets = Snippet::with_primary_span(
337            &clashing.location,
338            format!("the {} option ...", clashing.spec).into(),
339        );
340        add_span(
341            &function.location.code,
342            Span {
343                range: function.location.byte_range(),
344                role: SpanRole::Primary {
345                    label: "... cannot be used for -f/--functions".into(),
346                },
347            },
348            &mut report.snippets,
349        );
350        report
351    }
352}
353
354impl<'a> From<&'a InterpretError<'a>> for Report<'a> {
355    #[inline]
356    fn from(error: &'a InterpretError) -> Self {
357        error.to_report()
358    }
359}
360
361/// Interprets options and operands into a command.
362///
363/// You can pass the result of [`parse`] to this function to get a command.
364///
365/// If `options` contain an `OptionSpec` that is not contained in
366/// [`ALL_OPTIONS`], this function will panic.
367pub fn interpret(
368    options: Vec<OptionOccurrence>,
369    operands: Vec<Field>,
370) -> Result<Command, InterpretError> {
371    let mut functions_option_index = None;
372    let mut global_option_index = None;
373    let mut print = operands.is_empty();
374    let mut attrs = Vec::new();
375    for (index, option) in options.iter().enumerate() {
376        match option.spec.short {
377            'f' => functions_option_index = Some(index),
378            'g' => global_option_index = Some(index),
379            'p' => print = true,
380            'X' => attrs.push((index, Attr::Export, !option.state)),
381            _ => attrs.push((index, option.spec.attr.unwrap(), option.state)),
382        }
383    }
384
385    if let Some(functions_option_index) = functions_option_index {
386        if let Some(global_option_index) = global_option_index {
387            return Err(InterpretError::OptionInapplicableForFunction {
388                clashing: options[global_option_index].clone(),
389                function: options[functions_option_index].clone(),
390            });
391        }
392
393        let functions = operands;
394        let attrs = attrs
395            .into_iter()
396            .map(|(index, attr, state)| Ok((attr.try_into().or(Err(index))?, state)))
397            .collect::<Result<Vec<(FunctionAttr, State)>, usize>>()
398            .map_err(|attr_index| InterpretError::OptionInapplicableForFunction {
399                clashing: options[attr_index].clone(),
400                function: options[functions_option_index].clone(),
401            })?;
402
403        if print {
404            Ok((PrintFunctions { functions, attrs }).into())
405        } else {
406            Ok((SetFunctions { functions, attrs }).into())
407        }
408    } else {
409        let variables = operands;
410        let attrs = attrs
411            .into_iter()
412            .map(|(_index, attr, state)| Ok((attr.try_into()?, state)))
413            .collect::<Result<Vec<(VariableAttr, State)>, UnsupportedAttr>>()
414            .expect("all attributes should be convertible to VariableAttr");
415        let scope = match global_option_index {
416            Some(_) => Scope::Global,
417            None => Scope::Local,
418        };
419
420        if print {
421            let pv = PrintVariables {
422                variables,
423                attrs,
424                scope,
425            };
426            Ok(pv.into())
427        } else {
428            let sv = SetVariables {
429                variables,
430                attrs,
431                scope,
432            };
433            Ok(sv.into())
434        }
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441    use assert_matches::assert_matches;
442
443    #[test]
444    fn parse_empty_arguments() {
445        let result = parse(&[], vec![]).unwrap();
446        assert_eq!(result, (vec![], vec![]));
447    }
448
449    #[test]
450    fn parse_some_operands_without_options() {
451        let vars = Field::dummies(["foo", "bar"]);
452        let result = parse(&[], vars.clone()).unwrap();
453        assert_eq!(result, (vec![], vars));
454    }
455
456    #[test]
457    fn parse_short_print_option_without_operands() {
458        let result = parse(ALL_OPTIONS, Field::dummies(["-p"])).unwrap();
459        assert_matches!(&result.0[..], [option] => {
460            assert_eq!(option.spec, &PRINT_OPTION);
461            assert_eq!(option.state, State::On);
462            assert_eq!(option.location, Location::dummy("-p"));
463        });
464        assert_eq!(result.1, []);
465    }
466
467    #[test]
468    fn parse_many_short_options() {
469        let args = Field::dummies(["-p", "+xr"]);
470        let result = parse(ALL_OPTIONS, args.clone()).unwrap();
471        assert_matches!(&result.0[..], [option1, option2, option3] => {
472            assert_eq!(option1.spec, &PRINT_OPTION);
473            assert_eq!(option1.state, State::On);
474            assert_eq!(option1.location, Location::dummy("-p"));
475            assert_eq!(option2.spec, &EXPORT_OPTION);
476            assert_eq!(option2.state, State::Off);
477            assert_eq!(option2.location, Location::dummy("+xr"));
478            assert_eq!(option3.spec, &READONLY_OPTION);
479            assert_eq!(option3.state, State::Off);
480            assert_eq!(option3.location, Location::dummy("+xr"));
481        });
482        assert_eq!(result.1, []);
483    }
484
485    #[test]
486    fn parse_long_print_option_without_operands() {
487        let result = parse(ALL_OPTIONS, Field::dummies(["--print"])).unwrap();
488        assert_matches!(&result.0[..], [option] => {
489            assert_eq!(option.spec, &PRINT_OPTION);
490            assert_eq!(option.state, State::On);
491            assert_eq!(option.location, Location::dummy("--print"));
492        });
493        assert_eq!(result.1, []);
494    }
495
496    #[test]
497    fn parse_print_option_with_operands() {
498        let vars = Field::dummies(["foo", "var"]);
499        let mut args = Field::dummies(["-p"]);
500        args.extend(vars.iter().cloned());
501        let result = parse(ALL_OPTIONS, args).unwrap();
502        assert_matches!(&result.0[..], [option] => {
503            assert_eq!(option.spec, &PRINT_OPTION);
504            assert_eq!(option.state, State::On);
505            assert_eq!(option.location, Location::dummy("-p"));
506        });
507        assert_eq!(result.1, vars);
508    }
509
510    #[test]
511    fn parse_abbreviated_long_option() {
512        let result = parse(ALL_OPTIONS, Field::dummies(["--pri"])).unwrap();
513        assert_matches!(&result.0[..], [option] => {
514            assert_eq!(option.spec, &PRINT_OPTION);
515            assert_eq!(option.state, State::On);
516            assert_eq!(option.location, Location::dummy("--pri"));
517        });
518        assert_eq!(result.1, []);
519    }
520
521    #[test]
522    fn parse_negated_short_export_option() {
523        let result = parse(ALL_OPTIONS, Field::dummies(["+x"])).unwrap();
524        assert_matches!(&result.0[..], [option] => {
525            assert_eq!(option.spec, &EXPORT_OPTION);
526            assert_eq!(option.state, State::Off);
527            assert_eq!(option.location, Location::dummy("+x"));
528        });
529        assert_eq!(result.1, []);
530    }
531
532    #[test]
533    fn parse_negated_long_export_option() {
534        let result = parse(ALL_OPTIONS, Field::dummies(["++export"])).unwrap();
535        assert_matches!(&result.0[..], [option] => {
536            assert_eq!(option.spec, &EXPORT_OPTION);
537            assert_eq!(option.state, State::Off);
538            assert_eq!(option.location, Location::dummy("++export"));
539        });
540        assert_eq!(result.1, []);
541    }
542
543    #[test]
544    fn parse_separator() {
545        let args = Field::dummies(["-p", "--", "-x"]);
546        let result = parse(ALL_OPTIONS, args.clone()).unwrap();
547        assert_matches!(&result.0[..], [option] => {
548            assert_eq!(option.spec, &PRINT_OPTION);
549            assert_eq!(option.state, State::On);
550            assert_eq!(option.location, Location::dummy("-p"));
551        });
552        assert_eq!(result.1, Field::dummies(["-x"]));
553    }
554
555    #[test]
556    fn parse_unknown_short_option() {
557        assert_eq!(
558            parse(&[], Field::dummies(["-p"])),
559            Err(ParseError::UnknownShortOption('p', Field::dummy("-p"))),
560        );
561    }
562
563    #[test]
564    fn parse_unknown_long_option() {
565        assert_eq!(
566            parse(&[], Field::dummies(["--print"])),
567            Err(ParseError::UnknownLongOption(Field::dummy("--print"))),
568        );
569    }
570
571    #[test]
572    fn parse_negated_short_print_option() {
573        assert_eq!(
574            parse(ALL_OPTIONS, Field::dummies(["+p"])),
575            Err(ParseError::UncancelableShortOption('p', Field::dummy("+p"))),
576        );
577    }
578
579    #[test]
580    fn parse_negated_long_print_option() {
581        assert_eq!(
582            parse(ALL_OPTIONS, Field::dummies(["++print"])),
583            Err(ParseError::UncancelableLongOption(Field::dummy("++print"))),
584        );
585    }
586
587    #[test]
588    fn parse_ambiguous_long_option() {
589        pub const EXPAND_OPTION: OptionSpec<'static> = OptionSpec {
590            short: 'x',
591            long: "expand",
592            attr: None,
593        };
594        assert_eq!(
595            parse(&[EXPORT_OPTION, EXPAND_OPTION], Field::dummies(["++exp"])),
596            Err(ParseError::AmbiguousLongOption(Field::dummy("++exp"))),
597        );
598    }
599
600    #[test]
601    fn interpret_empty_arguments() {
602        let result = interpret(vec![], vec![]).unwrap();
603        assert_matches!(result, Command::PrintVariables(pv) => {
604            assert_eq!(pv.variables, []);
605            assert_eq!(pv.attrs, []);
606            assert_eq!(pv.scope, Scope::Local);
607        });
608    }
609
610    #[test]
611    fn interpret_some_operands_without_options() {
612        let vars = Field::dummies(["foo", "bar"]);
613        let result = interpret(vec![], vars.clone()).unwrap();
614        assert_matches!(result, Command::SetVariables(sv) => {
615            assert_eq!(sv.variables, vars);
616            assert_eq!(sv.attrs, []);
617            assert_eq!(sv.scope, Scope::Local);
618        });
619    }
620
621    fn dummy_option_occurrence<'a>(spec: &'a OptionSpec<'a>, state: State) -> OptionOccurrence<'a> {
622        OptionOccurrence {
623            spec,
624            state,
625            location: Location::dummy(""),
626        }
627    }
628
629    #[test]
630    fn interpret_functions_option_without_operands() {
631        let result = interpret(
632            vec![dummy_option_occurrence(&FUNCTIONS_OPTION, State::On)],
633            vec![],
634        );
635        assert_matches!(result, Ok(Command::PrintFunctions(pf)) => {
636            assert_eq!(pf.functions, []);
637            assert_eq!(pf.attrs, []);
638        });
639    }
640
641    #[test]
642    fn interpret_functions_option_with_operands() {
643        let functions = Field::dummies(["foo", "bar"]);
644        let result = interpret(
645            vec![dummy_option_occurrence(&FUNCTIONS_OPTION, State::On)],
646            functions.clone(),
647        );
648        assert_matches!(result, Ok(Command::SetFunctions(sf)) => {
649            assert_eq!(sf.functions, functions);
650            assert_eq!(sf.attrs, []);
651        });
652    }
653
654    #[test]
655    fn interpret_global_option_without_operands() {
656        let result = interpret(
657            vec![dummy_option_occurrence(&GLOBAL_OPTION, State::On)],
658            vec![],
659        );
660        assert_matches!(result, Ok(Command::PrintVariables(pv)) => {
661            assert_eq!(pv.variables, []);
662            assert_eq!(pv.attrs, []);
663            assert_eq!(pv.scope, Scope::Global);
664        });
665    }
666
667    #[test]
668    fn interpret_global_option_with_operands() {
669        let vars = Field::dummies(["foo", "var"]);
670        let result = interpret(
671            vec![dummy_option_occurrence(&GLOBAL_OPTION, State::On)],
672            vars.clone(),
673        );
674        assert_matches!(result, Ok(Command::SetVariables(sv)) => {
675            assert_eq!(sv.variables, vars);
676            assert_eq!(sv.attrs, []);
677            assert_eq!(sv.scope, Scope::Global);
678        });
679    }
680
681    #[test]
682    fn interpret_print_option_without_operands() {
683        let result = interpret(
684            vec![dummy_option_occurrence(&PRINT_OPTION, State::On)],
685            vec![],
686        );
687        assert_matches!(result, Ok(Command::PrintVariables(pv)) => {
688            assert_eq!(pv.variables, []);
689            assert_eq!(pv.attrs, []);
690            assert_eq!(pv.scope, Scope::Local);
691        });
692    }
693
694    #[test]
695    fn interpret_print_option_with_operands() {
696        let vars = Field::dummies(["foo", "var"]);
697        let result = interpret(
698            vec![dummy_option_occurrence(&PRINT_OPTION, State::On)],
699            vars.clone(),
700        );
701        assert_matches!(result, Ok(Command::PrintVariables(pv)) => {
702            assert_eq!(pv.variables, vars);
703            assert_eq!(pv.attrs, []);
704            assert_eq!(pv.scope, Scope::Local);
705        });
706    }
707
708    #[test]
709    fn interpret_negated_export_option_without_operands() {
710        let result = interpret(
711            vec![dummy_option_occurrence(&EXPORT_OPTION, State::Off)],
712            vec![],
713        );
714        assert_matches!(result, Ok(Command::PrintVariables(pv)) => {
715            assert_eq!(pv.variables, []);
716            assert_eq!(pv.attrs, [(VariableAttr::Export, State::Off)]);
717            assert_eq!(pv.scope, Scope::Local);
718        });
719    }
720
721    #[test]
722    fn interpret_negated_export_option_with_operands() {
723        let vars = Field::dummies(["foo", "bar"]);
724        let result = interpret(
725            vec![dummy_option_occurrence(&EXPORT_OPTION, State::Off)],
726            vars.clone(),
727        );
728        assert_matches!(result, Ok(Command::SetVariables(sv)) => {
729            assert_eq!(sv.variables, vars);
730            assert_eq!(sv.attrs, [(VariableAttr::Export, State::Off)]);
731            assert_eq!(sv.scope, Scope::Local);
732        });
733    }
734
735    #[test]
736    fn interpret_function_names_for_printing() {
737        let functions = Field::dummies(["foo", "bar"]);
738        let result = interpret(
739            vec![
740                dummy_option_occurrence(&FUNCTIONS_OPTION, State::On),
741                dummy_option_occurrence(&PRINT_OPTION, State::On),
742            ],
743            functions.clone(),
744        );
745        assert_matches!(result, Ok(Command::PrintFunctions(pf)) => {
746            assert_eq!(pf.functions, functions);
747            assert_eq!(pf.attrs, []);
748        });
749    }
750
751    #[test]
752    fn interpret_function_attributes_for_printing() {
753        let result = interpret(
754            vec![
755                dummy_option_occurrence(&FUNCTIONS_OPTION, State::On),
756                dummy_option_occurrence(&PRINT_OPTION, State::On),
757                dummy_option_occurrence(&READONLY_OPTION, State::Off),
758            ],
759            vec![],
760        );
761        assert_matches!(result, Ok(Command::PrintFunctions(pf)) => {
762            assert_eq!(pf.functions, vec![]);
763            assert_eq!(pf.attrs, [(FunctionAttr::ReadOnly, State::Off)]);
764        });
765    }
766
767    #[test]
768    fn interpret_function_attributes_for_setting() {
769        let functions = Field::dummies(["func"]);
770        let result = interpret(
771            vec![
772                dummy_option_occurrence(&FUNCTIONS_OPTION, State::On),
773                dummy_option_occurrence(&READONLY_OPTION, State::On),
774            ],
775            functions.clone(),
776        );
777        assert_matches!(result, Ok(Command::SetFunctions(sf)) => {
778            assert_eq!(sf.functions, functions);
779            assert_eq!(sf.attrs, [(FunctionAttr::ReadOnly, State::On)]);
780        });
781    }
782
783    #[test]
784    fn interpret_inapplicable_attribute_option_for_functions() {
785        let f_option = dummy_option_occurrence(&FUNCTIONS_OPTION, State::On);
786        let x_option = dummy_option_occurrence(&EXPORT_OPTION, State::On);
787        let result = interpret(vec![f_option.clone(), x_option.clone()], vec![]);
788        assert_eq!(
789            result,
790            Err(InterpretError::OptionInapplicableForFunction {
791                clashing: x_option,
792                function: f_option,
793            }),
794        );
795    }
796
797    #[test]
798    fn interpret_global_option_with_functions_option() {
799        let f_option = dummy_option_occurrence(&FUNCTIONS_OPTION, State::On);
800        let g_option = dummy_option_occurrence(&GLOBAL_OPTION, State::On);
801        let result = interpret(vec![f_option.clone(), g_option.clone()], vec![]);
802        assert_eq!(
803            result,
804            Err(InterpretError::OptionInapplicableForFunction {
805                clashing: g_option,
806                function: f_option,
807            }),
808        );
809    }
810
811    #[test]
812    fn interpret_unexport_option_for_variables() {
813        let result = interpret(
814            vec![dummy_option_occurrence(&UNEXPORT_OPTION, State::On)],
815            vec![],
816        );
817        assert_matches!(result, Ok(Command::PrintVariables(pv)) => {
818            assert_eq!(pv.variables, vec![]);
819            assert_eq!(pv.attrs, [(VariableAttr::Export, State::Off)]);
820            assert_eq!(pv.scope, Scope::Local);
821        });
822    }
823}