1pub(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#[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
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 results.extend(std::iter::once(Annotation::new(
325 AnnotationType::Info,
326 label.into(),
327 location,
328 )))
329 }
330
331 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 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
374pub type Result<T> = std::result::Result<T, Error>;
376
377pub 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 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
397pub 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 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
420pub 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
440pub 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 let phrase = word.expand(&mut env).await?;
461
462 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 for field in split_fields {
481 results.extend(glob(env.inner, field));
482 }
483
484 Ok(env.last_command_subst_exit_status)
485}
486
487pub 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
519pub 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
545pub 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}