diff --git a/meli.conf.5 b/meli.conf.5 index 93d5f3da..7c7b83e5 100644 --- a/meli.conf.5 +++ b/meli.conf.5 @@ -40,6 +40,15 @@ Newline means LF (0x0A) or CRLF (0x0D 0x0A). .El .Pp Refer to TOML documentation for valid TOML syntax. + +Thought not valid TOML syntax, +.Nm +can have nested configuration files by using the following include directive, which though starting with +.Em \&# +is not a comment: +.Bd -literal +#include "/path/to/file" +.Ed .Sh SECTIONS The top level sections of the config are accounts, shortcuts, notifications, pager, composing, pgp, terminal. .Pp diff --git a/ui/src/conf.rs b/ui/src/conf.rs index 2aec8de5..61fa24fb 100644 --- a/ui/src/conf.rs +++ b/ui/src/conf.rs @@ -49,8 +49,8 @@ use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::collections::HashMap; use std::env; -use std::fs::{File, OpenOptions}; -use std::io::{self, BufRead, Read, Write}; +use std::fs::OpenOptions; +use std::io::{self, BufRead, Write}; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; @@ -310,25 +310,15 @@ impl FileSettings { } } - let mut file = File::open(config_path.to_str().unwrap())?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - let s = toml::from_str(&contents); - if let Err(e) = s { - return Err(MeliError::new(format!( - "Config file contains errors: {}", - e.to_string() - ))); - } - - Ok(s.unwrap()) + FileSettings::validate(config_path.to_str().unwrap())?; + let s = pp::pp(config_path.to_str().unwrap()).unwrap(); + let s: FileSettings = toml::from_str(&s).unwrap(); + Ok(s) } pub fn validate(path: &str) -> Result<()> { - let mut file = File::open(path)?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - let s: FileSettings = toml::from_str(&contents).map_err(|e| { + let s = pp::pp(path)?; + let s: FileSettings = toml::from_str(&s).map_err(|e| { MeliError::new(format!("Config file contains errors: {}", e.to_string())) })?; let backends = melib::backends::Backends::new(); @@ -598,3 +588,104 @@ pub fn create_config_file(p: &Path) -> Result<()> { file.set_permissions(permissions)?; Ok(()) } + +mod pp { + use melib::{ + error::{MeliError, Result}, + parsec::*, + }; + use std::borrow::Cow; + use std::io::Read; + + fn include_directive<'a>() -> impl Parser<'a, Option<&'a str>> { + move |input: &'a str| { + enum State { + Start, + Directive, + Path, + } + use State::*; + let mut state = State::Start; + + let mut i = 0; + while i < input.len() { + match (&state, input.as_bytes()[i]) { + (Start, b'#') => { + state = Directive; + } + (Start, b) if (b as char).is_whitespace() => { /* consume */ } + (Start, _) => { + return Ok(("", None)); + } + (Directive, b) if (b as char).is_whitespace() => { /* consume */ } + (Directive, _) if input.as_bytes()[i..].starts_with(b"include") => { + i += "include".len(); + state = Path; + continue; + } + (Directive, _) => { + return Ok(("", None)); + } + (Path, b) if (b as char).is_whitespace() => { /* consume */ } + (Path, b'"') | (Path, b'\'') => { + let mut end = i + 1; + while end < input.len() && input.as_bytes()[end] != input.as_bytes()[i] { + end += 1; + } + if end == input.len() { + return Err(input); + } + let ret = &input[i + 1..end]; + end += 1; + while end < input.len() { + if !(input.as_bytes()[end] as char).is_whitespace() { + /* Nothing else allowed in line */ + return Err(input); + } + end += 1; + } + return Ok(("", Some(ret))); + } + (Path, _) => return Err(input), + } + i += 1; + } + return Ok(("", None)); + } + } + + fn pp_helper(path: &str, level: u8) -> Result> { + if level > 7 { + return Err(MeliError::new(format!("Maximum recursion limit reached while unfolding include directives in {}. Have you included a config file within itself?", path))); + } + let mut contents = String::new(); + let mut file = std::fs::File::open(path)?; + file.read_to_string(&mut contents)?; + + let mut includes = Vec::new(); + for (i, l) in contents.lines().enumerate() { + if let (_, Some(path)) = include_directive().parse(l).map_err(|l| { + MeliError::new(format!( + "Malformed include directive in line {} of file {}: {}", + i, path, l + )) + })? { + includes.push(path); + } + } + + if includes.is_empty() { + Ok(Cow::from(contents)) + } else { + let mut ret = String::with_capacity(contents.len()); + for path in includes { + ret.extend(pp_helper(path, level + 1)?.chars()); + } + ret.extend(contents.chars()); + Ok(Cow::from(ret)) + } + } + pub fn pp(path: &str) -> Result> { + pp_helper(path, 0) + } +}