Clap can do most of the argument parsing for us, which allows dropping
lots of parsing code and simplifying the argument-parsing logic.
I tried to keep the same general structure in order to keep this diff
easy to read. In reality, the CompileCommand and InterpreterCommand
should just take an Option<PathBuf> as parameter, and actually reading
the file should be done as part of the command execution -- but that
change would have been quite invasive in an already invasive patch, so
I've left that out of scope for now.
---
v2: Set the command name to `mml`, like in f71d1534
Cargo.lock | 14 ++++
Cargo.toml | 1 +
src/compl/args.rs | 37 ++-------
src/main.rs | 63 +++++++--------
src/man/args.rs | 49 +++---------
src/man/handlers.rs | 6 +-
src/mml/args.rs | 183 ++++++++++++++++----------------------------
7 files changed, 127 insertions(+), 226 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 7c3befe..1d64779 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -359,6 +359,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03aef18ddf7d879c15ce20f04826ef8418101c7e528014c3eeea13321047dca3"
dependencies = [
"clap_builder",
+ "clap_derive",
+ "once_cell",
]
[[package]]
@@ -382,6 +384,18 @@ dependencies = [
"clap",
]
+[[package]]
+name = "clap_derive"
+version = "4.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.13",
+]
+
[[package]]
name = "clap_lex"
version = "0.5.0"
diff --git a/Cargo.toml b/Cargo.toml
index dda769c..3e647fe 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -55,6 +55,7 @@ version = "0.1.0"
# cli
[dependencies.clap]
version = "4.3"
+features = ["derive"]
[dependencies.clap_complete]
version = "4.3"
diff --git a/src/compl/args.rs b/src/compl/args.rs
index 137d31f..4af61e9 100644
--- a/src/compl/args.rs
+++ b/src/compl/args.rs
@@ -2,38 +2,11 @@
//!
//! This module provides subcommands and a command matcher related to completion.
-use anyhow::Result;
-use clap::{value_parser, Arg, ArgMatches, Command};
+use clap::Parser;
use clap_complete::Shell;
-use log::debug;
-const ARG_SHELL: &str = "shell";
-const CMD_COMPLETION: &str = "completion";
-
-type SomeShell = Shell;
-
-/// Completion commands.
-pub enum Cmd {
- /// Generate completion script for the given shell.
- Generate(SomeShell),
-}
-
-/// Completion command matcher.
-pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
- if let Some(m) = m.subcommand_matches(CMD_COMPLETION) {
- let shell = m.get_one::<Shell>(ARG_SHELL).cloned().unwrap();
- debug!("shell: {:?}", shell);
- return Ok(Some(Cmd::Generate(shell)));
- };
-
- Ok(None)
-}
-
-/// Completion subcommands.
-pub fn subcmd() -> Command {
- Command::new(CMD_COMPLETION)
- .about("Generates the completion script for the given shell")
- .args(&[Arg::new(ARG_SHELL)
- .value_parser(value_parser!(Shell))
- .required(true)])
+/// Generates the completion script for the given shell.
+#[derive(Parser, Debug)]
+pub struct GenerateCompletionCommand {
+ pub shell: Shell,
}
diff --git a/src/main.rs b/src/main.rs
index 026a84e..4979c7d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,21 +3,30 @@ mod man;
mod mml;
use anyhow::Result;
-use clap::Command;
+use clap::{CommandFactory, Parser, Subcommand};
use env_logger::{Builder as LoggerBuilder, Env, DEFAULT_FILTER_ENV};
-use std::env;
-fn create_app() -> Command {
- Command::new("mml")
- .version(env!("CARGO_PKG_VERSION"))
- .about(env!("CARGO_PKG_DESCRIPTION"))
- .author(env!("CARGO_PKG_AUTHORS"))
- .propagate_version(true)
- .infer_subcommands(true)
- .arg_required_else_help(true)
- .subcommand(compl::args::subcmd())
- .subcommand(man::args::subcmd())
- .subcommands(mml::args::subcmds())
+#[cfg(feature = "compiler")]
+use crate::mml::args::CompileCommand;
+#[cfg(feature = "interpreter")]
+use crate::mml::args::InterpreterCommand;
+use crate::{compl::args::GenerateCompletionCommand, man::args::GenerateManCommand};
+
+#[derive(Parser, Debug)]
+#[command(name= "mml", author, version, about, long_about = None, propagate_version = true)]
+struct Cli {
+ #[command(subcommand)]
+ command: Commands,
+}
+
+#[derive(Subcommand, Debug)]
+enum Commands {
+ Completion(GenerateCompletionCommand),
+ Man(GenerateManCommand),
+ #[cfg(feature = "compiler")]
+ Compile(CompileCommand),
+ #[cfg(feature = "interpreter")]
+ Interpret(InterpreterCommand),
}
#[tokio::main]
@@ -27,29 +36,13 @@ async fn main() -> Result<()> {
.format_timestamp(None)
.init();
- let app = create_app();
- let m = app.get_matches();
-
- if let Some(compl::args::Cmd::Generate(shell)) = compl::args::matches(&m)? {
- return compl::handlers::generate(create_app(), shell);
- }
-
- if let Some(man::args::Cmd::GenerateAll(dir)) = man::args::matches(&m)? {
- return man::handlers::generate(dir, create_app());
- }
-
- // finally check mml commands
- match mml::args::matches(&m).await? {
+ let cli = Cli::parse();
+ match cli.command {
+ Commands::Completion(cmd) => compl::handlers::generate(Cli::command(), cmd.shell),
+ Commands::Man(cmd) => man::handlers::generate(cmd.dir, Cli::command()),
#[cfg(feature = "compiler")]
- Some(mml::args::Cmd::Compile(mml)) => {
- return mml::handlers::compile(mml).await;
- }
+ Commands::Compile(cmd) => mml::handlers::compile(cmd.mml()).await,
#[cfg(feature = "interpreter")]
- Some(mml::args::Cmd::Interpret(mime)) => {
- return mml::handlers::interpret(mime).await;
- }
- _ => (),
+ Commands::Interpret(cmd) => mml::handlers::interpret(cmd.mime()).await,
}
-
- Ok(())
}
diff --git a/src/man/args.rs b/src/man/args.rs
index a85f953..43c5adb 100644
--- a/src/man/args.rs
+++ b/src/man/args.rs
@@ -3,41 +3,16 @@
//! This module provides subcommands and a command matcher related to
//! man.
-use anyhow::Result;
-use clap::{Arg, ArgMatches, Command};
-use log::debug;
-
-const ARG_DIR: &str = "dir";
-const CMD_MAN: &str = "man";
-
-/// Man commands.
-pub enum Cmd<'a> {
- /// Generates all man pages to the specified directory.
- GenerateAll(&'a str),
-}
-
-/// Man command matcher.
-pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
- if let Some(m) = m.subcommand_matches(CMD_MAN) {
- let dir = m.get_one::<String>(ARG_DIR).map(String::as_str).unwrap();
- debug!("directory: {}", dir);
- return Ok(Some(Cmd::GenerateAll(dir)));
- };
-
- Ok(None)
-}
-
-/// Man subcommands.
-pub fn subcmd() -> Command {
- Command::new(CMD_MAN)
- .about("Generate all man pages to the given directory")
- .arg(
- Arg::new(ARG_DIR)
- .help("Directory to generate man files in")
- .long_help(
- "Represents the directory where all man files of
-all commands and subcommands should be generated in.",
- )
- .required(true),
- )
+use std::path::PathBuf;
+
+use clap::Parser;
+
+/// Generate all man pages to the given directory
+#[derive(Parser, Debug)]
+pub struct GenerateManCommand {
+ /// Directory in which to generate man files.
+ ///
+ /// Represents the directory in which all man files of all commands and subcommands should be
+ /// generated.
+ pub dir: PathBuf,
}
diff --git a/src/man/handlers.rs b/src/man/handlers.rs
index bb0e7b6..426a5e3 100644
--- a/src/man/handlers.rs
+++ b/src/man/handlers.rs
@@ -8,19 +8,19 @@ use clap_mangen::Man;
use std::{fs, path::PathBuf};
/// Generates all man pages of all subcommands in the given directory.
-pub fn generate(dir: &str, cmd: Command) -> Result<()> {
+pub fn generate(dir: PathBuf, cmd: Command) -> Result<()> {
let mut buffer = Vec::new();
let cmd_name = cmd.get_name().to_string();
let subcmds = cmd.get_subcommands().cloned().collect::<Vec<_>>();
Man::new(cmd).render(&mut buffer)?;
- fs::write(PathBuf::from(dir).join(format!("{}.1", cmd_name)), buffer)?;
+ fs::write(&dir.join(format!("{}.1", cmd_name)), buffer)?;
for subcmd in subcmds {
let mut buffer = Vec::new();
let subcmd_name = subcmd.get_name().to_string();
Man::new(subcmd).render(&mut buffer)?;
fs::write(
- PathBuf::from(dir).join(format!("{}-{}.1", cmd_name, subcmd_name)),
+ &dir.join(format!("{}-{}.1", cmd_name, subcmd_name)),
buffer,
)?;
}
diff --git a/src/mml/args.rs b/src/mml/args.rs
index c5720d7..f56d22b 100644
--- a/src/mml/args.rs
+++ b/src/mml/args.rs
@@ -4,138 +4,83 @@
)]
use anyhow::Result;
-use atty::Stream;
-use clap::{Arg, ArgMatches, Command};
-use log::{debug, warn};
+use clap::Parser;
+use log::warn;
use shellexpand_utils::try_shellexpand_path;
-use std::{ffi::OsStr, path::PathBuf};
-use tokio::{
+use std::{
fs,
- io::{self, AsyncBufReadExt, BufReader},
+ io::{self, BufRead, BufReader},
+ path::PathBuf,
};
-const ARG_RAW: &str = "raw";
-const ARG_PATH_OR_RAW: &str = "path-or-raw";
-const CMD_COMPILE: &str = "compile";
-const CMD_INTERPRET: &str = "interpret";
-
-type MmlMessage = String;
-type MimeMessage = String;
-
-/// Represents the server commands.
-#[derive(Debug, PartialEq, Eq)]
-pub enum Cmd {
- #[cfg(feature = "compiler")]
- Compile(MmlMessage),
- #[cfg(feature = "interpreter")]
- Interpret(MimeMessage),
+pub type MmlMessage = String;
+pub type MimeMessage = String;
+
+/// Compile the given MML message to a valid MIME message
+#[cfg(feature = "compiler")]
+#[derive(Parser, Debug)]
+pub struct CompileCommand {
+ /// Read the mssage from the given file path.
+ #[arg(value_parser = parse_mml)]
+ mml: Option<MmlMessage>,
}
-/// Represents the server command matcher.
-pub async fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
- #[cfg(feature = "compiler")]
- if let Some(ref m) = m.subcommand_matches(CMD_COMPILE) {
- debug!("compile MML message command matched");
-
- let mml = match parse_path_or_raw_arg(m) {
- Some(mml) => match try_shellexpand_path(mml) {
- Ok(path) => fs::read_to_string(PathBuf::from(path)).await?,
- Err(err) => {
- warn!("{err}");
- warn!("invalid path, processing it as raw MML message");
- format_str(mml)
- }
- },
- None if atty::is(Stream::Stdin) => format_str(&parse_raw_arg(m)),
- None => format_stdin().await,
- };
-
- return Ok(Some(Cmd::Compile(mml)));
- }
-
- #[cfg(feature = "interpreter")]
- if let Some(ref m) = m.subcommand_matches(CMD_INTERPRET) {
- debug!("interpret MIME message command matched");
-
- let mime = match parse_path_or_raw_arg(m) {
- Some(mime) => match try_shellexpand_path(mime) {
- Ok(path) => fs::read_to_string(PathBuf::from(path)).await?,
- Err(err) => {
- warn!("{err}");
- warn!("invalid path, processing it as raw MIME message");
- format_str(mime)
- }
- },
- None if atty::is(Stream::Stdin) => format_str(&parse_raw_arg(m)),
- None => format_stdin().await,
- };
-
- return Ok(Some(Cmd::Interpret(mime)));
+#[cfg(feature = "compiler")]
+impl CompileCommand {
+ /// Return the command-line provided message or read one from stdin.
+ pub fn mml(self) -> MmlMessage {
+ match self.mml {
+ Some(mml) => mml,
+ None => format_stdin(),
+ }
}
-
- Ok(None)
}
-/// Represents the email raw argument.
-pub fn path_or_raw_arg() -> Arg {
- Arg::new(ARG_PATH_OR_RAW)
- .help("Take data from the given file path or from the argument itself")
- .long_help(
- "Take data from the given file path or from the argument itself.
-
-If the argument points to a valid file, its content is used.
-Otherwise the argument is treated as raw data.",
- )
+/// Interpret the given MIME message as a MML message
+#[cfg(feature = "interpreter")]
+#[derive(Parser, Debug)]
+pub struct InterpreterCommand {
+ /// Read the mssage from the given file path.
+ #[arg(value_parser = parse_mime)]
+ mime: Option<MimeMessage>,
}
-/// Represents the email raw argument parser.
-pub fn parse_path_or_raw_arg(m: &ArgMatches) -> Option<&str> {
- m.get_one::<String>(ARG_PATH_OR_RAW).map(String::as_str)
-}
-
-/// Represents the email raw argument.
-pub fn raw_arg() -> Arg {
- Arg::new(ARG_RAW)
- .help("Take data from the standard input or from the argument itself")
- .long_help(
- "Take data from the standard input or from the argument itself.
-
-If the current terminal is considered interactive, take data from stdin.
-Otherwise all arguments after -- are treated as raw data.",
- )
- .raw(true)
+#[cfg(feature = "interpreter")]
+impl InterpreterCommand {
+ /// Return the command-line provided message or read one from stdin.
+ pub fn mime(self) -> MimeMessage {
+ match self.mime {
+ Some(mime) => mime,
+ None => format_stdin(),
+ }
+ }
}
-/// Represents the email raw argument parser.
-pub fn parse_raw_arg(m: &ArgMatches) -> String {
- m.get_raw(ARG_RAW)
- .map(|arg| {
- arg.flat_map(OsStr::to_str)
- .fold(String::new(), |mut args, arg| {
- if !args.is_empty() {
- args.push(' ')
- }
- args.push_str(arg);
- args
- })
- })
- .unwrap_or_default()
+#[cfg(feature = "compiler")]
+fn parse_mml(raw: &str) -> Result<MmlMessage, String> {
+ let mml = match try_shellexpand_path(raw) {
+ Ok(path) => fs::read_to_string(PathBuf::from(path)).map_err(|e| e.to_string())?,
+ Err(err) => {
+ warn!("{err}");
+ warn!("invalid path, processing it as raw MML message");
+ format_str(raw)
+ }
+ };
+
+ Ok(mml)
}
-/// Represents the client subcommands.
-pub fn subcmds() -> Vec<Command> {
- vec![
- #[cfg(feature = "compiler")]
- Command::new(CMD_COMPILE)
- .about("Compile the given MML message to a valid MIME message")
- .arg(path_or_raw_arg())
- .arg(raw_arg()),
- #[cfg(feature = "interpreter")]
- Command::new(CMD_INTERPRET)
- .about("Interpret the given MIME message as a MML message")
- .arg(path_or_raw_arg())
- .arg(raw_arg()),
- ]
+#[cfg(feature = "interpreter")]
+fn parse_mime(raw: &str) -> Result<MimeMessage, String> {
+ let mime = match try_shellexpand_path(raw) {
+ Ok(path) => fs::read_to_string(PathBuf::from(path)).map_err(|e| e.to_string())?,
+ Err(err) => {
+ warn!("{err}");
+ warn!("invalid path, processing it as raw MIME message");
+ format_str(raw)
+ }
+ };
+ Ok(mime)
}
fn format_str(input: &str) -> String {
@@ -152,11 +97,11 @@ fn format_str(input: &str) -> String {
output
}
-async fn format_stdin() -> String {
+fn format_stdin() -> String {
let mut lines = BufReader::new(io::stdin()).lines();
let mut output = String::new();
- while let Ok(Some(ref line)) = lines.next_line().await {
+ while let Some(Ok(ref line)) = lines.next() {
output.push_str(line);
output.push('\r');
output.push('\n');
--
2.42.0