diff --git a/Cargo.toml b/Cargo.toml index 48e67b4..79164e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap='4.4.*' -toml='0.8.*' +clap = { version = "4.4.*", features = ["derive"] } +toml = "0.8.*" +itertools = "0.12.*" diff --git a/src/args/arg.rs b/src/args/arg.rs index bf68316..2e1d280 100644 --- a/src/args/arg.rs +++ b/src/args/arg.rs @@ -1,227 +1,24 @@ use std::path::PathBuf; -use std::collections::BTreeMap; -use std::error::Error; -use std::fmt; -use clap::{Arg, ArgAction, ArgMatches, ArgGroup, Command}; +use clap::Parser; +#[derive(Parser)] +#[command(version, about, long_about = None)] +pub struct Cli { + #[arg(short, long, value_name = "FILE")] + pub config: Option, -pub struct Args { - pub copy_to_sys: bool, - pub dry_run: bool, - pub override_manager_dir: Option, - pub override_device: Option, + #[arg(short, long, value_name = "FILE")] + pub manager: Option, + + #[arg(short, long, default_value_t=false)] + pub from: bool, + + #[arg(short, long, default_value_t=false)] + pub dry: bool, } -impl Args { - pub fn parse_args() -> Self { - - let cli = Args::get_cli(); - - let matches = cli.get_matches(); - let values = Value::from_matches(&matches); - - let (flags, unprocessed) = Args::process_flags(values); - let (args, _) = Args::process_args(unprocessed); - - let or_mgr_dir = match &args[0] { - Some(path) => Some(PathBuf::from(path)), - None => None, - }; - - Args { - copy_to_sys: flags[0], - dry_run: flags[1], - override_manager_dir: or_mgr_dir, - override_device: args[1].clone(), - } - } - - - fn process_flags(values: Vec<(clap::Id, Value)> ) -> (Vec, Vec<(clap::Id, Value)>) { - - let (copy_to_sys, unmatched) = Args::get_arg(values, "from-git"); - let (dry_run, unmatched) = Args::get_arg(unmatched, "dry-run"); - - let copy_to_sys = if let Value::Bool(val) = copy_to_sys.unwrap_or(Value::None) { val } - else { false }; - - let dry_run = if let Value::Bool(val) = dry_run.unwrap_or(Value::None) { val } - else { false }; - - - (vec![copy_to_sys, dry_run], unmatched) - } - - - fn process_args(values: Vec<(clap::Id, Value)>) -> (Vec>, Vec<(clap::Id, Value)>) { - - let (override_manager_dir, unmatched) = Args::get_arg(values, "manager-dir"); - let (override_device, unmatched) = Args::get_arg(unmatched, "device"); - - - let or_mngr_dir = if let Value::String(val) = override_manager_dir.unwrap_or(Value::None) { Some(String::from(val)) } - else { None }; - - let or_device = if let Value::String(val) = override_device.unwrap_or(Value::None) { Some(String::from(val))} - else { None }; - - - (vec![or_mngr_dir, or_device], unmatched) - } - - - - fn get_cli() -> Command { - - let from_git = Arg::new("from-git") - .short('f') - .long("from-git") - .action(ArgAction::SetTrue); - - let override_manager_dir = Arg::new("manager-dir") - .long("manager-dir") - .action(ArgAction::Append); - - let override_device = Arg::new("device") - .short('d') - .long("device") - .action(ArgAction::Append); - - let dry_run = Arg::new("dry-run") - .short('n') - .long("dry") - .action(ArgAction::SetTrue); - - let cli = Command::new("dotfiles") - .group(ArgGroup::new("flags").multiple(true)) - .next_help_heading("FLAGS") - .args([ - from_git, - dry_run, - ]) - .group(ArgGroup::new("overrides").multiple(true)) - .next_help_heading("OVERRIDES") - .args([ - override_manager_dir, - override_device, - ]); - - cli - } - - fn get_arg(args: Vec<(clap::Id, Value)>, id: &str) -> (Box>, Vec<(clap::Id, Value)>) { - - let (matches, non_matches): (Vec<_>, Vec<_>) = args.into_iter().partition(|value| value.0 == id); - - let arg_match = match matches.len() { - 0 => Box::new(None), - 1 => Box::new(Some(matches[0].1.clone())), - _ => unreachable!(), - }; - - (arg_match, non_matches) - } - - -} - - - - -#[derive(Clone, PartialEq, Eq, Hash, Debug)] -pub enum Value { - Bool(bool), - String(String), - None, -} - - -impl Value { - fn from_matches(matches: &ArgMatches) -> Vec<(clap::Id, Self)> { - - let mut values = BTreeMap::new(); - - let _ = matches.ids().into_iter().for_each(|id| { - let source = matches - .value_source(id.as_str()) - .expect("id came from matches"); - - if matches.try_get_many::(id.as_str()).is_ok() { return () } - if source != clap::parser::ValueSource::CommandLine { return () } - if Self::extract::(matches, id, &mut values) { return () } - if Self::extract::(matches, id, &mut values) { return () } - }); - - values.into_values().collect() - - } - - - fn extract + Send + Sync + 'static>( - matches: &ArgMatches, - id: &clap::Id, - output: &mut BTreeMap, - ) -> bool { - - match matches.try_get_many::(id.as_str()) { - Ok(Some(values)) => { - values.zip( - matches - .indices_of(id.as_str()) - .expect("id came from matches") - ) - .for_each(|(value, index)| { - output.insert(index, (id.clone(), value.clone().into())); - }); - - true - }, - Err(clap::parser::MatchesError::Downcast { .. }) => false, - Ok(None) => { - unreachable!("ids only reports what is present") - }, - Err(_) => { - unreachable!("id came from matches") - }, - } - } -} - -impl From for Value { - fn from(other: String) -> Value { - Value::String(other) - } -} - -impl From for Value { - fn from(other: bool) -> Value { - Value::Bool(other) - } -} - -impl Default for Value { - fn default() -> Value { - Value::None - } -} - - - -#[derive(Debug)] -pub enum ArgError { - ArgParseError -} - -impl Error for ArgError {} - -impl fmt::Display for ArgError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ArgError::ArgParseError => { - write!(f, "Error parsing arguments") - } - } - } +pub fn parse_args() -> Cli { + Cli::parse() } diff --git a/src/dotfile/dot.rs b/src/dotfile/dot.rs index 8a90195..14a5265 100644 --- a/src/dotfile/dot.rs +++ b/src/dotfile/dot.rs @@ -73,7 +73,7 @@ impl ManagedDotfile { } - pub fn get_dotfile_dir_errors(&self) -> Vec<&dir::DirError> { + pub fn get_dir_errors(&self) -> Vec<&dir::DirError> { let manager_errors = if let Dotfile::Dir(dir) = &self.manager_dotfile { Some(dir.errors.iter()) diff --git a/src/lib.rs b/src/lib.rs index e4c922b..80fa55f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ use std::error::Error; use std::fmt; +use itertools::{Itertools, Either}; + use crate::config::cfg; use crate::dotfile::dot; use crate::args::arg; @@ -13,62 +15,79 @@ pub mod fs; -pub fn run(args: arg::Args, config: cfg::Config) -> Result<(), ManagerError> { +pub fn run(args: arg::Cli, config: cfg::Config) -> Result<(), ManagerError> { - let copy_to_sys = args.copy_to_sys; + let copy_to_sys = args.from; + + let _dry_run = args.dry; let dotfiles = config.dotfiles; - let valid_dotfiles: Vec<_> = dotfiles - .iter() - .filter_map(|dotfile| match dotfile { - Ok(dotfile) => Some(dotfile), - Err(e) => { - eprintln!("Failed to read a dotfile: {:?}", e); - None - }, - }).collect(); + let (valid, unrecoverable_errors): (Vec<_>, Vec<_>) = dotfiles.into_iter().partition_result(); + + if unrecoverable_errors.len() > 0 { + for error in unrecoverable_errors.into_iter() { + eprintln!("{:#?}", error); + return Err(ManagerError::DotfileCreateError) + } + } - let errored_dotfiles = valid_dotfiles - .iter() - .filter_map(|dotfile| - match dotfile.get_dotfile_dir_errors() { - errors if !errors.is_empty() => Some(dotfile), - _ => None + let (error_free, contains_errors): (Vec<_>, Vec<_>) = valid + .into_iter() + .partition_map( + |dotfile| + match dotfile.get_dir_errors() { + errors if errors.is_empty() => Either::Left(dotfile), + _ => Either::Right(dotfile), } ); + if contains_errors.len() > 0 { + log_errored_dotfiles(&contains_errors).expect("Dotfile path is invalid"); + } - let _ = errored_dotfiles.map(|dotfile| { + let copy_results = error_free + .iter() + .map(|dotfile| dotfile.copy_dotfile(copy_to_sys)); - if let dot::Dotfile::Dir(manager_dotfile) = &dotfile.manager_dotfile { - println!("Error copying dotfile: {}", manager_dotfile.path.to_str()?); + + for result in copy_results { + match result { + Err(e) => println!("Failed to copy dotfile: {:?}", e), + _ => (), + } + } + + + + Ok(()) +} + +fn log_errored_dotfiles(errors: &Vec) -> Result<(), ManagerError> { + + for error in errors.into_iter() { + + if let dot::Dotfile::Dir(manager_dotfile) = &error.manager_dotfile { + let dot_path = manager_dotfile.path.to_str().unwrap(); + println!("Error copying dotfile: {}", dot_path); manager_dotfile.errors .iter() .for_each(|error| println!("Error: {:?}", error)); }; - if let dot::Dotfile::Dir(system_dotfile) = &dotfile.system_dotfile { - println!("Error copying dotfile: {}", system_dotfile.path.to_str()?); + if let dot::Dotfile::Dir(system_dotfile) = &error.system_dotfile { + let Some(dot_path) = system_dotfile.path.to_str() else { + return Err(ManagerError::DotfileInvalidPathError) + }; + + println!("Error copying dotfile: {}", dot_path); system_dotfile.errors .iter() .for_each(|error| println!("Error: {:?}", error)); }; - Some(()) - }); - - let copy_results = valid_dotfiles.iter().map(|dotfile| dotfile.copy_dotfile(copy_to_sys)); - - copy_results.for_each(|result| { - match result { - Err(e) => println!("Failed to copy dotfile: {:?}", e), - _ => (), - } - }); - - + } Ok(()) } @@ -80,6 +99,8 @@ pub fn run(args: arg::Args, config: cfg::Config) -> Result<(), ManagerError> { pub enum ManagerError { DotfileCopyError(dot::DotfileError), ConfigParseError(cfg::ConfigParseError), + DotfileCreateError, + DotfileInvalidPathError, } impl Error for ManagerError {} @@ -92,6 +113,12 @@ impl fmt::Display for ManagerError { }, ManagerError::ConfigParseError(parse_error) => { write!(f, "{}", parse_error) + }, + ManagerError::DotfileCreateError => { + write!(f, "Failed to read dotfiles") + } + ManagerError::DotfileInvalidPathError => { + write!(f, "Dotfile has an invalid path") } } } diff --git a/src/main.rs b/src/main.rs index 9d6238d..ccba786 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,10 +8,10 @@ use dotfiles_manager::config::cfg; fn main() -> Result<(), dotfiles_manager::ManagerError> { - let args = arg::Args::parse_args(); + let cmd = arg::parse_args(); let program_config = cfg::Config::parse(PathBuf::from("/home/eesim/.config/dotfiles/config"))?; - dotfiles_manager::run(args, program_config) + dotfiles_manager::run(cmd, program_config) }