use crate::{
display::Quotable,
error::{UError, UResult},
};
use clap::ArgMatches;
use std::{
env,
error::Error,
fmt::{Debug, Display},
path::{Path, PathBuf},
};
pub static BACKUP_CONTROL_VALUES: &[&str] = &[
"simple", "never", "numbered", "t", "existing", "nil", "none", "off",
];
pub const BACKUP_CONTROL_LONG_HELP: &str =
"The backup suffix is '~', unless set with --suffix or SIMPLE_BACKUP_SUFFIX.
The version control method may be selected via the --backup option or through
the VERSION_CONTROL environment variable. Here are the values:
none, off never make backups (even if --backup is given)
numbered, t make numbered backups
existing, nil numbered if numbered backups exist, simple otherwise
simple, never always make simple backups";
static VALID_ARGS_HELP: &str = "Valid arguments are:
- 'none', 'off'
- 'simple', 'never'
- 'existing', 'nil'
- 'numbered', 't'";
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum BackupMode {
NoBackup,
SimpleBackup,
NumberedBackup,
ExistingBackup,
}
#[derive(Debug, Eq, PartialEq)]
pub enum BackupError {
InvalidArgument(String, String),
AmbiguousArgument(String, String),
BackupImpossible(),
}
impl UError for BackupError {
fn code(&self) -> i32 {
match self {
Self::BackupImpossible() => 2,
_ => 1,
}
}
fn usage(&self) -> bool {
matches!(
self,
Self::InvalidArgument(_, _) | Self::AmbiguousArgument(_, _)
)
}
}
impl Error for BackupError {}
impl Display for BackupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidArgument(arg, origin) => write!(
f,
"invalid argument {} for '{}'\n{}",
arg.quote(),
origin,
VALID_ARGS_HELP
),
Self::AmbiguousArgument(arg, origin) => write!(
f,
"ambiguous argument {} for '{}'\n{}",
arg.quote(),
origin,
VALID_ARGS_HELP
),
Self::BackupImpossible() => write!(f, "cannot create backup"),
}
}
}
pub mod arguments {
use clap::ArgAction;
extern crate clap;
pub static OPT_BACKUP: &str = "backupopt_backup";
pub static OPT_BACKUP_NO_ARG: &str = "backupopt_b";
pub static OPT_SUFFIX: &str = "backupopt_suffix";
pub fn backup() -> clap::Arg {
clap::Arg::new(OPT_BACKUP)
.long("backup")
.help("make a backup of each existing destination file")
.action(clap::ArgAction::Set)
.require_equals(true)
.num_args(0..=1)
.value_name("CONTROL")
}
pub fn backup_no_args() -> clap::Arg {
clap::Arg::new(OPT_BACKUP_NO_ARG)
.short('b')
.help("like --backup but does not accept an argument")
.action(ArgAction::SetTrue)
}
pub fn suffix() -> clap::Arg {
clap::Arg::new(OPT_SUFFIX)
.short('S')
.long("suffix")
.help("override the usual backup suffix")
.action(clap::ArgAction::Set)
.value_name("SUFFIX")
.allow_hyphen_values(true)
}
}
pub fn determine_backup_suffix(matches: &ArgMatches) -> String {
let supplied_suffix = matches.get_one::<String>(arguments::OPT_SUFFIX);
if let Some(suffix) = supplied_suffix {
String::from(suffix)
} else {
env::var("SIMPLE_BACKUP_SUFFIX").unwrap_or_else(|_| "~".to_owned())
}
}
pub fn determine_backup_mode(matches: &ArgMatches) -> UResult<BackupMode> {
if matches.contains_id(arguments::OPT_BACKUP) {
if let Some(method) = matches.get_one::<String>(arguments::OPT_BACKUP) {
match_method(method, "backup type")
} else if let Ok(method) = env::var("VERSION_CONTROL") {
match_method(&method, "$VERSION_CONTROL")
} else {
Ok(BackupMode::ExistingBackup)
}
} else if matches.get_flag(arguments::OPT_BACKUP_NO_ARG) {
Ok(BackupMode::ExistingBackup)
} else {
Ok(BackupMode::NoBackup)
}
}
fn match_method(method: &str, origin: &str) -> UResult<BackupMode> {
let matches: Vec<&&str> = BACKUP_CONTROL_VALUES
.iter()
.filter(|val| val.starts_with(method))
.collect();
if matches.len() == 1 {
match *matches[0] {
"simple" | "never" => Ok(BackupMode::SimpleBackup),
"numbered" | "t" => Ok(BackupMode::NumberedBackup),
"existing" | "nil" => Ok(BackupMode::ExistingBackup),
"none" | "off" => Ok(BackupMode::NoBackup),
_ => unreachable!(), }
} else if matches.is_empty() {
Err(BackupError::InvalidArgument(method.to_string(), origin.to_string()).into())
} else {
Err(BackupError::AmbiguousArgument(method.to_string(), origin.to_string()).into())
}
}
pub fn get_backup_path(
backup_mode: BackupMode,
backup_path: &Path,
suffix: &str,
) -> Option<PathBuf> {
match backup_mode {
BackupMode::NoBackup => None,
BackupMode::SimpleBackup => Some(simple_backup_path(backup_path, suffix)),
BackupMode::NumberedBackup => Some(numbered_backup_path(backup_path)),
BackupMode::ExistingBackup => Some(existing_backup_path(backup_path, suffix)),
}
}
fn simple_backup_path(path: &Path, suffix: &str) -> PathBuf {
let mut p = path.to_string_lossy().into_owned();
p.push_str(suffix);
PathBuf::from(p)
}
fn numbered_backup_path(path: &Path) -> PathBuf {
for i in 1_u64.. {
let path_str = &format!("{}.~{}~", path.to_string_lossy(), i);
let path = Path::new(path_str);
if !path.exists() {
return path.to_path_buf();
}
}
panic!("cannot create backup")
}
fn existing_backup_path(path: &Path, suffix: &str) -> PathBuf {
let test_path_str = &format!("{}.~1~", path.to_string_lossy());
let test_path = Path::new(test_path_str);
if test_path.exists() {
numbered_backup_path(path)
} else {
simple_backup_path(path, suffix)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use clap::Command;
use once_cell::sync::Lazy;
use std::sync::Mutex;
static TEST_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
static ENV_VERSION_CONTROL: &str = "VERSION_CONTROL";
fn make_app() -> clap::Command {
Command::new("command")
.arg(arguments::backup())
.arg(arguments::backup_no_args())
.arg(arguments::suffix())
}
#[test]
fn test_backup_mode_short_only() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "-b"]);
let result = determine_backup_mode(&matches).unwrap();
assert_eq!(result, BackupMode::ExistingBackup);
}
#[test]
fn test_backup_mode_long_preferred_over_short() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "-b", "--backup=none"]);
let result = determine_backup_mode(&matches).unwrap();
assert_eq!(result, BackupMode::NoBackup);
}
#[test]
fn test_backup_mode_long_without_args_no_env() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "--backup"]);
let result = determine_backup_mode(&matches).unwrap();
assert_eq!(result, BackupMode::ExistingBackup);
}
#[test]
fn test_backup_mode_long_with_args() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "--backup=simple"]);
let result = determine_backup_mode(&matches).unwrap();
assert_eq!(result, BackupMode::SimpleBackup);
}
#[test]
fn test_backup_mode_long_with_args_invalid() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "--backup=foobar"]);
let result = determine_backup_mode(&matches);
assert!(result.is_err());
let text = format!("{}", result.unwrap_err());
assert!(text.contains("invalid argument 'foobar' for 'backup type'"));
}
#[test]
fn test_backup_mode_long_with_args_ambiguous() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "--backup=n"]);
let result = determine_backup_mode(&matches);
assert!(result.is_err());
let text = format!("{}", result.unwrap_err());
assert!(text.contains("ambiguous argument 'n' for 'backup type'"));
}
#[test]
fn test_backup_mode_long_with_arg_shortened() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "--backup=si"]);
let result = determine_backup_mode(&matches).unwrap();
assert_eq!(result, BackupMode::SimpleBackup);
}
#[test]
fn test_backup_mode_short_only_ignore_env() {
let _dummy = TEST_MUTEX.lock().unwrap();
env::set_var(ENV_VERSION_CONTROL, "none");
let matches = make_app().get_matches_from(vec!["command", "-b"]);
let result = determine_backup_mode(&matches).unwrap();
assert_eq!(result, BackupMode::ExistingBackup);
env::remove_var(ENV_VERSION_CONTROL);
}
#[test]
fn test_backup_mode_long_without_args_with_env() {
let _dummy = TEST_MUTEX.lock().unwrap();
env::set_var(ENV_VERSION_CONTROL, "none");
let matches = make_app().get_matches_from(vec!["command", "--backup"]);
let result = determine_backup_mode(&matches).unwrap();
assert_eq!(result, BackupMode::NoBackup);
env::remove_var(ENV_VERSION_CONTROL);
}
#[test]
fn test_backup_mode_long_with_env_var_invalid() {
let _dummy = TEST_MUTEX.lock().unwrap();
env::set_var(ENV_VERSION_CONTROL, "foobar");
let matches = make_app().get_matches_from(vec!["command", "--backup"]);
let result = determine_backup_mode(&matches);
assert!(result.is_err());
let text = format!("{}", result.unwrap_err());
assert!(text.contains("invalid argument 'foobar' for '$VERSION_CONTROL'"));
env::remove_var(ENV_VERSION_CONTROL);
}
#[test]
fn test_backup_mode_long_with_env_var_ambiguous() {
let _dummy = TEST_MUTEX.lock().unwrap();
env::set_var(ENV_VERSION_CONTROL, "n");
let matches = make_app().get_matches_from(vec!["command", "--backup"]);
let result = determine_backup_mode(&matches);
assert!(result.is_err());
let text = format!("{}", result.unwrap_err());
assert!(text.contains("ambiguous argument 'n' for '$VERSION_CONTROL'"));
env::remove_var(ENV_VERSION_CONTROL);
}
#[test]
fn test_backup_mode_long_with_env_var_shortened() {
let _dummy = TEST_MUTEX.lock().unwrap();
env::set_var(ENV_VERSION_CONTROL, "si");
let matches = make_app().get_matches_from(vec!["command", "--backup"]);
let result = determine_backup_mode(&matches).unwrap();
assert_eq!(result, BackupMode::SimpleBackup);
env::remove_var(ENV_VERSION_CONTROL);
}
#[test]
fn test_suffix_takes_hyphen_value() {
let _dummy = TEST_MUTEX.lock().unwrap();
let matches = make_app().get_matches_from(vec!["command", "-b", "--suffix", "-v"]);
let result = determine_backup_suffix(&matches);
assert_eq!(result, "-v");
}
}