1pub 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#[derive(Clone, Debug, Eq, Error, PartialEq)]
120#[error("cannot assign to read-only variable {name:?}")]
121pub struct AssignReadOnlyError {
122 pub name: String,
124 pub new_value: Value,
126 pub read_only_location: Location,
128 pub vacancy: Option<Vacancy>,
135}
136
137#[derive(Clone, Debug, Eq, Error, PartialEq)]
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
165impl ErrorCause {
166 #[must_use]
168 pub fn message(&self) -> &str {
169 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 #[must_use]
183 pub fn label(&self) -> Cow<'_, str> {
184 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 #[must_use]
207 pub fn related_location(&self) -> Option<(&Location, &'static str)> {
208 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 #[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#[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 #[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 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
304impl<'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 results.extend(std::iter::once(Annotation::new(
326 AnnotationType::Info,
327 label.into(),
328 location,
329 )))
330 }
331
332 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 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
375pub type Result<T> = std::result::Result<T, Error>;
377
378pub 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 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
398pub 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 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
421pub 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
441pub 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 let phrase = word.expand(&mut env).await?;
462
463 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 for field in split_fields {
482 results.extend(glob(env.inner, field));
483 }
484
485 Ok(env.last_command_subst_exit_status)
486}
487
488pub 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
520pub 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
546pub 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}