use super::Command;
use super::Mode;
use std::borrow::Cow;
use thiserror::Error;
use yash_env::Env;
use yash_env::path::Path;
use yash_env::path::PathBuf;
use yash_env::semantics::ExitStatus;
use yash_env::variable::HOME;
use yash_env::variable::OLDPWD;
use yash_syntax::source::Location;
use yash_syntax::source::pretty::Annotation;
use yash_syntax::source::pretty::AnnotationType;
use yash_syntax::source::pretty::MessageBase;
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum Origin {
Home,
Oldpwd,
Cdpath,
Literal,
}
#[derive(Debug, Clone, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum TargetError {
#[error("$HOME not set")]
UnsetHome {
location: Location,
},
#[error("$OLDPWD not set")]
UnsetOldpwd {
location: Location,
},
#[error("change target contains non-existing directory component")]
NonExistingDirectory {
missing: PathBuf,
target: PathBuf,
location: Location,
},
}
impl TargetError {
#[must_use]
pub fn exit_status(&self) -> ExitStatus {
match self {
TargetError::UnsetHome { .. } | TargetError::UnsetOldpwd { .. } => {
super::EXIT_STATUS_UNSET_VARIABLE
}
TargetError::NonExistingDirectory { .. } => super::EXIT_STATUS_CANNOT_CANONICALIZE,
}
}
}
impl MessageBase for TargetError {
fn message_title(&self) -> Cow<'_, str> {
self.to_string().into()
}
fn main_annotation(&self) -> Annotation<'_> {
use TargetError::*;
match self {
UnsetHome { location } => Annotation::new(
AnnotationType::Error,
"cd built-in used without operand requires non-empty $HOME".into(),
location,
),
UnsetOldpwd { location } => Annotation::new(
AnnotationType::Error,
"'-' operand requires non-empty $OLDPWD".into(),
location,
),
NonExistingDirectory {
missing,
target: _,
location,
} => Annotation::new(
AnnotationType::Error,
format!("intermediate directory '{}' not found", missing.display()).into(),
location,
),
}
}
fn additional_annotations<'a, T: Extend<Annotation<'a>>>(&'a self, results: &mut T) {
use TargetError::*;
match self {
UnsetHome { location: _ } | UnsetOldpwd { location: _ } => {}
NonExistingDirectory {
missing: _,
target,
location,
} => results.extend(std::iter::once(Annotation::new(
AnnotationType::Info,
format!(
"while resolving '..' in target directory '{}'",
target.display()
)
.into(),
location,
))),
}
}
}
fn get_scalar<'a>(env: &'a Env, name: &str) -> Option<&'a str> {
env.variables
.get_scalar(name)
.filter(|value| !value.is_empty())
}
pub fn target(env: &Env, command: &Command, pwd: &str) -> Result<(PathBuf, Origin), TargetError> {
let (mut curpath, mut origin) = match &command.operand {
None => {
let home = get_scalar(env, HOME).ok_or_else(|| {
let builtin = env.stack.current_builtin();
let location =
builtin.map_or_else(|| Location::dummy(""), |b| b.name.origin.clone());
TargetError::UnsetHome { location }
})?;
(PathBuf::from(home), Origin::Home)
}
Some(operand) if operand.value == "-" => {
let oldpwd = get_scalar(env, OLDPWD).ok_or_else(|| TargetError::UnsetOldpwd {
location: operand.origin.clone(),
})?;
(PathBuf::from(&oldpwd), Origin::Oldpwd)
}
Some(operand) => (PathBuf::from(&operand.value), Origin::Literal),
};
if let Some(path) = super::cdpath::search(env, &curpath) {
curpath = path;
origin = Origin::Cdpath;
}
if command.mode == Mode::Physical {
return Ok((curpath, origin));
}
curpath = Path::new(pwd).join(curpath);
curpath = super::canonicalize::canonicalize(&env.system, &curpath).map_err(|e| {
TargetError::NonExistingDirectory {
missing: e.missing,
target: curpath,
location: {
let field = command.operand.as_ref();
let field = field.or_else(|| env.stack.current_builtin().map(|b| &b.name));
field.map_or_else(|| Location::dummy(""), |f| f.origin.clone())
},
}
})?;
Ok((curpath, origin))
}
#[cfg(test)]
mod tests {
use super::*;
use yash_env::semantics::Field;
use yash_env::stack::Builtin;
use yash_env::stack::Frame;
use yash_env::variable::Scope;
#[test]
fn default_home() {
let mut env = Env::new_virtual();
let command = Command {
mode: Mode::default(),
ensure_pwd: false,
operand: None,
};
env.get_or_create_variable(HOME, Scope::Global)
.assign("/home/user", None)
.unwrap();
let target = target(&env, &command, "").unwrap();
assert_eq!(target, (PathBuf::from("/home/user"), Origin::Home));
}
#[test]
fn home_unset() {
let mut env = Env::new_virtual();
let command = Command {
mode: Mode::default(),
ensure_pwd: false,
operand: None,
};
let arg0 = Field::dummy("cd");
let location = arg0.origin.clone();
let env = env.push_frame(Frame::Builtin(Builtin {
name: arg0,
is_special: false,
}));
let e = target(&env, &command, "").unwrap_err();
assert_eq!(e, TargetError::UnsetHome { location });
}
#[test]
fn home_empty() {
let mut env = Env::new_virtual();
let command = Command {
mode: Mode::default(),
ensure_pwd: false,
operand: None,
};
let arg0 = Field::dummy("cd");
let location = arg0.origin.clone();
let mut env = env.push_frame(Frame::Builtin(Builtin {
name: arg0,
is_special: false,
}));
env.get_or_create_variable(HOME, Scope::Global)
.assign("", None)
.unwrap();
let e = target(&env, &command, "/ignored").unwrap_err();
assert_eq!(e, TargetError::UnsetHome { location });
}
#[test]
fn oldpwd() {
let mut env = Env::new_virtual();
let command = Command {
mode: Mode::default(),
ensure_pwd: false,
operand: Some(Field::dummy("-")),
};
env.get_or_create_variable(OLDPWD, Scope::Global)
.assign("/old/dir", None)
.unwrap();
let target = target(&env, &command, "/ignored").unwrap();
assert_eq!(target, (PathBuf::from("/old/dir"), Origin::Oldpwd));
}
#[test]
fn oldpwd_unset() {
let env = Env::new_virtual();
let operand = Field::dummy("-");
let location = operand.origin.clone();
let command = Command {
mode: Mode::default(),
ensure_pwd: false,
operand: Some(operand),
};
let e = target(&env, &command, "/ignored").unwrap_err();
assert_eq!(e, TargetError::UnsetOldpwd { location });
}
#[test]
fn oldpwd_empty() {
let mut env = Env::new_virtual();
let operand = Field::dummy("-");
let location = operand.origin.clone();
let command = Command {
mode: Mode::default(),
ensure_pwd: false,
operand: Some(operand),
};
env.get_or_create_variable(OLDPWD, Scope::Global)
.assign("", None)
.unwrap();
let e = target(&env, &command, "/ignored").unwrap_err();
assert_eq!(e, TargetError::UnsetOldpwd { location });
}
#[test]
fn literal_physical() {
let env = Env::new_virtual();
let result = target(
&env,
&Command {
mode: Mode::Physical,
ensure_pwd: false,
operand: Some(Field::dummy("foo")),
},
"/ignored",
)
.unwrap();
assert_eq!(result, (PathBuf::from("foo"), Origin::Literal));
let result = target(
&env,
&Command {
mode: Mode::Physical,
ensure_pwd: false,
operand: Some(Field::dummy("foo/bar")),
},
"/ignored",
)
.unwrap();
assert_eq!(result, (PathBuf::from("foo/bar"), Origin::Literal));
}
#[test]
fn literal_logical_absolute() {
let env = Env::new_virtual();
let result = target(
&env,
&Command {
mode: Mode::Logical,
ensure_pwd: false,
operand: Some(Field::dummy("/foo")),
},
"/ignored",
)
.unwrap();
assert_eq!(result, (PathBuf::from("/foo"), Origin::Literal));
let result = target(
&env,
&Command {
mode: Mode::Logical,
ensure_pwd: false,
operand: Some(Field::dummy("/foo/bar")),
},
"/ignored",
)
.unwrap();
assert_eq!(result, (PathBuf::from("/foo/bar"), Origin::Literal));
}
#[test]
fn literal_logical_relative() {
let env = Env::new_virtual();
let command = Command {
mode: Mode::Logical,
ensure_pwd: false,
operand: Some(Field::dummy("foo/bar")),
};
assert_eq!(
target(&env, &command, "/current/pwd").unwrap(),
(PathBuf::from("/current/pwd/foo/bar"), Origin::Literal)
);
}
}