1pub(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;
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#[derive(Clone, Debug, Eq, Error, PartialEq)]
119#[error("cannot assign to read-only variable {name:?}")]
120pub struct AssignReadOnlyError {
121 pub name: String,
123 pub new_value: Value,
125 pub read_only_location: Location,
127 pub vacancy: Option<Vacancy>,
134}
135
136#[derive(Clone, Debug, Eq, Error, PartialEq)]
138pub enum ErrorCause {
139 #[error("error in command substitution: {0}")]
141 CommandSubstError(Errno),
142
143 #[error(transparent)]
145 ArithError(#[from] ArithError),
146
147 #[error(transparent)]
149 AssignReadOnly(#[from] AssignReadOnlyError),
150
151 #[error("unset parameter `{param}`")]
153 UnsetParameter { param: Param },
154
155 #[error(transparent)]
157 VacantExpansion(#[from] VacantError),
158
159 #[error(transparent)]
161 NonassignableParameter(#[from] NonassignableError),
162}
163
164impl ErrorCause {
165 #[must_use]
167 pub fn message(&self) -> &str {
168 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 #[must_use]
182 pub fn label(&self) -> Cow<'_, str> {
183 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 #[must_use]
206 pub fn related_location(&self) -> Option<(&Location, &'static str)> {
207 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 #[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#[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 #[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 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
303impl<'a> From<&'a Error> for Report<'a> {
305 #[inline(always)]
306 fn from(error: &'a Error) -> Self {
307 error.to_report()
308 }
309}
310
311pub type Result<T> = std::result::Result<T, Error>;
313
314pub async fn expand_text<S: Runtime + 'static>(
321 env: &mut yash_env::Env<S>,
322 text: &Text,
323) -> Result<(String, Option<ExitStatus>)> {
324 let mut env = initial::Env::new(env);
325 let phrase = text.expand(&mut env).await?;
329 let chars = phrase.ifs_join(&env.inner.variables);
330 let result = skip_quotes(chars).strip().collect();
331 Ok((result, env.last_command_subst_exit_status))
332}
333
334pub async fn expand_word_attr<S: Runtime + 'static>(
343 env: &mut yash_env::Env<S>,
344 word: &Word,
345) -> Result<(AttrField, Option<ExitStatus>)> {
346 let mut env = initial::Env::new(env);
347 let phrase = word.expand(&mut env).await?;
351 let chars = phrase.ifs_join(&env.inner.variables);
352 let origin = word.location.clone();
353 let field = AttrField { chars, origin };
354 Ok((field, env.last_command_subst_exit_status))
355}
356
357pub async fn expand_word<S: Runtime + 'static>(
369 env: &mut yash_env::Env<S>,
370 word: &Word,
371) -> Result<(Field, Option<ExitStatus>)> {
372 let (field, exit_status) = expand_word_attr(env, word).await?;
373 let field = field.remove_quotes_and_strip();
374 Ok((field, exit_status))
375}
376
377pub async fn expand_word_multiple<S, R>(
387 env: &mut yash_env::Env<S>,
388 word: &Word,
389 results: &mut R,
390) -> Result<Option<ExitStatus>>
391where
392 S: Runtime + 'static,
393 R: Extend<Field>,
394{
395 let mut env = initial::Env::new(env);
396
397 let phrase = word.expand(&mut env).await?;
399
400 let ifs = env
404 .inner
405 .variables
406 .get_scalar(IFS)
407 .map(Ifs::new)
408 .unwrap_or_default();
409 let mut split_fields = Vec::with_capacity(phrase.field_count());
410 for chars in phrase {
411 let origin = word.location.clone();
412 let attr_field = AttrField { chars, origin };
413 split::split_into(attr_field, &ifs, &mut split_fields);
414 }
415 drop(ifs);
416
417 for field in split_fields {
419 results.extend(glob(env.inner, field));
420 }
421
422 Ok(env.last_command_subst_exit_status)
423}
424
425pub async fn expand_word_with_mode<S, R>(
439 env: &mut yash_env::Env<S>,
440 word: &Word,
441 mode: ExpansionMode,
442 results: &mut R,
443) -> Result<Option<ExitStatus>>
444where
445 S: Runtime + 'static,
446 R: Extend<Field>,
447{
448 match mode {
449 ExpansionMode::Single => {
450 let (field, exit_status) = expand_word(env, word).await?;
451 results.extend(std::iter::once(field));
452 Ok(exit_status)
453 }
454 ExpansionMode::Multiple => expand_word_multiple(env, word, results).await,
455 }
456}
457
458pub async fn expand_words<'a, S, I>(
468 env: &mut yash_env::Env<S>,
469 words: I,
470) -> Result<(Vec<Field>, Option<ExitStatus>)>
471where
472 S: Runtime + 'static,
473 I: IntoIterator<Item = &'a Word>,
474{
475 let mut fields = Vec::new();
476 let mut last_exit_status = None;
477
478 for word in words {
479 let exit_status = expand_word_multiple(env, word, &mut fields).await?;
480 if exit_status.is_some() {
481 last_exit_status = exit_status;
482 }
483 }
484
485 Ok((fields, last_exit_status))
486}
487
488pub async fn expand_value<S: Runtime + 'static>(
496 env: &mut yash_env::Env<S>,
497 value: &yash_syntax::syntax::Value,
498) -> Result<(yash_env::variable::Value, Option<ExitStatus>)> {
499 match value {
500 yash_syntax::syntax::Scalar(word) => {
501 let (field, exit_status) = expand_word(env, word).await?;
502 Ok((yash_env::variable::Scalar(field.value), exit_status))
503 }
504 yash_syntax::syntax::Array(words) => {
505 let (fields, exit_status) = expand_words(env, words).await?;
506 let fields = fields.into_iter().map(|f| f.value).collect();
507 Ok((yash_env::variable::Array(fields), exit_status))
508 }
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515 use crate::tests::echo_builtin;
516 use crate::tests::return_builtin;
517 use assert_matches::assert_matches;
518 use futures_util::FutureExt;
519 use yash_env::variable::Scope;
520 use yash_env_test_helper::in_virtual_system;
521
522 #[test]
523 fn from_error_for_report() {
524 let error = Error {
525 cause: ErrorCause::AssignReadOnly(AssignReadOnlyError {
526 name: "foo".into(),
527 new_value: "value".into(),
528 read_only_location: Location::dummy("ROL"),
529 vacancy: None,
530 }),
531 location: Location {
532 range: 2..4,
533 ..Location::dummy("hello")
534 },
535 };
536
537 let report = Report::from(&error);
538
539 assert_eq!(report.r#type, ReportType::Error);
540 assert_eq!(report.title, "error assigning to variable");
541 assert_eq!(report.snippets.len(), 2);
542 assert_eq!(*report.snippets[0].code.value.borrow(), "hello");
543 assert_eq!(report.snippets[0].spans.len(), 1);
544 assert_eq!(report.snippets[0].spans[0].range, 2..4);
545 assert_matches!(
546 &report.snippets[0].spans[0].role,
547 SpanRole::Primary { label } if label == "cannot assign to read-only variable \"foo\""
548 );
549 assert_eq!(*report.snippets[1].code.value.borrow(), "ROL");
550 assert_eq!(report.snippets[1].spans.len(), 1);
551 assert_eq!(report.snippets[1].spans[0].range, 0..3);
552 assert_matches!(
553 &report.snippets[1].spans[0].role,
554 SpanRole::Supplementary { label } if label == "the variable was made read-only here"
555 );
556 assert_eq!(report.footnotes, []);
557 }
558
559 #[test]
560 fn from_error_for_report_with_vacancy() {
561 let error = Error {
562 cause: ErrorCause::AssignReadOnly(AssignReadOnlyError {
563 name: "foo".into(),
564 new_value: "value".into(),
565 read_only_location: Location::dummy("ROL"),
566 vacancy: Some(Vacancy::EmptyScalar),
567 }),
568 location: Location {
569 range: 2..4,
570 ..Location::dummy("hello")
571 },
572 };
573
574 let report = Report::from(&error);
575
576 assert_eq!(
577 report.footnotes,
578 [Footnote {
579 r#type: FootnoteType::Note,
580 label: "assignment was attempted because the parameter was an empty string".into(),
581 }]
582 );
583 }
584
585 #[test]
586 fn expand_word_multiple_performs_initial_expansion() {
587 in_virtual_system(|mut env, _state| async move {
588 env.builtins.insert("echo", echo_builtin());
589 env.builtins.insert("return", return_builtin());
590 let word = "[$(echo echoed; return -n 42)]".parse().unwrap();
591 let mut fields = Vec::new();
592 let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
593 .await
594 .unwrap();
595 assert_eq!(exit_status, Some(ExitStatus(42)));
596 assert_matches!(fields.as_slice(), [f] => {
597 assert_eq!(f.value, "[echoed]");
598 });
599 })
600 }
601
602 #[test]
603 fn expand_word_multiple_performs_field_splitting_possibly_with_default_ifs() {
604 let mut env = yash_env::Env::new_virtual();
605 env.variables
606 .get_or_new("v", Scope::Global)
607 .assign("foo bar ", None)
608 .unwrap();
609 let word = "$v".parse().unwrap();
610 let mut fields = Vec::new();
611 let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
612 .now_or_never()
613 .unwrap()
614 .unwrap();
615 assert_eq!(exit_status, None);
616 assert_matches!(fields.as_slice(), [f1, f2] => {
617 assert_eq!(f1.value, "foo");
618 assert_eq!(f2.value, "bar");
619 });
620 }
621
622 #[test]
623 fn expand_word_multiple_performs_field_splitting_with_current_ifs() {
624 let mut env = yash_env::Env::new_virtual();
625 env.variables
626 .get_or_new("v", Scope::Global)
627 .assign("foo bar ", None)
628 .unwrap();
629 env.variables
630 .get_or_new(IFS, Scope::Global)
631 .assign(" o", None)
632 .unwrap();
633 let word = "$v".parse().unwrap();
634 let mut fields = Vec::new();
635 let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
636 .now_or_never()
637 .unwrap()
638 .unwrap();
639 assert_eq!(exit_status, None);
640 assert_matches!(fields.as_slice(), [f1, f2, f3] => {
641 assert_eq!(f1.value, "f");
642 assert_eq!(f2.value, "");
643 assert_eq!(f3.value, "bar");
644 });
645 }
646
647 #[test]
648 fn expand_word_multiple_performs_quote_removal() {
649 let mut env = yash_env::Env::new_virtual();
650 let word = "\"foo\"'$v'".parse().unwrap();
651 let mut fields = Vec::new();
652 let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
653 .now_or_never()
654 .unwrap()
655 .unwrap();
656 assert_eq!(exit_status, None);
657 assert_matches!(fields.as_slice(), [f] => {
658 assert_eq!(f.value, "foo$v");
659 });
660 }
661
662 #[test]
663 fn expand_words_returns_exit_status_of_last_command_substitution() {
664 in_virtual_system(|mut env, _state| async move {
665 env.builtins.insert("return", return_builtin());
666 let word1 = "$(return -n 12)".parse().unwrap();
667 let word2 = "$(return -n 34)$(return -n 56)".parse().unwrap();
668 let (_, exit_status) = expand_words(&mut env, &[word1, word2]).await.unwrap();
669 assert_eq!(exit_status, Some(ExitStatus(56)));
670 })
671 }
672
673 #[test]
674 fn expand_words_performs_field_splitting() {
675 let mut env = yash_env::Env::new_virtual();
676 env.variables
677 .get_or_new("v", Scope::Global)
678 .assign(" foo bar ", None)
679 .unwrap();
680 let word = "$v".parse().unwrap();
681 let (fields, _) = expand_words(&mut env, &[word])
682 .now_or_never()
683 .unwrap()
684 .unwrap();
685 assert_matches!(fields.as_slice(), [f1, f2] => {
686 assert_eq!(f1.value, "foo");
687 assert_eq!(f2.value, "bar");
688 });
689 }
690
691 #[test]
692 fn expand_value_scalar() {
693 let mut env = yash_env::Env::new_virtual();
694 let value = yash_syntax::syntax::Scalar(r"1\\".parse().unwrap());
695 let (result, exit_status) = expand_value(&mut env, &value)
696 .now_or_never()
697 .unwrap()
698 .unwrap();
699 let content = assert_matches!(result, yash_env::variable::Scalar(content) => content);
700 assert_eq!(content, r"1\");
701 assert_eq!(exit_status, None);
702 }
703
704 #[test]
705 fn expand_value_array() {
706 let mut env = yash_env::Env::new_virtual();
707 let value =
708 yash_syntax::syntax::Array(vec!["''".parse().unwrap(), r"2\\".parse().unwrap()]);
709 let result = expand_value(&mut env, &value).now_or_never().unwrap();
710 let (result, exit_status) = result.unwrap();
711 let content = assert_matches!(result, yash_env::variable::Array(content) => content);
712 assert_eq!(content, ["".to_string(), r"2\".to_string()]);
713 assert_eq!(exit_status, None);
714 }
715}