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;
105use yash_syntax::syntax::ExpansionMode;
106use yash_syntax::syntax::Param;
107use yash_syntax::syntax::Text;
108use yash_syntax::syntax::Word;
109
110#[doc(no_inline)]
111pub use yash_env::semantics::Field;
112#[doc(no_inline)]
113pub use yash_env::semantics::expansion::{attr, attr_strip, quote_removal, split};
114
115#[derive(Clone, Debug, Eq, Error, PartialEq)]
117#[error("cannot assign to read-only variable {name:?}")]
118pub struct AssignReadOnlyError {
119 pub name: String,
121 pub new_value: Value,
123 pub read_only_location: Location,
125 pub vacancy: Option<Vacancy>,
132}
133
134#[derive(Clone, Debug, Eq, Error, PartialEq)]
136pub enum ErrorCause {
137 #[error("error in command substitution: {0}")]
139 CommandSubstError(Errno),
140
141 #[error(transparent)]
143 ArithError(#[from] ArithError),
144
145 #[error(transparent)]
147 AssignReadOnly(#[from] AssignReadOnlyError),
148
149 #[error("unset parameter `{param}`")]
151 UnsetParameter { param: Param },
152
153 #[error(transparent)]
155 VacantExpansion(#[from] VacantError),
156
157 #[error(transparent)]
159 NonassignableParameter(#[from] NonassignableError),
160}
161
162impl ErrorCause {
163 #[must_use]
165 pub fn message(&self) -> &str {
166 use ErrorCause::*;
168 match self {
169 CommandSubstError(_) => "error performing the command substitution",
170 ArithError(_) => "error evaluating the arithmetic expansion",
171 AssignReadOnly(_) => "error assigning to variable",
172 UnsetParameter { .. } => "cannot expand unset parameter",
173 VacantExpansion(error) => error.message_or_default(),
174 NonassignableParameter(_) => "cannot assign to parameter",
175 }
176 }
177
178 #[must_use]
180 pub fn label(&self) -> Cow<'_, str> {
181 use ErrorCause::*;
183 match self {
184 CommandSubstError(e) => e.to_string(),
185 ArithError(e) => e.to_string(),
186 AssignReadOnly(e) => e.to_string(),
187 UnsetParameter { param } => format!("parameter `{param}` is not set"),
188 VacantExpansion(e) => match e.vacancy {
189 Vacancy::Unset => format!("parameter `{}` is not set", e.param),
190 Vacancy::EmptyScalar => format!("parameter `{}` is an empty string", e.param),
191 Vacancy::ValuelessArray => format!("parameter `{}` is an empty array", e.param),
192 Vacancy::EmptyValueArray => {
193 format!("parameter `{}` is an array of an empty string", e.param)
194 }
195 },
196 NonassignableParameter(e) => e.to_string(),
197 }
198 .into()
199 }
200
201 #[must_use]
204 pub fn related_location(&self) -> Option<(&Location, &'static str)> {
205 use ErrorCause::*;
207 match self {
208 CommandSubstError(_) => None,
209 ArithError(e) => e.related_location(),
210 AssignReadOnly(e) => Some((
211 &e.read_only_location,
212 "the variable was made read-only here",
213 )),
214 UnsetParameter { .. } => None,
215 VacantExpansion(_) => None,
216 NonassignableParameter(_) => None,
217 }
218 }
219
220 #[must_use]
222 pub fn footer(&self) -> Option<&'static str> {
223 use ErrorCause::*;
224 match self {
225 CommandSubstError(_)
226 | ArithError(_)
227 | AssignReadOnly(_)
228 | VacantExpansion(_)
229 | NonassignableParameter(_) => None,
230
231 UnsetParameter { .. } => Some("unset parameters are disallowed by the nounset option"),
232 }
233 }
234}
235
236#[derive(Clone, Debug, Eq, Error, PartialEq)]
238#[error("{cause}")]
239pub struct Error {
240 pub cause: ErrorCause,
241 pub location: Location,
242}
243
244impl Error {
245 #[must_use]
247 pub fn to_report(&self) -> Report<'_> {
248 let mut report = Report::new();
249 report.r#type = ReportType::Error;
250 report.title = self.cause.message().into();
251 report.snippets = Snippet::with_primary_span(&self.location, self.cause.label());
252
253 if let Some((location, label)) = self.cause.related_location() {
254 let label = label.into();
255 let span = Span {
256 range: location.byte_range(),
257 role: SpanRole::Supplementary { label },
258 };
259 add_span(&location.code, span, &mut report.snippets);
260 }
261
262 if let Some(footer) = self.cause.footer() {
263 report.footnotes.push(Footnote {
264 r#type: FootnoteType::Note,
265 label: footer.into(),
266 });
267 }
268
269 let vacancy = match &self.cause {
271 ErrorCause::CommandSubstError(_) => None,
272 ErrorCause::ArithError(_) => None,
273 ErrorCause::AssignReadOnly(e) => e.vacancy,
274 ErrorCause::UnsetParameter { .. } => None,
275 ErrorCause::VacantExpansion(_) => None,
276 ErrorCause::NonassignableParameter(e) => Some(e.vacancy),
277 };
278 if let Some(vacancy) = vacancy {
279 let message = match vacancy {
280 Vacancy::Unset => "assignment was attempted because the parameter was not set",
281 Vacancy::EmptyScalar => {
282 "assignment was attempted because the parameter was an empty string"
283 }
284 Vacancy::ValuelessArray => {
285 "assignment was attempted because the parameter was an empty array"
286 }
287 Vacancy::EmptyValueArray => {
288 "assignment was attempted because the parameter was an array of an empty string"
289 }
290 };
291 report.footnotes.push(Footnote {
292 r#type: FootnoteType::Note,
293 label: message.into(),
294 });
295 }
296
297 report
298 }
299}
300
301impl<'a> From<&'a Error> for Report<'a> {
303 #[inline(always)]
304 fn from(error: &'a Error) -> Self {
305 error.to_report()
306 }
307}
308
309pub type Result<T> = std::result::Result<T, Error>;
311
312pub async fn expand_text(
319 env: &mut yash_env::Env,
320 text: &Text,
321) -> Result<(String, Option<ExitStatus>)> {
322 let mut env = initial::Env::new(env);
323 let phrase = text.expand(&mut env).await?;
327 let chars = phrase.ifs_join(&env.inner.variables);
328 let result = skip_quotes(chars).strip().collect();
329 Ok((result, env.last_command_subst_exit_status))
330}
331
332pub async fn expand_word_attr(
341 env: &mut yash_env::Env,
342 word: &Word,
343) -> Result<(AttrField, Option<ExitStatus>)> {
344 let mut env = initial::Env::new(env);
345 let phrase = word.expand(&mut env).await?;
349 let chars = phrase.ifs_join(&env.inner.variables);
350 let origin = word.location.clone();
351 let field = AttrField { chars, origin };
352 Ok((field, env.last_command_subst_exit_status))
353}
354
355pub async fn expand_word(
367 env: &mut yash_env::Env,
368 word: &Word,
369) -> Result<(Field, Option<ExitStatus>)> {
370 let (field, exit_status) = expand_word_attr(env, word).await?;
371 let field = field.remove_quotes_and_strip();
372 Ok((field, exit_status))
373}
374
375pub async fn expand_word_multiple<R>(
385 env: &mut yash_env::Env,
386 word: &Word,
387 results: &mut R,
388) -> Result<Option<ExitStatus>>
389where
390 R: Extend<Field>,
391{
392 let mut env = initial::Env::new(env);
393
394 let phrase = word.expand(&mut env).await?;
396
397 let ifs = env
401 .inner
402 .variables
403 .get_scalar(IFS)
404 .map(Ifs::new)
405 .unwrap_or_default();
406 let mut split_fields = Vec::with_capacity(phrase.field_count());
407 for chars in phrase {
408 let origin = word.location.clone();
409 let attr_field = AttrField { chars, origin };
410 split::split_into(attr_field, &ifs, &mut split_fields);
411 }
412 drop(ifs);
413
414 for field in split_fields {
416 results.extend(glob(env.inner, field));
417 }
418
419 Ok(env.last_command_subst_exit_status)
420}
421
422pub async fn expand_word_with_mode<R>(
436 env: &mut yash_env::Env,
437 word: &Word,
438 mode: ExpansionMode,
439 results: &mut R,
440) -> Result<Option<ExitStatus>>
441where
442 R: Extend<Field>,
443{
444 match mode {
445 ExpansionMode::Single => {
446 let (field, exit_status) = expand_word(env, word).await?;
447 results.extend(std::iter::once(field));
448 Ok(exit_status)
449 }
450 ExpansionMode::Multiple => expand_word_multiple(env, word, results).await,
451 }
452}
453
454pub async fn expand_words<'a, I: IntoIterator<Item = &'a Word>>(
464 env: &mut yash_env::Env,
465 words: I,
466) -> Result<(Vec<Field>, Option<ExitStatus>)> {
467 let mut fields = Vec::new();
468 let mut last_exit_status = None;
469
470 for word in words {
471 let exit_status = expand_word_multiple(env, word, &mut fields).await?;
472 if exit_status.is_some() {
473 last_exit_status = exit_status;
474 }
475 }
476
477 Ok((fields, last_exit_status))
478}
479
480pub async fn expand_value(
488 env: &mut yash_env::Env,
489 value: &yash_syntax::syntax::Value,
490) -> Result<(yash_env::variable::Value, Option<ExitStatus>)> {
491 match value {
492 yash_syntax::syntax::Scalar(word) => {
493 let (field, exit_status) = expand_word(env, word).await?;
494 Ok((yash_env::variable::Scalar(field.value), exit_status))
495 }
496 yash_syntax::syntax::Array(words) => {
497 let (fields, exit_status) = expand_words(env, words).await?;
498 let fields = fields.into_iter().map(|f| f.value).collect();
499 Ok((yash_env::variable::Array(fields), exit_status))
500 }
501 }
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507 use crate::tests::echo_builtin;
508 use crate::tests::return_builtin;
509 use assert_matches::assert_matches;
510 use futures_util::FutureExt;
511 use yash_env::variable::Scope;
512 use yash_env_test_helper::in_virtual_system;
513
514 #[test]
515 fn from_error_for_report() {
516 let error = Error {
517 cause: ErrorCause::AssignReadOnly(AssignReadOnlyError {
518 name: "foo".into(),
519 new_value: "value".into(),
520 read_only_location: Location::dummy("ROL"),
521 vacancy: None,
522 }),
523 location: Location {
524 range: 2..4,
525 ..Location::dummy("hello")
526 },
527 };
528
529 let report = Report::from(&error);
530
531 assert_eq!(report.r#type, ReportType::Error);
532 assert_eq!(report.title, "error assigning to variable");
533 assert_eq!(report.snippets.len(), 2);
534 assert_eq!(*report.snippets[0].code.value.borrow(), "hello");
535 assert_eq!(report.snippets[0].spans.len(), 1);
536 assert_eq!(report.snippets[0].spans[0].range, 2..4);
537 assert_matches!(
538 &report.snippets[0].spans[0].role,
539 SpanRole::Primary { label } if label == "cannot assign to read-only variable \"foo\""
540 );
541 assert_eq!(*report.snippets[1].code.value.borrow(), "ROL");
542 assert_eq!(report.snippets[1].spans.len(), 1);
543 assert_eq!(report.snippets[1].spans[0].range, 0..3);
544 assert_matches!(
545 &report.snippets[1].spans[0].role,
546 SpanRole::Supplementary { label } if label == "the variable was made read-only here"
547 );
548 assert_eq!(report.footnotes, []);
549 }
550
551 #[test]
552 fn from_error_for_report_with_vacancy() {
553 let error = Error {
554 cause: ErrorCause::AssignReadOnly(AssignReadOnlyError {
555 name: "foo".into(),
556 new_value: "value".into(),
557 read_only_location: Location::dummy("ROL"),
558 vacancy: Some(Vacancy::EmptyScalar),
559 }),
560 location: Location {
561 range: 2..4,
562 ..Location::dummy("hello")
563 },
564 };
565
566 let report = Report::from(&error);
567
568 assert_eq!(
569 report.footnotes,
570 [Footnote {
571 r#type: FootnoteType::Note,
572 label: "assignment was attempted because the parameter was an empty string".into(),
573 }]
574 );
575 }
576
577 #[test]
578 fn expand_word_multiple_performs_initial_expansion() {
579 in_virtual_system(|mut env, _state| async move {
580 env.builtins.insert("echo", echo_builtin());
581 env.builtins.insert("return", return_builtin());
582 let word = "[$(echo echoed; return -n 42)]".parse().unwrap();
583 let mut fields = Vec::new();
584 let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
585 .await
586 .unwrap();
587 assert_eq!(exit_status, Some(ExitStatus(42)));
588 assert_matches!(fields.as_slice(), [f] => {
589 assert_eq!(f.value, "[echoed]");
590 });
591 })
592 }
593
594 #[test]
595 fn expand_word_multiple_performs_field_splitting_possibly_with_default_ifs() {
596 let mut env = yash_env::Env::new_virtual();
597 env.variables
598 .get_or_new("v", Scope::Global)
599 .assign("foo bar ", None)
600 .unwrap();
601 let word = "$v".parse().unwrap();
602 let mut fields = Vec::new();
603 let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
604 .now_or_never()
605 .unwrap()
606 .unwrap();
607 assert_eq!(exit_status, None);
608 assert_matches!(fields.as_slice(), [f1, f2] => {
609 assert_eq!(f1.value, "foo");
610 assert_eq!(f2.value, "bar");
611 });
612 }
613
614 #[test]
615 fn expand_word_multiple_performs_field_splitting_with_current_ifs() {
616 let mut env = yash_env::Env::new_virtual();
617 env.variables
618 .get_or_new("v", Scope::Global)
619 .assign("foo bar ", None)
620 .unwrap();
621 env.variables
622 .get_or_new(IFS, Scope::Global)
623 .assign(" o", None)
624 .unwrap();
625 let word = "$v".parse().unwrap();
626 let mut fields = Vec::new();
627 let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
628 .now_or_never()
629 .unwrap()
630 .unwrap();
631 assert_eq!(exit_status, None);
632 assert_matches!(fields.as_slice(), [f1, f2, f3] => {
633 assert_eq!(f1.value, "f");
634 assert_eq!(f2.value, "");
635 assert_eq!(f3.value, "bar");
636 });
637 }
638
639 #[test]
640 fn expand_word_multiple_performs_quote_removal() {
641 let mut env = yash_env::Env::new_virtual();
642 let word = "\"foo\"'$v'".parse().unwrap();
643 let mut fields = Vec::new();
644 let exit_status = expand_word_multiple(&mut env, &word, &mut fields)
645 .now_or_never()
646 .unwrap()
647 .unwrap();
648 assert_eq!(exit_status, None);
649 assert_matches!(fields.as_slice(), [f] => {
650 assert_eq!(f.value, "foo$v");
651 });
652 }
653
654 #[test]
655 fn expand_words_returns_exit_status_of_last_command_substitution() {
656 in_virtual_system(|mut env, _state| async move {
657 env.builtins.insert("return", return_builtin());
658 let word1 = "$(return -n 12)".parse().unwrap();
659 let word2 = "$(return -n 34)$(return -n 56)".parse().unwrap();
660 let (_, exit_status) = expand_words(&mut env, &[word1, word2]).await.unwrap();
661 assert_eq!(exit_status, Some(ExitStatus(56)));
662 })
663 }
664
665 #[test]
666 fn expand_words_performs_field_splitting() {
667 let mut env = yash_env::Env::new_virtual();
668 env.variables
669 .get_or_new("v", Scope::Global)
670 .assign(" foo bar ", None)
671 .unwrap();
672 let word = "$v".parse().unwrap();
673 let (fields, _) = expand_words(&mut env, &[word])
674 .now_or_never()
675 .unwrap()
676 .unwrap();
677 assert_matches!(fields.as_slice(), [f1, f2] => {
678 assert_eq!(f1.value, "foo");
679 assert_eq!(f2.value, "bar");
680 });
681 }
682
683 #[test]
684 fn expand_value_scalar() {
685 let mut env = yash_env::Env::new_virtual();
686 let value = yash_syntax::syntax::Scalar(r"1\\".parse().unwrap());
687 let (result, exit_status) = expand_value(&mut env, &value)
688 .now_or_never()
689 .unwrap()
690 .unwrap();
691 let content = assert_matches!(result, yash_env::variable::Scalar(content) => content);
692 assert_eq!(content, r"1\");
693 assert_eq!(exit_status, None);
694 }
695
696 #[test]
697 fn expand_value_array() {
698 let mut env = yash_env::Env::new_virtual();
699 let value =
700 yash_syntax::syntax::Array(vec!["''".parse().unwrap(), r"2\\".parse().unwrap()]);
701 let result = expand_value(&mut env, &value).now_or_never().unwrap();
702 let (result, exit_status) = result.unwrap();
703 let content = assert_matches!(result, yash_env::variable::Array(content) => content);
704 assert_eq!(content, ["".to_string(), r"2\".to_string()]);
705 assert_eq!(exit_status, None);
706 }
707}