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