meli/subcommands: accept - for stdio in {create,test}_config

Accept "-" operand in create_config and test_config subcommands as
stdout and stdin respectively.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
pull/438/head
Manos Pitsidianakis 2 months ago
parent 814af0e94d
commit 475860c946
No known key found for this signature in database
GPG Key ID: 7729C7707F7E09D0

@ -69,9 +69,25 @@ Start meli with given configuration file.
Create configuration file in
.Pa path
if given, or at
.Pa $XDG_CONFIG_HOME/meli/config.toml
.Pa $XDG_CONFIG_HOME/meli/config.toml Ns
\&.
If
.Ar path
is
.Ar \-
the result is printed to the standard output stream.
.It Cm test-config Op Ar path
Test a configuration file for syntax issues or missing options.
Test a configuration for syntax issues or missing options.
The configuration is read from
.Pa path
if given, or from
.Pa $XDG_CONFIG_HOME/meli/config.toml Ns
\&.
If
.Ar path
is
.Ar \-
the configuration is read from the standard input stream.
.It Cm man Op Ar page
Print documentation page and exit (Piping to a pager is recommended).
.It Cm install-man Op Ar path

@ -21,10 +21,29 @@
//! Command line arguments.
use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
use super::*;
#[cfg(feature = "cli-docs")]
use crate::manpages;
fn try_path_or_stdio(input: &OsStr) -> PathOrStdio {
if input.as_bytes() == b"-" {
PathOrStdio::Stdio
} else {
PathOrStdio::Path(PathBuf::from(input))
}
}
/// `Pathbuf` or standard stream (`-` operand).
#[derive(Debug)]
pub enum PathOrStdio {
/// Path
Path(PathBuf),
/// standard stream (`-` operand)
Stdio,
}
#[derive(Debug, StructOpt)]
#[structopt(name = "meli", about = "terminal mail client", version_short = "v")]
pub struct Opt {
@ -51,17 +70,21 @@ pub enum SubCommand {
EditConfig,
/// create a sample configuration file with available configuration options.
/// If `PATH` is not specified, meli will try to create it in
/// `$XDG_CONFIG_HOME/meli/config.toml`
/// `$XDG_CONFIG_HOME/meli/config.toml`. Path `-` will output to standard
/// output instead.
#[structopt(display_order = 1)]
CreateConfig {
#[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str))]
path: Option<PathBuf>,
#[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str = try_path_or_stdio))]
path: Option<PathOrStdio>,
},
/// test a configuration file for syntax issues or missing options.
/// If `PATH` is not specified, meli will try to read it from
/// `$XDG_CONFIG_HOME/meli/config.toml`. Path `-` will read input from
/// standard input instead.
#[structopt(display_order = 2)]
TestConfig {
#[structopt(value_name = "CONFIG_PATH", parse(from_os_str))]
path: Option<PathBuf>,
#[structopt(value_name = "CONFIG_PATH", parse(from_os_str = try_path_or_stdio))]
path: Option<PathOrStdio>,
},
#[structopt(display_order = 3)]
/// Testing tools such as IMAP, SMTP shells for debugging.

@ -428,6 +428,8 @@ define(`include', `builtin_include(substr($1,1,decr(decr(len($1)))))dnl')dnl
}
impl FileSettings {
pub const EXAMPLE_CONFIG: &'static str = include_str!("../docs/samples/sample-config.toml");
pub fn new() -> Result<Self> {
let config_path = get_config_file()?;
if !config_path.exists() {
@ -462,6 +464,131 @@ impl FileSettings {
Self::validate(config_path, true, false)
}
/// Validate configuration from `input` string.
pub fn validate_string(s: String, interactive: bool, clear_extras: bool) -> Result<Self> {
let map: toml::value::Table = toml::from_str(&s).map_err(|err| {
Error::new("Config file is invalid TOML")
.set_source(Some(Arc::new(err)))
.set_kind(ErrorKind::ValueError)
})?;
// Check that a global composing option is set and return a user-friendly
// error message because the default serde one is confusing.
if !map.contains_key("composing") {
let err_msg = r#"You must set a global `composing` option. If you override `composing` in each account, you can use a dummy global like follows:
[composing]
send_mail = 'false'
This is required so that you don't accidentally start meli and find out later that you can't send emails."#;
if interactive {
println!("{}", err_msg);
let ask = Ask {
message: "Would you like to append this dummy value in your configuration \
input and continue?"
.to_string(),
};
if ask.run() {
let mut s = s;
s.push_str("[composing]\nsend_mail = 'false'\n");
return Self::validate_string(s, interactive, clear_extras);
}
}
return Err(Error::new(err_msg).set_kind(ErrorKind::Configuration));
}
let mut s: Self = toml::from_str(&s).map_err(|err| {
Error::new("Input contains errors")
.set_source(Some(Arc::new(err)))
.set_kind(ErrorKind::Configuration)
})?;
let backends = melib::backends::Backends::new();
let Themes {
light: default_light,
dark: default_dark,
..
} = Themes::default();
for (k, v) in default_light.keys.into_iter() {
if !s.terminal.themes.light.contains_key(&k) {
s.terminal.themes.light.insert(k, v);
}
}
for theme in s.terminal.themes.other_themes.values_mut() {
for (k, v) in default_dark.keys.clone().into_iter() {
if !theme.contains_key(&k) {
theme.insert(k, v);
}
}
}
for (k, v) in default_dark.keys.into_iter() {
if !s.terminal.themes.dark.contains_key(&k) {
s.terminal.themes.dark.insert(k, v);
}
}
match s.terminal.theme.as_str() {
themes::DARK | themes::LIGHT => {}
t if s.terminal.themes.other_themes.contains_key(t) => {}
t => {
return Err(Error::new(format!("Theme `{}` was not found.", t))
.set_kind(ErrorKind::Configuration));
}
}
s.terminal.themes.validate()?;
for (name, acc) in s.accounts.iter_mut() {
let FileAccount {
root_mailbox,
format,
identity,
extra_identities,
read_only,
display_name,
order,
subscribed_mailboxes,
mailboxes,
extra,
manual_refresh,
default_mailbox: _,
refresh_command: _,
search_backend: _,
conf_override: _,
} = acc.clone();
let lowercase_format = format.to_lowercase();
let mut s = AccountSettings {
name: name.to_string(),
root_mailbox,
format: format.clone(),
identity,
extra_identities,
read_only,
display_name,
order,
subscribed_mailboxes,
manual_refresh,
mailboxes: mailboxes
.into_iter()
.map(|(k, v)| (k, v.mailbox_conf))
.collect(),
extra: extra.into_iter().collect(),
};
s.validate_config()?;
backends.validate_config(&lowercase_format, &mut s)?;
if !s.extra.is_empty() {
return Err(Error::new(format!(
"Unrecognised configuration values: {:?}",
s.extra
))
.set_kind(ErrorKind::Configuration));
}
if clear_extras {
acc.extra.clear();
}
}
Ok(s)
}
/// Validate `path` and print errors.
pub fn validate(path: PathBuf, interactive: bool, clear_extras: bool) -> Result<Self> {
let s = pp::pp(&path)?;
let map: toml::value::Table = toml::from_str(&s).map_err(|err| {
@ -880,7 +1007,7 @@ pub fn create_config_file(p: &Path) -> Result<()> {
.create_new(true)
.open(p)
.chain_err_summary(|| format!("Cannot create configuration file in {}", p.display()))?;
file.write_all(include_bytes!("../docs/samples/sample-config.toml"))
file.write_all(FileSettings::EXAMPLE_CONFIG.as_bytes())
.and_then(|()| file.flush())
.chain_err_summary(|| format!("Could not write to configuration file {}", p.display()))?;
println!("Written example configuration to {}", p.display());
@ -1407,8 +1534,6 @@ server_password_command = "false"
send_mail = 'false'
"#;
const EXAMPLE_CONFIG: &str = include_str!("../docs/samples/sample-config.toml");
#[test]
fn test_config_parse() {
let tempdir = tempfile::tempdir().unwrap();
@ -1446,7 +1571,7 @@ send_mail = 'false'
/* Test sample config */
let example_config = EXAMPLE_CONFIG.replace("\n#", "\n");
let example_config = FileSettings::EXAMPLE_CONFIG.replace("\n#", "\n");
let re = regex::Regex::new(r#"root_mailbox\s*=\s*"[^"]*""#).unwrap();
let example_config = re.replace_all(
&example_config,

@ -29,13 +29,16 @@ use std::{
use crossbeam::channel::{Receiver, Sender};
use melib::{Result, ShellExpandTrait};
use crate::*;
use crate::{args::PathOrStdio, *};
pub fn create_config(path: Option<PathBuf>) -> Result<()> {
let config_path = if let Some(path) = path {
path.expand()
} else {
conf::get_config_file()?
pub fn create_config(path: Option<PathOrStdio>) -> Result<()> {
let config_path = match path {
Some(PathOrStdio::Stdio) => {
std::io::stdout().write_all(conf::FileSettings::EXAMPLE_CONFIG.as_bytes())?;
return Ok(());
}
Some(PathOrStdio::Path(path)) => path.expand(),
None => conf::get_config_file()?,
};
if config_path.exists() {
return Err(Error::new(format!(
@ -53,12 +56,12 @@ pub fn edit_config() -> Result<()> {
.map_err(|err| {
format!("Could not find any value in environment variables EDITOR and VISUAL. {err}")
})?;
let config_path = crate::conf::get_config_file()?;
let config_path = conf::get_config_file()?;
let mut cmd = Command::new(editor);
let mut handle = &mut cmd;
for c in crate::conf::get_included_configs(config_path)? {
for c in conf::get_included_configs(config_path)? {
handle = handle.arg(&c);
}
let mut handle = handle
@ -76,17 +79,8 @@ pub fn man(page: manpages::ManPages, source: bool) -> Result<String> {
}
#[cfg(feature = "cli-docs")]
pub fn pager(v: String, no_raw: Option<Option<bool>>) -> Result<()> {
if let Some(no_raw) = no_raw {
match no_raw {
Some(true) => {}
None if (unsafe { libc::isatty(libc::STDOUT_FILENO) == 1 }) => {}
Some(false) | None => {
println!("{v}");
return Ok(());
}
}
} else if unsafe { libc::isatty(libc::STDOUT_FILENO) != 1 } {
pub fn pager(v: String, no_raw: bool) -> Result<()> {
if no_raw || !terminal::is_tty() {
println!("{v}");
return Ok(());
}
@ -105,7 +99,7 @@ pub fn pager(v: String, no_raw: Option<Option<bool>>) -> Result<()> {
}
#[cfg(not(feature = "cli-docs"))]
pub fn man(_: crate::args::ManOpt) -> Result<()> {
pub fn man(_: args::ManOpt) -> Result<()> {
Err(Error::new("error: this version of meli was not build with embedded documentation (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli-email.org"))
}
@ -129,11 +123,19 @@ pub fn compiled_with() -> Result<()> {
Ok(())
}
pub fn test_config(path: Option<PathBuf>) -> Result<()> {
let config_path = if let Some(path) = path {
path.expand()
} else {
crate::conf::get_config_file()?
pub fn test_config(path: Option<PathOrStdio>) -> Result<()> {
let config_path = match path {
Some(PathOrStdio::Stdio) => {
let mut input = String::new();
std::io::stdin().read_to_string(&mut input)?;
if input.trim().is_empty() {
return Err(Error::new("Input was empty.").set_kind(ErrorKind::ValueError));
}
conf::FileSettings::validate_string(input, true, false)?;
return Ok(());
}
Some(PathOrStdio::Path(path)) => path.expand(),
None => conf::get_config_file()?,
};
conf::FileSettings::validate(config_path, true, false)?;
Ok(())
@ -173,7 +175,7 @@ pub fn view(
Ok(state)
}
pub fn tool(path: Option<PathBuf>, opt: crate::args::ToolOpt) -> Result<()> {
pub fn tool(path: Option<PathBuf>, opt: args::ToolOpt) -> Result<()> {
use melib::utils::futures::timeout;
use crate::{args::ToolOpt, conf::composing::SendMail};
@ -181,7 +183,7 @@ pub fn tool(path: Option<PathBuf>, opt: crate::args::ToolOpt) -> Result<()> {
let config_path = if let Some(path) = path {
path.expand()
} else {
crate::conf::get_config_file()?
conf::get_config_file()?
};
let conf = conf::FileSettings::validate(config_path, true, false)?;
let account = match opt {

@ -260,3 +260,8 @@ impl TextPresentation for str {
Cow::from(self)
}
}
/// Returns `true` if standard output corresponds to an interactive TTY session.
pub fn is_tty() -> bool {
unsafe { libc::isatty(libc::STDOUT_FILENO) == 1 }
}

@ -114,6 +114,38 @@ fn test_cli_subcommands() {
}
}
fn test_subcommand_config_stdio() {
{
let mut cmd = Command::cargo_bin("meli").unwrap();
let output = cmd.arg("create-config").arg("-").output().unwrap().assert();
output.code(0).stdout(predicates::str::is_empty().not());
}
{
let mut cmd = Command::cargo_bin("meli").unwrap();
let output = cmd
.arg("test-config")
.arg("-")
.write_stdin(
br#"
[accounts.imap]
root_mailbox = "INBOX"
format = "imap"
identity="username@example.com"
server_username = "null"
server_hostname = "example.com"
server_password_command = "false"
[composing]
send_mail = 'false'
"#,
)
.output()
.unwrap()
.assert();
output.code(0).stdout(predicates::str::is_empty());
}
}
fn test_subcommand_man() {
for (man, title) in [
("meli.1", "MELI(1)"),
@ -176,6 +208,7 @@ fn test_cli_subcommands() {
test_subcommand_succeeds("compiled-with");
test_subcommand_succeeds("man");
test_subcommand_man();
test_subcommand_config_stdio();
let tmp_dir = TempDir::new().unwrap();

Loading…
Cancel
Save