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