Skip to main content

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