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