use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum WrappedError {
#[error("{command}: unknown verb '{word}'. Allowed: {}", allowed_list(allowed))]
UnknownVerb {
command: String,
word: String,
allowed: Vec<String>,
},
#[error("{command}: no verb given. Allowed: {}", allowed_list(allowed))]
MissingVerb {
command: String,
allowed: Vec<String>,
},
#[error("{command}: unknown flag '{word}' for '{scope}'. Allowed: {}", allowed_list(allowed))]
UnknownFlag {
command: String,
scope: String,
word: String,
allowed: Vec<String>,
},
#[error("{command}: '{word}' is not a flag for '{scope}'. Use {separated}.")]
ClusteredShort {
command: String,
scope: String,
word: String,
separated: String,
},
#[error("{command}: '{word}' is not a flag for '{scope}'. Use {separated}.")]
GluedShortValue {
command: String,
scope: String,
word: String,
separated: String,
},
#[error("{command}: '{flag}' needs a value for '{scope}'.")]
MissingFlagValue {
command: String,
scope: String,
flag: String,
},
#[error("{command}: '{flag}' takes no value for '{scope}'.")]
UnexpectedFlagValue {
command: String,
scope: String,
flag: String,
},
#[error("{command}: '{flag}' given more than once for '{scope}'.")]
RepeatedFlag {
command: String,
scope: String,
flag: String,
},
#[error("{command}: unexpected argument '{word}'")]
UnexpectedArgument {
command: String,
word: String,
},
#[error("{command}: unexpected argument '{word}' for '{scope}'. Write -- before arguments meant for the program.")]
UndeclaredPositional {
command: String,
scope: String,
word: String,
},
#[error("{command}: required flag '{flag}' not given for '{scope}'.")]
MissingRequiredFlag {
command: String,
scope: String,
flag: String,
},
#[error("{command}: required argument '{positional}' not given for '{scope}'.")]
MissingRequiredPositional {
command: String,
scope: String,
positional: String,
},
#[error("{command}: '{flag}' takes an integer. Got '{value}'.")]
NotAnInteger {
command: String,
flag: String,
value: String,
},
#[error("{command}: '{flag}' must be one of: {}. Got '{value}'.", choices.join(", "))]
NotInChoices {
command: String,
flag: String,
value: String,
choices: Vec<String>,
},
#[error("{command}: '{positional}' must be under {}. Got '{value}'.", root.display())]
PathOutsideRoot {
command: String,
positional: String,
root: PathBuf,
value: String,
},
#[error("{command}: '{positional}' must be under {}, which does not resolve.", root.display())]
PathRootUnresolvable {
command: String,
positional: String,
root: PathBuf,
},
#[error("{command}: argument {position} contains a NUL byte. argv cannot carry NUL; remove it.")]
NulByte {
command: String,
position: usize,
},
#[error("{command}: argument {position} is binary ({byte_len} bytes). argv carries text; encode it or write it to a file.")]
BinaryArgument {
command: String,
position: usize,
byte_len: usize,
},
}
impl WrappedError {
pub fn exit_code(&self) -> i64 {
2
}
pub fn command(&self) -> &str {
match self {
WrappedError::UnknownVerb { command, .. }
| WrappedError::MissingVerb { command, .. }
| WrappedError::UnknownFlag { command, .. }
| WrappedError::ClusteredShort { command, .. }
| WrappedError::GluedShortValue { command, .. }
| WrappedError::MissingFlagValue { command, .. }
| WrappedError::UnexpectedFlagValue { command, .. }
| WrappedError::RepeatedFlag { command, .. }
| WrappedError::UnexpectedArgument { command, .. }
| WrappedError::UndeclaredPositional { command, .. }
| WrappedError::MissingRequiredFlag { command, .. }
| WrappedError::MissingRequiredPositional { command, .. }
| WrappedError::NotAnInteger { command, .. }
| WrappedError::NotInChoices { command, .. }
| WrappedError::PathOutsideRoot { command, .. }
| WrappedError::PathRootUnresolvable { command, .. }
| WrappedError::NulByte { command, .. }
| WrappedError::BinaryArgument { command, .. } => command,
}
}
pub(crate) fn attributed_to(mut self, command: &str, positional: &str) -> Self {
match &mut self {
WrappedError::PathOutsideRoot {
command: c,
positional: p,
..
}
| WrappedError::PathRootUnresolvable {
command: c,
positional: p,
..
} => {
c.clear();
c.push_str(command);
p.clear();
p.push_str(positional);
}
_ => {}
}
self
}
}
fn allowed_list(allowed: &[String]) -> String {
if allowed.is_empty() {
"(none)".to_string()
} else {
allowed.join(", ")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_verb_reads_as_the_declaration_documents() {
let error = WrappedError::UnknownVerb {
command: "git".into(),
word: "comit".into(),
allowed: vec![
"commit".into(),
"diff".into(),
"log".into(),
"push".into(),
"status".into(),
],
};
assert_eq!(
error.to_string(),
"git: unknown verb 'comit'. Allowed: commit, diff, log, push, status"
);
}
#[test]
fn unknown_flag_names_the_scope_and_the_allowed_set() {
let error = WrappedError::UnknownFlag {
command: "git".into(),
scope: "git log".into(),
word: "--output".into(),
allowed: vec!["-n/--max-count".into(), "--oneline".into(), "--since".into()],
};
assert_eq!(
error.to_string(),
"git: unknown flag '--output' for 'git log'. Allowed: -n/--max-count, --oneline, --since"
);
}
#[test]
fn an_empty_allowed_set_reads_as_none() {
let error = WrappedError::UnknownFlag {
command: "python".into(),
scope: "python".into(),
word: "-c".into(),
allowed: Vec::new(),
};
assert_eq!(
error.to_string(),
"python: unknown flag '-c' for 'python'. Allowed: (none)"
);
}
#[test]
fn every_refusal_exits_two() {
let error = WrappedError::UnexpectedArgument {
command: "cargo".into(),
word: "extra".into(),
};
assert_eq!(error.exit_code(), 2);
assert_eq!(error.to_string(), "cargo: unexpected argument 'extra'");
}
#[test]
fn attribution_fills_a_bare_path_failure() {
let error = WrappedError::PathOutsideRoot {
command: String::new(),
positional: String::new(),
root: PathBuf::from("/opt/app/scripts"),
value: "/etc/passwd".into(),
}
.attributed_to("python", "script");
assert_eq!(
error.to_string(),
"python: 'script' must be under /opt/app/scripts. Got '/etc/passwd'."
);
}
}