Skip to main content

yash_semantics/
expansion.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2021 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//! Word expansion.
18//!
19//! The word expansion involves many kinds of operations described below.
20//! The [`expand_word_multiple`] function performs all of them and produces
21//! any number of fields depending on the expanded word. The [`expand_word_attr`]
22//! and [`expand_word`] functions omit some of them to ensure that the result is
23//! a single field. Other functions in this module are provided for convenience
24//! in specific situations.
25//!
26//! # Initial expansion
27//!
28//! The [initial expansion](self::initial) is the first step of the word
29//! expansion that evaluates a [`Word`] to a [`Phrase`](self::phrase). It is
30//! performed by recursively calling [`Expand`] implementors' methods. Notable
31//! (sub)expansions that may occur in the initial expansion include the tilde
32//! expansion, parameter expansion, command substitution, and arithmetic
33//! expansion.
34//!
35//! A successful initial expansion of a word usually results in a single-field
36//! phrase. Still, it may yield any number of fields if the word contains a
37//! parameter expansion of `$@` or `$*`.
38//!
39//! # Multi-field expansion
40//!
41//! The multi-field expansion is a group of operation steps performed after the
42//! initial expansion to render final multi-field results.
43//!
44//! ## Brace expansion
45//!
46//! The brace expansion produces copies of a field containing a pair of braces.
47//! (TODO: This feature is not yet implemented.)
48//!
49//! ## Field splitting
50//!
51//! The [field splitting](split) divides a field into smaller parts delimited by
52//! a character contained in `$IFS`. Consequently, this operation removes empty
53//! fields from the results of the previous steps.
54//!
55//! ## Pathname expansion
56//!
57//! The [pathname expansion](mod@glob) performs pattern matching on the name of
58//! existing files to produce pathnames. This operation is also known as
59//! "globbing."
60//!
61//! # Quote removal and attribute stripping
62//!
63//! The [quote removal](self::quote_removal) drops characters quoting other
64//! characters, and the [attribute stripping](self::attr_strip) converts
65//! [`AttrField`]s into bare [`Field`]s. In [`expand_word_multiple`], the quote
66//! removal is performed between the field splitting and pathname expansion, and
67//! the attribute stripping is part of the pathname expansion. In
68//! [`expand_word`], they are carried out as the last step of the whole
69//! expansion.
70
71pub(crate) mod attr_fnmatch;
72pub mod glob;
73pub mod initial;
74pub mod phrase;
75
76use crate::Runtime;
77
78use self::attr::AttrChar;
79use self::attr::AttrField;
80use self::attr::Origin;
81use self::attr_strip::Strip;
82use self::glob::glob;
83use self::initial::ArithError;
84#[cfg(doc)]
85use self::initial::Expand;
86use self::initial::Expand as _;
87use self::initial::NonassignableError;
88use self::initial::Vacancy;
89use self::initial::VacantError;
90use self::quote_removal::skip_quotes;
91use self::split::Ifs;
92use std::borrow::Cow;
93use thiserror::Error;
94use yash_env::semantics::ExitStatus;
95use yash_env::system::Errno;
96use yash_env::variable::IFS;
97use yash_env::variable::Value;
98use yash_syntax::source::Location;
99use yash_syntax::source::pretty::Footnote;
100use yash_syntax::source::pretty::FootnoteType;
101use yash_syntax::source::pretty::Report;
102use yash_syntax::source::pretty::ReportType;
103use yash_syntax::source::pretty::Snippet;
104use yash_syntax::source::pretty::Span;
105use yash_syntax::source::pretty::SpanRole;
106use yash_syntax::source::pretty::add_span;
107use yash_syntax::syntax::ExpansionMode;
108use yash_syntax::syntax::Param;
109use yash_syntax::syntax::Text;
110use yash_syntax::syntax::Word;
111
112#[doc(no_inline)]
113pub use yash_env::semantics::Field;
114#[doc(no_inline)]
115pub use yash_env::semantics::expansion::{attr, attr_strip, quote_removal, split};
116
117/// Error returned on assigning to a read-only variable
118#[derive(Clone, Debug, Eq, Error, PartialEq)]
119#[error("cannot assign to read-only variable {name:?}")]
120pub struct AssignReadOnlyError {
121    /// Name of the read-only variable
122    pub name: String,
123    /// Value that was being assigned
124    pub new_value: Value,
125    /// Location where the variable was made read-only
126    pub read_only_location: Location,
127    /// State of the variable before the assignment
128    ///
129    /// If this assignment error occurred in a parameter expansion as in
130    /// `${foo=bar}` or `${foo:=bar}`, this field is `Some`, and the value is
131    /// the state of the variable before the assignment. In other cases, this
132    /// field is `None`.
133    pub vacancy: Option<Vacancy>,
134}
135
136/// Types of errors that may occur in the word expansion.
137#[derive(Clone, Debug, Eq, Error, PartialEq)]
138pub enum ErrorCause {
139    /// System error while performing a command substitution.
140    #[error("error in command substitution: {0}")]
141    CommandSubstError(Errno),
142
143    /// Error while evaluating an arithmetic expansion.
144    #[error(transparent)]
145    ArithError(#[from] ArithError),
146
147    /// Assignment to a read-only variable.
148    #[error(transparent)]
149    AssignReadOnly(#[from] AssignReadOnlyError),
150
151    /// Expansion of an unset parameter with the `nounset` option
152    #[error("unset parameter `{param}`")]
153    UnsetParameter { param: Param },
154
155    /// Expansion of an empty value with an error switch
156    #[error(transparent)]
157    VacantExpansion(#[from] VacantError),
158
159    /// Assignment to a nonassignable parameter
160    #[error(transparent)]
161    NonassignableParameter(#[from] NonassignableError),
162}
163
164impl ErrorCause {
165    /// Returns an error message describing the error.
166    #[must_use]
167    pub fn message(&self) -> &str {
168        // TODO Localize
169        use ErrorCause::*;
170        match self {
171            CommandSubstError(_) => "error performing the command substitution",
172            ArithError(_) => "error evaluating the arithmetic expansion",
173            AssignReadOnly(_) => "error assigning to variable",
174            UnsetParameter { .. } => "cannot expand unset parameter",
175            VacantExpansion(error) => error.message_or_default(),
176            NonassignableParameter(_) => "cannot assign to parameter",
177        }
178    }
179
180    /// Returns a label for annotating the error location.
181    #[must_use]
182    pub fn label(&self) -> Cow<'_, str> {
183        // TODO Localize
184        use ErrorCause::*;
185        match self {
186            CommandSubstError(e) => e.to_string(),
187            ArithError(e) => e.to_string(),
188            AssignReadOnly(e) => e.to_string(),
189            UnsetParameter { param } => format!("parameter `{param}` is not set"),
190            VacantExpansion(e) => match e.vacancy {
191                Vacancy::Unset => format!("parameter `{}` is not set", e.param),
192                Vacancy::EmptyScalar => format!("parameter `{}` is an empty string", e.param),
193                Vacancy::ValuelessArray => format!("parameter `{}` is an empty array", e.param),
194                Vacancy::EmptyValueArray => {
195                    format!("parameter `{}` is an array of an empty string", e.param)
196                }
197            },
198            NonassignableParameter(e) => e.to_string(),
199        }
200        .into()
201    }
202
203    /// Returns a location related with the error cause and a message describing
204    /// the location.
205    #[must_use]
206    pub fn related_location(&self) -> Option<(&Location, &'static str)> {
207        // TODO Localize
208        use ErrorCause::*;
209        match self {
210            CommandSubstError(_) => None,
211            ArithError(e) => e.related_location(),
212            AssignReadOnly(e) => Some((
213                &e.read_only_location,
214                "the variable was made read-only here",
215            )),
216            UnsetParameter { .. } => None,
217            VacantExpansion(_) => None,
218            NonassignableParameter(_) => None,
219        }
220    }
221
222    /// Returns a footer message for the error.
223    #[must_use]
224    pub fn footer(&self) -> Option<&'static str> {
225        use ErrorCause::*;
226        match self {
227            CommandSubstError(_)
228            | ArithError(_)
229            | AssignReadOnly(_)
230            | VacantExpansion(_)
231            | NonassignableParameter(_) => None,
232
233            UnsetParameter { .. } => Some("unset parameters are disallowed by the nounset option"),
234        }
235    }
236}
237
238/// Explanation of an expansion failure.
239#[derive(Clone, Debug, Eq, Error, PartialEq)]
240#[error("{cause}")]
241pub struct Error {
242    pub cause: ErrorCause,
243    pub location: Location,
244}
245
246impl Error {
247    /// Returns a report for the error.
248    #[must_use]
249    pub fn to_report(&self) -> Report<'_> {
250        let mut report = Report::new();
251        report.r#type = ReportType::Error;
252        report.title = self.cause.message().into();
253        report.snippets = Snippet::with_primary_span(&self.location, self.cause.label());
254
255        if let Some((location, label)) = self.cause.related_location() {
256            let label = label.into();
257            let span = Span {
258                range: location.byte_range(),
259                role: SpanRole::Supplementary { label },
260            };
261            add_span(&location.code, span, &mut report.snippets);
262        }
263
264        if let Some(footer) = self.cause.footer() {
265            report.footnotes.push(Footnote {
266                r#type: FootnoteType::Note,
267                label: footer.into(),
268            });
269        }
270
271        // Report the vacancy that caused the assignment that led to the error.
272        let vacancy = match &self.cause {
273            ErrorCause::CommandSubstError(_) => None,
274            ErrorCause::ArithError(_) => None,
275            ErrorCause::AssignReadOnly(e) => e.vacancy,
276            ErrorCause::UnsetParameter { .. } => None,
277            ErrorCause::VacantExpansion(_) => None,
278            ErrorCause::NonassignableParameter(e) => Some(e.vacancy),
279        };
280        if let Some(vacancy) = vacancy {
281            let message = match vacancy {
282                Vacancy::Unset => "assignment was attempted because the parameter was not set",
283                Vacancy::EmptyScalar => {
284                    "assignment was attempted because the parameter was an empty string"
285                }
286                Vacancy::ValuelessArray => {
287                    "assignment was attempted because the parameter was an empty array"
288                }
289                Vacancy::EmptyValueArray => {
290                    "assignment was attempted because the parameter was an array of an empty string"
291                }
292            };
293            report.footnotes.push(Footnote {
294                r#type: FootnoteType::Note,
295                label: message.into(),
296            });
297        }
298
299        report
300    }
301}
302
303/// Converts the error into a report by calling [`Error::to_report`].
304impl<'a> From<&'a Error> for Report<'a> {
305    #[inline(always)]
306    fn from(error: &'a Error) -> Self {
307        error.to_report()
308    }
309}
310
311/// Result of word expansion.
312pub type Result<T> = std::result::Result<T, Error>;
313
314/// Expands a text to a string.
315///
316/// This function performs the initial expansion, quote removal, and attribute
317/// stripping.
318/// The second field of the result tuple is the exit status of the last command
319/// substitution performed during the expansion, if any.
320pub async fn expand_text<S: Runtime + 'static>(
321    env: &mut yash_env::Env<S>,
322    text: &Text,
323) -> Result<(String, Option<ExitStatus>)> {
324    let mut env = initial::Env::new(env);
325    // It would be technically correct to set `will_split` to false, but it does
326    // not affect the final results because we will join the results anyway.
327    // env.will_split = false;
328    let phrase = text.expand(&mut env).await?;
329    let chars = phrase.ifs_join(&env.inner.variables);
330    let result = skip_quotes(chars).strip().collect();
331    Ok((result, env.last_command_subst_exit_status))
332}
333
334/// Expands a word to an attributed field.
335///
336/// This function performs initial expansion and joins the resultant phrase into
337/// a field. The second field of the result tuple is the exit status of the last
338/// command substitution performed during the expansion, if any.
339///
340/// Compare [`expand_word`] that performs not only initial expansion but also
341/// quote removal and attribute stripping.
342pub async fn expand_word_attr<S: Runtime + 'static>(
343    env: &mut yash_env::Env<S>,
344    word: &Word,
345) -> Result<(AttrField, Option<ExitStatus>)> {
346    let mut env = initial::Env::new(env);
347    // It would be technically correct to set `will_split` to false, but it does
348    // not affect the final results because we will join the results anyway.
349    // env.will_split = false;
350    let phrase = word.expand(&mut env).await?;
351    let chars = phrase.ifs_join(&env.inner.variables);
352    let origin = word.location.clone();
353    let field = AttrField { chars, origin };
354    Ok((field, env.last_command_subst_exit_status))
355}
356
357/// Expands a word to a field.
358///
359/// This function performs the initial expansion, quote removal, and attribute
360/// stripping.
361/// The second field of the result tuple is the exit status of the last command
362/// substitution performed during the expansion, if any.
363///
364/// To expand a word to an [`AttrField`] without performing quote removal or
365/// attribute stripping, use [`expand_word_attr`].
366/// To expand a word to multiple fields, use [`expand_word_multiple`].
367/// To expand multiple words to multiple fields, use [`expand_words`].
368pub async fn expand_word<S: Runtime + 'static>(
369    env: &mut yash_env::Env<S>,
370    word: &Word,
371) -> Result<(Field, Option<ExitStatus>)> {
372    let (field, exit_status) = expand_word_attr(env, word).await?;
373    let field = field.remove_quotes_and_strip();
374    Ok((field, exit_status))
375}
376
377/// Expands a word to fields.
378///
379/// This function performs the initial expansion and multi-field expansion,
380/// including quote removal and attribute stripping. The results are appended to
381/// the given collection. The return value is the exit status of the last
382/// command substitution performed during the expansion, if any.
383///
384/// To expand a single word to a single field, use [`expand_word`].
385/// To expand multiple words to fields, use [`expand_words`].
386pub async fn expand_word_multiple<S, R>(
387    env: &mut yash_env::Env<S>,
388    word: &Word,
389    results: &mut R,
390) -> Result<Option<ExitStatus>>
391where
392    S: Runtime + 'static,
393    R: Extend<Field>,
394{
395    let mut env = initial::Env::new(env);
396
397    // initial expansion //
398    let phrase = word.expand(&mut env).await?;
399
400    // TODO brace expansion //
401
402    // field splitting //
403    let ifs = env
404        .inner
405        .variables
406        .get_scalar(IFS)
407        .map(Ifs::new)
408        .unwrap_or_default();
409    let mut split_fields = Vec::with_capacity(phrase.field_count());
410    for chars in phrase {
411        let origin = word.location.clone();
412        let attr_field = AttrField { chars, origin };
413        split::split_into(attr_field, &ifs, &mut split_fields);
414    }
415    drop(ifs);
416
417    // pathname expansion (including quote removal and attribute stripping) //
418    for field in split_fields {
419        results.extend(glob(env.inner, field));
420    }
421
422    Ok(env.last_command_subst_exit_status)
423}
424
425/// Expands a word to fields.
426///
427/// This function expands a word to fields using the specified expansion mode
428/// and appends the results to the given collection.
429///
430/// If the specified mode is [`ExpansionMode::Multiple`], this function performs
431/// the initial expansion and multi-field expansion, including quote removal and
432/// attribute stripping (see [`expand_word_multiple`]). If the mode is
433/// [`ExpansionMode::Single`], this function performs the initial expansion,
434/// quote removal, and attribute stripping, but not multi-field expansion (see
435/// [`expand_word`]).
436///
437/// The results are appended to the given collection.
438pub async fn expand_word_with_mode<S, R>(
439    env: &mut yash_env::Env<S>,
440    word: &Word,
441    mode: ExpansionMode,
442    results: &mut R,
443) -> Result<Option<ExitStatus>>
444where
445    S: Runtime + 'static,
446    R: Extend<Field>,
447{
448    match mode {
449        ExpansionMode::Single => {
450            let (field, exit_status) = expand_word(env, word).await?;
451            results.extend(std::iter::once(field));
452            Ok(exit_status)
453        }
454        ExpansionMode::Multiple => expand_word_multiple(env, word, results).await,
455    }
456}
457
458/// Expands words to fields.
459///
460/// This function performs the initial expansion and multi-field expansion,
461/// including quote removal and attribute stripping.
462/// The second field of the result tuple is the exit status of the last command
463/// substitution performed during the expansion, if any.
464///
465/// To expand a single word to a single field, use [`expand_word`].
466/// To expand a single word to multiple fields, use [`expand_word_multiple`].
467pub async fn expand_words<'a, S, I>(
468    env: &mut yash_env::Env<S>,
469    words: I,
470) -> Result<(Vec<Field>, Option<ExitStatus>)>
471where
472    S: Runtime + 'static,
473    I: IntoIterator<Item = &'a Word>,
474{
475    let mut fields = Vec::new();
476    let mut last_exit_status = None;
477
478    for word in words {
479        let exit_status = expand_word_multiple(env, word, &mut fields).await?;
480        if exit_status.is_some() {
481            last_exit_status = exit_status;
482        }
483    }
484
485    Ok((fields, last_exit_status))
486}
487
488/// Expands an assignment value.
489///
490/// This function expands a [`yash_syntax::syntax::Value`] to a
491/// [`yash_env::variable::Value`]. A scalar and array value are expanded by
492/// [`expand_word`] and [`expand_words`], respectively.
493/// The second field of the result tuple is the exit status of the last command
494/// substitution performed during the expansion, if any.
495pub async fn expand_value<S: Runtime + 'static>(
496    env: &mut yash_env::Env<S>,
497    value: &yash_syntax::syntax::Value,
498) -> Result<(yash_env::variable::Value, Option<ExitStatus>)> {
499    match value {
500        yash_syntax::syntax::Scalar(word) => {
501            let (field, exit_status) = expand_word(env, word).await?;
502            Ok((yash_env::variable::Scalar(field.value), exit_status))
503        }
504        yash_syntax::syntax::Array(words) => {
505            let (fields, exit_status) = expand_words(env, words).await?;
506            let fields = fields.into_iter().map(|f| f.value).collect();
507            Ok((yash_env::variable::Array(fields), exit_status))
508        }
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515    use crate::tests::echo_builtin;
516    use crate::tests::return_builtin;
517    use assert_matches::assert_matches;
518    use futures_util::FutureExt;
519    use yash_env::variable::Scope;
520    use yash_env_test_helper::in_virtual_system;
521
522    #[test]
523    fn from_error_for_report() {
524        let error = Error {
525            cause: ErrorCause::AssignReadOnly(AssignReadOnlyError {
526                name: "foo".into(),
527                new_value: "value".into(),
528                read_only_location: Location::dummy("ROL"),
529                vacancy: None,
530            }),
531            location: Location {
532                range: 2..4,
533                ..Location::dummy("hello")
534            },
535        };
536
537        let report = Report::from(&error);
538
539        assert_eq!(report.r#type, ReportType::Error);
540        assert_eq!(report.title, "error assigning to variable");
541        assert_eq!(report.snippets.len(), 2);
542        assert_eq!(*report.snippets[0].code.value.borrow(), "hello");
543        assert_eq!(report.snippets[0].spans.len(), 1);
544        assert_eq!(report.snippets[0].spans[0].range, 2..4);
545        assert_matches!(
546            &report.snippets[0].spans[0].role,
547            SpanRole::Primary { label } if label == "cannot assign to read-only variable \"foo\""
548        );
549        assert_eq!(*report.snippets[1].code.value.borrow(), "ROL");
550        assert_eq!(report.snippets[1].spans.len(), 1);
551        assert_eq!(report.snippets[1].spans[0].range, 0..3);
552        assert_matches!(
553            &report.snippets[1].spans[0].role,
554            SpanRole::Supplementary { label } if label == "the variable was made read-only here"
555        );
556        assert_eq!(report.footnotes, []);
557    }
558
559    #[test]
560    fn from_error_for_report_with_vacancy() {
561        let error = Error {
562            cause: ErrorCause::AssignReadOnly(AssignReadOnlyError {
563                name: "foo".into(),
564                new_value: "value".into(),
565                read_only_location: Location::dummy("ROL"),
566                vacancy: Some(Vacancy::EmptyScalar),
567            }),
568            location: Location {
569                range: 2..4,
570                ..Location::dummy("hello")
571            },
572        };
573
574        let report = Report::from(&error);
575
576        assert_eq!(
577            report.footnotes,
578            [Footnote {
579                r#type: FootnoteType::Note,
580                label: "assignment was attempted because the parameter was an empty string".into(),
581            }]
582        );
583    }
584
585    #[test]
586    fn expand_word_multiple_performs_initial_expansion() {
587        in_virtual_system(|mut env, _state| async move {
588            env.builtins.insert("echo", echo_builtin());
589            env.builtins.insert("return", return_builtin());
590            let word = "[$(echo echoed; return -n 42)]".parse().unwrap();
591            let mut fields = Vec::new();
592            let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
593                .await
594                .unwrap();
595            assert_eq!(exit_status, Some(ExitStatus(42)));
596            assert_matches!(fields.as_slice(), [f] => {
597                assert_eq!(f.value, "[echoed]");
598            });
599        })
600    }
601
602    #[test]
603    fn expand_word_multiple_performs_field_splitting_possibly_with_default_ifs() {
604        let mut env = yash_env::Env::new_virtual();
605        env.variables
606            .get_or_new("v", Scope::Global)
607            .assign("foo  bar ", None)
608            .unwrap();
609        let word = "$v".parse().unwrap();
610        let mut fields = Vec::new();
611        let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
612            .now_or_never()
613            .unwrap()
614            .unwrap();
615        assert_eq!(exit_status, None);
616        assert_matches!(fields.as_slice(), [f1, f2] => {
617            assert_eq!(f1.value, "foo");
618            assert_eq!(f2.value, "bar");
619        });
620    }
621
622    #[test]
623    fn expand_word_multiple_performs_field_splitting_with_current_ifs() {
624        let mut env = yash_env::Env::new_virtual();
625        env.variables
626            .get_or_new("v", Scope::Global)
627            .assign("foo  bar ", None)
628            .unwrap();
629        env.variables
630            .get_or_new(IFS, Scope::Global)
631            .assign(" o", None)
632            .unwrap();
633        let word = "$v".parse().unwrap();
634        let mut fields = Vec::new();
635        let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
636            .now_or_never()
637            .unwrap()
638            .unwrap();
639        assert_eq!(exit_status, None);
640        assert_matches!(fields.as_slice(), [f1, f2, f3] => {
641            assert_eq!(f1.value, "f");
642            assert_eq!(f2.value, "");
643            assert_eq!(f3.value, "bar");
644        });
645    }
646
647    #[test]
648    fn expand_word_multiple_performs_quote_removal() {
649        let mut env = yash_env::Env::new_virtual();
650        let word = "\"foo\"'$v'".parse().unwrap();
651        let mut fields = Vec::new();
652        let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
653            .now_or_never()
654            .unwrap()
655            .unwrap();
656        assert_eq!(exit_status, None);
657        assert_matches!(fields.as_slice(), [f] => {
658            assert_eq!(f.value, "foo$v");
659        });
660    }
661
662    #[test]
663    fn expand_words_returns_exit_status_of_last_command_substitution() {
664        in_virtual_system(|mut env, _state| async move {
665            env.builtins.insert("return", return_builtin());
666            let word1 = "$(return -n 12)".parse().unwrap();
667            let word2 = "$(return -n 34)$(return -n 56)".parse().unwrap();
668            let (_, exit_status) = expand_words(&mut env, &[word1, word2]).await.unwrap();
669            assert_eq!(exit_status, Some(ExitStatus(56)));
670        })
671    }
672
673    #[test]
674    fn expand_words_performs_field_splitting() {
675        let mut env = yash_env::Env::new_virtual();
676        env.variables
677            .get_or_new("v", Scope::Global)
678            .assign(" foo  bar ", None)
679            .unwrap();
680        let word = "$v".parse().unwrap();
681        let (fields, _) = expand_words(&mut env, &[word])
682            .now_or_never()
683            .unwrap()
684            .unwrap();
685        assert_matches!(fields.as_slice(), [f1, f2] => {
686            assert_eq!(f1.value, "foo");
687            assert_eq!(f2.value, "bar");
688        });
689    }
690
691    #[test]
692    fn expand_value_scalar() {
693        let mut env = yash_env::Env::new_virtual();
694        let value = yash_syntax::syntax::Scalar(r"1\\".parse().unwrap());
695        let (result, exit_status) = expand_value(&mut env, &value)
696            .now_or_never()
697            .unwrap()
698            .unwrap();
699        let content = assert_matches!(result, yash_env::variable::Scalar(content) => content);
700        assert_eq!(content, r"1\");
701        assert_eq!(exit_status, None);
702    }
703
704    #[test]
705    fn expand_value_array() {
706        let mut env = yash_env::Env::new_virtual();
707        let value =
708            yash_syntax::syntax::Array(vec!["''".parse().unwrap(), r"2\\".parse().unwrap()]);
709        let result = expand_value(&mut env, &value).now_or_never().unwrap();
710        let (result, exit_status) = result.unwrap();
711        let content = assert_matches!(result, yash_env::variable::Array(content) => content);
712        assert_eq!(content, ["".to_string(), r"2\".to_string()]);
713        assert_eq!(exit_status, None);
714    }
715}