3 unstable releases

0.2.1 Oct 19, 2023
0.2.0 Oct 14, 2022
0.1.0 Oct 13, 2022

#86 in Configuration

Download history 590/week @ 2024-01-03 757/week @ 2024-01-10 561/week @ 2024-01-17 421/week @ 2024-01-24 661/week @ 2024-01-31 757/week @ 2024-02-07 625/week @ 2024-02-14 926/week @ 2024-02-21 926/week @ 2024-02-28 495/week @ 2024-03-06 620/week @ 2024-03-13 529/week @ 2024-03-20 596/week @ 2024-03-27 454/week @ 2024-04-03 623/week @ 2024-04-10 522/week @ 2024-04-17

2,301 downloads per month
Used in 2 crates

AGPL-3.0-or-later

16KB

Clap and Serde derive

Crates.io Crates.io Docs.rs License GitLab pipeline

With the ClapSerde procedural macro both clap and serde can be derived from a struct.
Then the struct can be parsed from clap and serde sources as in a layered config: the last source has the precedence.

Args::from(serde_parsed)
    .merge_clap();

In the snippet the precedence is:

  1. Command line from clap;
  2. Config file from serde;
  3. Default values.

Example

In this example we define a struct which derives both clap and serde. The struct has various parameter type and also various attributes on its fields.

Finally we parse the structure from a YAML file with serde and then from command line with clap. The arguments from clap will override those from serde; the default value will be used if no source contained the field.

use clap_serde_derive::{
    clap::{self, ArgAction},
    serde::Serialize,
    ClapSerde,
};

#[derive(ClapSerde, Serialize)]
#[derive(Debug)]
#[command(author, version, about)]
pub struct Args {
    /// Input files
    pub input: Vec<std::path::PathBuf>,

    /// String argument
    #[arg(short, long)]
    name: String,

    /// Skip serde deserialize
    #[default(13)]
    #[serde(skip_deserializing)]
    #[arg(long = "num")]
    pub clap_num: u32,

    /// Skip clap
    #[serde(rename = "number")]
    #[arg(skip)]
    pub serde_num: u32,

    /// Recursive fields
    #[clap_serde]
    #[command(flatten)]
    pub suboptions: SubConfig,
}

#[derive(ClapSerde, Serialize)]
#[derive(Debug)]
pub struct SubConfig {
    #[default(true)]
    #[arg(long = "no-flag", action = ArgAction::SetFalse)]
    pub flag: bool,
}

let args = Args::from(serde_yaml::from_str::<<Args as ClapSerde>::Opt>("number: 12").unwrap())
    .merge_clap();
assert_eq!(
    serde_yaml::to_string(&args).unwrap(),
    serde_yaml::to_string(&Args {
        serde_num: 12,
        clap_num: 13,
        ..Args::default()
    })
    .unwrap(),
);

Config path from command line

You can easily take the config file path from command line in this way.

use std::{fs::File, io::BufReader};

use clap_serde_derive::{
    clap::{self, Parser},
    ClapSerde,
};

#[derive(Parser)]
#[command(author, version, about)]
struct Args {
    /// Input files
    input: Vec<std::path::PathBuf>,

    /// Config file
    #[arg(short, long = "config", default_value = "config.yml")]
    config_path: std::path::PathBuf,

    /// Rest of arguments
    #[command(flatten)]
    pub config: <Config as ClapSerde>::Opt,
}

#[derive(ClapSerde)]
struct Config {
    /// String argument
    #[arg(short, long)]
    name: String,
}

// Parse whole args with clap
let mut args = Args::parse();

// Get config file
let config = if let Ok(f) = File::open(&args.config_path) {
    // Parse config with serde
    match serde_yaml::from_reader::<_, <Config as ClapSerde>::Opt>(BufReader::new(f)) {
        // merge config already parsed from clap
        Ok(config) => Config::from(config).merge(&mut args.config),
        Err(err) => panic!("Error in configuration file:\n{}", err),
    }
} else {
    // If there is not config file return only config parsed from clap
    Config::from(&mut args.config)
};

Dependencies

~2.5MB
~56K SLoC