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 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#[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)]
138#[non_exhaustive]
139pub enum ErrorCause {
140 #[error("error in command substitution: {0}")]
142 CommandSubstError(Errno),
143
144 #[error(transparent)]
146 ArithError(#[from] ArithError),
147
148 #[error(transparent)]
150 AssignReadOnly(#[from] AssignReadOnlyError),
151
152 #[error("unset parameter `{param}`")]
154 UnsetParameter { param: Param },
155
156 #[error(transparent)]
158 VacantExpansion(#[from] VacantError),
159
160 #[error(transparent)]
162 NonassignableParameter(#[from] NonassignableError),
163
164 #[error("expansion interrupted by SIGINT")]
170 Interrupted(ExitStatus),
171}
172
173impl ErrorCause {
174 #[must_use]
176 pub fn message(&self) -> &str {
177 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 #[must_use]
192 pub fn label(&self) -> Cow<'_, str> {
193 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 #[must_use]
217 pub fn related_location(&self) -> Option<(&Location, &'static str)> {
218 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 #[must_use]
236 pub fn footer(&self) -> Option<&'static str> {
237 use ErrorCause::*;
238 match self {
239 CommandSubstError(_)
240 | ArithError(_)
241 | AssignReadOnly(_)
242 | VacantExpansion(_)
243 | NonassignableParameter(_)
244 | Interrupted(_) => None,
245
246 UnsetParameter { .. } => Some("unset parameters are disallowed by the nounset option"),
247 }
248 }
249}
250
251#[derive(Clone, Debug, Eq, Error, PartialEq)]
253#[error("{cause}")]
254pub struct Error {
255 pub cause: ErrorCause,
256 pub location: Location,
257}
258
259impl Error {
260 #[must_use]
262 pub fn to_report(&self) -> Report<'_> {
263 let mut report = Report::new();
264 report.r#type = ReportType::Error;
265 report.title = self.cause.message().into();
266 report.snippets = Snippet::with_primary_span(&self.location, self.cause.label());
267
268 if let Some((location, label)) = self.cause.related_location() {
269 let label = label.into();
270 let span = Span {
271 range: location.byte_range(),
272 role: SpanRole::Supplementary { label },
273 };
274 add_span(&location.code, span, &mut report.snippets);
275 }
276
277 if let Some(footer) = self.cause.footer() {
278 report.footnotes.push(Footnote {
279 r#type: FootnoteType::Note,
280 label: footer.into(),
281 });
282 }
283
284 let vacancy = match &self.cause {
286 ErrorCause::CommandSubstError(_) => None,
287 ErrorCause::ArithError(_) => None,
288 ErrorCause::AssignReadOnly(e) => e.vacancy,
289 ErrorCause::UnsetParameter { .. } => None,
290 ErrorCause::VacantExpansion(_) => None,
291 ErrorCause::NonassignableParameter(e) => Some(e.vacancy),
292 ErrorCause::Interrupted(_) => None,
293 };
294 if let Some(vacancy) = vacancy {
295 let message = match vacancy {
296 Vacancy::Unset => "assignment was attempted because the parameter was not set",
297 Vacancy::EmptyScalar => {
298 "assignment was attempted because the parameter was an empty string"
299 }
300 Vacancy::ValuelessArray => {
301 "assignment was attempted because the parameter was an empty array"
302 }
303 Vacancy::EmptyValueArray => {
304 "assignment was attempted because the parameter was an array of an empty string"
305 }
306 };
307 report.footnotes.push(Footnote {
308 r#type: FootnoteType::Note,
309 label: message.into(),
310 });
311 }
312
313 report
314 }
315}
316
317impl<'a> From<&'a Error> for Report<'a> {
319 #[inline(always)]
320 fn from(error: &'a Error) -> Self {
321 error.to_report()
322 }
323}
324
325pub type Result<T> = std::result::Result<T, Error>;
327
328pub async fn expand_text<S: Runtime + 'static>(
335 env: &mut yash_env::Env<S>,
336 text: &Text,
337) -> Result<(String, Option<ExitStatus>)> {
338 let mut env = initial::Env::new(env);
339 let phrase = text.expand(&mut env).await?;
343 let chars = phrase.ifs_join(&env.inner.variables);
344 let result = skip_quotes(chars).strip().collect();
345 Ok((result, env.last_command_subst_exit_status))
346}
347
348pub async fn expand_word_attr<S: Runtime + 'static>(
357 env: &mut yash_env::Env<S>,
358 word: &Word,
359) -> Result<(AttrField, Option<ExitStatus>)> {
360 let mut env = initial::Env::new(env);
361 let phrase = word.expand(&mut env).await?;
365 let chars = phrase.ifs_join(&env.inner.variables);
366 let origin = word.location.clone();
367 let field = AttrField { chars, origin };
368 Ok((field, env.last_command_subst_exit_status))
369}
370
371pub async fn expand_word<S: Runtime + 'static>(
383 env: &mut yash_env::Env<S>,
384 word: &Word,
385) -> Result<(Field, Option<ExitStatus>)> {
386 let (field, exit_status) = expand_word_attr(env, word).await?;
387 let field = field.remove_quotes_and_strip();
388 Ok((field, exit_status))
389}
390
391pub async fn expand_word_multiple<S, R>(
401 env: &mut yash_env::Env<S>,
402 word: &Word,
403 results: &mut R,
404) -> Result<Option<ExitStatus>>
405where
406 S: Runtime + 'static,
407 R: Extend<Field>,
408{
409 let mut env = initial::Env::new(env);
410
411 let phrase = word.expand(&mut env).await?;
413
414 let ifs = env
418 .inner
419 .variables
420 .get_scalar(IFS)
421 .map(Ifs::new)
422 .unwrap_or_default();
423 let mut split_fields = Vec::with_capacity(phrase.field_count());
424 for chars in phrase {
425 let origin = word.location.clone();
426 let attr_field = AttrField { chars, origin };
427 split::split_into(attr_field, &ifs, &mut split_fields);
428 }
429 drop(ifs);
430
431 for field in split_fields {
433 let glob = glob(env.inner, field);
434 match glob.try_into_vec_iter() {
435 Ok(fields) => results.extend(fields),
436 Err(once) => match { once }.next().unwrap() {
437 Ok(field) => results.extend(std::iter::once(field)),
438 Err(interrupted) => {
439 return Err(Error {
440 cause: ErrorCause::Interrupted(interrupted.into()),
441 location: word.location.clone(),
442 });
443 }
444 },
445 }
446 }
447
448 Ok(env.last_command_subst_exit_status)
449}
450
451pub async fn expand_word_with_mode<S, R>(
465 env: &mut yash_env::Env<S>,
466 word: &Word,
467 mode: ExpansionMode,
468 results: &mut R,
469) -> Result<Option<ExitStatus>>
470where
471 S: Runtime + 'static,
472 R: Extend<Field>,
473{
474 match mode {
475 ExpansionMode::Single => {
476 let (field, exit_status) = expand_word(env, word).await?;
477 results.extend(std::iter::once(field));
478 Ok(exit_status)
479 }
480 ExpansionMode::Multiple => expand_word_multiple(env, word, results).await,
481 }
482}
483
484pub async fn expand_words<'a, S, I>(
494 env: &mut yash_env::Env<S>,
495 words: I,
496) -> Result<(Vec<Field>, Option<ExitStatus>)>
497where
498 S: Runtime + 'static,
499 I: IntoIterator<Item = &'a Word>,
500{
501 let mut fields = Vec::new();
502 let mut last_exit_status = None;
503
504 for word in words {
505 let exit_status = expand_word_multiple(env, word, &mut fields).await?;
506 if exit_status.is_some() {
507 last_exit_status = exit_status;
508 }
509 }
510
511 Ok((fields, last_exit_status))
512}
513
514pub async fn expand_value<S: Runtime + 'static>(
522 env: &mut yash_env::Env<S>,
523 value: &yash_syntax::syntax::Value,
524) -> Result<(yash_env::variable::Value, Option<ExitStatus>)> {
525 match value {
526 yash_syntax::syntax::Scalar(word) => {
527 let (field, exit_status) = expand_word(env, word).await?;
528 Ok((yash_env::variable::Scalar(field.value), exit_status))
529 }
530 yash_syntax::syntax::Array(words) => {
531 let (fields, exit_status) = expand_words(env, words).await?;
532 let fields = fields.into_iter().map(|f| f.value).collect();
533 Ok((yash_env::variable::Array(fields), exit_status))
534 }
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541 use crate::tests::echo_builtin;
542 use crate::tests::return_builtin;
543 use assert_matches::assert_matches;
544 use futures_util::FutureExt as _;
545 use yash_env::test_helper::in_virtual_system;
546 use yash_env::variable::Scope;
547
548 #[test]
549 fn from_error_for_report() {
550 let error = Error {
551 cause: ErrorCause::AssignReadOnly(AssignReadOnlyError {
552 name: "foo".into(),
553 new_value: "value".into(),
554 read_only_location: Location::dummy("ROL"),
555 vacancy: None,
556 }),
557 location: Location {
558 range: 2..4,
559 ..Location::dummy("hello")
560 },
561 };
562
563 let report = Report::from(&error);
564
565 assert_eq!(report.r#type, ReportType::Error);
566 assert_eq!(report.title, "error assigning to variable");
567 assert_eq!(report.snippets.len(), 2);
568 assert_eq!(*report.snippets[0].code.value.borrow(), "hello");
569 assert_eq!(report.snippets[0].spans.len(), 1);
570 assert_eq!(report.snippets[0].spans[0].range, 2..4);
571 assert_matches!(
572 &report.snippets[0].spans[0].role,
573 SpanRole::Primary { label } if label == "cannot assign to read-only variable \"foo\""
574 );
575 assert_eq!(*report.snippets[1].code.value.borrow(), "ROL");
576 assert_eq!(report.snippets[1].spans.len(), 1);
577 assert_eq!(report.snippets[1].spans[0].range, 0..3);
578 assert_matches!(
579 &report.snippets[1].spans[0].role,
580 SpanRole::Supplementary { label } if label == "the variable was made read-only here"
581 );
582 assert_eq!(report.footnotes, []);
583 }
584
585 #[test]
586 fn from_error_for_report_with_vacancy() {
587 let error = Error {
588 cause: ErrorCause::AssignReadOnly(AssignReadOnlyError {
589 name: "foo".into(),
590 new_value: "value".into(),
591 read_only_location: Location::dummy("ROL"),
592 vacancy: Some(Vacancy::EmptyScalar),
593 }),
594 location: Location {
595 range: 2..4,
596 ..Location::dummy("hello")
597 },
598 };
599
600 let report = Report::from(&error);
601
602 assert_eq!(
603 report.footnotes,
604 [Footnote {
605 r#type: FootnoteType::Note,
606 label: "assignment was attempted because the parameter was an empty string".into(),
607 }]
608 );
609 }
610
611 #[test]
612 fn expand_word_multiple_performs_initial_expansion() {
613 in_virtual_system(|mut env, _state| async move {
614 env.builtins.insert("echo", echo_builtin());
615 env.builtins.insert("return", return_builtin());
616 let word = "[$(echo echoed; return -n 42)]".parse().unwrap();
617 let mut fields = Vec::new();
618 let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
619 .await
620 .unwrap();
621 assert_eq!(exit_status, Some(ExitStatus(42)));
622 assert_matches!(fields.as_slice(), [f] => {
623 assert_eq!(f.value, "[echoed]");
624 });
625 })
626 }
627
628 #[test]
629 fn expand_word_multiple_performs_field_splitting_possibly_with_default_ifs() {
630 let mut env = yash_env::Env::new_virtual();
631 env.variables
632 .get_or_new("v", Scope::Global)
633 .assign("foo bar ", None)
634 .unwrap();
635 let word = "$v".parse().unwrap();
636 let mut fields = Vec::new();
637 let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
638 .now_or_never()
639 .unwrap()
640 .unwrap();
641 assert_eq!(exit_status, None);
642 assert_matches!(fields.as_slice(), [f1, f2] => {
643 assert_eq!(f1.value, "foo");
644 assert_eq!(f2.value, "bar");
645 });
646 }
647
648 #[test]
649 fn expand_word_multiple_performs_field_splitting_with_current_ifs() {
650 let mut env = yash_env::Env::new_virtual();
651 env.variables
652 .get_or_new("v", Scope::Global)
653 .assign("foo bar ", None)
654 .unwrap();
655 env.variables
656 .get_or_new(IFS, Scope::Global)
657 .assign(" o", None)
658 .unwrap();
659 let word = "$v".parse().unwrap();
660 let mut fields = Vec::new();
661 let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
662 .now_or_never()
663 .unwrap()
664 .unwrap();
665 assert_eq!(exit_status, None);
666 assert_matches!(fields.as_slice(), [f1, f2, f3] => {
667 assert_eq!(f1.value, "f");
668 assert_eq!(f2.value, "");
669 assert_eq!(f3.value, "bar");
670 });
671 }
672
673 #[test]
674 fn expand_word_multiple_performs_quote_removal() {
675 let mut env = yash_env::Env::new_virtual();
676 let word = "\"foo\"'$v'".parse().unwrap();
677 let mut fields = Vec::new();
678 let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
679 .now_or_never()
680 .unwrap()
681 .unwrap();
682 assert_eq!(exit_status, None);
683 assert_matches!(fields.as_slice(), [f] => {
684 assert_eq!(f.value, "foo$v");
685 });
686 }
687
688 #[test]
689 fn expand_words_returns_exit_status_of_last_command_substitution() {
690 in_virtual_system(|mut env, _state| async move {
691 env.builtins.insert("return", return_builtin());
692 let word1 = "$(return -n 12)".parse().unwrap();
693 let word2 = "$(return -n 34)$(return -n 56)".parse().unwrap();
694 let (_, exit_status) = expand_words(&mut env, &[word1, word2]).await.unwrap();
695 assert_eq!(exit_status, Some(ExitStatus(56)));
696 })
697 }
698
699 #[test]
700 fn expand_words_performs_field_splitting() {
701 let mut env = yash_env::Env::new_virtual();
702 env.variables
703 .get_or_new("v", Scope::Global)
704 .assign(" foo bar ", None)
705 .unwrap();
706 let word = "$v".parse().unwrap();
707 let (fields, _) = expand_words(&mut env, &[word])
708 .now_or_never()
709 .unwrap()
710 .unwrap();
711 assert_matches!(fields.as_slice(), [f1, f2] => {
712 assert_eq!(f1.value, "foo");
713 assert_eq!(f2.value, "bar");
714 });
715 }
716
717 #[test]
718 fn expand_value_scalar() {
719 let mut env = yash_env::Env::new_virtual();
720 let value = yash_syntax::syntax::Scalar(r"1\\".parse().unwrap());
721 let (result, exit_status) = expand_value(&mut env, &value)
722 .now_or_never()
723 .unwrap()
724 .unwrap();
725 let content = assert_matches!(result, yash_env::variable::Scalar(content) => content);
726 assert_eq!(content, r"1\");
727 assert_eq!(exit_status, None);
728 }
729
730 #[test]
731 fn expand_value_array() {
732 let mut env = yash_env::Env::new_virtual();
733 let value =
734 yash_syntax::syntax::Array(vec!["''".parse().unwrap(), r"2\\".parse().unwrap()]);
735 let result = expand_value(&mut env, &value).now_or_never().unwrap();
736 let (result, exit_status) = result.unwrap();
737 let content = assert_matches!(result, yash_env::variable::Array(content) => content);
738 assert_eq!(content, ["".to_string(), r"2\".to_string()]);
739 assert_eq!(exit_status, None);
740 }
741}