From fdd5255c5636852bf9df25efe0cd4fcf1ca9ab83 Mon Sep 17 00:00:00 2001 From: Dan Sully Date: Wed, 18 Sep 2024 13:51:53 -0700 Subject: [PATCH] Add --exit-code flag to exit with a specified value if defaults were applied. --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/cmd/apply.rs | 4 ++-- src/cmd/mod.rs | 4 ++-- src/defaults.rs | 51 ++++++++++++++++++++++++------------------------ src/main.rs | 20 ++++++++++++++----- 6 files changed, 47 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aaa0abc..fd52f22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -500,7 +500,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "macos-defaults" -version = "0.1.1" +version = "0.2.0" dependencies = [ "camino", "clap", diff --git a/Cargo.toml b/Cargo.toml index 8c5f6dc..c3d7da5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ license = "MIT" name = "macos-defaults" readme = "README.md" repository = "https://github.com/dsully/macos-defaults" -version = "0.1.1" +version = "0.2.0" [dependencies] camino = "1.1.9" diff --git a/src/cmd/apply.rs b/src/cmd/apply.rs index 4543bb4..a80f9f9 100644 --- a/src/cmd/apply.rs +++ b/src/cmd/apply.rs @@ -79,7 +79,7 @@ data: #[derive(Debug, Default, Serialize, Deserialize)] struct DefaultsConfig(HashMap>); -pub fn apply_defaults(path: &Utf8PathBuf) -> Result<()> { +pub fn apply_defaults(path: &Utf8PathBuf) -> Result { // let s = fs::read_to_string(path).map_err(|e| E::FileRead { path: path.to_owned(), @@ -129,7 +129,7 @@ pub fn apply_defaults(path: &Utf8PathBuf) -> Result<()> { } if errors.is_empty() { - return Ok(()); + return Ok(changed); } for error in &errors { diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index afe653b..573d5ff 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,5 +1,5 @@ pub mod apply; pub mod dump; -pub use apply::*; -pub use dump::*; +pub use apply::{apply_defaults, process_path}; +pub use dump::dump; diff --git a/src/defaults.rs b/src/defaults.rs index 17cbd56..d0ae670 100644 --- a/src/defaults.rs +++ b/src/defaults.rs @@ -1,17 +1,18 @@ //! Utility functions for updating plist files. // // NB: Most of this code originated from: https://github.com/gibfahn/up-rs, MIT & Apache 2.0 licensed. -// + +use std::collections::HashMap; +use std::fs::{self, File}; +use std::io::Read; +use std::mem; + use camino::{Utf8Path, Utf8PathBuf}; use color_eyre::eyre::{eyre, Result}; use duct::cmd; use log::{debug, info, trace, warn}; use plist::{Dictionary, Value}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fs::{self, File}; -use std::io::Read; -use std::mem; use super::errors::DefaultsError as E; @@ -356,12 +357,14 @@ fn merge_value(new_value: &mut Value, old_value: Option<&Value>) { /// But any duplicates between old and new values are removed, with the first value taking /// precedence. fn replace_ellipsis_array(new_value: &mut Value, old_value: Option<&Value>) { + // let Value::Array(new_array) = new_value else { trace!("Value isn't an array, skipping ellipsis replacement..."); return; }; let ellipsis = plist::Value::from(ELLIPSIS); + let Some(position) = new_array.iter().position(|x| x == &ellipsis) else { trace!("New value doesn't contain ellipsis, skipping ellipsis replacement..."); return; @@ -376,6 +379,7 @@ fn replace_ellipsis_array(new_value: &mut Value, old_value: Option<&Value>) { let array_copy: Vec<_> = std::mem::take(new_array); trace!("Performing array ellipsis replacement..."); + for element in array_copy { if element == ellipsis { for old_element in old_array { @@ -390,20 +394,23 @@ fn replace_ellipsis_array(new_value: &mut Value, old_value: Option<&Value>) { } } -/// Recursively merge dictionaries, unless the new value is empty `{}`. -/// If a dictionary -/// * is empty `{}` -/// * contains a key `{}` -/// Then the merge step will be skipped for it and its children. +// Recursively merge dictionaries, unless the new value is empty `{}`. +// If a dictionary +// * is empty `{}` +// * contains a key `{}` +// Then the merge step will be skipped for it and its children. fn deep_merge_dictionaries(new_value: &mut Value, old_value: Option<&Value>) { + // let Value::Dictionary(new_dict) = new_value else { trace!("New value is not a dictionary, Skipping merge..."); return; }; + if new_dict.is_empty() { trace!("New value is an empty dictionary. Skipping merge..."); return; } + // the "..." key is no longer used, and its merging behavior is performed by default. ignore it, for compatibility with older YAML. new_dict.remove(ELLIPSIS); @@ -411,9 +418,9 @@ fn deep_merge_dictionaries(new_value: &mut Value, old_value: Option<&Value>) { trace!("Old value wasn't a dict. Skipping merge..."); return; }; - + // for each value, recursively invoke this to merge any child dictionaries. - // also perform array ellipsis replacment. + // also perform array ellipsis replacement. // this occurs even if "!" is present. for (key, new_child_value) in &mut *new_dict { let old_child_value = old_dict.get(key); @@ -425,7 +432,9 @@ fn deep_merge_dictionaries(new_value: &mut Value, old_value: Option<&Value>) { new_dict.remove(BANG); return; } + trace!("Performing deep merge..."); + for (key, old_value) in old_dict { if !new_dict.contains_key(key) { new_dict.insert(key.clone(), old_value.clone()); @@ -497,10 +506,8 @@ mod tests { use crate::defaults::deep_merge_dictionaries; use super::{replace_ellipsis_array, NS_GLOBAL_DOMAIN}; - // use serial_test::serial; #[test] - // #[serial(home_dir)] // Test relies on or changes the $HOME env var. fn plist_path_tests() -> TestResult { let home_dir = dirs::home_dir().expect("Expected to be able to calculate the user's home directory."); @@ -585,7 +592,7 @@ mod tests { } #[test] - fn test_deep_merge_dictionaries() -> TestResult { + fn test_deep_merge_dictionaries() { use plist::{Dictionary, Value}; let old_value = Dictionary::from_iter([ @@ -612,12 +619,10 @@ mod tests { .into(); assert_eq!(new_value, expected); - - Ok(()) } #[test] - fn test_replace_ellipsis_dict_nested() -> TestResult { + fn test_replace_ellipsis_dict_nested() { use plist::{Dictionary, Value}; let old_value = Dictionary::from_iter([( @@ -660,12 +665,10 @@ mod tests { .into(); assert_eq!(new_value, expected); - - Ok(()) } #[test] - fn test_replace_ellipsis_dict_nested_bang() -> TestResult { + fn test_replace_ellipsis_dict_nested_bang() { use plist::{Dictionary, Value}; let old_value = Dictionary::from_iter([( @@ -702,12 +705,10 @@ mod tests { .into(); assert_eq!(new_value, expected); - - Ok(()) } #[test] - fn test_replace_ellipsis_array() -> TestResult { + fn test_replace_ellipsis_array() { let old_value = vec![ 10.into(), // ! 20.into(), // ! @@ -736,7 +737,7 @@ mod tests { 50.into(), ] .into(); + assert_eq!(new_value, expected); - Ok(()) } } diff --git a/src/main.rs b/src/main.rs index 19da498..ccf48eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,6 +62,10 @@ pub(crate) enum Commands { /// Sets the input file or path to use. #[arg(required = true, value_hint = ValueHint::FilePath)] path: Utf8PathBuf, + + /// If changes were applied, exit with this return code. + #[clap(short, long, default_value = "0")] + exit_code: i32, }, /// Generate shell completions to stdout. @@ -103,18 +107,22 @@ fn main() -> Result<()> { env_logger::Builder::new().filter_level(cli.verbose.log_level_filter()).init(); match cli.command { - Commands::Apply { path } => { + Commands::Apply { path, exit_code } => { + // + let mut changed = false; + for p in process_path(path)? { fs::metadata(&p).map_err(|e| E::FileRead { path: p.clone(), source: e })?; - apply_defaults(&p)?; + if apply_defaults(&p)? { + changed = true; + } } - Ok(()) + std::process::exit(if changed { exit_code } else { 0 }); } Commands::Completions { shell } => { generate(shell, &mut CLI::command(), "macos-defaults", &mut io::stdout().lock()); - Ok(()) } Commands::Dump { @@ -123,5 +131,7 @@ fn main() -> Result<()> { global_domain, domain, } => dump(current_host, path, global_domain, domain), - } + }?; + + std::process::exit(0); }