diff --git a/Cargo.lock b/Cargo.lock index 4f7b31d..9ce1a5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -620,7 +620,7 @@ checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" [[package]] name = "sauce" -version = "0.6.2" +version = "0.6.3" dependencies = [ "ansi_term 0.12.1", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index ad12935..023a9a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sauce" -version = "0.6.2" +version = "0.6.3" authors = ["Dan Cardin "] edition = "2018" description = "A tool for managing directory-specific state." diff --git a/README.md b/README.md index 7d9fb82..13b38e2 100644 --- a/README.md +++ b/README.md @@ -292,9 +292,6 @@ project) which would enable things like `sauce --as prod` or ## Planned Work -- only **print** `sauce config` command results when `--show` flag has - been used - - ability to subdivide targets by shell i.e. allow one to specify `[alias.fish]`. diff --git a/src/cli/shape.rs b/src/cli/shape.rs index e51fbe3..250dcc6 100644 --- a/src/cli/shape.rs +++ b/src/cli/shape.rs @@ -15,7 +15,9 @@ pub struct CliOptions { #[clap(long)] pub autoload: bool, - /// Only **show** the intended output of the command rather than executing it. + /// For typical commands such as `sauce` and `sauce clear` this outputs the exact + /// shell output that would have executed. For mutating commands like `sauce config` + /// and `sauce set`, the change is printed but not saved. #[clap(long)] pub show: bool, @@ -122,7 +124,8 @@ pub enum SetKinds { /// Key-value pairs, delimited by an "=". #[derive(Clap, Debug)] pub struct SetVarKind { - pub values: Vec, + #[clap(parse(try_from_str = crate::cli::utilities::parse_key_val))] + pub values: Vec<(String, String)>, } /// Key value pair, supplied as individual arguments diff --git a/src/cli/utilities.rs b/src/cli/utilities.rs index 13b17c0..9b5b739 100644 --- a/src/cli/utilities.rs +++ b/src/cli/utilities.rs @@ -16,7 +16,7 @@ where } /// Accept data from stdin -pub fn get_input(values: &[String]) -> Vec { +pub fn get_input(values: &[(String, String)]) -> Vec<(String, String)> { let mut result = Vec::new(); result.extend_from_slice(values); @@ -31,7 +31,10 @@ pub fn get_input(values: &[String]) -> Vec { if let Some(b) = buffer.strip_suffix("\n") { buffer = b.to_string(); } - result.push(buffer); + + if let Ok(keyval) = parse_key_val(&buffer) { + result.push(keyval); + } } } diff --git a/src/output.rs b/src/output.rs index 3694e87..bc1a37c 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,11 +1,17 @@ -use crate::colors::{TABLE_BLUE, TABLE_YELLOW}; +use crate::toml::{ensure_section, write_document}; +use crate::{ + colors::{BLUE, TABLE_BLUE, TABLE_YELLOW, YELLOW}, + toml::unwrap_toml_value, +}; use anyhow::Result; use comfy_table::{Attribute, Cell, ContentArrangement, Row, Table}; use std::{ fmt::Display, io::{stderr, stdout, Write}, ops::Deref, + path::Path, }; +use toml_edit::{Document, Item}; use ansi_term::{ANSIString, ANSIStrings}; @@ -175,6 +181,35 @@ impl Output { self.err.flush()?; Ok(()) } + + pub fn write_toml( + &mut self, + file: &Path, + document: &mut Document, + heading: &str, + values: I, + ) where + I: IntoIterator, + T: AsRef, + { + for (name, value) in values.into_iter() { + self.notify(&[ + "Setting ".into(), + BLUE.bold().paint(name.as_ref()), + " = ".into(), + YELLOW.paint(unwrap_toml_value(value.as_value().unwrap())), + ]); + + if !self.show { + let section = ensure_section(document, heading); + section[name.as_ref()] = value; + } + } + + if !self.show { + write_document(file, document, self); + } + } } impl Default for Output { diff --git a/src/saucefile.rs b/src/saucefile.rs index 9475e41..e479cd1 100644 --- a/src/saucefile.rs +++ b/src/saucefile.rs @@ -1,14 +1,11 @@ -use crate::settings::Settings; -use crate::toml::write_document; use crate::{filter::FilterOptions, Context}; +use crate::{settings::Settings, toml::unwrap_toml_value}; use indexmap::IndexMap; use itertools::iproduct; -use toml_edit::Table; use crate::toml::get_document; use std::path::PathBuf; -use std::str::FromStr; -use toml_edit::{value, Document, Item, Value}; +use toml_edit::{Document, Item, Value}; #[derive(Debug)] pub struct Saucefile { @@ -46,39 +43,6 @@ impl Saucefile { Settings::from_document(self.path.clone(), &self.document) } - pub fn set_var(&mut self, name: &str, raw_value: &str) { - let toml_value = Value::from_str(&raw_value).unwrap_or_else(|_| Value::from(raw_value)); - let env_section = self.document.as_table_mut().entry("environment"); - if env_section.is_none() { - *env_section = Item::Table(Table::new()); - } - self.document["environment"][&name] = value(toml_value); - } - - pub fn set_alias(&mut self, name: &str, raw_value: &str) { - let toml_value = Value::from_str(&raw_value).unwrap_or_else(|_| Value::from(raw_value)); - - let alias_section = self.document.as_table_mut().entry("alias"); - if alias_section.is_none() { - *alias_section = Item::Table(Table::new()); - } - self.document["alias"][&name] = value(toml_value); - } - - pub fn set_function(&mut self, name: &str, body: &str) { - let toml_value = Value::from_str(&body).unwrap_or_else(|_| Value::from(body)); - - let alias_section = self.document.as_table_mut().entry("function"); - if alias_section.is_none() { - *alias_section = Item::Table(Table::new()); - } - self.document["function"][&name] = value(toml_value); - } - - pub fn write(&mut self, context: &mut Context) { - write_document(&context.sauce_path, &self.document, &mut context.output); - } - fn section( &mut self, sections: &[&str], @@ -142,18 +106,6 @@ impl Default for Saucefile { } } -fn unwrap_toml_value(value: &Value) -> String { - match value { - Value::InlineTable(_) => value.as_inline_table().unwrap().to_string(), - Value::Array(_) => value.as_array().unwrap().to_string(), - Value::String(_) => value.as_str().unwrap().to_string(), - Value::Integer(_) => value.as_integer().unwrap().to_string(), - Value::Boolean(_) => value.as_bool().unwrap().to_string(), - Value::Float(_) => value.as_float().unwrap().to_string(), - Value::DateTime(_) => value.as_date_time().unwrap().to_string(), - } -} - #[cfg(test)] mod tests { mod section { @@ -312,15 +264,15 @@ mod tests { assert_eq!(result, &[]); } - #[test] - fn it_roundtrips_value() { - let mut sauce = Saucefile::default(); + // #[test] + // fn it_roundtrips_value() { + // let mut sauce = Saucefile::default(); - sauce.set_var("meow", "5"); - let result = sauce.vars(&FilterOptions::default()); + // sauce.set_var("meow", "5"); + // let result = sauce.vars(&FilterOptions::default()); - assert_eq!(result, vec![("meow", "5".to_string())]); - } + // assert_eq!(result, vec![("meow", "5".to_string())]); + // } } mod aliases { @@ -334,15 +286,15 @@ mod tests { assert_eq!(result, &[]); } - #[test] - fn it_roundtrips_value() { - let mut sauce = Saucefile::default(); + // #[test] + // fn it_roundtrips_value() { + // let mut sauce = Saucefile::default(); - sauce.set_alias("meow", "5"); - let result = sauce.aliases(&FilterOptions::default()); + // sauce.set_alias("meow", "5"); + // let result = sauce.aliases(&FilterOptions::default()); - assert_eq!(result, vec![("meow", "5".to_string())]); - } + // assert_eq!(result, vec![("meow", "5".to_string())]); + // } } mod functions { @@ -356,14 +308,14 @@ mod tests { assert_eq!(result, &[]); } - #[test] - fn it_roundtrips_value() { - let mut sauce = Saucefile::default(); + // #[test] + // fn it_roundtrips_value() { + // let mut sauce = Saucefile::default(); - sauce.set_function("meow", "5"); - let result = sauce.functions(&FilterOptions::default()); + // sauce.set_function("meow", "5"); + // let result = sauce.functions(&FilterOptions::default()); - assert_eq!(result, vec![("meow", "5".to_string())]); - } + // assert_eq!(result, vec![("meow", "5".to_string())]); + // } } } diff --git a/src/settings.rs b/src/settings.rs index c93cc57..f90e89b 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,7 +1,6 @@ use crate::{ colors::{RED, YELLOW}, output::ErrorCode, - toml::write_document, }; use anyhow::Result; use std::{ @@ -84,14 +83,13 @@ impl Settings { if settings_section.is_none() { *settings_section = Item::Table(Table::new()); } - let settings = &mut document["settings"]; - let mut values = pairs + let values = pairs .iter() .filter_map(|(setting, value)| match setting.as_ref() { "autoload" | "autoload-hook" => { if let Ok(parsed_value) = value.as_ref().parse::() { - Some((setting, toml_edit::value(parsed_value))) + Some((setting.as_ref(), toml_edit::value(parsed_value))) } else { output.notify_error( ErrorCode::ParseError, @@ -114,17 +112,13 @@ impl Settings { None } }) - .peekable(); + .collect::>(); - if values.peek().is_none() { + if values.is_empty() { return; } - for (setting, value) in values { - settings[setting.as_ref()] = value; - } - - write_document(&self.file, &document, output); + output.write_toml(&self.file, &mut document, "settings", values); } } diff --git a/src/shell/actions.rs b/src/shell/actions.rs index 03a0a53..654dd46 100644 --- a/src/shell/actions.rs +++ b/src/shell/actions.rs @@ -153,6 +153,8 @@ where #[cfg(test)] mod tests { + use crate::toml::{ensure_section, value_from_string}; + use crate::test_utils::{setup, TestShell}; use indoc::indoc; use std::path::Path; @@ -216,13 +218,19 @@ mod tests { let shell = TestShell {}; let (out, err, mut context) = setup(); let mut saucefile = Saucefile::default(); - saucefile.set_var("var", "varvalue"); - saucefile.set_var("alias", "aliasvalue"); - saucefile.set_var("function", "functionvalue"); + + let section = ensure_section(&mut saucefile.document, "environment"); + section["var"] = value_from_string("varvalue"); + + let section = ensure_section(&mut saucefile.document, "alias"); + section["alias"] = value_from_string("aliasvalue"); + + let section = ensure_section(&mut saucefile.document, "function"); + section["fn"] = value_from_string("fnvalue"); clear(&mut context, &shell, saucefile); - assert_eq!(out.value(), "unset var;\nunset alias;\nunset function;\n\n"); + assert_eq!(out.value(), "unset var;\n\nunalias alias;\n\nunset fn;\n\n"); assert_eq!(err.value(), "Cleared your sauce\n"); } } @@ -236,7 +244,9 @@ mod tests { fn it_shows_env_vars() { let (out, err, mut context) = setup(); let mut saucefile = Saucefile::default(); - saucefile.set_var("var", "varvalue"); + + let section = ensure_section(&mut saucefile.document, "environment"); + section["var"] = value_from_string("varvalue"); show(&mut context, Target::EnvVar, saucefile); @@ -259,7 +269,9 @@ mod tests { fn it_shows_aliases() { let (out, err, mut context) = setup(); let mut saucefile = Saucefile::default(); - saucefile.set_alias("alias", "aliasvalue"); + + let section = ensure_section(&mut saucefile.document, "alias"); + section["alias"] = value_from_string("aliasvalue"); show(&mut context, Target::Alias, saucefile); @@ -282,7 +294,9 @@ mod tests { fn it_shows_functions() { let (out, err, mut context) = setup(); let mut saucefile = Saucefile::default(); - saucefile.set_function("function", "git add\ngit commit"); + + let section = ensure_section(&mut saucefile.document, "function"); + section["function"] = value_from_string("git add\ngit commit"); show(&mut context, Target::Function, saucefile); @@ -313,15 +327,21 @@ mod tests { let shell = TestShell {}; let (out, err, mut context) = setup(); let mut saucefile = Saucefile::default(); - saucefile.set_var("var", "varvalue"); - saucefile.set_var("alias", "aliasvalue"); - saucefile.set_var("function", "functionvalue"); + + let section = ensure_section(&mut saucefile.document, "environment"); + section["var"] = value_from_string("varvalue"); + + let section = ensure_section(&mut saucefile.document, "alias"); + section["alias"] = value_from_string("aliasvalue"); + + let section = ensure_section(&mut saucefile.document, "function"); + section["fn"] = value_from_string("fnvalue"); execute(&mut context, &shell, saucefile, false); assert_eq!( out.value(), - "export var=varvalue;\nexport alias=aliasvalue;\nexport function=functionvalue;\n\n" + "export var=varvalue;\n\nalias alias=aliasvalue;\n\nfunction fn=fnvalue;\n\n" ); assert_eq!(err.value(), "Sourced \n"); } diff --git a/src/shell/context.rs b/src/shell/context.rs index dbfedfd..e3928bd 100644 --- a/src/shell/context.rs +++ b/src/shell/context.rs @@ -11,6 +11,7 @@ use crate::{ settings::Settings, shell::{actions, Shell}, target::Target, + toml::value_from_string, }; #[derive(Debug)] @@ -154,51 +155,44 @@ impl<'a> Context<'a> { .rev() } - pub fn set_var>(&mut self, values: &[T]) { + pub fn set_var>(&mut self, raw_values: &[(T, T)]) { let mut saucefile = self.saucefile(); - for values in values.iter() { - let mut parts = values.as_ref().splitn(2, '='); - let var = parts.next().unwrap_or(""); - let value = parts.next().unwrap_or(""); - saucefile.set_var(var, value); - self.output.notify(&[ - BLUE.paint("Set "), - YELLOW.paint(var), - BLUE.paint(" = "), - YELLOW.paint(value), - ]); - } - saucefile.write(self); + let values = raw_values + .iter() + .map(|(name, raw_value)| (name, value_from_string(raw_value.as_ref()))) + .collect::>(); + + self.output.write_toml( + &self.sauce_path, + &mut saucefile.document, + "environment", + values, + ); } - pub fn set_alias>(&mut self, values: &[T]) { + pub fn set_alias>(&mut self, raw_values: &[(T, T)]) { let mut saucefile = self.saucefile(); - for values in values.iter() { - let mut parts = values.as_ref().splitn(2, '='); - let var = parts.next().unwrap_or(""); - let value = parts.next().unwrap_or(""); - saucefile.set_alias(var, value); - self.output.notify(&[ - BLUE.paint("Set "), - YELLOW.paint(var), - BLUE.paint(" = "), - YELLOW.paint(value), - ]); - } - saucefile.write(self); + + let values = raw_values + .iter() + .map(|(name, raw_value)| (name, value_from_string(raw_value.as_ref()))) + .collect::>(); + + self.output + .write_toml(&self.sauce_path, &mut saucefile.document, "alias", values); } pub fn set_function(&mut self, name: &str, body: &str) { let mut saucefile = self.saucefile(); - saucefile.set_function(name, body); - self.output.notify(&[ - BLUE.paint("Set "), - YELLOW.paint(name), - BLUE.paint(" = "), - YELLOW.paint(body), - ]); - saucefile.write(self); + let values = vec![(name, value_from_string(body))]; + + self.output.write_toml( + &self.sauce_path, + &mut saucefile.document, + "function", + values, + ); } pub fn set_config>(&mut self, values: &[(T, T)], global: bool) { diff --git a/src/toml.rs b/src/toml.rs index 337de9a..28c2e5e 100644 --- a/src/toml.rs +++ b/src/toml.rs @@ -5,9 +5,10 @@ use crate::{ use std::{ fs::OpenOptions, io::{BufReader, BufWriter, Read}, + str::FromStr, }; use std::{io::Write, path::Path}; -use toml_edit::Document; +use toml_edit::{Document, Item, Table, Value}; pub fn get_document(path: &Path, output: &mut Output) -> Document { let content = read_file(path); @@ -68,3 +69,28 @@ pub fn write_document(file: &Path, document: &Document, output: &mut Output) { ); } } + +pub fn ensure_section<'a>(document: &'a mut Document, section: &str) -> &'a mut Item { + let env_section = document.as_table_mut().entry(section); + if env_section.is_none() { + *env_section = Item::Table(Table::new()); + } + env_section +} + +pub fn value_from_string(raw_value: &str) -> Item { + let value = Value::from_str(&raw_value).unwrap_or_else(|_| Value::from(raw_value)); + toml_edit::value(value) +} + +pub fn unwrap_toml_value(value: &Value) -> String { + match value { + Value::InlineTable(_) => value.as_inline_table().unwrap().to_string(), + Value::Array(_) => value.as_array().unwrap().to_string(), + Value::String(_) => value.as_str().unwrap().to_string(), + Value::Integer(_) => value.as_integer().unwrap().to_string(), + Value::Boolean(_) => value.as_bool().unwrap().to_string(), + Value::Float(_) => value.as_float().unwrap().to_string(), + Value::DateTime(_) => value.as_date_time().unwrap().to_string(), + } +}