Simplify option parsing using clap apis.

This commit is contained in:
jcamiel 2022-10-02 18:01:29 +02:00
parent c182a062ce
commit ef0230d65a
No known key found for this signature in database
GPG Key ID: 07FF11CFD55356CC

View File

@ -24,7 +24,7 @@ use std::path::{Path, PathBuf};
use std::time::Duration; use std::time::Duration;
use atty::Stream; use atty::Stream;
use clap::{ArgAction, ArgMatches, Command}; use clap::{value_parser, ArgAction, ArgMatches, Command};
use crate::cli; use crate::cli;
use crate::cli::CliError; use crate::cli::CliError;
@ -72,6 +72,17 @@ pub enum OutputType {
} }
pub fn app(version: &str) -> Command { pub fn app(version: &str) -> Command {
let ClientOptions {
connect_timeout: default_connect_timeout,
max_redirect: default_max_redirect,
timeout: default_timeout,
..
} = ClientOptions::default();
let default_connect_timeout = default_connect_timeout.as_secs();
let default_max_redirect = default_max_redirect.unwrap();
let default_timeout = default_timeout.as_secs();
Command::new("hurl") Command::new("hurl")
.about("Run hurl FILE(s) or standard input") .about("Run hurl FILE(s) or standard input")
.disable_colored_help(true) .disable_colored_help(true)
@ -107,6 +118,8 @@ pub fn app(version: &str) -> Command {
.long("connect-timeout") .long("connect-timeout")
.value_name("SECONDS") .value_name("SECONDS")
.help("Maximum time allowed for connection") .help("Maximum time allowed for connection")
.default_value(default_connect_timeout.to_string())
.value_parser(value_parser!(u64))
.num_args(1) .num_args(1)
) )
.arg( .arg(
@ -135,7 +148,7 @@ pub fn app(version: &str) -> Command {
clap::Arg::new("file_root") clap::Arg::new("file_root")
.long("file-root") .long("file-root")
.value_name("DIR") .value_name("DIR")
.help("Set root filesystem to import file (default is current directory)") .help("Set root filesystem to import files (default is current directory)")
.num_args(1) .num_args(1)
) )
.arg( .arg(
@ -149,7 +162,7 @@ pub fn app(version: &str) -> Command {
clap::Arg::new("glob") clap::Arg::new("glob")
.long("glob") .long("glob")
.value_name("GLOB") .value_name("GLOB")
.help("Specify input files that match the given blob. Multiple glob flags may be used") .help("Specify input files that match the given GLOB. Multiple glob flags may be used")
.action(ArgAction::Append) .action(ArgAction::Append)
.number_of_values(1) .number_of_values(1)
) )
@ -191,8 +204,10 @@ pub fn app(version: &str) -> Command {
clap::Arg::new("max_redirects") clap::Arg::new("max_redirects")
.long("max-redirs") .long("max-redirs")
.value_name("NUM") .value_name("NUM")
.help("Maximum number of redirects allowed") .help("Maximum number of redirects allowed, -1 for unlimited redirects")
.default_value(default_max_redirect.to_string())
.allow_hyphen_values(true) .allow_hyphen_values(true)
.value_parser(value_parser!(i32).range(-1..))
.num_args(1) .num_args(1)
) )
.arg( .arg(
@ -201,7 +216,9 @@ pub fn app(version: &str) -> Command {
.short('m') .short('m')
.value_name("NUM") .value_name("NUM")
.help("Maximum time allowed for the transfer") .help("Maximum time allowed for the transfer")
.default_value(default_timeout.to_string())
.allow_hyphen_values(true) .allow_hyphen_values(true)
.value_parser(value_parser!(u64))
.num_args(1) .num_args(1)
) )
.arg( .arg(
@ -245,14 +262,14 @@ pub fn app(version: &str) -> Command {
clap::Arg::new("junit") clap::Arg::new("junit")
.long("report-junit") .long("report-junit")
.value_name("FILE") .value_name("FILE")
.help("Write a Junit XML report to the given file") .help("Write a Junit XML report to FILE")
.num_args(1) .num_args(1)
) )
.arg( .arg(
clap::Arg::new("report_html") clap::Arg::new("report_html")
.long("report-html") .long("report-html")
.value_name("DIR") .value_name("DIR")
.help("Generate HTML report to dir") .help("Generate HTML report to DIR")
.num_args(1) .num_args(1)
) )
.arg( .arg(
@ -267,6 +284,8 @@ pub fn app(version: &str) -> Command {
.value_name("ENTRY_NUMBER") .value_name("ENTRY_NUMBER")
.help("Execute Hurl file to ENTRY_NUMBER (starting at 1)") .help("Execute Hurl file to ENTRY_NUMBER (starting at 1)")
.conflicts_with("interactive") .conflicts_with("interactive")
.allow_hyphen_values(true)
.value_parser(value_parser!(u32).range(1..))
.num_args(1) .num_args(1)
) )
.arg( .arg(
@ -316,8 +335,9 @@ pub fn app(version: &str) -> Command {
) )
} }
/// Parses command line options `matches`.
pub fn parse_options(matches: &ArgMatches) -> Result<CliOptions, CliError> { pub fn parse_options(matches: &ArgMatches) -> Result<CliOptions, CliError> {
let cacert_file = match get_string(matches, "cacert_file") { let cacert_file = match get::<String>(matches, "cacert_file") {
None => None, None => None,
Some(filename) => { Some(filename) => {
if !Path::new(&filename).is_file() { if !Path::new(&filename).is_file() {
@ -330,24 +350,15 @@ pub fn parse_options(matches: &ArgMatches) -> Result<CliOptions, CliError> {
}; };
let color = output_color(matches); let color = output_color(matches);
let compressed = has_flag(matches, "compressed"); let compressed = has_flag(matches, "compressed");
let connect_timeout = match get_string(matches, "connect_timeout") { let connect_timeout = get::<u64>(matches, "connect_timeout").unwrap();
None => ClientOptions::default().connect_timeout, let connect_timeout = Duration::from_secs(connect_timeout);
Some(s) => match s.parse::<u64>() { let cookie_input_file = get::<String>(matches, "cookies_input_file");
Ok(n) => Duration::from_secs(n), let cookie_output_file = get::<String>(matches, "cookies_output_file");
Err(_) => {
return Err(CliError {
message: "connect-timeout option can not be parsed".to_string(),
});
}
},
};
let cookie_input_file = get_string(matches, "cookies_input_file");
let cookie_output_file = get_string(matches, "cookies_output_file");
let fail_fast = !has_flag(matches, "fail_at_end"); let fail_fast = !has_flag(matches, "fail_at_end");
let file_root = get_string(matches, "file_root"); let file_root = get::<String>(matches, "file_root");
let follow_location = has_flag(matches, "follow_location"); let follow_location = has_flag(matches, "follow_location");
let glob_files = match_glob_files(matches)?; let glob_files = match_glob_files(matches)?;
let report_html = get_string(matches, "report_html"); let report_html = get::<String>(matches, "report_html");
let html_dir = if let Some(dir) = report_html { let html_dir = if let Some(dir) = report_html {
let path = Path::new(&dir); let path = Path::new(&dir);
if !path.exists() { if !path.exists() {
@ -373,21 +384,14 @@ pub fn parse_options(matches: &ArgMatches) -> Result<CliOptions, CliError> {
let include = has_flag(matches, "include"); let include = has_flag(matches, "include");
let insecure = has_flag(matches, "insecure"); let insecure = has_flag(matches, "insecure");
let interactive = has_flag(matches, "interactive"); let interactive = has_flag(matches, "interactive");
let junit_file = get_string(matches, "junit"); let junit_file = get::<String>(matches, "junit");
let max_redirect = match get_string(matches, "max_redirects").as_deref() { let max_redirect = get::<i32>(matches, "max_redirects").unwrap();
None => Some(50), let max_redirect = match max_redirect {
Some("-1") => None, m if m == -1 => None,
Some(s) => match s.parse::<usize>() { m => Some(m as usize),
Ok(x) => Some(x),
Err(_) => {
return Err(CliError {
message: "max_redirs option can not be parsed".to_string(),
});
}
},
}; };
let no_proxy = get_string(matches, "proxy"); let no_proxy = get::<String>(matches, "proxy");
let output = get_string(matches, "output"); let output = get::<String>(matches, "output");
let test = has_flag(matches, "test"); let test = has_flag(matches, "test");
let output_type = if has_flag(matches, "json") { let output_type = if has_flag(matches, "json") {
OutputType::Json OutputType::Json
@ -396,22 +400,13 @@ pub fn parse_options(matches: &ArgMatches) -> Result<CliOptions, CliError> {
} else { } else {
OutputType::ResponseBody OutputType::ResponseBody
}; };
let proxy = get_string(matches, "proxy"); let proxy = get::<String>(matches, "proxy");
let timeout = match get_string(matches, "max_time") { let timeout = get::<u64>(matches, "max_time").unwrap();
None => ClientOptions::default().timeout, let timeout = Duration::from_secs(timeout);
Some(s) => match s.parse::<u64>() { let to_entry = get::<u32>(matches, "to_entry").map(|x| x as usize);
Ok(n) => Duration::from_secs(n), let user = get::<String>(matches, "user");
Err(_) => { let user_agent = get::<String>(matches, "user_agent");
return Err(CliError { let variables = variables(matches)?;
message: "max_time option can not be parsed".to_string(),
});
}
},
};
let to_entry = to_entry(matches)?;
let user = get_string(matches, "user");
let user_agent = get_string(matches, "user_agent");
let variables = variables(matches.clone())?;
let very_verbose = has_flag(matches, "very_verbose"); let very_verbose = has_flag(matches, "very_verbose");
let verbose = has_flag(matches, "verbose") || has_flag(matches, "interactive") || very_verbose; let verbose = has_flag(matches, "verbose") || has_flag(matches, "interactive") || very_verbose;
@ -464,23 +459,11 @@ pub fn output_color(matches: &ArgMatches) -> bool {
atty::is(Stream::Stdout) atty::is(Stream::Stdout)
} }
fn to_entry(matches: &ArgMatches) -> Result<Option<usize>, CliError> { /// Returns a map of variables from the command line options `matches`.
match get_string(matches, "to_entry") { fn variables(matches: &ArgMatches) -> Result<HashMap<String, Value>, CliError> {
Some(value) => match value.parse() {
Ok(v) => Ok(Some(v)),
Err(_) => Err(CliError {
message: "Invalid value for option --to-entry - must be a positive integer!"
.to_string(),
}),
},
None => Ok(None),
}
}
fn variables(matches: ArgMatches) -> Result<HashMap<String, Value>, CliError> {
let mut variables = HashMap::new(); let mut variables = HashMap::new();
// use environment variables prefix by HURL_ // Use environment variables prefix by HURL_
for (env_name, env_value) in env::vars() { for (env_name, env_value) in env::vars() {
if let Some(name) = env_name.strip_prefix("HURL_") { if let Some(name) = env_name.strip_prefix("HURL_") {
let value = cli::parse_variable_value(env_value.as_str())?; let value = cli::parse_variable_value(env_value.as_str())?;
@ -488,7 +471,7 @@ fn variables(matches: ArgMatches) -> Result<HashMap<String, Value>, CliError> {
} }
} }
if let Some(filename) = get_string(&matches, "variables_file") { if let Some(filename) = get::<String>(matches, "variables_file") {
let path = Path::new(&filename); let path = Path::new(&filename);
if !path.exists() { if !path.exists() {
return Err(CliError { return Err(CliError {
@ -516,7 +499,7 @@ fn variables(matches: ArgMatches) -> Result<HashMap<String, Value>, CliError> {
} }
} }
if let Some(input) = get_strings(&matches, "variable") { if let Some(input) = get_strings(matches, "variable") {
for s in input { for s in input {
let (name, value) = cli::parse_variable(&s)?; let (name, value) = cli::parse_variable(&s)?;
variables.insert(name.to_string(), value); variables.insert(name.to_string(), value);
@ -526,12 +509,7 @@ fn variables(matches: ArgMatches) -> Result<HashMap<String, Value>, CliError> {
Ok(variables) Ok(variables)
} }
/// /// Returns a list of path names from the command line options `matches`.
/// Returns a list of path names that match `matches`.
///
/// # Arguments
/// * `matches` - A pattern to be matched
///
fn match_glob_files(matches: &ArgMatches) -> Result<Vec<String>, CliError> { fn match_glob_files(matches: &ArgMatches) -> Result<Vec<String>, CliError> {
let mut filenames = vec![]; let mut filenames = vec![];
if let Some(exprs) = get_strings(matches, "glob") { if let Some(exprs) = get_strings(matches, "glob") {
@ -566,16 +544,19 @@ fn match_glob_files(matches: &ArgMatches) -> Result<Vec<String>, CliError> {
Ok(filenames) Ok(filenames)
} }
fn get_string(matches: &ArgMatches, name: &str) -> Option<String> { /// Returns a optional value of type `T` from the command line `matches` given the option `name`.
matches.get_one::<String>(name).map(|x| x.to_string()) fn get<T: Clone + Send + Sync + 'static>(matches: &ArgMatches, name: &str) -> Option<T> {
matches.get_one::<T>(name).cloned()
} }
/// Returns a list of `String` from the command line options `matches` given the option `name`.
pub fn get_strings(matches: &ArgMatches, name: &str) -> Option<Vec<String>> { pub fn get_strings(matches: &ArgMatches, name: &str) -> Option<Vec<String>> {
matches matches
.get_many::<String>(name) .get_many::<String>(name)
.map(|v| v.map(|x| x.to_string()).collect()) .map(|v| v.map(|x| x.to_string()).collect())
} }
/// Returns true if the command line options `matches` has a given flag `name`.
pub fn has_flag(matches: &ArgMatches, name: &str) -> bool { pub fn has_flag(matches: &ArgMatches, name: &str) -> bool {
matches.get_one::<bool>(name) == Some(&true) matches.get_one::<bool>(name) == Some(&true)
} }