diff --git a/Cargo.lock b/Cargo.lock index 4fba42b..d831c75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1057,11 +1057,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -1178,44 +1178,42 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "serde", + "indexmap", + "serde_core", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_parser", + "toml_writer", + "winnow 0.7.13", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "serde", + "serde_core", ] [[package]] -name = "toml_edit" -version = "0.22.27" +name = "toml_parser" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", + "winnow 1.0.4", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_writer" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "unicode-ident" @@ -1616,9 +1614,12 @@ name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" [[package]] name = "wit-bindgen" diff --git a/Cargo.toml b/Cargo.toml index 1d98992..5849184 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ log = "0.4" serde = "1.0.21" serde_derive = "1.0.21" ureq = { version = "3.0.8", default-features = false, features = ["gzip", "socks-proxy"] } -toml = "0.8.19" +toml = "0.9" yansi = "1" zip = { version = "5.1.1", default-features = false, features = ["deflate"] } diff --git a/docs/src/config.md b/docs/src/config.md index a662b57..de3460e 100644 --- a/docs/src/config.md +++ b/docs/src/config.md @@ -57,3 +57,16 @@ auto_update = true The directory where the configuration file resides may be overwritten by the environment variable `TEALDEER_CONFIG_DIR`. Remember to use an absolute path. Variable expansion will not be performed on the path. + +## Override Config Values + +Individual config values can be overridden using the `--override-config` command +line argument. The overrides take place after reading the user config file, but +before the raw config is evaluated. + +```shell +$ tldr --override-config "display.compact = true" tealdeer +``` + +Each override is of the form ` = ` where `name` is a config key and +`value` is any TOML value. diff --git a/docs/src/usage.txt b/docs/src/usage.txt index 6a04de7..0db9cc9 100644 --- a/docs/src/usage.txt +++ b/docs/src/usage.txt @@ -7,26 +7,29 @@ Arguments: [COMMAND]... The command to show (e.g. `tar` or `git log`) Options: - -l, --list List all commands in the cache - --edit-page Edit custom page with `EDITOR` - --edit-patch Edit custom patch with `EDITOR` - -f, --render Render a specific markdown file - -p, --platform Override the operating system, can be specified multiple times in order - of preference [possible values: linux, macos, sunos, windows, android, - freebsd, netbsd, openbsd, common] - -L, --language Override the language - -u, --update Update the local cache - --no-auto-update If auto update is configured, disable it for this run - -c, --clear-cache Clear the local cache - --config-path Override config file location - --pager Use a pager to page output - -r, --raw Display the raw markdown instead of rendering it - -q, --quiet Suppress informational messages - --show-paths Show file and directory paths used by tealdeer - --seed-config Create a basic config - --color Control whether to use color [possible values: always, auto, never] - -v, --version Print the version - -h, --help Print help + -l, --list List all commands in the cache + --edit-page Edit custom page with `EDITOR` + --edit-patch Edit custom patch with `EDITOR` + -f, --render Render a specific markdown file + -p, --platform Override the operating system, can be specified multiple times + in order of preference [possible values: linux, macos, sunos, + windows, android, freebsd, netbsd, openbsd, common] + -L, --language Override the language + -u, --update Update the local cache + --no-auto-update If auto update is configured, disable it for this run + -c, --clear-cache Clear the local cache + --config-path Override config file location + --override-config Override config values after reading config file (example: + `updates.auto_update = true`) + --pager Use a pager to page output + -r, --raw Display the raw markdown instead of rendering it + -q, --quiet Suppress informational messages + --show-paths Show file and directory paths used by tealdeer + --seed-config Create a basic config + --color Control whether to use color [possible values: always, auto, + never] + -v, --version Print the version + -h, --help Print help To view the user documentation, please visit https://tealdeer-rs.github.io/tealdeer/. diff --git a/src/cli.rs b/src/cli.rs index 161d69d..761a25b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -80,6 +80,10 @@ pub(crate) struct Cli { #[arg(long = "config-path", value_name = "FILE")] pub config_path: Option, + /// Override config values after reading config file (example: `updates.auto_update = true`) + #[arg(long, action = ArgAction::Append, value_name = "OVERRIDE")] + pub override_config: Vec, + /// Use a pager to page output #[arg(long = "pager", requires = "command_or_file")] pub pager: bool, diff --git a/src/config.rs b/src/config.rs index f0feeb4..84b1dac 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,11 +4,12 @@ use std::{ fs::{self, File}, io::{ErrorKind, Write}, path::{Component, Path, PathBuf}, + str::FromStr, sync::LazyLock, time::Duration, }; -use anyhow::{anyhow, ensure, Context, Result}; +use anyhow::{anyhow, bail, ensure, Context, Result}; use clap::ValueEnum; use log::info; use serde::Serialize as _; @@ -245,16 +246,22 @@ impl Default for RawIndent { } } +impl From for Indent { + fn from(raw_indent: RawIndent) -> Self { + Self { + base: raw_indent.base, + command: raw_indent.command, + } + } +} + impl From<&RawDisplayConfig> for DisplayConfig { fn from(raw_display_config: &RawDisplayConfig) -> Self { Self { compact: raw_display_config.compact, use_pager: raw_display_config.use_pager, show_title: raw_display_config.show_title, - indent: Indent { - base: raw_display_config.indent.base, - command: raw_display_config.indent.command, - }, + indent: raw_display_config.indent.into(), } } } @@ -742,44 +749,87 @@ pub struct ConfigLoader { } impl ConfigLoader { - fn read_internal(path: PathWithSource, allow_not_found: bool) -> Result { - match fs::read_to_string(&path.path) { - Ok(content) => Ok(Self { - raw: toml::from_str(&content).with_context(|| { - format!( - "Could not parse config file contents as toml from {}.", - path.path.display() - ) - })?, - path, - }), - Err(e) if allow_not_found && e.kind() == ErrorKind::NotFound => Ok(Self { - raw: RawConfig::default(), - path, - }), - Err(e) => Err(e).context(format!( - "Could not read config file contents from {}.", - path.path().display() - )), + fn read_internal( + path: PathWithSource, + allow_not_found: bool, + overrides: &[String], + ) -> Result { + let read_raw_config = match fs::read_to_string(&path.path) { + Ok(content) => toml::from_str(&content).with_context(|| { + format!( + "Could not parse config file contents as toml from {}.", + path.path.display() + ) + })?, + Err(e) if allow_not_found && e.kind() == ErrorKind::NotFound => RawConfig::default(), + Err(e) => { + return Err(e).context(format!( + "Could not read config file contents from {}.", + path.path().display() + )) + } + }; + + let read_config_table = toml::Table::try_from(read_raw_config)?; + let used_config_table = Self::override_config_with(read_config_table, overrides) + .context("Failed to apply config overrides")?; + let raw = used_config_table.try_into()?; + + Ok(Self { raw, path }) + } + + fn override_config_with( + config_table: toml::Table, + overrides: &[String], + ) -> Result { + let mut config_table = toml::Value::Table(config_table); + for override_str in overrides { + let (name, value) = override_str + .split_once('=') + .ok_or(anyhow!("Invalid override-string: {override_str} (correct example: \"display.compact = true\")"))?; + + let name = name.trim(); + let value = toml::Value::from_str(value.trim())?; + + let mut entry = &mut config_table; + for subkey in name.split('.') { + let toml::Value::Table(entry_table) = entry else { + bail!("\"{name}\" is not a valid identifier since \"{subkey}\" already refers to a value which is not a toml-Table."); + }; + + entry = entry_table + .entry(subkey) + .or_insert(toml::Value::Table(Default::default())); + } + + *entry = value; + } + + match config_table { + toml::Value::Table(config_table) => Ok(config_table), + _ => unreachable!("root table is never modified"), } } /// Create a loader that uses the config at `path`. - pub fn read(path: PathBuf) -> Result { + /// `overrides`: If set, overrides the default values of the config + pub fn read(path: PathBuf, overrides: &[String]) -> Result { Self::read_internal( PathWithSource { path, source: PathSource::Cli, }, false, + overrides, ) } /// Create a loader that uses the default config file location. If no file is present at the default location, the /// default configuration is used. - pub fn read_default_path() -> Result { + /// `overrides`: If set, overrides the default values of the config + pub fn read_default_path(overrides: &[String]) -> Result { let path = get_default_config_path(); - Self::read_internal(path, true) + Self::read_internal(path, true, overrides) } /// Parse the read [`RawConfig`] into a [`Config`]. @@ -954,6 +1004,96 @@ mod test { ); } + mod override_config { + use super::*; + use toml::Value; + + fn base_config() -> toml::Table { + toml::Table::from_str( + " + global_value = false + + [some] + value = 0 + + [some.inner] + value1 = 1 + value2 = \"a string\" + + [some.other] + value1 = 3 + value2 = [ 1, \"text\", true ] + ", + ) + .unwrap() + } + + #[test] + fn basic() { + let original_config = base_config(); + let overrides = &["some.inner.value1 = 'some text'".to_string()]; + + let new_config = ConfigLoader::override_config_with(original_config, overrides) + .expect("config should be successfully overwritten"); + + assert_eq!(new_config["some"]["value"], Value::Integer(0)); + assert_eq!( + new_config["some"]["inner"]["value1"], + Value::String("some text".to_string()), + ); + assert_eq!( + new_config["some"]["inner"]["value2"], + Value::String("a string".to_string()), + ); + assert_eq!(new_config["some"]["other"]["value1"], Value::Integer(3)); + assert_eq!(new_config["global_value"], Value::Boolean(false)); + } + + macro_rules! style_config_with { + ($config:ident, $overrides:expr) => { + let loader = ConfigLoader::read( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/style-config.toml"), + $overrides, + ) + .unwrap(); + let $config = loader.load().unwrap(); + }; + } + + #[test] + fn dependent_config() { + style_config_with!(config, &["search.languages = ['de', 'it']".to_string()]); + assert_eq!(config.search.languages, [Language("de"), Language("it")]); + // Value is copied after override is applied + assert_eq!( + config.updates.download_languages, + [Language("de"), Language("it")] + ); + } + + #[test] + fn order() { + style_config_with!( + config, + &[ + "display.compact = false".to_string(), + "display.compact = true".to_string(), + ] + ); + assert!(config.display.compact); + } + + #[test] + fn override_with_table() { + style_config_with!(config, &["display = {'compact' = true}".to_string()]); + assert!(config.display.compact); + assert_eq!( + config.display.indent, + RawConfig::default().display.indent.into() + ); + } + } + mod language { use super::*; diff --git a/src/main.rs b/src/main.rs index 6678ceb..f077c7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -202,12 +202,10 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { // Look up config file, if none is found fall back to default config. debug!("Loading config"); let config_loader = match &args.config_path { - Some(path) if !args.seed_config => { - ConfigLoader::read(path.clone()).context("Could not read config from given path")? - } - _ => { - ConfigLoader::read_default_path().context("Could not read config from default path")? - } + Some(path) if !args.seed_config => ConfigLoader::read(path.clone(), &args.override_config) + .context("Could not read config from given path")?, + _ => ConfigLoader::read_default_path(&args.override_config) + .context("Could not read config from default path")?, }; let mut config = config_loader.load()?;