use super::Command;
use super::Mode;
use crate::common::syntax::OptionSpec;
use crate::common::syntax::parse_arguments;
use std::borrow::Cow;
use std::collections::VecDeque;
use thiserror::Error;
use yash_env::Env;
use yash_env::semantics::Field;
use yash_syntax::source::Location;
use yash_syntax::source::pretty::Annotation;
use yash_syntax::source::pretty::AnnotationType;
use yash_syntax::source::pretty::MessageBase;
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
CommonError(#[from] crate::common::syntax::ParseError<'static>),
#[error("-e option must be used with -P (and not -L)")]
EnsurePwdNotPhysical(Location),
#[error("empty operand")]
EmptyOperand(Field),
#[error("unexpected operand")]
UnexpectedOperands(Vec<Field>),
}
impl MessageBase for Error {
fn message_title(&self) -> Cow<'_, str> {
self.to_string().into()
}
fn main_annotation(&self) -> Annotation<'_> {
use Error::*;
match self {
CommonError(e) => e.main_annotation(),
EnsurePwdNotPhysical(location) => {
Annotation::new(AnnotationType::Error, "-e option".into(), location)
}
EmptyOperand(operand) => Annotation::new(
AnnotationType::Error,
"empty operand".into(),
&operand.origin,
),
UnexpectedOperands(operands) => Annotation::new(
AnnotationType::Error,
format!("{}: unexpected operand", operands[0].value).into(),
&operands[0].origin,
),
}
}
}
pub type Result = std::result::Result<Command, Error>;
const OPTION_SPECS: &[OptionSpec] = &[
OptionSpec::new().short('e').long("ensure-pwd"),
OptionSpec::new().short('L').long("logical"),
OptionSpec::new().short('P').long("physical"),
];
pub fn parse(env: &Env, args: Vec<Field>) -> Result {
let parser_mode = crate::common::syntax::Mode::with_env(env);
let (options, operands) = parse_arguments(OPTION_SPECS, parser_mode, args)?;
let mut ensure_pwd_option_location = None;
let mut mode = Mode::default();
for option in options {
match option.spec.get_short() {
Some('e') => ensure_pwd_option_location = Some(option.location),
Some('L') => mode = Mode::Logical,
Some('P') => mode = Mode::Physical,
_ => unreachable!(),
}
}
let ensure_pwd = match (ensure_pwd_option_location, mode) {
(Some(_), Mode::Physical) => true,
(Some(location), _) => return Err(Error::EnsurePwdNotPhysical(location)),
(None, _) => false,
};
let mut operands = VecDeque::from(operands);
let operand = operands.pop_front();
if !operands.is_empty() {
return Err(Error::UnexpectedOperands(operands.into()));
}
let operand = match operand {
Some(operand) if operand.value.is_empty() => return Err(Error::EmptyOperand(operand)),
operand => operand,
};
Ok(Command {
mode,
ensure_pwd,
operand,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_arguments() {
let env = Env::new_virtual();
let result = parse(&env, vec![]);
assert_eq!(
result,
Ok(Command {
mode: Mode::Logical,
ensure_pwd: false,
operand: None,
})
);
}
#[test]
fn logical_option() {
let env = Env::new_virtual();
let result = parse(&env, Field::dummies(["-L"]));
assert_eq!(
result,
Ok(Command {
mode: Mode::Logical,
ensure_pwd: false,
operand: None,
})
);
}
#[test]
fn physical_option() {
let env = Env::new_virtual();
let result = parse(&env, Field::dummies(["-P"]));
assert_eq!(
result,
Ok(Command {
mode: Mode::Physical,
ensure_pwd: false,
operand: None,
})
);
}
#[test]
fn last_option_wins() {
let env = Env::new_virtual();
let result = parse(&env, Field::dummies(["-L", "-P"]));
assert_eq!(result.unwrap().mode, Mode::Physical);
let result = parse(&env, Field::dummies(["-P", "-L"]));
assert_eq!(result.unwrap().mode, Mode::Logical);
let result = parse(&env, Field::dummies(["-L", "-P", "-L"]));
assert_eq!(result.unwrap().mode, Mode::Logical);
let result = parse(&env, Field::dummies(["-PLP"]));
assert_eq!(result.unwrap().mode, Mode::Physical);
}
#[test]
fn ensure_pwd_option_with_physical_option() {
let env = Env::new_virtual();
let result = parse(&env, Field::dummies(["-e", "-P"]));
assert!(result.unwrap().ensure_pwd);
let result = parse(&env, Field::dummies(["-P", "-e"]));
assert!(result.unwrap().ensure_pwd);
let result = parse(&env, Field::dummies(["-eLP"]));
assert!(result.unwrap().ensure_pwd);
}
#[test]
fn with_operand() {
let env = Env::new_virtual();
let operand = Field::dummy("foo/bar");
let result = parse(&env, vec![operand.clone()]);
assert_eq!(
result,
Ok(Command {
mode: Mode::default(),
ensure_pwd: false,
operand: Some(operand),
})
);
}
#[test]
fn option_and_operand() {
let env = Env::new_virtual();
let operand = Field::dummy("foo/bar");
let args = vec![Field::dummy("-L"), Field::dummy("--"), operand.clone()];
let result = parse(&env, args);
assert_eq!(
result,
Ok(Command {
mode: Mode::Logical,
ensure_pwd: false,
operand: Some(operand),
})
);
}
#[test]
fn ensure_pwd_option_with_logical_option() {
let env = Env::new_virtual();
let e = Field::dummy("-e");
let result = parse(&env, vec![Field::dummy("-L"), e.clone()]);
assert_eq!(result, Err(Error::EnsurePwdNotPhysical(e.origin.clone())));
let result = parse(&env, vec![e.clone()]);
assert_eq!(result, Err(Error::EnsurePwdNotPhysical(e.origin)));
}
#[test]
fn empty_operand() {
let env = Env::new_virtual();
let operand = Field::dummy("");
let result = parse(&env, vec![operand.clone()]);
assert_eq!(result, Err(Error::EmptyOperand(operand)));
}
#[test]
fn unexpected_operand() {
let env = Env::new_virtual();
let operand1 = Field::dummy("foo");
let operand2 = Field::dummy("bar");
let result = parse(&env, vec![operand1, operand2.clone()]);
assert_eq!(result, Err(Error::UnexpectedOperands(vec![operand2])));
}
#[test]
fn unexpected_operands_after_options() {
let env = Env::new_virtual();
let args = Field::dummies(["-LP", "-L", "--", "one", "two", "three"]);
let extra_operands = args[4..].to_vec();
let result = parse(&env, args);
assert_eq!(result, Err(Error::UnexpectedOperands(extra_operands)));
}
}