#[derive(Debug, Eq, PartialEq)]
pub enum Command {
Sync,
RSync,
Link,
Status,
Diff,
Clean,
Run,
Nuts,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Default, Eq, PartialEq)]
pub struct Args {
pub command: Option<Command>,
pub pull_before_sync: bool,
pub reversed_diff: bool,
#[allow(clippy::struct_field_names)]
pub run_args: Vec<String>,
pub root: Option<String>,
pub short_help: bool,
pub long_help: bool,
pub version: bool,
pub verbose: bool,
}
impl Args {
pub fn build_from_args<I>(mut cli_args: I) -> Result<Self, String>
where
I: Iterator<Item: AsRef<str> + ToString>,
{
let mut args = Self::default();
while let Some(arg) = cli_args.next() {
let some_command = args.command.is_some();
let some_root = args.root.is_some();
let is_sync = args.command == Some(Command::Sync);
let is_diff = args.command == Some(Command::Diff);
match arg.as_ref() {
"sync" | "s" if !some_command => args.command = Some(Command::Sync),
"-p" | "--pull" if is_sync => args.pull_before_sync = true,
"rsync" | "rs" if !some_command => args.command = Some(Command::RSync),
"link" | "l" if !some_command => args.command = Some(Command::Link),
"status" | "st" if !some_command => args.command = Some(Command::Status),
"diff" | "df" if !some_command => args.command = Some(Command::Diff),
"-r" | "--reversed" if is_diff => args.reversed_diff = !args.reversed_diff,
"clean" | "c" if !some_command => args.command = Some(Command::Clean),
"run" | "r" if !some_command => {
args.command = Some(Command::Run);
args.run_args
.extend(cli_args.by_ref().map(|arg| arg.to_string()));
}
"nuts" if !some_command => args.command = Some(Command::Nuts),
"-h" => args.short_help = true,
"--help" => args.long_help = true,
"-V" | "--version" => args.version = true,
"-v" | "--verbose" => args.verbose = true,
"--" if some_command && !some_root => {
args.root = cli_args.next().map(|root| root.to_string());
}
root if some_command && !some_root => args.root = Some(root.to_string()),
unknown => {
return Err(format!("Unknown argument: '{unknown}'"));
}
}
}
Ok(args)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::iter_on_single_items)]
use super::*;
#[test]
fn command_sync_regular() {
let args = Args::build_from_args(["sync"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Sync));
}
#[test]
fn command_sync_shortcut() {
let args = Args::build_from_args(["s"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Sync));
}
#[test]
fn second_command_does_not_override_sync() {
let args = Args::build_from_args(["sync", "rsync"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Sync));
}
#[test]
fn command_sync_pull() {
let args = Args::build_from_args(["sync", "--pull"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Sync));
assert!(args.pull_before_sync);
}
#[test]
fn command_rsync_regular() {
let args = Args::build_from_args(["rsync"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::RSync));
}
#[test]
fn command_rsync_shortcut() {
let args = Args::build_from_args(["rs"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::RSync));
}
#[test]
fn second_command_does_not_override_rsync() {
let args = Args::build_from_args(["rsync", "sync"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::RSync));
}
#[test]
fn command_link_regular() {
let args = Args::build_from_args(["link"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Link));
}
#[test]
fn command_link_shortcut() {
let args = Args::build_from_args(["l"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Link));
}
#[test]
fn second_command_does_not_override_link() {
let args = Args::build_from_args(["link", "sync"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Link));
}
#[test]
fn command_status_regular() {
let args = Args::build_from_args(["status"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Status));
}
#[test]
fn command_status_shortcut() {
let args = Args::build_from_args(["st"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Status));
}
#[test]
fn second_command_does_not_override_status() {
let args = Args::build_from_args(["status", "sync"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Status));
}
#[test]
fn command_diff_regular() {
let args = Args::build_from_args(["diff"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Diff));
}
#[test]
fn command_diff_shortcut() {
let args = Args::build_from_args(["df"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Diff));
}
#[test]
fn second_command_does_not_override_diff() {
let args = Args::build_from_args(["diff", "sync"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Diff));
}
#[test]
fn command_diff_reversed() {
let args = Args::build_from_args(["diff", "--reversed"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Diff));
assert!(args.reversed_diff);
}
#[test]
fn command_reversed_without_diff_is_noop() {
let args = Args::build_from_args(["status", "--reversed"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Status));
assert!(!args.reversed_diff);
}
#[test]
fn command_diff_reversed_multiple_cancel_each_other() {
let args = Args::build_from_args(["diff", "--reversed", "--reversed"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Diff));
assert!(!args.reversed_diff);
}
#[test]
fn command_clean_regular() {
let args = Args::build_from_args(["clean"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Clean));
}
#[test]
fn command_clean_shortcut() {
let args = Args::build_from_args(["c"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Clean));
}
#[test]
fn second_command_does_not_override_clean() {
let args = Args::build_from_args(["clean", "sync"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Clean));
}
#[test]
fn command_run_regular() {
let args = Args::build_from_args(["run"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Run));
}
#[test]
fn command_run_shortcut() {
let args = Args::build_from_args(["r"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Run));
}
#[test]
fn second_command_does_not_override_run() {
let args = Args::build_from_args(["run", "sync"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Run));
}
#[test]
fn command_run_drains_all_remaining_arguments() {
let args = Args::build_from_args(["run", "git", "pull"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Run));
assert_eq!(args.run_args, ["git", "pull"]);
}
#[test]
fn command_nuts_regular() {
let args = Args::build_from_args(["nuts"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Nuts));
}
#[test]
fn second_command_does_not_override_nuts() {
let args = Args::build_from_args(["nuts", "sync"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Nuts));
}
#[test]
fn command_unknown_is_error() {
let err = Args::build_from_args(["unknown"].iter()).unwrap_err();
assert!(err.contains("'unknown'"));
}
#[test]
fn option_short_help_regular() {
let args = Args::build_from_args(["-h"].iter()).unwrap();
assert!(args.short_help);
assert!(!args.long_help);
}
#[test]
fn option_long_help_regular() {
let args = Args::build_from_args(["--help"].iter()).unwrap();
assert!(!args.short_help);
assert!(args.long_help);
}
#[test]
fn option_short_version_regular() {
let args = Args::build_from_args(["-V"].iter()).unwrap();
assert!(args.version);
}
#[test]
fn option_long_version_regular() {
let args = Args::build_from_args(["--version"].iter()).unwrap();
assert!(args.version);
}
#[test]
fn option_short_verbose_regular() {
let args = Args::build_from_args(["-v"].iter()).unwrap();
assert!(args.verbose);
}
#[test]
fn option_long_verbose_regular() {
let args = Args::build_from_args(["--verbose"].iter()).unwrap();
assert!(args.verbose);
}
#[test]
fn double_dash_regular() {
let args = Args::build_from_args(["sync", "--", "~/configs"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Sync));
assert!(args.root.is_some_and(|r| r == "~/configs"));
}
#[test]
fn double_dash_not_followed_by_anything_is_noop() {
let args = Args::build_from_args(["sync", "--"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Sync));
assert!(args.root.is_none());
}
#[test]
fn double_dash_correctly_interprets_what_comes_next_as_root() {
let args = Args::build_from_args(["sync", "--", "--verbose"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Sync));
assert!(args.root.is_some_and(|r| r == "--verbose"));
assert!(!args.verbose);
}
#[test]
fn double_dash_not_preceded_by_command_is_error() {
let err = Args::build_from_args(["--", "~/configs"].iter()).unwrap_err();
assert!(err.contains("'--'"));
}
#[test]
fn double_dash_with_previous_root_is_error() {
let err =
Args::build_from_args(["sync", "~/other-root", "--", "~/configs"].iter()).unwrap_err();
assert!(err.contains("'--'"));
}
#[test]
fn root_regular() {
let args = Args::build_from_args(["sync", "~/configs"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Sync));
assert!(args.root.is_some_and(|r| r == "~/configs"));
}
#[test]
fn root_implicit_is_noop() {
let args = Args::build_from_args(["sync"].iter()).unwrap();
assert!(args.command.is_some_and(|c| c == Command::Sync));
assert!(args.root.is_none());
}
#[test]
fn root_not_preceded_by_command_is_error() {
let err = Args::build_from_args(["~/configs"].iter()).unwrap_err();
assert!(err.contains("'~/configs'"));
}
#[test]
fn root_with_previous_root_is_error() {
let err = Args::build_from_args(["sync", "~/other-root", "~/configs"].iter()).unwrap_err();
assert!(err.contains("'~/configs'"));
}
}