yash_semantics/expansion/initial/param/
switch.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2022 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//! Parameter expansion switch semantics
18
19use super::Env;
20use super::Error;
21use super::Phrase;
22use super::to_field;
23use crate::expansion::AssignReadOnlyError;
24use crate::expansion::ErrorCause;
25use crate::expansion::attr::Origin;
26use crate::expansion::attr_strip::Strip;
27use crate::expansion::expand_word;
28use crate::expansion::initial::Expand as _;
29use crate::expansion::quote_removal::skip_quotes;
30use yash_env::variable::Scope;
31use yash_env::variable::Value;
32use yash_syntax::source::Location;
33use yash_syntax::syntax::Param;
34use yash_syntax::syntax::ParamType;
35use yash_syntax::syntax::Switch;
36use yash_syntax::syntax::SwitchAction;
37use yash_syntax::syntax::SwitchCondition;
38use yash_syntax::syntax::Word;
39
40/// Subdivision of [value](Value) states that may be considered as "not set"
41#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
42#[non_exhaustive]
43pub enum Vacancy {
44    /// The variable is not set.
45    Unset,
46    /// The value is a scalar with no characters.
47    EmptyScalar,
48    /// The value is an array with no elements.
49    ValuelessArray,
50    /// The value is an array with one element containing no characters.
51    EmptyValueArray,
52}
53
54impl Vacancy {
55    /// Evaluates the vacancy of a value.
56    ///
57    /// Returns `None` if the value does not fall into any of the `Vacancy`
58    /// categories.
59    #[inline]
60    #[must_use]
61    pub fn of<'a, I: Into<Option<&'a Value>>>(value: I) -> Option<Vacancy> {
62        fn inner(value: Option<&Value>) -> Option<Vacancy> {
63            use Vacancy::*;
64            match value {
65                None => Some(Unset),
66                Some(Value::Scalar(scalar)) if scalar.is_empty() => Some(EmptyScalar),
67                Some(Value::Array(array)) if array.is_empty() => Some(ValuelessArray),
68                Some(Value::Array(array)) if array.len() == 1 && array[0].is_empty() => {
69                    Some(EmptyValueArray)
70                }
71                Some(_) => None,
72            }
73        }
74        inner(value.into())
75    }
76
77    pub fn description(&self) -> &'static str {
78        use Vacancy::*;
79        match self {
80            Unset => "unset variable",
81            EmptyScalar => "empty string",
82            ValuelessArray => "empty array",
83            EmptyValueArray => "array with empty string",
84        }
85    }
86}
87
88impl std::fmt::Display for Vacancy {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        self.description().fmt(f)
91    }
92}
93
94/// Error caused by a [`Switch`] of [`SwitchAction::Error`]
95///
96/// `VacantError` is an error that is returned when you apply an error switch to
97/// a [vacant](Vacancy) value.
98#[derive(Clone, Debug, Eq, Error, Hash, PartialEq)]
99#[error("{} ({}: {})", self.message_or_default(), .param, .vacancy)]
100#[non_exhaustive]
101pub struct VacantError {
102    /// Parameter that caused this error
103    pub param: Param,
104    /// State of the parameter value that caused this error
105    pub vacancy: Vacancy,
106    /// Error message specified in the switch
107    pub message: Option<String>,
108}
109
110impl VacantError {
111    /// Returns the message.
112    ///
113    /// If `self.message` is `Some(_)`, its content is returned. Otherwise, the
114    /// default message is returned.
115    #[must_use]
116    pub fn message_or_default(&self) -> &str {
117        self.message
118            .as_deref()
119            .unwrap_or("parameter expansion with empty value")
120    }
121}
122
123/// Error caused by an assign switch
124#[derive(Clone, Debug, Eq, Error, Hash, PartialEq)]
125#[error("{cause}")]
126pub struct NonassignableError {
127    /// Cause of the error
128    pub cause: NonassignableErrorCause,
129    /// State of the parameter value that caused an attempt to assign an
130    /// alternate value that resulted in this error
131    pub vacancy: Vacancy,
132}
133
134#[derive(Clone, Debug, Eq, Error, Hash, PartialEq)]
135#[non_exhaustive]
136pub enum NonassignableErrorCause {
137    /// The parameter is not a variable.
138    #[error("parameter `{param}` is not an assignable variable")]
139    NotVariable { param: Param },
140    // /// The parameter expansion refers to an array but does not index a single
141    // /// element.
142    // #[error("cannot assign to a non-scalar array range")]
143    // TODO ArrayIndex,
144
145    // /// The parameter expansion is nested.
146    // #[error("cannot assign to a nested parameter expansion")]
147    // TODO Nested,
148}
149
150/// Abstract state of a [value](Value) that determines the effect of a switch
151#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
152enum ValueCondition {
153    Occupied,
154    Vacant(Vacancy),
155}
156
157impl ValueCondition {
158    fn with<V: Into<Option<Vacancy>>>(cond: SwitchCondition, vacancy: V) -> Self {
159        fn inner(cond: SwitchCondition, vacancy: Option<Vacancy>) -> ValueCondition {
160            match (cond, vacancy) {
161                (_, None) => ValueCondition::Occupied,
162
163                (SwitchCondition::UnsetOrEmpty, Some(vacancy)) => ValueCondition::Vacant(vacancy),
164
165                (_, Some(Vacancy::Unset)) => ValueCondition::Vacant(Vacancy::Unset),
166
167                (
168                    SwitchCondition::Unset,
169                    Some(Vacancy::EmptyScalar | Vacancy::ValuelessArray | Vacancy::EmptyValueArray),
170                ) => ValueCondition::Occupied,
171            }
172        }
173        inner(cond, vacancy.into())
174    }
175}
176
177/// Modifies the origin of characters in the phrase to `SoftExpansion`.
178///
179/// This function updates the result of [`expand`]. Since the switch modifier is
180/// part of a parameter expansion, the substitution produced by the switch
181/// should be regarded as originating from a parameter expansion.
182fn attribute(mut phrase: Phrase) -> Phrase {
183    phrase.for_each_char_mut(|c| match c.origin {
184        Origin::Literal => c.origin = Origin::SoftExpansion,
185        Origin::HardExpansion | Origin::SoftExpansion => (),
186    });
187    phrase
188}
189
190/// Assigns the expansion of `value` to variable `name`.
191///
192/// As specified in the POSIX standard, this function expands the `value` and
193/// performs quote removal. The result is assigned to the variable `name` in the
194/// global scope and returned as a [`Phrase`].
195async fn assign(
196    env: &mut Env<'_>,
197    param: &Param,
198    vacancy: Vacancy,
199    value: &Word,
200    location: Location,
201) -> Result<Phrase, Error> {
202    // TODO Support assignment to an array element
203    if param.r#type != ParamType::Variable {
204        let param = param.clone();
205        let cause = NonassignableErrorCause::NotVariable { param };
206        let cause = ErrorCause::NonassignableParameter(NonassignableError { cause, vacancy });
207        return Err(Error { cause, location });
208    }
209    let value_phrase = attribute(value.expand(env).await?);
210    let joined_value = value_phrase.ifs_join(&env.inner.variables);
211    let final_value = skip_quotes(joined_value).strip().collect::<String>();
212    let result = to_field(&final_value).into();
213    env.inner
214        .get_or_create_variable(&param.id, Scope::Global)
215        .assign(final_value, location)
216        .map_err(|e| {
217            let location = e.assigned_location.unwrap();
218            let cause = ErrorCause::AssignReadOnly(AssignReadOnlyError {
219                name: param.id.to_owned(),
220                new_value: e.new_value,
221                read_only_location: e.read_only_location,
222                vacancy: Some(vacancy),
223            });
224            Error { cause, location }
225        })?;
226    Ok(result)
227}
228
229/// Expands a word to be used as a vacant expansion error message.
230async fn vacant_expansion_error_message(
231    env: &mut Env<'_>,
232    message_word: &Word,
233) -> Result<Option<String>, Error> {
234    if message_word.units.is_empty() {
235        return Ok(None);
236    }
237
238    let (message_field, exit_status) = expand_word(env.inner, message_word).await?;
239    if exit_status.is_some() {
240        env.last_command_subst_exit_status = exit_status;
241    }
242    Ok(Some(message_field.value))
243}
244
245/// Constructs a vacant expansion error.
246async fn vacant_expansion_error(
247    env: &mut Env<'_>,
248    param: &Param,
249    vacancy: Vacancy,
250    message_word: &Word,
251    location: Location,
252) -> Error {
253    let message = match vacant_expansion_error_message(env, message_word).await {
254        Ok(message) => message,
255        Err(error) => return error,
256    };
257    let cause = ErrorCause::VacantExpansion(VacantError {
258        param: param.clone(),
259        vacancy,
260        message,
261    });
262    Error { cause, location }
263}
264
265/// Applies a switch.
266///
267/// If this function returns `Some(_)`, that should be the result of the whole
268/// parameter expansion containing the switch. Otherwise, the parameter
269/// expansion should continue processing other modifiers.
270pub async fn apply(
271    env: &mut Env<'_>,
272    switch: &Switch,
273    param: &Param,
274    value: Option<&Value>,
275    location: &Location,
276) -> Option<Result<Phrase, Error>> {
277    use SwitchAction::*;
278    use ValueCondition::*;
279    let cond = ValueCondition::with(switch.condition, Vacancy::of(value));
280    match (switch.action, cond) {
281        (Alter, Vacant(_)) | (Default, Occupied) | (Assign, Occupied) | (Error, Occupied) => None,
282
283        (Alter, Occupied) | (Default, Vacant(_)) => {
284            Some(switch.word.expand(env).await.map(attribute))
285        }
286
287        (Assign, Vacant(vacancy)) => {
288            Some(assign(env, param, vacancy, &switch.word, location.clone()).await)
289        }
290
291        (Error, Vacant(vacancy)) => Some(Err(vacant_expansion_error(
292            env,
293            param,
294            vacancy,
295            &switch.word,
296            location.clone(),
297        )
298        .await)),
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::super::to_field;
305    use super::*;
306    use crate::expansion::attr::AttrChar;
307    use assert_matches::assert_matches;
308    use futures_util::FutureExt;
309    use yash_env::variable::IFS;
310    use yash_syntax::syntax::SpecialParam;
311    use yash_syntax::syntax::SwitchAction::*;
312    use yash_syntax::syntax::SwitchCondition::*;
313
314    #[test]
315    fn vacancy_of_values() {
316        let vacancy = Vacancy::of(&None);
317        assert_eq!(vacancy, Some(Vacancy::Unset));
318        let vacancy = Vacancy::of(&Some(Value::scalar("")));
319        assert_eq!(vacancy, Some(Vacancy::EmptyScalar));
320        let vacancy = Vacancy::of(&Some(Value::scalar(".")));
321        assert_eq!(vacancy, None);
322        let vacancy = Vacancy::of(&Some(Value::Array(vec![])));
323        assert_eq!(vacancy, Some(Vacancy::ValuelessArray));
324        let vacancy = Vacancy::of(&Some(Value::array([""])));
325        assert_eq!(vacancy, Some(Vacancy::EmptyValueArray));
326        let vacancy = Vacancy::of(&Some(Value::array(["."])));
327        assert_eq!(vacancy, None);
328        let vacancy = Vacancy::of(&Some(Value::array(["", ""])));
329        assert_eq!(vacancy, None);
330    }
331
332    #[test]
333    fn attributing() {
334        let phrase = Phrase::Field(vec![
335            AttrChar {
336                value: 'a',
337                origin: Origin::Literal,
338                is_quoted: false,
339                is_quoting: false,
340            },
341            AttrChar {
342                value: 'b',
343                origin: Origin::SoftExpansion,
344                is_quoted: false,
345                is_quoting: false,
346            },
347            AttrChar {
348                value: 'c',
349                origin: Origin::HardExpansion,
350                is_quoted: false,
351                is_quoting: false,
352            },
353        ]);
354
355        let phrase = attribute(phrase);
356        assert_eq!(
357            phrase,
358            Phrase::Field(vec![
359                AttrChar {
360                    value: 'a',
361                    origin: Origin::SoftExpansion,
362                    is_quoted: false,
363                    is_quoting: false,
364                },
365                AttrChar {
366                    value: 'b',
367                    origin: Origin::SoftExpansion,
368                    is_quoted: false,
369                    is_quoting: false,
370                },
371                AttrChar {
372                    value: 'c',
373                    origin: Origin::HardExpansion,
374                    is_quoted: false,
375                    is_quoting: false,
376                },
377            ])
378        );
379    }
380
381    #[test]
382    fn alter_with_vacant_value() {
383        let mut env = yash_env::Env::new_virtual();
384        let mut env = Env::new(&mut env);
385        let switch = Switch {
386            action: Alter,
387            condition: Unset,
388            word: "foo".parse().unwrap(),
389        };
390        let param = Param::variable("var");
391        let location = Location::dummy("somewhere");
392        let result = apply(&mut env, &switch, &param, None, &location)
393            .now_or_never()
394            .unwrap();
395        assert_eq!(result, None);
396    }
397
398    #[test]
399    fn alter_with_occupied_value() {
400        let mut env = yash_env::Env::new_virtual();
401        let mut env = Env::new(&mut env);
402        let switch = Switch {
403            action: Alter,
404            condition: Unset,
405            word: "foo".parse().unwrap(),
406        };
407        let param = Param::variable("var");
408        let value = Value::scalar("bar");
409        let location = Location::dummy("somewhere");
410        let result = apply(&mut env, &switch, &param, Some(&value), &location)
411            .now_or_never()
412            .unwrap();
413        assert_eq!(result, Some(Ok(Phrase::Field(to_field("foo")))));
414    }
415
416    #[test]
417    fn default_with_vacant_value() {
418        let mut env = yash_env::Env::new_virtual();
419        let mut env = Env::new(&mut env);
420        let switch = Switch {
421            action: Default,
422            condition: Unset,
423            word: "foo".parse().unwrap(),
424        };
425        let param = Param::variable("var");
426        let location = Location::dummy("somewhere");
427        let result = apply(&mut env, &switch, &param, None, &location)
428            .now_or_never()
429            .unwrap();
430        assert_eq!(result, Some(Ok(Phrase::Field(to_field("foo")))));
431    }
432
433    #[test]
434    fn default_with_occupied_value() {
435        let mut env = yash_env::Env::new_virtual();
436        let mut env = Env::new(&mut env);
437        let switch = Switch {
438            action: Default,
439            condition: Unset,
440            word: "foo".parse().unwrap(),
441        };
442        let param = Param::variable("var");
443        let value = Value::scalar("bar");
444        let location = Location::dummy("somewhere");
445        let result = apply(&mut env, &switch, &param, Some(&value), &location)
446            .now_or_never()
447            .unwrap();
448        assert_eq!(result, None);
449    }
450
451    #[test]
452    fn assign_with_vacant_value() {
453        let mut env = yash_env::Env::new_virtual();
454        let mut env = Env::new(&mut env);
455        let switch = Switch {
456            action: Assign,
457            condition: Unset,
458            word: "foo".parse().unwrap(),
459        };
460        let param = Param::variable("var");
461        let location = Location::dummy("somewhere");
462
463        let result = apply(&mut env, &switch, &param, None, &location)
464            .now_or_never()
465            .unwrap();
466        assert_eq!(result, Some(Ok(Phrase::Field(to_field("foo")))));
467
468        let var = env.inner.variables.get("var").unwrap();
469        assert_eq!(var.value, Some(Value::scalar("foo")));
470        assert_eq!(var.last_assigned_location, Some(location));
471        assert!(!var.is_exported);
472        assert_eq!(var.read_only_location, None);
473    }
474
475    #[test]
476    fn assign_array_word() {
477        let mut env = yash_env::Env::new_virtual();
478        env.variables.positional_params_mut().values =
479            vec!["1".to_string(), "2  2".to_string(), "3".to_string()];
480        env.variables
481            .get_or_new(IFS, Scope::Global)
482            .assign("~", None)
483            .unwrap();
484        let mut env = Env::new(&mut env);
485        let switch = Switch {
486            action: Assign,
487            condition: Unset,
488            word: "\"$@\"".parse().unwrap(),
489        };
490        let param = Param::variable("var");
491        let location = Location::dummy("somewhere");
492
493        let result = apply(&mut env, &switch, &param, None, &location)
494            .now_or_never()
495            .unwrap();
496
497        fn char(value: char) -> AttrChar {
498            AttrChar {
499                value,
500                origin: Origin::SoftExpansion,
501                is_quoted: false,
502                is_quoting: false,
503            }
504        }
505        assert_eq!(
506            result,
507            Some(Ok(Phrase::Field(vec![
508                char('1'),
509                char('~'),
510                char('2'),
511                char(' '),
512                char(' '),
513                char('2'),
514                char('~'),
515                char('3'),
516            ])))
517        );
518
519        let var = env.inner.variables.get("var").unwrap();
520        assert_eq!(var.value, Some(Value::scalar("1~2  2~3")));
521        assert_eq!(var.last_assigned_location, Some(location));
522        assert!(!var.is_exported);
523        assert_eq!(var.read_only_location, None);
524    }
525
526    // TODO assign_with_array_index
527
528    #[test]
529    fn assign_with_occupied_value() {
530        let mut env = yash_env::Env::new_virtual();
531        let mut env = Env::new(&mut env);
532        let switch = Switch {
533            action: Assign,
534            condition: Unset,
535            word: "foo".parse().unwrap(),
536        };
537        let param = Param::variable("var");
538        let value = Value::scalar("bar");
539        let location = Location::dummy("somewhere");
540        let result = apply(&mut env, &switch, &param, Some(&value), &location)
541            .now_or_never()
542            .unwrap();
543        assert_eq!(result, None);
544    }
545
546    #[test]
547    fn assign_with_read_only_variable() {
548        let mut env = yash_env::Env::new_virtual();
549        let mut var = env.variables.get_or_new("var", Scope::Global);
550        var.assign("", None).unwrap();
551        var.make_read_only(Location::dummy("read-only"));
552        let save_var = var.clone();
553        let mut env = Env::new(&mut env);
554        let switch = Switch {
555            action: Assign,
556            condition: UnsetOrEmpty,
557            word: "foo".parse().unwrap(),
558        };
559        let param = Param::variable("var");
560        let value = save_var.value.as_ref();
561        let location = Location::dummy("somewhere");
562
563        let result = apply(&mut env, &switch, &param, value, &location)
564            .now_or_never()
565            .unwrap();
566        assert_matches!(result, Some(Err(error)) => {
567            assert_eq!(error.location, location);
568            assert_matches!(error.cause, ErrorCause::AssignReadOnly(e) => {
569                assert_eq!(e.name, "var");
570                assert_eq!(e.new_value, Value::scalar("foo"));
571                assert_eq!(e.read_only_location, Location::dummy("read-only"));
572                assert_eq!(e.vacancy, Some(Vacancy::EmptyScalar));
573            });
574        });
575        assert_eq!(env.inner.variables.get("var"), Some(&save_var));
576    }
577
578    #[test]
579    fn assign_to_special_parameter() {
580        let mut env = yash_env::Env::new_virtual();
581        let mut env = Env::new(&mut env);
582        let switch = Switch {
583            action: Assign,
584            condition: UnsetOrEmpty,
585            word: "foo".parse().unwrap(),
586        };
587        let param = Param::from(SpecialParam::Hyphen);
588        let value = Value::scalar("");
589        let location = Location::dummy("somewhere");
590
591        let result = apply(&mut env, &switch, &param, Some(&value), &location)
592            .now_or_never()
593            .unwrap();
594        let error = result.unwrap().unwrap_err();
595        assert_matches!(
596            error.cause,
597            ErrorCause::NonassignableParameter(error) => {
598                assert_eq!(error.cause, NonassignableErrorCause::NotVariable { param });
599                assert_eq!(error.vacancy, Vacancy::EmptyScalar);
600            }
601        );
602        assert_eq!(error.location, location);
603    }
604
605    #[test]
606    fn error_with_vacant_value_and_non_empty_word() {
607        let mut env = yash_env::Env::new_virtual();
608        let mut env = Env::new(&mut env);
609        let switch = Switch {
610            action: Error,
611            condition: Unset,
612            word: "foo".parse().unwrap(),
613        };
614        let param = Param::variable("var");
615        let location = Location::dummy("somewhere");
616        let result = apply(&mut env, &switch, &param, None, &location)
617            .now_or_never()
618            .unwrap();
619        let error = result.unwrap().unwrap_err();
620        assert_matches!(error.cause, ErrorCause::VacantExpansion(e) => {
621            assert_eq!(e.param, param);
622            assert_eq!(e.message, Some("foo".to_string()));
623            assert_eq!(e.vacancy, Vacancy::Unset);
624        });
625    }
626
627    #[test]
628    fn error_with_empty_scalar_and_non_empty_word() {
629        let mut env = yash_env::Env::new_virtual();
630        let mut env = Env::new(&mut env);
631        let switch = Switch {
632            action: Error,
633            condition: UnsetOrEmpty,
634            word: "bar".parse().unwrap(),
635        };
636        let param = Param::variable("var");
637        let value = Value::scalar("");
638        let location = Location::dummy("somewhere");
639        let result = apply(&mut env, &switch, &param, Some(&value), &location)
640            .now_or_never()
641            .unwrap();
642        let error = result.unwrap().unwrap_err();
643        assert_matches!(error.cause, ErrorCause::VacantExpansion(e) => {
644            assert_eq!(e.param, param);
645            assert_eq!(e.message, Some("bar".to_string()));
646            assert_eq!(e.vacancy, Vacancy::EmptyScalar);
647        });
648    }
649
650    #[test]
651    fn error_with_valueless_array_and_empty_word() {
652        let mut env = yash_env::Env::new_virtual();
653        let mut env = Env::new(&mut env);
654        let switch = Switch {
655            action: Error,
656            condition: UnsetOrEmpty,
657            word: "".parse().unwrap(),
658        };
659        let param = Param::variable("var");
660        let value = Value::Array(vec![]);
661        let location = Location::dummy("somewhere");
662        let result = apply(&mut env, &switch, &param, Some(&value), &location)
663            .now_or_never()
664            .unwrap();
665        let error = result.unwrap().unwrap_err();
666        assert_matches!(error.cause, ErrorCause::VacantExpansion(e) => {
667            assert_eq!(e.param, param);
668            assert_eq!(e.message, None);
669            assert_eq!(e.vacancy, Vacancy::ValuelessArray);
670        });
671        assert_eq!(error.location, location);
672    }
673
674    #[test]
675    fn error_with_set_value() {
676        let mut env = yash_env::Env::new_virtual();
677        let mut env = Env::new(&mut env);
678        let switch = Switch {
679            action: Error,
680            condition: Unset,
681            word: "foo".parse().unwrap(),
682        };
683        let param = Param::variable("var");
684        let value = Value::scalar("");
685        let location = Location::dummy("somewhere");
686        let result = apply(&mut env, &switch, &param, Some(&value), &location)
687            .now_or_never()
688            .unwrap();
689        assert_eq!(result, None);
690    }
691}