~soywod/pimalaya

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
1

[PATCH mml v2] Use clap's derive feature for argument parsing

Details
Message ID
<20230829200534.25441-2-hugo@whynothugo.nl>
DKIM signature
pass
Download raw message
Patch: +127 -226
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
Details
Message ID
<17801aa826e0d52d.f9706245cd3a3f97.3b41d60ef9e2fbfb@soywod>
In-Reply-To
<20230829200534.25441-2-hugo@whynothugo.nl> (view parent)
DKIM signature
pass
Download raw message
Patch applied, thanks a lot! It is sth I always wanted to do but I was
scared not to have the same behaviour as before.
Reply to thread Export thread (mbox)