Use structopt for command line parsing

master
Manos Pitsidianakis 2020-06-07 18:02:20 +03:00
parent a17f0b4fd4
commit 4bc8ff2ce9
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
5 changed files with 269 additions and 221 deletions

133
Cargo.lock generated
View File

@ -1,5 +1,14 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
dependencies = [
"winapi 0.3.8",
]
[[package]]
name = "arc-swap"
version = "0.4.7"
@ -27,6 +36,17 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi 0.3.8",
]
[[package]]
name = "autocfg"
version = "1.0.0"
@ -119,6 +139,21 @@ dependencies = [
"time",
]
[[package]]
name = "clap"
version = "2.33.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
@ -489,6 +524,15 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "heck"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "hermit-abi"
version = "0.1.13"
@ -784,6 +828,7 @@ dependencies = [
"signal-hook",
"signal-hook-registry",
"smallvec",
"structopt",
"termion",
"toml",
"unicode-segmentation",
@ -1170,6 +1215,32 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea"
[[package]]
name = "proc-macro-error"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn-mid",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.18"
@ -1542,6 +1613,36 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3"
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "structopt"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "863246aaf5ddd0d6928dfeb1a9ca65f505599e4e1b399935ef7e75107516b4ef"
dependencies = [
"clap",
"lazy_static",
"structopt-derive",
]
[[package]]
name = "structopt-derive"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d239ca4b13aee7a2142e6795cbd69e457665ff8037aed33b3effdc430d2f927a"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "1.0.30"
@ -1553,6 +1654,17 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "syn-mid"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tempfile"
version = "3.1.0"
@ -1586,6 +1698,15 @@ dependencies = [
"melib",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "thread_local"
version = "1.0.1"
@ -1701,6 +1822,12 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
[[package]]
name = "unicode-width"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
[[package]]
name = "unicode-xid"
version = "0.2.0"
@ -1734,6 +1861,12 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55d1e41d56121e07f1e223db0a4def204e45c85425f6a16d462fd07c8d10d74c"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.2"

View File

@ -49,6 +49,8 @@ rmp-serde = "^0.14.0"
smallvec = { version = "1.1.0", features = ["serde", ] }
bitflags = "1.0"
pcre2 = { version = "0.2.3", optional = true }
structopt = { version = "0.3.14" }
[profile.release]
lto = true

18
meli.1
View File

@ -27,28 +27,26 @@
.Nm
.Op Fl -help | h
.Op Fl -version | v
.Op Fl -create-config Op Ar path
.Op Fl -test-config Op Ar path
.Op Fl -config Ar path
.Op Fl -print-default-theme
.Op Fl -print-loaded-themes
.Bl -tag -width flag -offset indent
.It Fl -help | h
Show help message and exit.
.It Fl -version | v
Show version and exit.
.It Fl -create-config Op Ar path
.It Fl -config Ar path
Start meli with given configuration file.
.It Cm create-config Op Ar path
Create configuration file in
.Pa path
if given, or at
.Pa $XDG_CONFIG_HOME/meli/config.toml
.It Fl -test-config Op Ar path
.It Cm test-config Op Ar path
Test a configuration file for syntax issues or missing options.
.It Fl -config Ar path
Start meli with given configuration file.
.It Fl -print-default-theme
.It Cm man Op Ar page
Print documentation page and exit (Piping to a pager is recommended.)
.It Cm print-default-theme
Print default theme keys and values in TOML syntax, to be used as a blueprint.
.It Fl -print-loaded-themes
.It Cm print-loaded-themes
Print all loaded themes in TOML syntax.
.El
.Sh DESCRIPTION

View File

@ -29,7 +29,7 @@
use std::alloc::System;
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
extern crate notify_rust;
extern crate xdg_utils;
#[macro_use]
@ -86,8 +86,6 @@ pub mod plugins;
use nix;
use std::os::raw::c_int;
use xdg;
fn notify(
signals: &[c_int],
sender: crossbeam::channel::Sender<ThreadEvent>,
@ -143,29 +141,75 @@ fn notify(
Ok(r)
}
macro_rules! error_and_exit {
($($err:expr),*) => {{
return Err(MeliError::new(format!($($err),*)));
}}
fn parse_manpage(src: &str) -> Result<ManPages> {
match src {
"" | "meli" | "main" => Ok(ManPages::Main),
"meli.conf" | "conf" | "config" | "configuration" => Ok(ManPages::Conf),
"meli-themes" | "themes" | "theming" | "theme" => Ok(ManPages::Themes),
_ => Err(MeliError::new(format!(
"Invalid documentation page: {}",
src
))),
}
}
use structopt::StructOpt;
#[derive(Debug)]
/// Choose manpage
enum ManPages {
/// meli(1)
Main = 0,
/// meli.conf(5)
Conf = 1,
/// meli-themes(5)
Themes = 2,
}
struct CommandLineArguments {
print_manpage: Option<ManPages>,
create_config: Option<String>,
test_config: Option<String>,
config: Option<String>,
help: bool,
version: bool,
#[derive(Debug, StructOpt)]
#[structopt(name = "meli", about = "terminal mail client", version_short = "v")]
struct Opt {
/// use specified configuration file
#[structopt(short, long, parse(from_os_str))]
config: Option<PathBuf>,
#[structopt(subcommand)]
subcommand: Option<SubCommand>,
}
#[derive(Debug, StructOpt)]
enum SubCommand {
/// print default theme in full to stdout and exit.
PrintDefaultTheme,
/// print loaded themes in full to stdout and exit.
PrintLoadedThemes,
/// 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
#[structopt(display_order = 1)]
CreateConfig {
#[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str))]
path: Option<PathBuf>,
},
/// test a configuration file for syntax issues or missing options.
#[structopt(display_order = 2)]
TestConfig {
#[structopt(value_name = "CONFIG_PATH", parse(from_os_str))]
path: Option<PathBuf>,
},
#[structopt(visible_alias="docs", aliases=&["docs", "manpage", "manpages"])]
#[structopt(display_order = 3)]
/// print documentation page and exit (Piping to a pager is recommended.).
Man(ManOpt),
}
#[derive(Debug, StructOpt)]
struct ManOpt {
#[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes"], value_name="PAGE", parse(try_from_str = parse_manpage))]
page: ManPages,
}
fn main() {
::std::process::exit(match run_app() {
let opt = Opt::from_args();
::std::process::exit(match run_app(opt) {
Ok(()) => 0,
Err(err) => {
eprintln!("{}", err);
@ -174,184 +218,39 @@ fn main() {
});
}
fn run_app() -> Result<()> {
enum CommandLineFlags {
PrintManPage,
CreateConfig,
TestConfig,
Config,
fn run_app(opt: Opt) -> Result<()> {
if let Some(config_location) = opt.config.as_ref() {
std::env::set_var("MELI_CONFIG", config_location);
}
use CommandLineFlags::*;
let mut prev: Option<CommandLineFlags> = None;
let mut args = CommandLineArguments {
print_manpage: None,
create_config: None,
test_config: None,
config: None,
help: false,
version: false,
};
for i in std::env::args().skip(1) {
match i.as_str() {
"--test-config" => match prev {
None => prev = Some(TestConfig),
Some(CreateConfig) => error_and_exit!("invalid value for flag `--create-config`"),
Some(Config) => error_and_exit!("invalid value for flag `--config`"),
Some(TestConfig) => error_and_exit!("invalid value for flag `--test-config`"),
Some(PrintManPage) => {
error_and_exit!("invalid value for flag `--print-documentation`")
}
},
"--create-config" => match prev {
None => prev = Some(CreateConfig),
Some(CreateConfig) => error_and_exit!("invalid value for flag `--create-config`"),
Some(TestConfig) => error_and_exit!("invalid value for flag `--test-config`"),
Some(Config) => error_and_exit!("invalid value for flag `--config`"),
Some(PrintManPage) => {
error_and_exit!("invalid value for flag `--print-documentation`")
}
},
"--config" | "-c" => match prev {
None => prev = Some(Config),
Some(CreateConfig) if args.create_config.is_none() => {
args.config = Some(String::new());
prev = Some(Config);
}
Some(CreateConfig) => error_and_exit!("invalid value for flag `--create-config`"),
Some(Config) => error_and_exit!("invalid value for flag `--config`"),
Some(TestConfig) => error_and_exit!("invalid value for flag `--test-config`"),
Some(PrintManPage) => {
error_and_exit!("invalid value for flag `--print-documentation`")
}
},
"--help" | "-h" => {
args.help = true;
}
"--version" | "-v" => {
args.version = true;
}
"--print-loaded-themes" => {
let s = conf::FileSettings::new()?;
print!("{}", s.terminal.themes.to_string());
return Ok(());
}
"--print-default-theme" => {
print!("{}", conf::Themes::default().key_to_string("dark", false));
return Ok(());
}
"--print-documentation" => {
if args.print_manpage.is_some() {
error_and_exit!("Multiple invocations of --print-documentation");
}
prev = Some(PrintManPage);
args.print_manpage = Some(ManPages::Main);
}
e => match prev {
None => error_and_exit!("error: value without command {}", e),
Some(CreateConfig) if args.create_config.is_none() => {
args.create_config = Some(i);
prev = None;
}
Some(Config) if args.config.is_none() => {
args.config = Some(i);
prev = None;
}
Some(TestConfig) if args.test_config.is_none() => {
args.test_config = Some(i);
prev = None;
}
Some(PrintManPage) => {
match e {
"meli" | "main" => { /* This is the default */ }
"meli.conf" | "conf" | "config" | "configuration" => {
args.print_manpage = Some(ManPages::Conf);
}
"meli-themes" | "themes" | "theming" | "theme" => {
args.print_manpage = Some(ManPages::Themes);
}
_ => error_and_exit!("Invalid documentation page: {}", e),
}
}
Some(TestConfig) => error_and_exit!("Duplicate value for flag `--test-config`"),
Some(CreateConfig) => error_and_exit!("Duplicate value for flag `--create-config`"),
Some(Config) => error_and_exit!("Duplicate value for flag `--config`"),
},
match opt.subcommand {
Some(SubCommand::TestConfig { path }) => {
let config_path = if let Some(path) = path {
path
} else {
crate::conf::get_config_file()?
};
conf::FileSettings::validate(config_path)?;
return Ok(());
}
Some(SubCommand::CreateConfig { path }) => {
let config_path = if let Some(path) = path {
path
} else {
crate::conf::get_config_file()?
};
if config_path.exists() {
return Err(MeliError::new(format!(
"File `{}` already exists.\nMaybe you meant to specify another path?",
config_path.display()
)));
}
conf::create_config_file(&config_path)?;
return Ok(());
}
}
if args.help {
println!("usage:\tmeli [--config PATH|-c PATH]");
println!("\tmeli --help");
println!("\tmeli --version");
println!("");
println!("Other options:");
println!("\t--help, -h\t\tshow this message and exit");
println!("\t--version, -v\t\tprint version and exit");
println!("\t--create-config[ PATH]\tcreate 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");
println!(
"\t--test-config PATH\ttest a configuration file for syntax issues or missing options."
);
println!("\t--config PATH, -c PATH\tuse specified configuration file");
println!("\t--print-loaded-themes\tprint loaded themes in full to stdout and exit.");
println!("\t--print-default-theme\tprint default theme in full to stdout and exit.");
#[cfg(feature = "cli-docs")]
{
println!("\t--print-documentation [meli conf themes]\n\t\t\t\tprint documentation page and exit (Piping to a pager is recommended.).");
}
return Ok(());
}
if args.version {
println!("meli {}", option_env!("CARGO_PKG_VERSION").unwrap_or("0.0"));
return Ok(());
}
match prev {
None => {}
Some(CreateConfig) if args.create_config.is_none() => args.create_config = Some("".into()),
Some(CreateConfig) => error_and_exit!("Duplicate value for flag `--create-config`"),
Some(Config) => error_and_exit!("error: flag without value: `--config`"),
Some(TestConfig) => error_and_exit!("error: flag without value: `--test-config`"),
Some(PrintManPage) => {}
};
if (args.print_manpage.is_some()
^ args.test_config.is_some()
^ args.create_config.is_some()
^ args.config.is_some())
&& !(args.print_manpage.is_some()
|| args.test_config.is_some()
|| args.create_config.is_some()
|| args.config.is_some())
{
error_and_exit!("error: illegal command-line flag combination");
}
if let Some(config_path) = args.test_config.as_ref() {
conf::FileSettings::validate(config_path)?;
return Ok(());
} else if let Some(config_path) = args.create_config.as_mut() {
let config_path: PathBuf = if config_path.is_empty() {
let xdg_dirs = xdg::BaseDirectories::with_prefix("meli").unwrap();
xdg_dirs.place_config_file("config.toml").map_err(|e| {
MeliError::new(format!(
"Cannot create configuration directory in {}:\n{}",
xdg_dirs.get_config_home().display(),
e
))
})?
} else {
Path::new(config_path).to_path_buf()
};
if config_path.exists() {
return Err(MeliError::new(format!("File `{}` already exists.\nMaybe you meant to specify another path with --create-config=PATH", config_path.display())));
}
conf::create_config_file(&config_path)?;
return Ok(());
} else if let Some(_page) = args.print_manpage {
#[cfg(feature = "cli-docs")]
{
Some(SubCommand::Man(manopt)) => {
let _page = manopt.page;
const MANPAGES: [&'static str; 3] = [
include_str!(concat!(env!("OUT_DIR"), "/meli.txt")),
include_str!(concat!(env!("OUT_DIR"), "/meli.conf.txt")),
@ -361,13 +260,19 @@ fn run_app() -> Result<()> {
return Ok(());
}
#[cfg(not(feature = "cli-docs"))]
{
error_and_exit!("error: this version of meli was not build with embedded documentation. You might have it installed as manpages (eg `man meli`), otherwise check https://meli.delivery");
Some(SubCommand::Man(_manopt)) => {
return Err(MeliError::new("error: this version of meli was not build with embedded documentation. You might have it installed as manpages (eg `man meli`), otherwise check https://meli.delivery"));
}
}
if let Some(config_location) = args.config.as_ref() {
std::env::set_var("MELI_CONFIG", config_location);
Some(SubCommand::PrintLoadedThemes) => {
let s = conf::FileSettings::new()?;
print!("{}", s.terminal.themes.to_string());
return Ok(());
}
Some(SubCommand::PrintDefaultTheme) => {
print!("{}", conf::Themes::default().key_to_string("dark", false));
return Ok(());
}
None => {}
}
/* Create a channel to communicate with other threads. The main process is the sole receiver.

View File

@ -313,17 +313,30 @@ pub struct Settings {
pub log: LogSettings,
}
pub fn get_config_file() -> Result<PathBuf> {
let xdg_dirs = xdg::BaseDirectories::with_prefix("meli").map_err(|err| {
MeliError::new(format!(
"Could not detect XDG directories for user: {}",
err
))
.set_source(Some(std::sync::Arc::new(Box::new(err))))
})?;
match env::var("MELI_CONFIG") {
Ok(path) => Ok(PathBuf::from(path)),
Err(_) => Ok(xdg_dirs
.place_config_file("config.toml")
.chain_err_summary(|| {
format!(
"Cannot create configuration directory in {}",
xdg_dirs.get_config_home().display()
)
})?),
}
}
impl FileSettings {
pub fn new() -> Result<FileSettings> {
let xdg_dirs = xdg::BaseDirectories::with_prefix("meli");
let config_path = match env::var("MELI_CONFIG") {
Ok(path) => PathBuf::from(path),
Err(_) => xdg_dirs
.as_ref()
.unwrap()
.place_config_file("config.toml")
.expect("cannot create configuration directory"),
};
let config_path = get_config_file()?;
if !config_path.exists() {
println!(
"No configuration found. Would you like to generate one in {}? [Y/n]",
@ -359,18 +372,15 @@ impl FileSettings {
}
}
let path = config_path
.to_str()
.expect("Configuration file path was not valid UTF-8");
FileSettings::validate(path)
FileSettings::validate(config_path)
}
pub fn validate(path: &str) -> Result<Self> {
let s = pp::pp(path)?;
pub fn validate(path: PathBuf) -> Result<Self> {
let s = pp::pp(&path)?;
let mut s: FileSettings = toml::from_str(&s).map_err(|e| {
MeliError::new(format!(
"{}:\nConfig file contains errors: {}",
path,
path.display(),
e.to_string()
))
})?;