use super::Env;
use super::Error;
use super::Phrase;
use super::to_field;
use crate::Runtime;
use crate::expansion::AssignReadOnlyError;
use crate::expansion::ErrorCause;
use crate::expansion::attr::Origin;
use crate::expansion::attr_strip::Strip as _;
use crate::expansion::expand_word;
use crate::expansion::initial::Expand as _;
use crate::expansion::quote_removal::skip_quotes;
use yash_env::variable::Scope;
use yash_env::variable::Value;
use yash_syntax::source::Location;
use yash_syntax::syntax::Param;
use yash_syntax::syntax::ParamType;
use yash_syntax::syntax::Switch;
use yash_syntax::syntax::SwitchAction;
use yash_syntax::syntax::SwitchCondition;
use yash_syntax::syntax::Word;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum Vacancy {
Unset,
EmptyScalar,
ValuelessArray,
EmptyValueArray,
}
impl Vacancy {
#[inline]
#[must_use]
pub fn of<'a, I: Into<Option<&'a Value>>>(value: I) -> Option<Vacancy> {
fn inner(value: Option<&Value>) -> Option<Vacancy> {
use Vacancy::*;
match value {
None => Some(Unset),
Some(Value::Scalar(scalar)) if scalar.is_empty() => Some(EmptyScalar),
Some(Value::Array(array)) if array.is_empty() => Some(ValuelessArray),
Some(Value::Array(array)) if array.len() == 1 && array[0].is_empty() => {
Some(EmptyValueArray)
}
Some(_) => None,
}
}
inner(value.into())
}
pub fn description(&self) -> &'static str {
use Vacancy::*;
match self {
Unset => "unset variable",
EmptyScalar => "empty string",
ValuelessArray => "empty array",
EmptyValueArray => "array with empty string",
}
}
}
impl std::fmt::Display for Vacancy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.description().fmt(f)
}
}
#[derive(Clone, Debug, Eq, Error, Hash, PartialEq)]
#[error("{} ({}: {})", self.message_or_default(), .param, .vacancy)]
#[non_exhaustive]
pub struct VacantError {
pub param: Param,
pub vacancy: Vacancy,
pub message: Option<String>,
}
impl VacantError {
#[must_use]
pub fn message_or_default(&self) -> &str {
self.message
.as_deref()
.unwrap_or("parameter expansion with empty value")
}
}
#[derive(Clone, Debug, Eq, Error, Hash, PartialEq)]
#[error("{cause}")]
pub struct NonassignableError {
pub cause: NonassignableErrorCause,
pub vacancy: Vacancy,
}
#[derive(Clone, Debug, Eq, Error, Hash, PartialEq)]
#[non_exhaustive]
pub enum NonassignableErrorCause {
#[error("parameter `{param}` is not an assignable variable")]
NotVariable { param: Param },
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum ValueCondition {
Occupied,
Vacant(Vacancy),
}
impl ValueCondition {
fn with<V: Into<Option<Vacancy>>>(cond: SwitchCondition, vacancy: V) -> Self {
fn inner(cond: SwitchCondition, vacancy: Option<Vacancy>) -> ValueCondition {
match (cond, vacancy) {
(_, None) => ValueCondition::Occupied,
(SwitchCondition::UnsetOrEmpty, Some(vacancy)) => ValueCondition::Vacant(vacancy),
(_, Some(Vacancy::Unset)) => ValueCondition::Vacant(Vacancy::Unset),
(
SwitchCondition::Unset,
Some(Vacancy::EmptyScalar | Vacancy::ValuelessArray | Vacancy::EmptyValueArray),
) => ValueCondition::Occupied,
}
}
inner(cond, vacancy.into())
}
}
fn attribute(mut phrase: Phrase) -> Phrase {
phrase.for_each_char_mut(|c| match c.origin {
Origin::Literal => c.origin = Origin::SoftExpansion,
Origin::HardExpansion | Origin::SoftExpansion => (),
});
phrase
}
async fn assign<S: Runtime + 'static>(
env: &mut Env<'_, S>,
param: &Param,
vacancy: Vacancy,
value: &Word,
location: Location,
) -> Result<Phrase, Error> {
if param.r#type != ParamType::Variable {
let param = param.clone();
let cause = NonassignableErrorCause::NotVariable { param };
let cause = ErrorCause::NonassignableParameter(NonassignableError { cause, vacancy });
return Err(Error { cause, location });
}
let value_phrase = attribute(value.expand(env).await?);
let joined_value = value_phrase.ifs_join(&env.inner.variables);
let final_value = skip_quotes(joined_value).strip().collect::<String>();
let result = to_field(&final_value).into();
env.inner
.get_or_create_variable(¶m.id, Scope::Global)
.assign(final_value, location)
.map_err(|e| {
let location = e.assigned_location.unwrap();
let cause = ErrorCause::AssignReadOnly(AssignReadOnlyError {
name: param.id.to_owned(),
new_value: e.new_value,
read_only_location: e.read_only_location,
vacancy: Some(vacancy),
});
Error { cause, location }
})?;
Ok(result)
}
async fn vacant_expansion_error_message<S: Runtime + 'static>(
env: &mut Env<'_, S>,
message_word: &Word,
) -> Result<Option<String>, Error> {
if message_word.units.is_empty() {
return Ok(None);
}
let (message_field, exit_status) = expand_word(env.inner, message_word).await?;
if exit_status.is_some() {
env.last_command_subst_exit_status = exit_status;
}
Ok(Some(message_field.value))
}
async fn vacant_expansion_error<S: Runtime + 'static>(
env: &mut Env<'_, S>,
param: &Param,
vacancy: Vacancy,
message_word: &Word,
location: Location,
) -> Error {
let message = match vacant_expansion_error_message(env, message_word).await {
Ok(message) => message,
Err(error) => return error,
};
let cause = ErrorCause::VacantExpansion(VacantError {
param: param.clone(),
vacancy,
message,
});
Error { cause, location }
}
pub async fn apply<S: Runtime + 'static>(
env: &mut Env<'_, S>,
switch: &Switch,
param: &Param,
value: Option<&Value>,
location: &Location,
) -> Option<Result<Phrase, Error>> {
use SwitchAction::*;
use ValueCondition::*;
let cond = ValueCondition::with(switch.condition, Vacancy::of(value));
match (switch.action, cond) {
(Alter, Vacant(_)) | (Default, Occupied) | (Assign, Occupied) | (Error, Occupied) => None,
(Alter, Occupied) | (Default, Vacant(_)) => {
Some(switch.word.expand(env).await.map(attribute))
}
(Assign, Vacant(vacancy)) => {
Some(assign(env, param, vacancy, &switch.word, location.clone()).await)
}
(Error, Vacant(vacancy)) => Some(Err(vacant_expansion_error(
env,
param,
vacancy,
&switch.word,
location.clone(),
)
.await)),
}
}
#[cfg(test)]
mod tests {
use super::super::to_field;
use super::*;
use crate::expansion::attr::AttrChar;
use assert_matches::assert_matches;
use futures_util::FutureExt as _;
use yash_env::variable::IFS;
use yash_syntax::syntax::SpecialParam;
use yash_syntax::syntax::SwitchAction::*;
use yash_syntax::syntax::SwitchCondition::*;
#[test]
fn vacancy_of_values() {
let vacancy = Vacancy::of(&None);
assert_eq!(vacancy, Some(Vacancy::Unset));
let vacancy = Vacancy::of(&Some(Value::scalar("")));
assert_eq!(vacancy, Some(Vacancy::EmptyScalar));
let vacancy = Vacancy::of(&Some(Value::scalar(".")));
assert_eq!(vacancy, None);
let vacancy = Vacancy::of(&Some(Value::Array(vec![])));
assert_eq!(vacancy, Some(Vacancy::ValuelessArray));
let vacancy = Vacancy::of(&Some(Value::array([""])));
assert_eq!(vacancy, Some(Vacancy::EmptyValueArray));
let vacancy = Vacancy::of(&Some(Value::array(["."])));
assert_eq!(vacancy, None);
let vacancy = Vacancy::of(&Some(Value::array(["", ""])));
assert_eq!(vacancy, None);
}
#[test]
fn attributing() {
let phrase = Phrase::Field(vec![
AttrChar {
value: 'a',
origin: Origin::Literal,
is_quoted: false,
is_quoting: false,
},
AttrChar {
value: 'b',
origin: Origin::SoftExpansion,
is_quoted: false,
is_quoting: false,
},
AttrChar {
value: 'c',
origin: Origin::HardExpansion,
is_quoted: false,
is_quoting: false,
},
]);
let phrase = attribute(phrase);
assert_eq!(
phrase,
Phrase::Field(vec![
AttrChar {
value: 'a',
origin: Origin::SoftExpansion,
is_quoted: false,
is_quoting: false,
},
AttrChar {
value: 'b',
origin: Origin::SoftExpansion,
is_quoted: false,
is_quoting: false,
},
AttrChar {
value: 'c',
origin: Origin::HardExpansion,
is_quoted: false,
is_quoting: false,
},
])
);
}
#[test]
fn alter_with_vacant_value() {
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
let switch = Switch {
action: Alter,
condition: Unset,
word: "foo".parse().unwrap(),
};
let param = Param::variable("var");
let location = Location::dummy("somewhere");
let result = apply(&mut env, &switch, ¶m, None, &location)
.now_or_never()
.unwrap();
assert_eq!(result, None);
}
#[test]
fn alter_with_occupied_value() {
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
let switch = Switch {
action: Alter,
condition: Unset,
word: "foo".parse().unwrap(),
};
let param = Param::variable("var");
let value = Value::scalar("bar");
let location = Location::dummy("somewhere");
let result = apply(&mut env, &switch, ¶m, Some(&value), &location)
.now_or_never()
.unwrap();
assert_eq!(result, Some(Ok(Phrase::Field(to_field("foo")))));
}
#[test]
fn default_with_vacant_value() {
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
let switch = Switch {
action: Default,
condition: Unset,
word: "foo".parse().unwrap(),
};
let param = Param::variable("var");
let location = Location::dummy("somewhere");
let result = apply(&mut env, &switch, ¶m, None, &location)
.now_or_never()
.unwrap();
assert_eq!(result, Some(Ok(Phrase::Field(to_field("foo")))));
}
#[test]
fn default_with_occupied_value() {
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
let switch = Switch {
action: Default,
condition: Unset,
word: "foo".parse().unwrap(),
};
let param = Param::variable("var");
let value = Value::scalar("bar");
let location = Location::dummy("somewhere");
let result = apply(&mut env, &switch, ¶m, Some(&value), &location)
.now_or_never()
.unwrap();
assert_eq!(result, None);
}
#[test]
fn assign_with_vacant_value() {
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
let switch = Switch {
action: Assign,
condition: Unset,
word: "foo".parse().unwrap(),
};
let param = Param::variable("var");
let location = Location::dummy("somewhere");
let result = apply(&mut env, &switch, ¶m, None, &location)
.now_or_never()
.unwrap();
assert_eq!(result, Some(Ok(Phrase::Field(to_field("foo")))));
let var = env.inner.variables.get("var").unwrap();
assert_eq!(var.value, Some(Value::scalar("foo")));
assert_eq!(var.last_assigned_location, Some(location));
assert!(!var.is_exported);
assert_eq!(var.read_only_location, None);
}
#[test]
fn assign_array_word() {
let mut env = yash_env::Env::new_virtual();
env.variables.positional_params_mut().values =
vec!["1".to_string(), "2 2".to_string(), "3".to_string()];
env.variables
.get_or_new(IFS, Scope::Global)
.assign("~", None)
.unwrap();
let mut env = Env::new(&mut env);
let switch = Switch {
action: Assign,
condition: Unset,
word: "\"$@\"".parse().unwrap(),
};
let param = Param::variable("var");
let location = Location::dummy("somewhere");
let result = apply(&mut env, &switch, ¶m, None, &location)
.now_or_never()
.unwrap();
fn char(value: char) -> AttrChar {
AttrChar {
value,
origin: Origin::SoftExpansion,
is_quoted: false,
is_quoting: false,
}
}
assert_eq!(
result,
Some(Ok(Phrase::Field(vec![
char('1'),
char('~'),
char('2'),
char(' '),
char(' '),
char('2'),
char('~'),
char('3'),
])))
);
let var = env.inner.variables.get("var").unwrap();
assert_eq!(var.value, Some(Value::scalar("1~2 2~3")));
assert_eq!(var.last_assigned_location, Some(location));
assert!(!var.is_exported);
assert_eq!(var.read_only_location, None);
}
#[test]
fn assign_with_occupied_value() {
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
let switch = Switch {
action: Assign,
condition: Unset,
word: "foo".parse().unwrap(),
};
let param = Param::variable("var");
let value = Value::scalar("bar");
let location = Location::dummy("somewhere");
let result = apply(&mut env, &switch, ¶m, Some(&value), &location)
.now_or_never()
.unwrap();
assert_eq!(result, None);
}
#[test]
fn assign_with_read_only_variable() {
let mut env = yash_env::Env::new_virtual();
let mut var = env.variables.get_or_new("var", Scope::Global);
var.assign("", None).unwrap();
var.make_read_only(Location::dummy("read-only"));
let save_var = var.clone();
let mut env = Env::new(&mut env);
let switch = Switch {
action: Assign,
condition: UnsetOrEmpty,
word: "foo".parse().unwrap(),
};
let param = Param::variable("var");
let value = save_var.value.as_ref();
let location = Location::dummy("somewhere");
let result = apply(&mut env, &switch, ¶m, value, &location)
.now_or_never()
.unwrap();
assert_matches!(result, Some(Err(error)) => {
assert_eq!(error.location, location);
assert_matches!(error.cause, ErrorCause::AssignReadOnly(e) => {
assert_eq!(e.name, "var");
assert_eq!(e.new_value, Value::scalar("foo"));
assert_eq!(e.read_only_location, Location::dummy("read-only"));
assert_eq!(e.vacancy, Some(Vacancy::EmptyScalar));
});
});
assert_eq!(env.inner.variables.get("var"), Some(&save_var));
}
#[test]
fn assign_to_special_parameter() {
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
let switch = Switch {
action: Assign,
condition: UnsetOrEmpty,
word: "foo".parse().unwrap(),
};
let param = Param::from(SpecialParam::Hyphen);
let value = Value::scalar("");
let location = Location::dummy("somewhere");
let result = apply(&mut env, &switch, ¶m, Some(&value), &location)
.now_or_never()
.unwrap();
let error = result.unwrap().unwrap_err();
assert_matches!(
error.cause,
ErrorCause::NonassignableParameter(error) => {
assert_eq!(error.cause, NonassignableErrorCause::NotVariable { param });
assert_eq!(error.vacancy, Vacancy::EmptyScalar);
}
);
assert_eq!(error.location, location);
}
#[test]
fn error_with_vacant_value_and_non_empty_word() {
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
let switch = Switch {
action: Error,
condition: Unset,
word: "foo".parse().unwrap(),
};
let param = Param::variable("var");
let location = Location::dummy("somewhere");
let result = apply(&mut env, &switch, ¶m, None, &location)
.now_or_never()
.unwrap();
let error = result.unwrap().unwrap_err();
assert_matches!(error.cause, ErrorCause::VacantExpansion(e) => {
assert_eq!(e.param, param);
assert_eq!(e.message, Some("foo".to_string()));
assert_eq!(e.vacancy, Vacancy::Unset);
});
}
#[test]
fn error_with_empty_scalar_and_non_empty_word() {
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
let switch = Switch {
action: Error,
condition: UnsetOrEmpty,
word: "bar".parse().unwrap(),
};
let param = Param::variable("var");
let value = Value::scalar("");
let location = Location::dummy("somewhere");
let result = apply(&mut env, &switch, ¶m, Some(&value), &location)
.now_or_never()
.unwrap();
let error = result.unwrap().unwrap_err();
assert_matches!(error.cause, ErrorCause::VacantExpansion(e) => {
assert_eq!(e.param, param);
assert_eq!(e.message, Some("bar".to_string()));
assert_eq!(e.vacancy, Vacancy::EmptyScalar);
});
}
#[test]
fn error_with_valueless_array_and_empty_word() {
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
let switch = Switch {
action: Error,
condition: UnsetOrEmpty,
word: "".parse().unwrap(),
};
let param = Param::variable("var");
let value = Value::Array(vec![]);
let location = Location::dummy("somewhere");
let result = apply(&mut env, &switch, ¶m, Some(&value), &location)
.now_or_never()
.unwrap();
let error = result.unwrap().unwrap_err();
assert_matches!(error.cause, ErrorCause::VacantExpansion(e) => {
assert_eq!(e.param, param);
assert_eq!(e.message, None);
assert_eq!(e.vacancy, Vacancy::ValuelessArray);
});
assert_eq!(error.location, location);
}
#[test]
fn error_with_set_value() {
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
let switch = Switch {
action: Error,
condition: Unset,
word: "foo".parse().unwrap(),
};
let param = Param::variable("var");
let value = Value::scalar("");
let location = Location::dummy("somewhere");
let result = apply(&mut env, &switch, ¶m, Some(&value), &location)
.now_or_never()
.unwrap();
assert_eq!(result, None);
}
}