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