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;
105#[allow(deprecated)]
106use yash_syntax::source::pretty::{Annotation, AnnotationType, Footer, MessageBase};
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#[allow(deprecated)]
312impl MessageBase for Error {
313    fn message_title(&self) -> Cow<'_, str> {
314        self.cause.message().into()
315    }
316
317    fn main_annotation(&self) -> Annotation<'_> {
318        Annotation::new(AnnotationType::Error, self.cause.label(), &self.location)
319    }
320
321    fn additional_annotations<'a, T: Extend<Annotation<'a>>>(&'a self, results: &mut T) {
322        if let Some((location, label)) = self.cause.related_location() {
323            // TODO Use Extend::extend_one
324            results.extend(std::iter::once(Annotation::new(
325                AnnotationType::Info,
326                label.into(),
327                location,
328            )))
329        }
330
331        // Report the vacancy that caused the assignment that led to the error.
332        let vacancy = match &self.cause {
333            ErrorCause::CommandSubstError(_) => None,
334            ErrorCause::ArithError(_) => None,
335            ErrorCause::AssignReadOnly(e) => e.vacancy,
336            ErrorCause::UnsetParameter { .. } => None,
337            ErrorCause::VacantExpansion(_) => None,
338            ErrorCause::NonassignableParameter(e) => Some(e.vacancy),
339        };
340        if let Some(vacancy) = vacancy {
341            let message = match vacancy {
342                Vacancy::Unset => "assignment was attempted because the parameter was not set",
343                Vacancy::EmptyScalar => {
344                    "assignment was attempted because the parameter was an empty string"
345                }
346                Vacancy::ValuelessArray => {
347                    "assignment was attempted because the parameter was an empty array"
348                }
349                Vacancy::EmptyValueArray => {
350                    "assignment was attempted because the parameter was an array of an empty string"
351                }
352            };
353            // TODO Use Extend::extend_one
354            results.extend(std::iter::once(Annotation::new(
355                AnnotationType::Info,
356                message.into(),
357                &self.location,
358            )));
359        }
360    }
361
362    fn footers(&self) -> Vec<Footer<'_>> {
363        self.cause
364            .footer()
365            .into_iter()
366            .map(|label| Footer {
367                r#type: AnnotationType::Info,
368                label: label.into(),
369            })
370            .collect()
371    }
372}
373
374/// Result of word expansion.
375pub type Result<T> = std::result::Result<T, Error>;
376
377/// Expands a text to a string.
378///
379/// This function performs the initial expansion, quote removal, and attribute
380/// stripping.
381/// The second field of the result tuple is the exit status of the last command
382/// substitution performed during the expansion, if any.
383pub async fn expand_text(
384    env: &mut yash_env::Env,
385    text: &Text,
386) -> Result<(String, Option<ExitStatus>)> {
387    let mut env = initial::Env::new(env);
388    // It would be technically correct to set `will_split` to false, but it does
389    // not affect the final results because we will join the results anyway.
390    // env.will_split = false;
391    let phrase = text.expand(&mut env).await?;
392    let chars = phrase.ifs_join(&env.inner.variables);
393    let result = skip_quotes(chars).strip().collect();
394    Ok((result, env.last_command_subst_exit_status))
395}
396
397/// Expands a word to an attributed field.
398///
399/// This function performs initial expansion and joins the resultant phrase into
400/// a field. The second field of the result tuple is the exit status of the last
401/// command substitution performed during the expansion, if any.
402///
403/// Compare [`expand_word`] that performs not only initial expansion but also
404/// quote removal and attribute stripping.
405pub async fn expand_word_attr(
406    env: &mut yash_env::Env,
407    word: &Word,
408) -> Result<(AttrField, Option<ExitStatus>)> {
409    let mut env = initial::Env::new(env);
410    // It would be technically correct to set `will_split` to false, but it does
411    // not affect the final results because we will join the results anyway.
412    // env.will_split = false;
413    let phrase = word.expand(&mut env).await?;
414    let chars = phrase.ifs_join(&env.inner.variables);
415    let origin = word.location.clone();
416    let field = AttrField { chars, origin };
417    Ok((field, env.last_command_subst_exit_status))
418}
419
420/// Expands a word to a field.
421///
422/// This function performs the initial expansion, quote removal, and attribute
423/// stripping.
424/// The second field of the result tuple is the exit status of the last command
425/// substitution performed during the expansion, if any.
426///
427/// To expand a word to an [`AttrField`] without performing quote removal or
428/// attribute stripping, use [`expand_word_attr`].
429/// To expand a word to multiple fields, use [`expand_word_multiple`].
430/// To expand multiple words to multiple fields, use [`expand_words`].
431pub async fn expand_word(
432    env: &mut yash_env::Env,
433    word: &Word,
434) -> Result<(Field, Option<ExitStatus>)> {
435    let (field, exit_status) = expand_word_attr(env, word).await?;
436    let field = field.remove_quotes_and_strip();
437    Ok((field, exit_status))
438}
439
440/// Expands a word to fields.
441///
442/// This function performs the initial expansion and multi-field expansion,
443/// including quote removal and attribute stripping. The results are appended to
444/// the given collection. The return value is the exit status of the last
445/// command substitution performed during the expansion, if any.
446///
447/// To expand a single word to a single field, use [`expand_word`].
448/// To expand multiple words to fields, use [`expand_words`].
449pub async fn expand_word_multiple<R>(
450    env: &mut yash_env::Env,
451    word: &Word,
452    results: &mut R,
453) -> Result<Option<ExitStatus>>
454where
455    R: Extend<Field>,
456{
457    let mut env = initial::Env::new(env);
458
459    // initial expansion //
460    let phrase = word.expand(&mut env).await?;
461
462    // TODO brace expansion //
463
464    // field splitting //
465    let ifs = env
466        .inner
467        .variables
468        .get_scalar(IFS)
469        .map(Ifs::new)
470        .unwrap_or_default();
471    let mut split_fields = Vec::with_capacity(phrase.field_count());
472    for chars in phrase {
473        let origin = word.location.clone();
474        let attr_field = AttrField { chars, origin };
475        split::split_into(attr_field, &ifs, &mut split_fields);
476    }
477    drop(ifs);
478
479    // pathname expansion (including quote removal and attribute stripping) //
480    for field in split_fields {
481        results.extend(glob(env.inner, field));
482    }
483
484    Ok(env.last_command_subst_exit_status)
485}
486
487/// Expands a word to fields.
488///
489/// This function expands a word to fields using the specified expansion mode
490/// and appends the results to the given collection.
491///
492/// If the specified mode is [`ExpansionMode::Multiple`], this function performs
493/// the initial expansion and multi-field expansion, including quote removal and
494/// attribute stripping (see [`expand_word_multiple`]). If the mode is
495/// [`ExpansionMode::Single`], this function performs the initial expansion,
496/// quote removal, and attribute stripping, but not multi-field expansion (see
497/// [`expand_word`]).
498///
499/// The results are appended to the given collection.
500pub async fn expand_word_with_mode<R>(
501    env: &mut yash_env::Env,
502    word: &Word,
503    mode: ExpansionMode,
504    results: &mut R,
505) -> Result<Option<ExitStatus>>
506where
507    R: Extend<Field>,
508{
509    match mode {
510        ExpansionMode::Single => {
511            let (field, exit_status) = expand_word(env, word).await?;
512            results.extend(std::iter::once(field));
513            Ok(exit_status)
514        }
515        ExpansionMode::Multiple => expand_word_multiple(env, word, results).await,
516    }
517}
518
519/// Expands words to fields.
520///
521/// This function performs the initial expansion and multi-field expansion,
522/// including quote removal and attribute stripping.
523/// The second field of the result tuple is the exit status of the last command
524/// substitution performed during the expansion, if any.
525///
526/// To expand a single word to a single field, use [`expand_word`].
527/// To expand a single word to multiple fields, use [`expand_word_multiple`].
528pub async fn expand_words<'a, I: IntoIterator<Item = &'a Word>>(
529    env: &mut yash_env::Env,
530    words: I,
531) -> Result<(Vec<Field>, Option<ExitStatus>)> {
532    let mut fields = Vec::new();
533    let mut last_exit_status = None;
534
535    for word in words {
536        let exit_status = expand_word_multiple(env, word, &mut fields).await?;
537        if exit_status.is_some() {
538            last_exit_status = exit_status;
539        }
540    }
541
542    Ok((fields, last_exit_status))
543}
544
545/// Expands an assignment value.
546///
547/// This function expands a [`yash_syntax::syntax::Value`] to a
548/// [`yash_env::variable::Value`]. A scalar and array value are expanded by
549/// [`expand_word`] and [`expand_words`], respectively.
550/// The second field of the result tuple is the exit status of the last command
551/// substitution performed during the expansion, if any.
552pub async fn expand_value(
553    env: &mut yash_env::Env,
554    value: &yash_syntax::syntax::Value,
555) -> Result<(yash_env::variable::Value, Option<ExitStatus>)> {
556    match value {
557        yash_syntax::syntax::Scalar(word) => {
558            let (field, exit_status) = expand_word(env, word).await?;
559            Ok((yash_env::variable::Scalar(field.value), exit_status))
560        }
561        yash_syntax::syntax::Array(words) => {
562            let (fields, exit_status) = expand_words(env, words).await?;
563            let fields = fields.into_iter().map(|f| f.value).collect();
564            Ok((yash_env::variable::Array(fields), exit_status))
565        }
566    }
567}
568
569#[cfg(test)]
570mod tests {
571    use super::*;
572    use crate::tests::echo_builtin;
573    use crate::tests::return_builtin;
574    use assert_matches::assert_matches;
575    use futures_util::FutureExt;
576    use yash_env::variable::Scope;
577    use yash_env_test_helper::in_virtual_system;
578    #[allow(deprecated)]
579    use yash_syntax::source::pretty::Message;
580
581    #[test]
582    fn from_error_for_report() {
583        let error = Error {
584            cause: ErrorCause::AssignReadOnly(AssignReadOnlyError {
585                name: "foo".into(),
586                new_value: "value".into(),
587                read_only_location: Location::dummy("ROL"),
588                vacancy: None,
589            }),
590            location: Location {
591                range: 2..4,
592                ..Location::dummy("hello")
593            },
594        };
595
596        let report = Report::from(&error);
597
598        assert_eq!(report.r#type, ReportType::Error);
599        assert_eq!(report.title, "error assigning to variable");
600        assert_eq!(report.snippets.len(), 2);
601        assert_eq!(*report.snippets[0].code.value.borrow(), "hello");
602        assert_eq!(report.snippets[0].spans.len(), 1);
603        assert_eq!(report.snippets[0].spans[0].range, 2..4);
604        assert_matches!(
605            &report.snippets[0].spans[0].role,
606            SpanRole::Primary { label } if label == "cannot assign to read-only variable \"foo\""
607        );
608        assert_eq!(*report.snippets[1].code.value.borrow(), "ROL");
609        assert_eq!(report.snippets[1].spans.len(), 1);
610        assert_eq!(report.snippets[1].spans[0].range, 0..3);
611        assert_matches!(
612            &report.snippets[1].spans[0].role,
613            SpanRole::Supplementary { label } if label == "the variable was made read-only here"
614        );
615        assert_eq!(report.footnotes, []);
616    }
617
618    #[test]
619    fn from_error_for_report_with_vacancy() {
620        let error = Error {
621            cause: ErrorCause::AssignReadOnly(AssignReadOnlyError {
622                name: "foo".into(),
623                new_value: "value".into(),
624                read_only_location: Location::dummy("ROL"),
625                vacancy: Some(Vacancy::EmptyScalar),
626            }),
627            location: Location {
628                range: 2..4,
629                ..Location::dummy("hello")
630            },
631        };
632
633        let report = Report::from(&error);
634
635        assert_eq!(
636            report.footnotes,
637            [Footnote {
638                r#type: FootnoteType::Note,
639                label: "assignment was attempted because the parameter was an empty string".into(),
640            }]
641        );
642    }
643
644    #[allow(deprecated)]
645    #[test]
646    fn from_error_for_message() {
647        let error = Error {
648            cause: ErrorCause::AssignReadOnly(AssignReadOnlyError {
649                name: "foo".into(),
650                new_value: "value".into(),
651                read_only_location: Location::dummy("ROL"),
652                vacancy: None,
653            }),
654            location: Location {
655                range: 2..4,
656                ..Location::dummy("hello")
657            },
658        };
659        let message = Message::from(&error);
660        assert_eq!(message.r#type, AnnotationType::Error);
661        assert_eq!(message.title, "error assigning to variable");
662        assert_eq!(message.annotations.len(), 2);
663        assert_eq!(message.annotations[0].r#type, AnnotationType::Error);
664        assert_eq!(
665            message.annotations[0].label,
666            "cannot assign to read-only variable \"foo\""
667        );
668        assert_eq!(message.annotations[0].location, &error.location);
669        assert_eq!(message.annotations[1].r#type, AnnotationType::Info);
670        assert_eq!(
671            message.annotations[1].label,
672            "the variable was made read-only here"
673        );
674        assert_eq!(message.annotations[1].location, &Location::dummy("ROL"));
675    }
676
677    #[allow(deprecated)]
678    #[test]
679    fn from_error_for_message_with_vacancy() {
680        let error = Error {
681            cause: ErrorCause::AssignReadOnly(AssignReadOnlyError {
682                name: "foo".into(),
683                new_value: "value".into(),
684                read_only_location: Location::dummy("ROL"),
685                vacancy: Some(Vacancy::EmptyScalar),
686            }),
687            location: Location {
688                range: 2..4,
689                ..Location::dummy("hello")
690            },
691        };
692
693        let message = Message::from(&error);
694        assert!(
695            message.annotations.iter().any(|a| {
696                a.r#type == AnnotationType::Info
697                    && a.label
698                        == "assignment was attempted because the parameter was an empty string"
699                    && a.location == &error.location
700            }),
701            "{message:?}"
702        );
703    }
704
705    #[test]
706    fn expand_word_multiple_performs_initial_expansion() {
707        in_virtual_system(|mut env, _state| async move {
708            env.builtins.insert("echo", echo_builtin());
709            env.builtins.insert("return", return_builtin());
710            let word = "[$(echo echoed; return -n 42)]".parse().unwrap();
711            let mut fields = Vec::new();
712            let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
713                .await
714                .unwrap();
715            assert_eq!(exit_status, Some(ExitStatus(42)));
716            assert_matches!(fields.as_slice(), [f] => {
717                assert_eq!(f.value, "[echoed]");
718            });
719        })
720    }
721
722    #[test]
723    fn expand_word_multiple_performs_field_splitting_possibly_with_default_ifs() {
724        let mut env = yash_env::Env::new_virtual();
725        env.variables
726            .get_or_new("v", Scope::Global)
727            .assign("foo  bar ", None)
728            .unwrap();
729        let word = "$v".parse().unwrap();
730        let mut fields = Vec::new();
731        let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
732            .now_or_never()
733            .unwrap()
734            .unwrap();
735        assert_eq!(exit_status, None);
736        assert_matches!(fields.as_slice(), [f1, f2] => {
737            assert_eq!(f1.value, "foo");
738            assert_eq!(f2.value, "bar");
739        });
740    }
741
742    #[test]
743    fn expand_word_multiple_performs_field_splitting_with_current_ifs() {
744        let mut env = yash_env::Env::new_virtual();
745        env.variables
746            .get_or_new("v", Scope::Global)
747            .assign("foo  bar ", None)
748            .unwrap();
749        env.variables
750            .get_or_new(IFS, Scope::Global)
751            .assign(" o", None)
752            .unwrap();
753        let word = "$v".parse().unwrap();
754        let mut fields = Vec::new();
755        let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
756            .now_or_never()
757            .unwrap()
758            .unwrap();
759        assert_eq!(exit_status, None);
760        assert_matches!(fields.as_slice(), [f1, f2, f3] => {
761            assert_eq!(f1.value, "f");
762            assert_eq!(f2.value, "");
763            assert_eq!(f3.value, "bar");
764        });
765    }
766
767    #[test]
768    fn expand_word_multiple_performs_quote_removal() {
769        let mut env = yash_env::Env::new_virtual();
770        let word = "\"foo\"'$v'".parse().unwrap();
771        let mut fields = Vec::new();
772        let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
773            .now_or_never()
774            .unwrap()
775            .unwrap();
776        assert_eq!(exit_status, None);
777        assert_matches!(fields.as_slice(), [f] => {
778            assert_eq!(f.value, "foo$v");
779        });
780    }
781
782    #[test]
783    fn expand_words_returns_exit_status_of_last_command_substitution() {
784        in_virtual_system(|mut env, _state| async move {
785            env.builtins.insert("return", return_builtin());
786            let word1 = "$(return -n 12)".parse().unwrap();
787            let word2 = "$(return -n 34)$(return -n 56)".parse().unwrap();
788            let (_, exit_status) = expand_words(&mut env, &[word1, word2]).await.unwrap();
789            assert_eq!(exit_status, Some(ExitStatus(56)));
790        })
791    }
792
793    #[test]
794    fn expand_words_performs_field_splitting() {
795        let mut env = yash_env::Env::new_virtual();
796        env.variables
797            .get_or_new("v", Scope::Global)
798            .assign(" foo  bar ", None)
799            .unwrap();
800        let word = "$v".parse().unwrap();
801        let (fields, _) = expand_words(&mut env, &[word])
802            .now_or_never()
803            .unwrap()
804            .unwrap();
805        assert_matches!(fields.as_slice(), [f1, f2] => {
806            assert_eq!(f1.value, "foo");
807            assert_eq!(f2.value, "bar");
808        });
809    }
810
811    #[test]
812    fn expand_value_scalar() {
813        let mut env = yash_env::Env::new_virtual();
814        let value = yash_syntax::syntax::Scalar(r"1\\".parse().unwrap());
815        let (result, exit_status) = expand_value(&mut env, &value)
816            .now_or_never()
817            .unwrap()
818            .unwrap();
819        let content = assert_matches!(result, yash_env::variable::Scalar(content) => content);
820        assert_eq!(content, r"1\");
821        assert_eq!(exit_status, None);
822    }
823
824    #[test]
825    fn expand_value_array() {
826        let mut env = yash_env::Env::new_virtual();
827        let value =
828            yash_syntax::syntax::Array(vec!["''".parse().unwrap(), r"2\\".parse().unwrap()]);
829        let result = expand_value(&mut env, &value).now_or_never().unwrap();
830        let (result, exit_status) = result.unwrap();
831        let content = assert_matches!(result, yash_env::variable::Array(content) => content);
832        assert_eq!(content, ["".to_string(), r"2\".to_string()]);
833        assert_eq!(exit_status, None);
834    }
835}