Search code examples
rustclap

Is there a way to get clap to use default values from a file?


I'm programming a CLI using clap to parse my arguments. I want to provide defaults for options, but if there's a config file, the config file should win against defaults.

It's easy to prioritize command line arguments over defaults, but I want the priority order of:

  1. command line arguments
  2. config file
  3. defaults

If the config file isn't set by the command line options, it's also easy to set that up, just by parsing the config file before running parse_args, and supplying the values from the parsed config file into default_value. The problem is that if you specify the config file in the command line, you can't change the defaults until after the parsing.

The only way I can think of doing it is by not setting a default_value and then manually match "" in value_of. The issue is that in that case, clap won't be able to build a useful --help.

Is there a way to get clap to read the config file itself?


Solution

  • From clap's documentation on default_value:

    NOTE: If the user does not use this argument at runtime ArgMatches::is_present will still return true. If you wish to determine whether the argument was used at runtime or not, consider ArgMatches::occurrences_of which will return 0 if the argument was not used at runtime.

    https://docs.rs/clap/2.32.0/clap/struct.Arg.html#method.default_value

    This can be utilized to get the behavior you described:

    extern crate clap;
    use clap::{App, Arg};
    use std::fs::File;
    use std::io::prelude::*;
    
    fn main() {
        let matches = App::new("MyApp")
            .version("0.1.0")
            .about("Example for StackOverflow")
            .arg(
                Arg::with_name("config")
                    .short("c")
                    .long("config")
                    .value_name("FILE")
                    .help("Sets a custom config file"),
            )
            .arg(
                Arg::with_name("example")
                    .short("e")
                    .long("example")
                    .help("Sets an example parameter")
                    .default_value("default_value")
                    .takes_value(true),
            )
            .get_matches();
    
        let mut value = String::new();
    
        if let Some(c) = matches.value_of("config") {
            let file = File::open(c);
            match file {
                Ok(mut f) => {
                    // Note: I have a file `config.txt` that has contents `file_value`
                    f.read_to_string(&mut value).expect("Error reading value");
                }
                Err(_) => println!("Error reading file"),
            }
    
            // Note: this lets us override the config file value with the
            // cli argument, if provided
            if matches.occurrences_of("example") > 0 {
                value = matches.value_of("example").unwrap().to_string();
            }
        } else {
            value = matches.value_of("example").unwrap().to_string();
        }
    
        println!("Value for config: {}", value);
    }
    
    // Code above licensed CC0
    // https://creativecommons.org/share-your-work/public-domain/cc0/ 
    

    Resulting in the behavior:

    ./target/debug/example
    Value for config: default_value
    ./target/debug/example --example cli_value
    Value for config: cli_value
    ./target/debug/example --config config.txt
    Value for config: file_value
    ./target/debug/example --example cli_value --config config.txt
    Value for config: cli_value