diff --git a/src/cli.rs b/src/cli.rs index ded246f..909fc90 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -84,6 +84,7 @@ pub fn app() -> App<'static, 'static> { .short("n") .help("Do not use color to highlight the diff"), ) + .arg(Arg::with_name("json").long("json").help("Return in JSON format")) .arg( // Legacy: This is exported now, and in fact this setting is ignored // completely if $__shadowenv_data is present in the environment. diff --git a/src/diff.rs b/src/diff.rs index b031dc3..c8f4ef8 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -2,25 +2,120 @@ use crate::undo; use std::collections::BTreeMap; use std::env; +use std::io; +use std::io::Write; -trait Logger { +enum ChangeType { + Add, + Remove, +} + +trait Logger<'out> { + fn with_output(o: &'out mut W, color: bool, json: bool) -> Self + where + Self: Sized; fn print(&mut self, value: String); + fn pre_decoration(&self, change: Option<&ChangeType>) -> &str { + match change { + None => " ", + Some(ChangeType::Add) => "+ ", + Some(ChangeType::Remove) => "- ", + } + } + fn post_decoration(&self, _change: Option<&ChangeType>) -> &str { + "" + } + fn serialize(&self, name: &str, value: &str, change: Option<&ChangeType>) -> String; + fn format_name_value(&self, name: &str, value: &str, change: Option<&ChangeType>) -> String { + format!( + "{pre}{serialized}{post}", + pre = self.pre_decoration(change), + post = self.post_decoration(change), + serialized = self.serialize(name, value, change) + ) + } +} + +struct OutputLogger<'out> { + output: &'out mut dyn Write, + color: bool, + json: bool, } -struct StdoutLogger; -impl Logger for StdoutLogger { +impl<'out> Logger<'out> for OutputLogger<'out> { + fn with_output(o: &'out mut W, color: bool, json: bool) -> Self + where + Self: Sized, + { + Self { + output: o, + color, + json, + } + } fn print(&mut self, value: String) { - println!("{}", value); + write!(self.output, "{}", value).expect("Could not print to output!"); + } + + fn pre_decoration(&self, change: Option<&ChangeType>) -> &str { + match (self.json, self.color, change) { + (true, _, _) => "", + (_, _, None) => " ", + (_, true, Some(ChangeType::Add)) => "\x1b[92m+ ", + (_, true, Some(ChangeType::Remove)) => "\x1b[91m- ", + (_, false, Some(ChangeType::Add)) => "+ ", + (_, false, Some(ChangeType::Remove)) => "- ", + } + } + + fn post_decoration(&self, _change: Option<&ChangeType>) -> &str { + if self.color { + // Clearing to EOL with \x1b[K prevents a weird issue where a wrapped line uses the last + // non-null background color for the newline character, filling the rest of the space in the + // line. + "\x1b[0m\x1b[K" + } else { + "" + } + } + + fn serialize(&self, name: &str, value: &str, change: Option<&ChangeType>) -> String { + if self.json { + format!( + "{{\"type\":\"{}\",\"name\":\"{}\",\"value\":{}}}", + match change { + None => "verbose", + Some(ChangeType::Add) => "add", + Some(ChangeType::Remove) => "remove", + }, + name, + { + let vals: Vec<&str> = value.split(":").collect(); + if vals.len() == 1 { + format!("\"{}\"", value) + } else { + format!( + "[{}]", + vals.iter() + .map(|v| format!("\"{}\"", v)) + .collect::>() + .join(",") + ) + } + }, + ) + } else { + format!("{}={}", name, value) + } } } /// print a diff of the env -pub fn run(verbose: bool, color: bool, shadowenv_data: String) -> i32 { +pub fn run(verbose: bool, color: bool, json: bool, shadowenv_data: String) -> i32 { run_with_logger( - &mut StdoutLogger {}, + &mut OutputLogger::with_output(&mut io::stdout().lock(), color, json), env::vars().collect(), verbose, - color, shadowenv_data, ) } @@ -29,7 +124,6 @@ fn run_with_logger( logger: &mut dyn Logger, env_vars: Vec<(String, String)>, verbose: bool, - color: bool, shadowenv_data: String, ) -> i32 { let mut parts = shadowenv_data.splitn(2, ':'); @@ -49,32 +143,24 @@ fn run_with_logger( for (name, value) in env_vars { if let Some(scalar) = scalars.remove(&name) { - diff_scalar(logger, &scalar, color) + diff_scalar(logger, &scalar) } else if let Some(list) = lists.remove(&name) { - diff_list(logger, &list, &value, color) + diff_list(logger, &list, &value) } else if verbose { print_verbose(logger, &name, &value) } } scalars .iter() - .for_each(|(_name, scalar)| diff_scalar(logger, scalar, color)); + .for_each(|(_name, scalar)| diff_scalar(logger, scalar)); lists .iter() - .for_each(|(_name, list)| diff_list(logger, list, "", color)); + .for_each(|(_name, list)| diff_list(logger, list, "")); 0 } -fn diff_list(logger: &mut dyn Logger, list: &undo::List, current: &str, color: bool) { - let formatted_deletions: Vec = if color { - list.deletions - .iter() - .map(|x| "\x1b[48;5;52m".to_string() + x + "\x1b[0;91m") - .collect() - } else { - list.deletions.clone() - }; - let mut prefix = formatted_deletions.join(":"); +fn diff_list(logger: &mut dyn Logger, list: &undo::List, current: &str) { + let mut prefix = list.deletions.clone().join(":"); let items = current .split(':') @@ -84,10 +170,10 @@ fn diff_list(logger: &mut dyn Logger, list: &undo::List, current: &str, color: b if !suffix.is_empty() && !prefix.is_empty() { prefix += ":"; } - diff_remove(logger, &list.name, &(prefix + &suffix), color); - + diff_remove(logger, &list.name, &(prefix + &suffix)); + // TODO: fix let items = current.split(':').map(|x| { - if list.additions.contains(&x.to_string()) && color { + if list.additions.contains(&x.to_string()) && false { "\x1b[48;5;22m".to_string() + x + "\x1b[0;92m" } else { x.to_string() @@ -95,58 +181,91 @@ fn diff_list(logger: &mut dyn Logger, list: &undo::List, current: &str, color: b }); let items: Vec = items.collect(); let newline = items.join(":"); - diff_add(logger, &list.name, &newline, color); + diff_add(logger, &list.name, &newline); } -fn diff_scalar(logger: &mut dyn Logger, scalar: &undo::Scalar, color: bool) { +fn diff_scalar(logger: &mut dyn Logger, scalar: &undo::Scalar) { if let Some(value) = &scalar.original { - diff_remove(logger, &scalar.name, value, color); + diff_remove(logger, &scalar.name, value); } if let Some(value) = &scalar.current { - diff_add(logger, &scalar.name, value, color); + diff_add(logger, &scalar.name, value); } } -fn diff_add(logger: &mut dyn Logger, name: &str, value: &str, color: bool) { - if color { - // Clearing to EOL with \x1b[K prevents a weird issue where a wrapped line uses the last - // non-null background color for the newline character, filling the rest of the space in the - // line. - logger.print(format!("\x1b[92m+ {}={}\x1b[0m\x1b[K", name, value)); - } else { - logger.print(format!("+ {}={}", name, value)); - } +fn diff_add(logger: &mut dyn Logger, name: &str, value: &str) { + logger.print(logger.format_name_value(name, value, Some(&ChangeType::Add))) } -fn diff_remove(logger: &mut dyn Logger, name: &str, value: &str, color: bool) { - if color { - // Clearing to EOL with \x1b[K prevents a weird issue where a wrapped line uses the last - // non-null background colour for the newline character, filling the rest of the space in the - // line. - logger.print(format!("\x1b[91m- {}={}\x1b[0m\x1b[K", name, value)); - } else { - logger.print(format!("- {}={}", name, value)); - } +fn diff_remove(logger: &mut dyn Logger, name: &str, value: &str) { + logger.print(logger.format_name_value(name, value, Some(&ChangeType::Remove))) } fn print_verbose(logger: &mut dyn Logger, name: &str, value: &str) { - logger.print(format!(" {}={}", name, value)) + logger.print(logger.format_name_value(name, value, None)) } #[cfg(test)] mod tests { use super::*; - #[derive(Default)] - struct DummyLogger(Vec); - impl Logger for DummyLogger { - fn print(&mut self, value: String) { - self.0.push(value); - } + + #[test] + fn color_test() { + let mut out: Vec = vec![]; + let mut logger = OutputLogger::with_output(&mut out, true, false); + let data = r#"62b0b9f86cda84d4:{"scalars":[],"lists":[{"name":"VAR_C","additions":["/added"],"deletions":["/removed"]},{"name":"VAR_B","additions":["/added"],"deletions":[]},{"name":"VAR_A","additions":["/added"],"deletions":[]}]}"#; + let env_vars = vec![ + ("VAR_A".to_string(), "/added:/existent".to_string()), + ("VAR_B".to_string(), "/added".to_string()), + ("VAR_C".to_string(), "/added:/existent".to_string()), + ]; + + let result = run_with_logger(&mut logger, env_vars, false, data.to_string()); + + let expected: String = vec![ + "\x1b[91m- VAR_A=/existent\x1b[0m\x1b[K", + "\x1b[92m+ VAR_A=/added:/existent\x1b[0m\x1b[K", + "\x1b[91m- VAR_B=\x1b[0m\x1b[K", + "\x1b[92m+ VAR_B=/added\x1b[0m\x1b[K", + "\x1b[91m- VAR_C=/removed:/existent\x1b[0m\x1b[K", + "\x1b[92m+ VAR_C=/added:/existent\x1b[0m\x1b[K", + ] + .join(""); + assert_eq!(result, 0); + assert_eq!(String::from_utf8(out).unwrap(), expected); + } + + #[test] + fn json_test() { + let mut out: Vec = vec![]; + let mut logger = OutputLogger::with_output(&mut out, false, true); + let data = r#"62b0b9f86cda84d4:{"scalars":[],"lists":[{"name":"VAR_C","additions":["/added"],"deletions":["/removed"]},{"name":"VAR_B","additions":["/added"],"deletions":[]},{"name":"VAR_A","additions":["/added"],"deletions":[]}]}"#; + let env_vars = vec![ + ("VAR_A".to_string(), "/added:/existent".to_string()), + ("VAR_B".to_string(), "/added".to_string()), + ("VAR_C".to_string(), "/added:/existent".to_string()), + ]; + + let result = run_with_logger(&mut logger, env_vars, false, data.to_string()); + + let expected: String = vec![ + // Did you try? + "[", + "{\"type\":\"add\",\"name\":\"VAR_A\",\"value\":[\"/added\",\"/existent\"]},", + "{\"type\":\"add\",\"name\":\"VAR_B\",\"value\":\"/added\"},", + "{\"type\":\"remove\",\"name\":\"VAR_C\",\"value\":[\"/removed\", \"/existent\"]},", + "{\"type\":\"add\",\"name\":\"VAR_C\",\"value\":[\"/existent\"]},", + "]", + ] + .join(""); + assert_eq!(result, 0); + assert_eq!(String::from_utf8(out).unwrap(), expected); } #[test] fn nominal_test() { - let mut logger = DummyLogger::default(); + let mut out: Vec = vec![]; + let mut logger = OutputLogger::with_output(&mut out, false, false); let env_vars = vec![ ("VAR_A".to_string(), "/added:/existent".to_string()), @@ -155,9 +274,9 @@ mod tests { ]; let data = r#"62b0b9f86cda84d4:{"scalars":[],"lists":[{"name":"VAR_C","additions":["/added"],"deletions":["/removed"]},{"name":"VAR_B","additions":["/added"],"deletions":[]},{"name":"VAR_A","additions":["/added"],"deletions":[]}]}"#; - let result = run_with_logger(&mut logger, env_vars, false, false, data.to_string()); + let result = run_with_logger(&mut logger, env_vars, false, data.to_string()); - let expected: Vec<_> = vec![ + let expected: String = vec![ "- VAR_A=/existent", "+ VAR_A=/added:/existent", "- VAR_B=", @@ -165,10 +284,8 @@ mod tests { "- VAR_C=/removed:/existent", "+ VAR_C=/added:/existent", ] - .iter() - .map(ToString::to_string) - .collect(); + .join(""); assert_eq!(result, 0); - assert_eq!(logger.0, expected); + assert_eq!(String::from_utf8(out).unwrap(), expected); } } diff --git a/src/main.rs b/src/main.rs index 73265a8..6312dab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,9 +51,10 @@ fn main() { ("diff", Some(matches)) => { let verbose = matches.is_present("verbose"); let color = !matches.is_present("no-color"); + let json = matches.is_present("json"); let legacy_fallback_data = matches.value_of("$__shadowenv_data").map(|d| d.to_string()); let data = Shadowenv::load_shadowenv_data_or_legacy_fallback(legacy_fallback_data); - process::exit(diff::run(verbose, color, data)); + process::exit(diff::run(verbose, color, json, data)); } ("trust", Some(_)) => { if let Err(err) = trust::run() {