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