1use super::Env;
20use super::Error;
21use super::Phrase;
22use super::to_field;
23use crate::Runtime;
24use crate::expansion::AssignReadOnlyError;
25use crate::expansion::ErrorCause;
26use crate::expansion::attr::Origin;
27use crate::expansion::attr_strip::Strip;
28use crate::expansion::expand_word;
29use crate::expansion::initial::Expand as _;
30use crate::expansion::quote_removal::skip_quotes;
31use yash_env::variable::Scope;
32use yash_env::variable::Value;
33use yash_syntax::source::Location;
34use yash_syntax::syntax::Param;
35use yash_syntax::syntax::ParamType;
36use yash_syntax::syntax::Switch;
37use yash_syntax::syntax::SwitchAction;
38use yash_syntax::syntax::SwitchCondition;
39use yash_syntax::syntax::Word;
40
41#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
43#[non_exhaustive]
44pub enum Vacancy {
45 Unset,
47 EmptyScalar,
49 ValuelessArray,
51 EmptyValueArray,
53}
54
55impl Vacancy {
56 #[inline]
61 #[must_use]
62 pub fn of<'a, I: Into<Option<&'a Value>>>(value: I) -> Option<Vacancy> {
63 fn inner(value: Option<&Value>) -> Option<Vacancy> {
64 use Vacancy::*;
65 match value {
66 None => Some(Unset),
67 Some(Value::Scalar(scalar)) if scalar.is_empty() => Some(EmptyScalar),
68 Some(Value::Array(array)) if array.is_empty() => Some(ValuelessArray),
69 Some(Value::Array(array)) if array.len() == 1 && array[0].is_empty() => {
70 Some(EmptyValueArray)
71 }
72 Some(_) => None,
73 }
74 }
75 inner(value.into())
76 }
77
78 pub fn description(&self) -> &'static str {
79 use Vacancy::*;
80 match self {
81 Unset => "unset variable",
82 EmptyScalar => "empty string",
83 ValuelessArray => "empty array",
84 EmptyValueArray => "array with empty string",
85 }
86 }
87}
88
89impl std::fmt::Display for Vacancy {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 self.description().fmt(f)
92 }
93}
94
95#[derive(Clone, Debug, Eq, Error, Hash, PartialEq)]
100#[error("{} ({}: {})", self.message_or_default(), .param, .vacancy)]
101#[non_exhaustive]
102pub struct VacantError {
103 pub param: Param,
105 pub vacancy: Vacancy,
107 pub message: Option<String>,
109}
110
111impl VacantError {
112 #[must_use]
117 pub fn message_or_default(&self) -> &str {
118 self.message
119 .as_deref()
120 .unwrap_or("parameter expansion with empty value")
121 }
122}
123
124#[derive(Clone, Debug, Eq, Error, Hash, PartialEq)]
126#[error("{cause}")]
127pub struct NonassignableError {
128 pub cause: NonassignableErrorCause,
130 pub vacancy: Vacancy,
133}
134
135#[derive(Clone, Debug, Eq, Error, Hash, PartialEq)]
136#[non_exhaustive]
137pub enum NonassignableErrorCause {
138 #[error("parameter `{param}` is not an assignable variable")]
140 NotVariable { param: Param },
141 }
150
151#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
153enum ValueCondition {
154 Occupied,
155 Vacant(Vacancy),
156}
157
158impl ValueCondition {
159 fn with<V: Into<Option<Vacancy>>>(cond: SwitchCondition, vacancy: V) -> Self {
160 fn inner(cond: SwitchCondition, vacancy: Option<Vacancy>) -> ValueCondition {
161 match (cond, vacancy) {
162 (_, None) => ValueCondition::Occupied,
163
164 (SwitchCondition::UnsetOrEmpty, Some(vacancy)) => ValueCondition::Vacant(vacancy),
165
166 (_, Some(Vacancy::Unset)) => ValueCondition::Vacant(Vacancy::Unset),
167
168 (
169 SwitchCondition::Unset,
170 Some(Vacancy::EmptyScalar | Vacancy::ValuelessArray | Vacancy::EmptyValueArray),
171 ) => ValueCondition::Occupied,
172 }
173 }
174 inner(cond, vacancy.into())
175 }
176}
177
178fn attribute(mut phrase: Phrase) -> Phrase {
184 phrase.for_each_char_mut(|c| match c.origin {
185 Origin::Literal => c.origin = Origin::SoftExpansion,
186 Origin::HardExpansion | Origin::SoftExpansion => (),
187 });
188 phrase
189}
190
191async fn assign<S: Runtime + 'static>(
197 env: &mut Env<'_, S>,
198 param: &Param,
199 vacancy: Vacancy,
200 value: &Word,
201 location: Location,
202) -> Result<Phrase, Error> {
203 if param.r#type != ParamType::Variable {
205 let param = param.clone();
206 let cause = NonassignableErrorCause::NotVariable { param };
207 let cause = ErrorCause::NonassignableParameter(NonassignableError { cause, vacancy });
208 return Err(Error { cause, location });
209 }
210 let value_phrase = attribute(value.expand(env).await?);
211 let joined_value = value_phrase.ifs_join(&env.inner.variables);
212 let final_value = skip_quotes(joined_value).strip().collect::<String>();
213 let result = to_field(&final_value).into();
214 env.inner
215 .get_or_create_variable(¶m.id, Scope::Global)
216 .assign(final_value, location)
217 .map_err(|e| {
218 let location = e.assigned_location.unwrap();
219 let cause = ErrorCause::AssignReadOnly(AssignReadOnlyError {
220 name: param.id.to_owned(),
221 new_value: e.new_value,
222 read_only_location: e.read_only_location,
223 vacancy: Some(vacancy),
224 });
225 Error { cause, location }
226 })?;
227 Ok(result)
228}
229
230async fn vacant_expansion_error_message<S: Runtime + 'static>(
232 env: &mut Env<'_, S>,
233 message_word: &Word,
234) -> Result<Option<String>, Error> {
235 if message_word.units.is_empty() {
236 return Ok(None);
237 }
238
239 let (message_field, exit_status) = expand_word(env.inner, message_word).await?;
240 if exit_status.is_some() {
241 env.last_command_subst_exit_status = exit_status;
242 }
243 Ok(Some(message_field.value))
244}
245
246async fn vacant_expansion_error<S: Runtime + 'static>(
248 env: &mut Env<'_, S>,
249 param: &Param,
250 vacancy: Vacancy,
251 message_word: &Word,
252 location: Location,
253) -> Error {
254 let message = match vacant_expansion_error_message(env, message_word).await {
255 Ok(message) => message,
256 Err(error) => return error,
257 };
258 let cause = ErrorCause::VacantExpansion(VacantError {
259 param: param.clone(),
260 vacancy,
261 message,
262 });
263 Error { cause, location }
264}
265
266pub async fn apply<S: Runtime + 'static>(
272 env: &mut Env<'_, S>,
273 switch: &Switch,
274 param: &Param,
275 value: Option<&Value>,
276 location: &Location,
277) -> Option<Result<Phrase, Error>> {
278 use SwitchAction::*;
279 use ValueCondition::*;
280 let cond = ValueCondition::with(switch.condition, Vacancy::of(value));
281 match (switch.action, cond) {
282 (Alter, Vacant(_)) | (Default, Occupied) | (Assign, Occupied) | (Error, Occupied) => None,
283
284 (Alter, Occupied) | (Default, Vacant(_)) => {
285 Some(switch.word.expand(env).await.map(attribute))
286 }
287
288 (Assign, Vacant(vacancy)) => {
289 Some(assign(env, param, vacancy, &switch.word, location.clone()).await)
290 }
291
292 (Error, Vacant(vacancy)) => Some(Err(vacant_expansion_error(
293 env,
294 param,
295 vacancy,
296 &switch.word,
297 location.clone(),
298 )
299 .await)),
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::super::to_field;
306 use super::*;
307 use crate::expansion::attr::AttrChar;
308 use assert_matches::assert_matches;
309 use futures_util::FutureExt;
310 use yash_env::variable::IFS;
311 use yash_syntax::syntax::SpecialParam;
312 use yash_syntax::syntax::SwitchAction::*;
313 use yash_syntax::syntax::SwitchCondition::*;
314
315 #[test]
316 fn vacancy_of_values() {
317 let vacancy = Vacancy::of(&None);
318 assert_eq!(vacancy, Some(Vacancy::Unset));
319 let vacancy = Vacancy::of(&Some(Value::scalar("")));
320 assert_eq!(vacancy, Some(Vacancy::EmptyScalar));
321 let vacancy = Vacancy::of(&Some(Value::scalar(".")));
322 assert_eq!(vacancy, None);
323 let vacancy = Vacancy::of(&Some(Value::Array(vec![])));
324 assert_eq!(vacancy, Some(Vacancy::ValuelessArray));
325 let vacancy = Vacancy::of(&Some(Value::array([""])));
326 assert_eq!(vacancy, Some(Vacancy::EmptyValueArray));
327 let vacancy = Vacancy::of(&Some(Value::array(["."])));
328 assert_eq!(vacancy, None);
329 let vacancy = Vacancy::of(&Some(Value::array(["", ""])));
330 assert_eq!(vacancy, None);
331 }
332
333 #[test]
334 fn attributing() {
335 let phrase = Phrase::Field(vec![
336 AttrChar {
337 value: 'a',
338 origin: Origin::Literal,
339 is_quoted: false,
340 is_quoting: false,
341 },
342 AttrChar {
343 value: 'b',
344 origin: Origin::SoftExpansion,
345 is_quoted: false,
346 is_quoting: false,
347 },
348 AttrChar {
349 value: 'c',
350 origin: Origin::HardExpansion,
351 is_quoted: false,
352 is_quoting: false,
353 },
354 ]);
355
356 let phrase = attribute(phrase);
357 assert_eq!(
358 phrase,
359 Phrase::Field(vec![
360 AttrChar {
361 value: 'a',
362 origin: Origin::SoftExpansion,
363 is_quoted: false,
364 is_quoting: false,
365 },
366 AttrChar {
367 value: 'b',
368 origin: Origin::SoftExpansion,
369 is_quoted: false,
370 is_quoting: false,
371 },
372 AttrChar {
373 value: 'c',
374 origin: Origin::HardExpansion,
375 is_quoted: false,
376 is_quoting: false,
377 },
378 ])
379 );
380 }
381
382 #[test]
383 fn alter_with_vacant_value() {
384 let mut env = yash_env::Env::new_virtual();
385 let mut env = Env::new(&mut env);
386 let switch = Switch {
387 action: Alter,
388 condition: Unset,
389 word: "foo".parse().unwrap(),
390 };
391 let param = Param::variable("var");
392 let location = Location::dummy("somewhere");
393 let result = apply(&mut env, &switch, ¶m, None, &location)
394 .now_or_never()
395 .unwrap();
396 assert_eq!(result, None);
397 }
398
399 #[test]
400 fn alter_with_occupied_value() {
401 let mut env = yash_env::Env::new_virtual();
402 let mut env = Env::new(&mut env);
403 let switch = Switch {
404 action: Alter,
405 condition: Unset,
406 word: "foo".parse().unwrap(),
407 };
408 let param = Param::variable("var");
409 let value = Value::scalar("bar");
410 let location = Location::dummy("somewhere");
411 let result = apply(&mut env, &switch, ¶m, Some(&value), &location)
412 .now_or_never()
413 .unwrap();
414 assert_eq!(result, Some(Ok(Phrase::Field(to_field("foo")))));
415 }
416
417 #[test]
418 fn default_with_vacant_value() {
419 let mut env = yash_env::Env::new_virtual();
420 let mut env = Env::new(&mut env);
421 let switch = Switch {
422 action: Default,
423 condition: Unset,
424 word: "foo".parse().unwrap(),
425 };
426 let param = Param::variable("var");
427 let location = Location::dummy("somewhere");
428 let result = apply(&mut env, &switch, ¶m, None, &location)
429 .now_or_never()
430 .unwrap();
431 assert_eq!(result, Some(Ok(Phrase::Field(to_field("foo")))));
432 }
433
434 #[test]
435 fn default_with_occupied_value() {
436 let mut env = yash_env::Env::new_virtual();
437 let mut env = Env::new(&mut env);
438 let switch = Switch {
439 action: Default,
440 condition: Unset,
441 word: "foo".parse().unwrap(),
442 };
443 let param = Param::variable("var");
444 let value = Value::scalar("bar");
445 let location = Location::dummy("somewhere");
446 let result = apply(&mut env, &switch, ¶m, Some(&value), &location)
447 .now_or_never()
448 .unwrap();
449 assert_eq!(result, None);
450 }
451
452 #[test]
453 fn assign_with_vacant_value() {
454 let mut env = yash_env::Env::new_virtual();
455 let mut env = Env::new(&mut env);
456 let switch = Switch {
457 action: Assign,
458 condition: Unset,
459 word: "foo".parse().unwrap(),
460 };
461 let param = Param::variable("var");
462 let location = Location::dummy("somewhere");
463
464 let result = apply(&mut env, &switch, ¶m, None, &location)
465 .now_or_never()
466 .unwrap();
467 assert_eq!(result, Some(Ok(Phrase::Field(to_field("foo")))));
468
469 let var = env.inner.variables.get("var").unwrap();
470 assert_eq!(var.value, Some(Value::scalar("foo")));
471 assert_eq!(var.last_assigned_location, Some(location));
472 assert!(!var.is_exported);
473 assert_eq!(var.read_only_location, None);
474 }
475
476 #[test]
477 fn assign_array_word() {
478 let mut env = yash_env::Env::new_virtual();
479 env.variables.positional_params_mut().values =
480 vec!["1".to_string(), "2 2".to_string(), "3".to_string()];
481 env.variables
482 .get_or_new(IFS, Scope::Global)
483 .assign("~", None)
484 .unwrap();
485 let mut env = Env::new(&mut env);
486 let switch = Switch {
487 action: Assign,
488 condition: Unset,
489 word: "\"$@\"".parse().unwrap(),
490 };
491 let param = Param::variable("var");
492 let location = Location::dummy("somewhere");
493
494 let result = apply(&mut env, &switch, ¶m, None, &location)
495 .now_or_never()
496 .unwrap();
497
498 fn char(value: char) -> AttrChar {
499 AttrChar {
500 value,
501 origin: Origin::SoftExpansion,
502 is_quoted: false,
503 is_quoting: false,
504 }
505 }
506 assert_eq!(
507 result,
508 Some(Ok(Phrase::Field(vec![
509 char('1'),
510 char('~'),
511 char('2'),
512 char(' '),
513 char(' '),
514 char('2'),
515 char('~'),
516 char('3'),
517 ])))
518 );
519
520 let var = env.inner.variables.get("var").unwrap();
521 assert_eq!(var.value, Some(Value::scalar("1~2 2~3")));
522 assert_eq!(var.last_assigned_location, Some(location));
523 assert!(!var.is_exported);
524 assert_eq!(var.read_only_location, None);
525 }
526
527 #[test]
530 fn assign_with_occupied_value() {
531 let mut env = yash_env::Env::new_virtual();
532 let mut env = Env::new(&mut env);
533 let switch = Switch {
534 action: Assign,
535 condition: Unset,
536 word: "foo".parse().unwrap(),
537 };
538 let param = Param::variable("var");
539 let value = Value::scalar("bar");
540 let location = Location::dummy("somewhere");
541 let result = apply(&mut env, &switch, ¶m, Some(&value), &location)
542 .now_or_never()
543 .unwrap();
544 assert_eq!(result, None);
545 }
546
547 #[test]
548 fn assign_with_read_only_variable() {
549 let mut env = yash_env::Env::new_virtual();
550 let mut var = env.variables.get_or_new("var", Scope::Global);
551 var.assign("", None).unwrap();
552 var.make_read_only(Location::dummy("read-only"));
553 let save_var = var.clone();
554 let mut env = Env::new(&mut env);
555 let switch = Switch {
556 action: Assign,
557 condition: UnsetOrEmpty,
558 word: "foo".parse().unwrap(),
559 };
560 let param = Param::variable("var");
561 let value = save_var.value.as_ref();
562 let location = Location::dummy("somewhere");
563
564 let result = apply(&mut env, &switch, ¶m, value, &location)
565 .now_or_never()
566 .unwrap();
567 assert_matches!(result, Some(Err(error)) => {
568 assert_eq!(error.location, location);
569 assert_matches!(error.cause, ErrorCause::AssignReadOnly(e) => {
570 assert_eq!(e.name, "var");
571 assert_eq!(e.new_value, Value::scalar("foo"));
572 assert_eq!(e.read_only_location, Location::dummy("read-only"));
573 assert_eq!(e.vacancy, Some(Vacancy::EmptyScalar));
574 });
575 });
576 assert_eq!(env.inner.variables.get("var"), Some(&save_var));
577 }
578
579 #[test]
580 fn assign_to_special_parameter() {
581 let mut env = yash_env::Env::new_virtual();
582 let mut env = Env::new(&mut env);
583 let switch = Switch {
584 action: Assign,
585 condition: UnsetOrEmpty,
586 word: "foo".parse().unwrap(),
587 };
588 let param = Param::from(SpecialParam::Hyphen);
589 let value = Value::scalar("");
590 let location = Location::dummy("somewhere");
591
592 let result = apply(&mut env, &switch, ¶m, Some(&value), &location)
593 .now_or_never()
594 .unwrap();
595 let error = result.unwrap().unwrap_err();
596 assert_matches!(
597 error.cause,
598 ErrorCause::NonassignableParameter(error) => {
599 assert_eq!(error.cause, NonassignableErrorCause::NotVariable { param });
600 assert_eq!(error.vacancy, Vacancy::EmptyScalar);
601 }
602 );
603 assert_eq!(error.location, location);
604 }
605
606 #[test]
607 fn error_with_vacant_value_and_non_empty_word() {
608 let mut env = yash_env::Env::new_virtual();
609 let mut env = Env::new(&mut env);
610 let switch = Switch {
611 action: Error,
612 condition: Unset,
613 word: "foo".parse().unwrap(),
614 };
615 let param = Param::variable("var");
616 let location = Location::dummy("somewhere");
617 let result = apply(&mut env, &switch, ¶m, None, &location)
618 .now_or_never()
619 .unwrap();
620 let error = result.unwrap().unwrap_err();
621 assert_matches!(error.cause, ErrorCause::VacantExpansion(e) => {
622 assert_eq!(e.param, param);
623 assert_eq!(e.message, Some("foo".to_string()));
624 assert_eq!(e.vacancy, Vacancy::Unset);
625 });
626 }
627
628 #[test]
629 fn error_with_empty_scalar_and_non_empty_word() {
630 let mut env = yash_env::Env::new_virtual();
631 let mut env = Env::new(&mut env);
632 let switch = Switch {
633 action: Error,
634 condition: UnsetOrEmpty,
635 word: "bar".parse().unwrap(),
636 };
637 let param = Param::variable("var");
638 let value = Value::scalar("");
639 let location = Location::dummy("somewhere");
640 let result = apply(&mut env, &switch, ¶m, Some(&value), &location)
641 .now_or_never()
642 .unwrap();
643 let error = result.unwrap().unwrap_err();
644 assert_matches!(error.cause, ErrorCause::VacantExpansion(e) => {
645 assert_eq!(e.param, param);
646 assert_eq!(e.message, Some("bar".to_string()));
647 assert_eq!(e.vacancy, Vacancy::EmptyScalar);
648 });
649 }
650
651 #[test]
652 fn error_with_valueless_array_and_empty_word() {
653 let mut env = yash_env::Env::new_virtual();
654 let mut env = Env::new(&mut env);
655 let switch = Switch {
656 action: Error,
657 condition: UnsetOrEmpty,
658 word: "".parse().unwrap(),
659 };
660 let param = Param::variable("var");
661 let value = Value::Array(vec![]);
662 let location = Location::dummy("somewhere");
663 let result = apply(&mut env, &switch, ¶m, Some(&value), &location)
664 .now_or_never()
665 .unwrap();
666 let error = result.unwrap().unwrap_err();
667 assert_matches!(error.cause, ErrorCause::VacantExpansion(e) => {
668 assert_eq!(e.param, param);
669 assert_eq!(e.message, None);
670 assert_eq!(e.vacancy, Vacancy::ValuelessArray);
671 });
672 assert_eq!(error.location, location);
673 }
674
675 #[test]
676 fn error_with_set_value() {
677 let mut env = yash_env::Env::new_virtual();
678 let mut env = Env::new(&mut env);
679 let switch = Switch {
680 action: Error,
681 condition: Unset,
682 word: "foo".parse().unwrap(),
683 };
684 let param = Param::variable("var");
685 let value = Value::scalar("");
686 let location = Location::dummy("somewhere");
687 let result = apply(&mut env, &switch, ¶m, Some(&value), &location)
688 .now_or_never()
689 .unwrap();
690 assert_eq!(result, None);
691 }
692}