From 401829d45655cb85d77f1bb1478449455db54eec Mon Sep 17 00:00:00 2001 From: sigoden Date: Wed, 10 Apr 2024 10:17:46 +0000 Subject: [PATCH 1/6] feat: support param bind-env --- docs/specification.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/specification.md b/docs/specification.md index d0cd846..58c7805 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -67,6 +67,7 @@ Define a positional argument. > **Syntax**\ > `@arg` [_name_] [_modifier_]? [_param-value_]? > [_notation_]? +> [_bind-env_]? > [_description_]? ```sh @@ -95,6 +96,7 @@ Define an option argument. > **Syntax**\ > `@option` [_short_]? [_long_] [_modifier_]? [_param-value_]? > [_notations_]? +> [_bind-env_]? > [_description_]? ```sh @@ -126,6 +128,7 @@ Define a flag argument. Flag is a special option that does not accept any value. > **Syntax**\ > `@flag` [_short_]? [_long_]`*`? +> [_bind-env_]? > [_description_]? ```sh @@ -164,6 +167,7 @@ Add a metadata. | `@meta version ` | any | Set the version for the command. | | `@meta author ` | any | Set the author for the command. | | `@meta dotenv []` | root | Load a dotenv file from a custom path, if persent. | +| `@meta env-prefix ` | root | Set the prefix for environment variables bind with flags/options. | | `@meta symbol ` | any | Define a symbolic parameter, e.g. `+toolchain`, `@argument-file`. | | `@meta man-section <1-8>` | root | Override the section for the man page, defaulting to 1. | | `@meta default-subcommand` | subcmd | Set the current subcommand as the default. | @@ -177,6 +181,7 @@ Add a metadata. # @meta author nobody # @meta dotenv # @meta dotenv .env.local +# @meta env-prefix PROG # @meta symbol +toolchain[`_choice_fn`] # @meta man-section 8 ``` @@ -290,6 +295,13 @@ A-Z a-z 0-9 `!` `#` `$` `%` `*` `+` `,` `.` `/` `:` `=` `?` `@` `[` `]` `^` `_` `,` `:` `@` `|` `/` +### bind-env + +Flags/options bind environment variables + +- `$$`: reference environment variable whose name is derived from the corresponding param name +- `$`[_NAME_]: reference environment variable whose name is *NAME* + ## description Plain text, can be multiple lines. @@ -313,6 +325,7 @@ Plain text, can be multiple lines. [_notation-modifier_]: #notation-modifier [_short-char_]: #short-char [_separated-char_]: #separated-char +[_bind-env_]: #bind-env [_description_]: #description [_name_]: #name [_value_]: #value From 710f9872c77dfbeedffbdaf0d7e2c133c3cf2e06 Mon Sep 17 00:00:00 2001 From: sigoden Date: Wed, 10 Apr 2024 10:32:18 +0000 Subject: [PATCH 2/6] update specification.md --- docs/specification.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/specification.md b/docs/specification.md index 58c7805..3ad7fe2 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -162,18 +162,18 @@ Add a metadata. > **Syntax**\ > `@meta` [_name_] [_value_]? -| syntax | scope | description | -| :--------------------------- | ------ | :------------------------------------------------------------------- | -| `@meta version ` | any | Set the version for the command. | -| `@meta author ` | any | Set the author for the command. | -| `@meta dotenv []` | root | Load a dotenv file from a custom path, if persent. | -| `@meta env-prefix ` | root | Set the prefix for environment variables bind with flags/options. | -| `@meta symbol ` | any | Define a symbolic parameter, e.g. `+toolchain`, `@argument-file`. | -| `@meta man-section <1-8>` | root | Override the section for the man page, defaulting to 1. | -| `@meta default-subcommand` | subcmd | Set the current subcommand as the default. | -| `@meta inherit-flag-options` | root | Subcommands will inherit the flags/options from their parent. | -| `@meta no-inherit-env` | root | Subcommands will not inherit the env vars from their parent. | -| `@meta combine-shorts` | root | Short flags/options can be combined, e.g. `prog -xf => prog -x -f `. | +| syntax | scope | description | +| :-------------------------------- | ------ | :------------------------------------------------------------------- | +| `@meta version ` | any | Set the version for the command. | +| `@meta author ` | any | Set the author for the command. | +| `@meta dotenv []` | root | Load a dotenv file from a custom path, if persent. | +| `@meta bind-env-prefix []` | root | Set the prefix for environment variables bind with flags/options. | +| `@meta symbol ` | any | Define a symbolic parameter, e.g. `+toolchain`, `@argument-file`. | +| `@meta man-section <1-8>` | root | Override the section for the man page, defaulting to 1. | +| `@meta default-subcommand` | subcmd | Set the current subcommand as the default. | +| `@meta inherit-flag-options` | root | Subcommands will inherit the flags/options from their parent. | +| `@meta no-inherit-env` | root | Subcommands will not inherit the env vars from their parent. | +| `@meta combine-shorts` | root | Short flags/options can be combined, e.g. `prog -xf => prog -x -f `. | ```sh @@ -299,8 +299,8 @@ A-Z a-z 0-9 `!` `#` `$` `%` `*` `+` `,` `.` `/` `:` `=` `?` `@` `[` `]` `^` `_` Flags/options bind environment variables -- `$$`: reference environment variable whose name is derived from the corresponding param name -- `$`[_NAME_]: reference environment variable whose name is *NAME* +- `$$`: bind environment variable whose name is derived from the corresponding param name +- `$`[_NAME_]: bind environment variable whose name is *NAME* ## description From d0026341c99c9b00ab86dfdbc7ef11b7abb00057 Mon Sep 17 00:00:00 2001 From: sigoden Date: Thu, 11 Apr 2024 10:22:18 +0000 Subject: [PATCH 3/6] modified parser and param --- docs/specification.md | 24 ++++---- src/command/mod.rs | 30 ++++----- src/mangen.rs | 8 +-- src/param.rs | 139 ++++++++++++++++++++++++------------------ src/parser.rs | 99 +++++++++++++++++++++++++----- 5 files changed, 194 insertions(+), 106 deletions(-) diff --git a/docs/specification.md b/docs/specification.md index 3ad7fe2..954d2c6 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -162,18 +162,17 @@ Add a metadata. > **Syntax**\ > `@meta` [_name_] [_value_]? -| syntax | scope | description | -| :-------------------------------- | ------ | :------------------------------------------------------------------- | -| `@meta version ` | any | Set the version for the command. | -| `@meta author ` | any | Set the author for the command. | -| `@meta dotenv []` | root | Load a dotenv file from a custom path, if persent. | -| `@meta bind-env-prefix []` | root | Set the prefix for environment variables bind with flags/options. | -| `@meta symbol ` | any | Define a symbolic parameter, e.g. `+toolchain`, `@argument-file`. | -| `@meta man-section <1-8>` | root | Override the section for the man page, defaulting to 1. | -| `@meta default-subcommand` | subcmd | Set the current subcommand as the default. | -| `@meta inherit-flag-options` | root | Subcommands will inherit the flags/options from their parent. | -| `@meta no-inherit-env` | root | Subcommands will not inherit the env vars from their parent. | -| `@meta combine-shorts` | root | Short flags/options can be combined, e.g. `prog -xf => prog -x -f `. | +| syntax | scope | description | +| :--------------------------- | ------ | :------------------------------------------------------------------- | +| `@meta version ` | any | Set the version for the command. | +| `@meta author ` | any | Set the author for the command. | +| `@meta dotenv []` | root | Load a dotenv file from a custom path, if persent. | +| `@meta symbol ` | any | Define a symbolic parameter, e.g. `+toolchain`, `@argument-file`. | +| `@meta man-section <1-8>` | root | Override the section for the man page, defaulting to 1. | +| `@meta default-subcommand` | subcmd | Set the current subcommand as the default. | +| `@meta inherit-flag-options` | root | Subcommands will inherit the flags/options from their parent. | +| `@meta no-inherit-env` | root | Subcommands will not inherit the env vars from their parent. | +| `@meta combine-shorts` | root | Short flags/options can be combined, e.g. `prog -xf => prog -x -f `. | ```sh @@ -181,7 +180,6 @@ Add a metadata. # @meta author nobody # @meta dotenv # @meta dotenv .env.local -# @meta env-prefix PROG # @meta symbol +toolchain[`_choice_fn`] # @meta man-section 8 ``` diff --git a/src/command/mod.rs b/src/command/mod.rs index 9af6eb8..2ed3682 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -376,10 +376,10 @@ impl Command { } output.push(self.render_usage()); output.push(String::new()); - output.extend(self.render_positionals(term_width)); - output.extend(self.render_flag_options(term_width)); - output.extend(self.render_subcommands(term_width)); - output.extend(self.render_envs(term_width)); + output.extend(self.render_positionals_help(term_width)); + output.extend(self.render_flag_options_help(term_width)); + output.extend(self.render_subcommands_help(term_width)); + output.extend(self.render_envs_help(term_width)); if output.is_empty() { return "\n".to_string(); } @@ -415,7 +415,7 @@ impl Command { output.join(" ") } - pub(crate) fn render_positionals(&self, term_width: Option) -> Vec { + pub(crate) fn render_positionals_help(&self, term_width: Option) -> Vec { let mut output = vec![]; if self.positional_params.is_empty() { return output; @@ -436,7 +436,7 @@ impl Command { output } - pub(crate) fn render_flag_options(&self, term_width: Option) -> Vec { + pub(crate) fn render_flag_options_help(&self, term_width: Option) -> Vec { let mut output = vec![]; if self.flag_option_params.is_empty() { return output; @@ -446,7 +446,7 @@ impl Command { .all_flag_options() .into_iter() .map(|param| { - let value = param.render_body(); + let value = param.render_help_body(); let describe = param.render_describe(); value_size = value_size.max(value.len()); (value, describe) @@ -458,7 +458,7 @@ impl Command { output } - pub(crate) fn render_subcommands(&self, term_width: Option) -> Vec { + pub(crate) fn render_subcommands_help(&self, term_width: Option) -> Vec { let mut output = vec![]; if self.subcommands.is_empty() { return output; @@ -496,7 +496,7 @@ impl Command { output } - pub(crate) fn render_envs(&self, term_width: Option) -> Vec { + pub(crate) fn render_envs_help(&self, term_width: Option) -> Vec { let mut output = vec![]; if self.env_params.is_empty() { return output; @@ -506,7 +506,7 @@ impl Command { .env_params .iter() .map(|param| { - let value = param.render_body(); + let value = param.render_help_body(); value_size = value_size.max(value.len()); (value, param.render_describe()) }) @@ -754,9 +754,10 @@ impl Command { } else { None }; + let mut param_data = ParamData::new("help"); + param_data.describe = describe.to_string(); Some(FlagOptionParam::new( - ParamData::new("help"), - describe, + param_data, true, short, long_prefix, @@ -777,9 +778,10 @@ impl Command { } else { None }; + let mut param_data = ParamData::new("version"); + param_data.describe = describe.to_string(); Some(FlagOptionParam::new( - ParamData::new("version"), - describe, + param_data, true, short, long_prefix, diff --git a/src/mangen.rs b/src/mangen.rs index b66d73c..b6ce336 100644 --- a/src/mangen.rs +++ b/src/mangen.rs @@ -113,9 +113,9 @@ fn render_options_section(roff: &mut Roff, cmd: &Command) { } let mut body = vec![]; let mut has_help_written = false; - if !param.describe.is_empty() { + if !param.describe().is_empty() { has_help_written = true; - render_describe(&mut body, ¶m.describe); + render_describe(&mut body, param.describe()); } roff.control("TP", []); roff.text(header); @@ -136,9 +136,9 @@ fn render_options_section(roff: &mut Roff, cmd: &Command) { } let mut body = vec![]; let mut has_help_written = false; - if !param.describe.is_empty() { + if !param.describe().is_empty() { has_help_written = true; - render_describe(&mut body, ¶m.describe); + render_describe(&mut body, param.describe()); } roff.control("TP", []); roff.text(header); diff --git a/src/param.rs b/src/param.rs index 5002df4..ea3a256 100644 --- a/src/param.rs +++ b/src/param.rs @@ -13,7 +13,6 @@ pub(crate) trait Param { fn data(&self) -> &ParamData; fn id(&self) -> &str; fn var_name(&self) -> String; - fn describe(&self) -> &str; fn tag_name(&self) -> &str; fn guard(&self) -> Result<()>; fn multiple_values(&self) -> bool; @@ -25,7 +24,10 @@ pub(crate) trait Param { } } fn render_describe(&self) -> String { - self.data().render_describe(self.describe()) + self.data().render_describe(self.describe(), self.id()) + } + fn describe(&self) -> &str { + &self.data().describe } fn required(&self) -> bool { @@ -52,18 +54,20 @@ pub(crate) trait Param { fn default(&self) -> Option<&DefaultValue> { self.data().default.as_ref() } + fn default_fn(&self) -> Option<&String> { + self.data().default_fn() + } fn default_value(&self) -> Option<&String> { self.data().default_value() } - fn default_fn(&self) -> Option<&String> { - self.data().default_fn() + fn bind_env(&self) -> Option> { + self.data().bind_env() } } #[derive(Debug, PartialEq, Eq, Clone)] pub(crate) struct FlagOptionParam { pub(crate) data: ParamData, - pub(crate) describe: String, pub(crate) is_flag: bool, pub(crate) short: Option, pub(crate) long_prefix: String, @@ -88,10 +92,6 @@ impl Param for FlagOptionParam { argc_var_name(self.id()) } - fn describe(&self) -> &str { - &self.describe - } - fn tag_name(&self) -> &str { if self.is_flag() { "@flag" @@ -134,14 +134,13 @@ impl Param for FlagOptionParam { output.push(format!( "{}{}", self.long_prefix, - self.data.render_name_value(&name_suffix) + self.data.render_source_of_name_value(&name_suffix) )); for raw_notation in &self.raw_notations { output.push(format!("<{}>", raw_notation)); } - if !self.describe.is_empty() { - output.push(self.describe.clone()); - } + self.data + .render_source_of_bind_env_and_describe(&mut output); output.join(" ") } } @@ -149,7 +148,6 @@ impl Param for FlagOptionParam { impl FlagOptionParam { pub(crate) fn new( mut data: ParamData, - describe: &str, is_flag: bool, short: Option<&str>, long_prefix: &str, @@ -187,7 +185,6 @@ impl FlagOptionParam { last_arg.push('~') } Self { - describe: describe.to_string(), is_flag, short: short.map(|v| v.to_string()), long_prefix: long_prefix.to_string(), @@ -206,7 +203,7 @@ impl FlagOptionParam { id: self.id().to_string(), long_name: self.render_long_name(), short_name: self.short.clone(), - describe: self.describe.clone(), + describe: self.describe().to_string(), is_flag: self.is_flag, notations: self.notations.clone(), required: self.required(), @@ -276,7 +273,28 @@ impl FlagOptionParam { output } - pub(crate) fn render_body(&self) -> String { + pub(crate) fn render_notations(&self) -> String { + let mut list = vec![]; + if self.notations.len() == 1 { + let name: &String = &self.notations[0]; + let value = match (self.required(), self.multiple_occurs()) { + (true, true) => format!("<{name}>..."), + (false, true) => format!("[{name}]..."), + (_, false) => format!("<{name}>"), + }; + list.push(value); + } else { + let values = self + .notations + .iter() + .map(|v| format!("<{v}>")) + .collect::>(); + list.extend(values); + } + list.join(" ") + } + + pub(crate) fn render_help_body(&self) -> String { let mut output = String::new(); if self.short.is_none() && self.long_prefix.len() == 1 && self.data.name.len() == 1 { output.push_str(&self.render_long_name()); @@ -302,27 +320,6 @@ impl FlagOptionParam { output } - pub(crate) fn render_notations(&self) -> String { - let mut list = vec![]; - if self.notations.len() == 1 { - let name: &String = &self.notations[0]; - let value = match (self.required(), self.multiple_occurs()) { - (true, true) => format!("<{name}>..."), - (false, true) => format!("[{name}]..."), - (_, false) => format!("<{name}>"), - }; - list.push(value); - } else { - let values = self - .notations - .iter() - .map(|v| format!("<{v}>")) - .collect::>(); - list.extend(values); - } - list.join(" ") - } - pub(crate) fn to_argc_value(&self, args: &[FlagOptionArg]) -> Option { let id = self.id().to_string(); if self.prefixed { @@ -460,7 +457,6 @@ pub struct FlagOptionValue { #[derive(Debug, PartialEq, Eq, Clone)] pub(crate) struct PositionalParam { pub(crate) data: ParamData, - pub(crate) describe: String, pub(crate) raw_notation: Option, pub(crate) notation: String, } @@ -478,10 +474,6 @@ impl Param for PositionalParam { argc_var_name(self.id()) } - fn describe(&self) -> &str { - &self.describe - } - fn tag_name(&self) -> &str { "@arg" } @@ -496,23 +488,21 @@ impl Param for PositionalParam { fn render_source(&self) -> String { let mut output = vec![]; - output.push(self.data.render_name_value("")); + output.push(self.data.render_source_of_name_value("")); if let Some(raw_notation) = self.raw_notation.as_ref() { output.push(format!("<{}>", raw_notation)); } - if !self.describe.is_empty() { - output.push(self.describe.clone()); - } + self.data + .render_source_of_bind_env_and_describe(&mut output); output.join(" ") } } impl PositionalParam { - pub(crate) fn new(data: ParamData, describe: &str, raw_notation: Option<&str>) -> Self { + pub(crate) fn new(data: ParamData, raw_notation: Option<&str>) -> Self { let name = data.name.clone(); PositionalParam { data, - describe: describe.to_string(), raw_notation: raw_notation.map(|v| v.to_string()), notation: raw_notation .or(Some(&name)) @@ -524,7 +514,7 @@ impl PositionalParam { pub(crate) fn export(&self) -> PositionalValue { PositionalValue { id: self.id().to_string(), - describe: self.describe.clone(), + describe: self.describe().to_string(), notation: self.notation.clone(), required: self.required(), multiple_values: self.multiple_values(), @@ -614,6 +604,9 @@ impl Param for EnvParam { if !matches!(self.data().modifer, Modifier::Optional | Modifier::Required) { bail!("can only be a single value") } + if self.bind_env().is_some() { + bail!("cannot bind another env") + } Ok(()) } @@ -622,11 +615,9 @@ impl Param for EnvParam { } fn render_source(&self) -> String { - let mut output = vec![]; - output.push(self.data.render_name_value("")); - if !self.describe.is_empty() { - output.push(self.describe.clone()); - } + let mut output = vec![self.data.render_source_of_name_value("")]; + self.data + .render_source_of_bind_env_and_describe(&mut output); output.join(" ") } } @@ -651,7 +642,7 @@ impl EnvParam { } } - pub(crate) fn render_body(&self) -> String { + pub(crate) fn render_help_body(&self) -> String { let marker = if self.required() { "*" } else { "" }; format!("{}{}", self.id(), marker) } @@ -680,18 +671,22 @@ pub struct EnvValue { #[derive(Debug, PartialEq, Eq, Clone)] pub(crate) struct ParamData { pub(crate) name: String, + pub(crate) describe: String, pub(crate) choice: Option, pub(crate) default: Option, pub(crate) modifer: Modifier, + pub(crate) bind_env: Option>, } impl ParamData { pub(crate) fn new(name: &str) -> Self { Self { name: name.to_string(), + describe: String::new(), choice: None, default: None, modifer: Modifier::Optional, + bind_env: None, } } @@ -735,7 +730,6 @@ impl ParamData { } } - #[allow(unused)] pub(crate) fn default_value(&self) -> Option<&String> { match &self.default { Some(DefaultValue::Value(v)) => Some(v), @@ -743,7 +737,19 @@ impl ParamData { } } - pub(crate) fn render_name_value(&self, name_suffix: &str) -> String { + pub(crate) fn bind_env(&self) -> Option> { + self.bind_env.as_ref().map(|v| v.as_ref()) + } + + pub(crate) fn normalize_bind_env(&self, id: &str) -> Option { + let bind_env = match self.bind_env()? { + Some(v) => v.clone(), + None => id.to_uppercase(), + }; + Some(bind_env) + } + + pub(crate) fn render_source_of_name_value(&self, name_suffix: &str) -> String { let mut output = format!("{}{name_suffix}", self.name); output.push_str(&self.modifer.render()); match (&self.choice, &self.default) { @@ -768,7 +774,19 @@ impl ParamData { output } - pub(crate) fn render_describe(&self, describe: &str) -> String { + pub(crate) fn render_source_of_bind_env_and_describe(&self, parts: &mut Vec) { + if let Some(bind_env) = self.bind_env() { + match bind_env { + Some(v) => parts.push(format!("${v}")), + None => parts.push("$$".into()), + } + } + if !self.describe.is_empty() { + parts.push(self.describe.clone()); + } + } + + pub(crate) fn render_describe(&self, describe: &str, id: &str) -> String { let mut output = describe.to_string(); let multiline = output.contains('\n'); let sep = if multiline { '\n' } else { ' ' }; @@ -785,6 +803,9 @@ impl ParamData { let values: Vec = values.iter().map(|v| escape_shell_words(v)).collect(); output.push_str(&format!("[possible values: {}]", values.join(", "))); } + if let Some(bind_env) = self.normalize_bind_env(id) { + output.push_str(&format!("[env: {bind_env}]")); + } output } diff --git a/src/parser.rs b/src/parser.rs index a8814ac..604f85e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -85,18 +85,27 @@ pub(crate) fn parse(source: &str) -> Result> { EventData::Cmd(text) } EventData::Env(mut param) => { - line_idx += - take_comment_lines(&lines, line_idx + 1, &mut param.describe); + line_idx += take_comment_lines( + &lines, + line_idx + 1, + &mut param.data.describe, + ); EventData::Env(param) } EventData::FlagOption(mut param) => { - line_idx += - take_comment_lines(&lines, line_idx + 1, &mut param.describe); + line_idx += take_comment_lines( + &lines, + line_idx + 1, + &mut param.data.describe, + ); EventData::FlagOption(param) } EventData::Positional(mut param) => { - line_idx += - take_comment_lines(&lines, line_idx + 1, &mut param.describe); + line_idx += take_comment_lines( + &lines, + line_idx + 1, + &mut param.data.describe, + ); EventData::Positional(param) } v => v, @@ -240,10 +249,13 @@ fn parse_with_long_option_param(input: &str) -> nom::IResult<&str, FlagOptionPar parse_param_modifer, )), parse_zero_or_many_value_notations, + parse_zero_or_one_bind_env, parse_tail, )), - |((short, long_prefix), arg, value_names, describe)| { - FlagOptionParam::new(arg, describe, false, short, long_prefix, &value_names) + |((short, long_prefix), mut arg, value_names, bind_env, describe)| { + arg.bind_env = bind_env; + arg.describe = describe.to_string(); + FlagOptionParam::new(arg, false, short, long_prefix, &value_names) }, )(input) } @@ -265,10 +277,13 @@ fn parse_no_long_option_param(input: &str) -> nom::IResult<&str, FlagOptionParam )), ), parse_zero_or_many_value_notations, + parse_zero_or_one_bind_env, parse_tail, )), - |(long_prefix, arg, value_names, describe)| { - FlagOptionParam::new(arg, describe, false, None, long_prefix, &value_names) + |(long_prefix, mut arg, value_names, bind_env, describe)| { + arg.bind_env = bind_env; + arg.describe = describe.to_string(); + FlagOptionParam::new(arg, false, None, long_prefix, &value_names) }, )(input) } @@ -304,9 +319,14 @@ fn parse_positional_param(input: &str) -> nom::IResult<&str, PositionalParam> { parse_param_modifer, )), parse_zero_or_one_value_notation, + parse_zero_or_one_bind_env, parse_tail, )), - |(arg, value_name, describe)| PositionalParam::new(arg, describe, value_name), + |(mut arg, value_name, bind_env, describe)| { + arg.bind_env = bind_env; + arg.describe = describe.to_string(); + PositionalParam::new(arg, value_name) + }, )(input) } @@ -318,9 +338,16 @@ fn parse_flag_param(input: &str) -> nom::IResult<&str, FlagOptionParam> { // Parse `@flag` fn parse_with_long_flag_param(input: &str) -> nom::IResult<&str, FlagOptionParam> { map( - tuple((parse_with_long_head, parse_with_long_flag_name, parse_tail)), - |((short, long_prefix), arg, describe)| { - FlagOptionParam::new(arg, describe, true, short, long_prefix, &[]) + tuple(( + parse_with_long_head, + parse_with_long_flag_name, + parse_zero_or_one_bind_env, + parse_tail, + )), + |((short, long_prefix), mut arg, bind_env, describe)| { + arg.bind_env = bind_env; + arg.describe = describe.to_string(); + FlagOptionParam::new(arg, true, short, long_prefix, &[]) }, )(input) } @@ -331,10 +358,13 @@ fn parse_no_long_flag_param(input: &str) -> nom::IResult<&str, FlagOptionParam> tuple(( preceded(space0, alt((tag("-"), tag("+")))), parse_no_long_flag_name, + parse_zero_or_one_bind_env, parse_tail, )), - |(long_prefix, arg, describe)| { - FlagOptionParam::new(arg, describe, true, None, long_prefix, &[]) + |(long_prefix, mut arg, bind_env, describe)| { + arg.bind_env = bind_env; + arg.describe = describe.to_string(); + FlagOptionParam::new(arg, true, None, long_prefix, &[]) }, )(input) } @@ -505,6 +535,10 @@ fn parse_value_notation(input: &str) -> nom::IResult<&str, &str> { )(input) } +fn parse_bind_env_name(input: &str) -> nom::IResult<&str, &str> { + take_while1(is_env_name_char)(input) +} + // Parse `a|b|c` fn parse_choices(input: &str) -> nom::IResult<&str, Vec<&str>> { map(separated_list1(char('|'), parse_choice_value), |choices| { @@ -648,6 +682,23 @@ fn parse_symbol_data(input: &str) -> nom::IResult<&str, (char, &str, Option<&str )(input) } +fn parse_zero_or_one_bind_env(input: &str) -> nom::IResult<&str, Option>> { + opt(parse_bind_env)(input) +} + +fn parse_bind_env(input: &str) -> nom::IResult<&str, Option> { + map( + preceded(tag(" $"), alt((tag("$"), parse_bind_env_name))), + |v| { + if v == "$" { + None + } else { + Some(v.to_string()) + } + }, + )(input) +} + fn notation_text(input: &str, balances: usize) -> nom::IResult<&str, usize> { let (i1, c1) = anychar(input)?; match c1 { @@ -687,6 +738,10 @@ fn is_name_char(c: char) -> bool { c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | ':' | '@') } +fn is_env_name_char(c: char) -> bool { + c.is_ascii_uppercase() || c == '_' +} + fn is_short_char(c: char) -> bool { c.is_ascii_alphanumeric() || matches!( @@ -875,19 +930,27 @@ mod tests { assert_parse_option_arg!("--foo <>"); assert_parse_option_arg!("--foo "); assert_parse_option_arg!("--foo <>"); + assert_parse_option_arg!("--foo $$"); + assert_parse_option_arg!("--foo $FOO"); } #[test] fn test_parse_with_long_option_arg_single_dash() { assert_parse_option_arg!("-f -foo=a A foo option"); + assert_parse_option_arg!("-foo:bar"); + assert_parse_option_arg!("-foo.bar"); assert_parse_option_arg!("-foo!"); assert_parse_option_arg!("-foo+"); assert_parse_option_arg!("-foo*"); + assert_parse_option_arg!("-foo+,"); + assert_parse_option_arg!("-foo*,"); assert_parse_option_arg!("-foo!"); assert_parse_option_arg!("-foo-*"); assert_parse_option_arg!("-foo-"); + assert_parse_option_arg!("-foo--"); assert_parse_option_arg!("-foo:*"); assert_parse_option_arg!("-foo:"); + assert_parse_option_arg!("-foo::"); assert_parse_option_arg!("-foo=a"); assert_parse_option_arg!("-foo=`_foo`"); assert_parse_option_arg!("-foo[a|b]"); @@ -911,6 +974,8 @@ mod tests { assert_parse_option_arg!("-foo <>"); assert_parse_option_arg!("-foo "); assert_parse_option_arg!("-foo <>"); + assert_parse_option_arg!("-foo $$"); + assert_parse_option_arg!("-foo $FOO"); } #[test] @@ -1002,6 +1067,8 @@ mod tests { assert_parse_positional_arg!("foo*[a|b]"); assert_parse_positional_arg!("foo*[`_foo`]"); assert_parse_positional_arg!("foo*[=a|b]"); + assert_parse_positional_arg!("foo $$"); + assert_parse_positional_arg!("foo $FOO"); } #[test] From 823f229c7490e713b0b8320807e4bd29952990cd Mon Sep 17 00:00:00 2001 From: sigoden Date: Fri, 12 Apr 2024 01:24:52 +0000 Subject: [PATCH 4/6] implement bind env --- docs/specification.md | 8 + examples/bind-envs.sh | 87 ++++++++ examples/options.sh | 2 +- src/bin/argc/main.rs | 4 +- src/command/mod.rs | 20 +- src/compgen.rs | 16 +- src/matcher.rs | 209 +++++++++++++++--- src/param.rs | 88 ++++---- src/parser.rs | 12 +- src/utils.rs | 26 ++- tests/bind_env.rs | 135 +++++++++++ tests/macros.rs | 30 +++ .../integration__bind_env__bind_env_arg1.snap | 13 ++ .../integration__bind_env__bind_env_arg2.snap | 13 ++ ...on__bind_env__bind_env_arg_choice_err.snap | 9 + ..._bind_env__bind_env_arg_choice_fn_err.snap | 9 + ...env__bind_env_cmd_three_required_args.snap | 15 ++ ..._bind_env_cmd_three_required_args_err.snap | 10 + ...integration__bind_env__bind_env_flags.snap | 16 ++ ...on__bind_env__bind_env_flags_bool_err.snap | 9 + ...ion__bind_env__bind_env_flags_bool_ok.snap | 13 ++ ...ration__bind_env__bind_env_flags_help.snap | 18 ++ ...egration__bind_env__bind_env_flags_ok.snap | 21 ++ ...ulti_arg_with_choice_fn_and_comma_sep.snap | 13 ++ ...tegration__bind_env__bind_env_options.snap | 24 ++ ...bind_env__bind_env_options_choice_err.snap | 9 + ...d_env__bind_env_options_choice_fn_err.snap | 9 + ..._bind_env__bind_env_options_choice_ok.snap | 16 ++ ...tion__bind_env__bind_env_options_help.snap | 25 +++ ...nd_env__bind_env_options_required_err.snap | 10 + tests/snapshots/integration__cli__export.snap | 160 +++++++++++--- .../integration__spec__option_help.snap | 8 +- tests/tests.rs | 1 + 33 files changed, 906 insertions(+), 152 deletions(-) create mode 100644 examples/bind-envs.sh create mode 100644 tests/bind_env.rs create mode 100644 tests/snapshots/integration__bind_env__bind_env_arg1.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_arg2.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_arg_choice_err.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_arg_choice_fn_err.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args_err.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_flags.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_flags_bool_err.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_flags_bool_ok.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_flags_help.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_flags_ok.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_multi_arg_with_choice_fn_and_comma_sep.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_options.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_options_choice_err.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_options_choice_fn_err.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_options_choice_ok.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_options_help.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_options_required_err.snap diff --git a/docs/specification.md b/docs/specification.md index 954d2c6..080a23d 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -75,6 +75,7 @@ Define a positional argument. # @arg vb! required # @arg vc* multi-values # @arg vd+ multi-values + required +# @arg vf*, multi-values + comma-separated list # @arg vna value notation # @arg vda=a default # @arg vdb=`_default_fn` default from fn @@ -87,6 +88,8 @@ Define a positional argument. # @arg vfc*[`_choice_fn`] multi-values + choice from fn # @arg vfd*,[`_choice_fn`] multi-values + choice from fn + comma-separated list # @arg vxa~ capture all remaining args +# @arg ea $$ bind-env +# @arg eb $BE bind-named-env ``` ### `@option` @@ -106,6 +109,7 @@ Define an option argument. # @option --oc! required # @option --od* multi-occurs # @option --oe+ required + multi-occurs +# @option --of*, multi-occurs + comma-separated list # @option --ona value notation # @option --onb two-args value notations # @option --onc unlimited-args value notations @@ -120,6 +124,8 @@ Define an option argument. # @option --ofc*[`_choice_fn`] multi-occurs + choice from fn # @option --ofd*,[`_choice_fn`] multi-occurs + choice from fn + comma-separated list # @option --oxa~ capture all remaining args +# @option --ea $$ bind-env +# @option --eb $BE bind-named-env ``` ### `@flag` @@ -136,6 +142,8 @@ Define a flag argument. Flag is a special option that does not accept any value. # @flag -b --fb short # @flag -c short only # @flag --fd* multi-occurs +# @flag --ea $$ bind-env +# @flag --eb $BE bind-named-env ``` ### `@env` diff --git a/examples/bind-envs.sh b/examples/bind-envs.sh new file mode 100644 index 0000000..80d90f1 --- /dev/null +++ b/examples/bind-envs.sh @@ -0,0 +1,87 @@ +# @cmd +# @flag --fa1 $$ +# @flag --fa2 $$ +# @flag --fa3 $FA +# @flag --fc* $$ +# @flag --fd $$ +flags() { + _debug "$@"; +} + +# @cmd +# @option --oa1 $$ +# @option --oa2 $$ +# @option --oa3 $OA +# @option --ob! $OB +# @option --oc*, $$ +# @option --oda=a $$ +# @option --odb=`_default_fn` $$ +# @option --oca[a|b] $$ +# @option --occ*[a|b] $$ +# @option --ofa[`_choice_fn`] $$ +# @option --ofd*,[`_choice_fn`] $$ +# @option --oxa~ $$ +options() { + _debug "$@"; +} + +# @cmd +# @arg val $$ +cmd_arg1() { + _debug "$@"; +} + +# @cmd +# @arg val $VA +cmd_arg2() { + _debug "$@"; +} + +# @cmd +# @arg val=xyz $$ +cmd_arg_with_default() { + _debug "$@"; +} + +# @cmd +# @arg val[x|y|z] $$ +cmd_arg_with_choice() { + _debug "$@"; +} + +# @cmd +# @arg val[`_choice_fn`] $$ +cmd_arg_with_choice_fn() { + _debug "$@"; +} + +# @cmd +# @arg val*,[`_choice_fn`] $$ +cmd_multi_arg_with_choice_fn_and_comma_sep() { + _debug "$@"; +} + +# @cmd +# @arg val1! $$ +# @arg val2! $$ +# @arg val3! $$ +cmd_three_required_args() { + _debug "$@"; +} + +_debug() { + ( set -o posix ; set ) | grep ^argc_ + echo "$argc__fn" "$@" +} + +_default_fn() { + echo argc +} + +_choice_fn() { + echo abc + echo def + echo ghi +} + +eval "$(argc --argc-eval "$0" "$@")" \ No newline at end of file diff --git a/examples/options.sh b/examples/options.sh index d14a4a4..0b83e96 100755 --- a/examples/options.sh +++ b/examples/options.sh @@ -7,6 +7,7 @@ # @option --oc! required # @option --od* multi-occurs # @option --oe+ required + multi-occurs +# @option --of*, multi-occurs + comma-separated list # @option --ona value notation # @option --onb two-args value notations # @option --onc unlimited-args value notations @@ -15,7 +16,6 @@ # @option --oca[a|b] choice # @option --ocb[=a|b] choice + default # @option --occ*[a|b] multi-occurs + choice -# @option --ocd+[a|b] required + multi-occurs + choice # @option --ofa[`_choice_fn`] choice from fn # @option --ofb[?`_choice_fn`] choice from fn + no validation # @option --ofc*[`_choice_fn`] multi-occurs + choice from fn diff --git a/src/bin/argc/main.rs b/src/bin/argc/main.rs index 7f7a76b..144524f 100644 --- a/src/bin/argc/main.rs +++ b/src/bin/argc/main.rs @@ -3,7 +3,7 @@ mod parallel; use anyhow::{anyhow, bail, Context, Result}; use argc::{ - utils::{escape_shell_words, get_current_dir, get_shell_path, termwidth}, + utils::{escape_shell_words, get_current_dir, get_shell_path, is_true_value, termwidth}, Shell, }; use base64::{engine::general_purpose, Engine as _}; @@ -214,7 +214,7 @@ fn run_compgen(mut args: Vec) -> Option<()> { } } let no_color = std::env::var("NO_COLOR") - .map(|v| v == "true" || v == "1") + .map(|v| is_true_value(&v)) .unwrap_or_default(); let output = if &args[4] == "argc" && (args[3].is_empty() || args[5].starts_with("--argc")) { let cmd_args = &args[4..]; diff --git a/src/command/mod.rs b/src/command/mod.rs index 2ed3682..bb86e86 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -376,10 +376,10 @@ impl Command { } output.push(self.render_usage()); output.push(String::new()); - output.extend(self.render_positionals_help(term_width)); - output.extend(self.render_flag_options_help(term_width)); - output.extend(self.render_subcommands_help(term_width)); - output.extend(self.render_envs_help(term_width)); + output.extend(self.render_positionals(term_width)); + output.extend(self.render_flag_options(term_width)); + output.extend(self.render_subcommands(term_width)); + output.extend(self.render_envs(term_width)); if output.is_empty() { return "\n".to_string(); } @@ -415,7 +415,7 @@ impl Command { output.join(" ") } - pub(crate) fn render_positionals_help(&self, term_width: Option) -> Vec { + pub(crate) fn render_positionals(&self, term_width: Option) -> Vec { let mut output = vec![]; if self.positional_params.is_empty() { return output; @@ -436,7 +436,7 @@ impl Command { output } - pub(crate) fn render_flag_options_help(&self, term_width: Option) -> Vec { + pub(crate) fn render_flag_options(&self, term_width: Option) -> Vec { let mut output = vec![]; if self.flag_option_params.is_empty() { return output; @@ -446,7 +446,7 @@ impl Command { .all_flag_options() .into_iter() .map(|param| { - let value = param.render_help_body(); + let value = param.render_body(); let describe = param.render_describe(); value_size = value_size.max(value.len()); (value, describe) @@ -458,7 +458,7 @@ impl Command { output } - pub(crate) fn render_subcommands_help(&self, term_width: Option) -> Vec { + pub(crate) fn render_subcommands(&self, term_width: Option) -> Vec { let mut output = vec![]; if self.subcommands.is_empty() { return output; @@ -496,7 +496,7 @@ impl Command { output } - pub(crate) fn render_envs_help(&self, term_width: Option) -> Vec { + pub(crate) fn render_envs(&self, term_width: Option) -> Vec { let mut output = vec![]; if self.env_params.is_empty() { return output; @@ -506,7 +506,7 @@ impl Command { .env_params .iter() .map(|param| { - let value = param.render_help_body(); + let value = param.render_body(); value_size = value_size.max(value.len()); (value, param.render_describe()) }) diff --git a/src/compgen.rs b/src/compgen.rs index 54cd1ae..fcfd78a 100644 --- a/src/compgen.rs +++ b/src/compgen.rs @@ -1,6 +1,6 @@ use crate::command::Command; use crate::matcher::Matcher; -use crate::utils::{is_quote_char, is_windows_path, run_param_fns}; +use crate::utils::{get_os, is_quote_char, is_true_value, is_windows_path, run_param_fns}; use crate::Result; use anyhow::bail; @@ -116,7 +116,7 @@ pub fn compgen( } else { let mut envs = HashMap::new(); envs.insert("ARGC_COMPGEN".into(), "1".into()); - envs.insert("ARGC_OS".into(), env::consts::OS.to_string()); + envs.insert("ARGC_OS".into(), get_os()); envs.insert("ARGC_CWORD".into(), argc_filter.clone()); envs.insert("ARGC_LAST_ARG".into(), last_arg.to_string()); run_param_fns(script_path, &[fn_name.as_str()], &new_args, envs) @@ -480,14 +480,10 @@ impl Shell { } pub(crate) fn with_description(&self) -> bool { - if let Ok(v) = std::env::var("ARGC_COMPGEN_DESCRIPTION") { - if v == "true" || v == "1" { - return true; - } else if v == "false" || v == "0" { - return false; - } + match env::var("ARGC_COMPGEN_DESCRIPTION") { + Ok(v) => is_true_value(&v), + Err(_) => true, } - true } pub(crate) fn escape(&self, value: &str) -> String { @@ -558,7 +554,7 @@ impl Shell { return vec![]; } match self { - Shell::Bash => match std::env::var("COMP_WORDBREAKS") { + Shell::Bash => match env::var("COMP_WORDBREAKS") { Ok(v) => [':', '=', '@'] .iter() .filter(|c| v.contains(**c)) diff --git a/src/matcher.rs b/src/matcher.rs index b2acf2f..3a01f71 100644 --- a/src/matcher.rs +++ b/src/matcher.rs @@ -1,16 +1,16 @@ #![allow(clippy::too_many_arguments)] -use std::{ - collections::{HashMap, HashSet}, - env, -}; +use std::collections::{HashMap, HashSet}; use crate::{ argc_value::ArgcValue, command::{Command, SymbolParam}, compgen::CompColor, param::{ChoiceValue, FlagOptionParam, Param, ParamData, PositionalParam}, - utils::{argc_var_name, load_dotenv, run_param_fns, META_COMBINE_SHORTS}, + utils::{ + all_envs, argc_var_name, get_os, is_true_value, load_dotenv, run_param_fns, + META_COMBINE_SHORTS, + }, Shell, }; @@ -28,7 +28,7 @@ pub(crate) struct Matcher<'a, 'b> { arg_comp: ArgComp, choice_fns: HashSet<&'a str>, script_path: Option, - envs: HashMap<&'a str, String>, + envs: HashMap, term_width: Option, split_last_arg_at: Option, comp_option: Option<&'a str>, @@ -64,6 +64,7 @@ pub(crate) enum MatchError { MissingRequiredEnvironments(Vec), NotMultipleArgument(usize, String), InvalidValue(usize, String, String, Vec), + InvalidBindEnvironment(usize, String, String), InvalidEnvironment(usize, String, String, Vec), MismatchValues(usize, String), NoFlagValue(usize, String), @@ -294,15 +295,10 @@ impl<'a, 'b> Matcher<'a, 'b> { let last_cmd = *cmds.last().unwrap(); - let mut envs = HashMap::new(); - let dotenv_vars = root_cmd.dotenv().and_then(load_dotenv).unwrap_or_default(); + let mut envs = all_envs(); + envs.extend(root_cmd.dotenv().and_then(load_dotenv).unwrap_or_default()); + for param in &last_cmd.env_params { - if let Some(value) = std::env::var(param.id()) - .ok() - .or_else(|| dotenv_vars.get(param.id()).cloned()) - { - envs.insert(param.id(), value); - } add_param_choice_fn(&mut choice_fns, param) } @@ -333,11 +329,12 @@ impl<'a, 'b> Matcher<'a, 'b> { } pub(crate) fn to_arg_values(&self) -> Vec { - if let Some(err) = self.validate() { + let bind_envs = self.build_bind_envs(); + if let Some(err) = self.validate(&bind_envs) { return vec![ArgcValue::Error(self.stringify_match_error(&err))]; } let last_cmd = self.last_cmd(); - let mut output = self.to_arg_values_base(); + let mut output = self.to_arg_values_base(&bind_envs); if last_cmd.positional_params.is_empty() && !self.positional_args.is_empty() { output.push(ArgcValue::ExtraPositionalMultiple( self.positional_args.iter().map(|v| v.to_string()).collect(), @@ -350,7 +347,8 @@ impl<'a, 'b> Matcher<'a, 'b> { } pub(crate) fn to_arg_values_for_param_fn(&self) -> Vec { - let mut output: Vec = self.to_arg_values_base(); + let bind_envs = self.build_bind_envs(); + let mut output: Vec = self.to_arg_values_base(&bind_envs); let cmds_len = self.cmds.len(); let level = cmds_len - 1; let last_cmd = self.cmds[level]; @@ -474,19 +472,18 @@ impl<'a, 'b> Matcher<'a, 'b> { self.split_last_arg_at } - fn to_arg_values_base(&self) -> Vec { + fn to_arg_values_base<'x>(&'x self, bind_envs: &BindEnvs<'a, 'x>) -> Vec { let mut output = vec![]; let root_cmd = self.cmds[0]; let cmds_len = self.cmds.len(); - let level = cmds_len - 1; - let last_cmd = self.cmds[level]; + let last_cmd = self.last_cmd(); if let Some(value) = root_cmd.dotenv() { output.push(ArgcValue::Dotenv(value.to_string())) } for param in &last_cmd.env_params { - if !self.envs.contains_key(¶m.id()) { + if !self.envs.contains_key(param.id()) { if let Some(value) = param.get_env_value() { output.push(value); } @@ -503,10 +500,31 @@ impl<'a, 'b> Matcher<'a, 'b> { } for level in 0..cmds_len { - let args = &self.flag_option_args[level]; + let args = self.flag_option_args[level].as_slice(); let cmd = self.cmds[level]; for param in cmd.flag_option_params.iter() { - if let Some(value) = param.to_argc_value(args) { + let mut args: Vec<_> = args + .iter() + .filter_map(|(name, value, param_name)| { + if param_name == &Some(param.id()) { + Some((*name, value.as_slice())) + } else { + None + } + }) + .collect(); + if args.is_empty() { + if let Some(env_values) = bind_envs.flag_options[level].get(param.id()) { + if param.is_flag() { + if is_true_value(env_values[0]) { + args = vec![("", env_values.as_slice())]; + } + } else { + args = vec![("", env_values.as_slice())]; + } + } + } + if let Some(value) = param.to_argc_value(&args) { output.push(value); } } @@ -514,10 +532,15 @@ impl<'a, 'b> Matcher<'a, 'b> { let positional_values = self.match_positionals(); for (i, param) in last_cmd.positional_params.iter().enumerate() { - let values = positional_values + let mut values = positional_values .get(i) .map(|v| v.as_slice()) .unwrap_or_default(); + if values.is_empty() { + if let Some(env_values) = bind_envs.positionals.get(param.id()) { + values = env_values.as_slice() + } + } if let Some(value) = param.to_argc_value(values) { output.push(value); } @@ -526,20 +549,49 @@ impl<'a, 'b> Matcher<'a, 'b> { output } - fn validate(&self) -> Option { + fn build_bind_envs<'x: 'a>(&'x self) -> BindEnvs<'a, 'x> { let cmds_len = self.cmds.len(); - let choices_fn_values = self.run_choices_fns().unwrap_or_default(); + let last_cmd = self.last_cmd(); + let mut bind_envs = BindEnvs::new(cmds_len); + + for level in 0..cmds_len { + let cmd = self.cmds[level]; + for param in cmd.flag_option_params.iter() { + if let Some(env_value) = param.bind_env().and_then(|v| self.envs.get(&v)) { + let values = delimit_arg_values(param, &[env_value]); + bind_envs.flag_options[level].insert(param.id(), values); + add_param_choice_fn(&mut bind_envs.choice_fns, param); + } + } + } + + for param in last_cmd.positional_params.iter() { + if let Some(env_value) = param.bind_env().and_then(|v| self.envs.get(&v)) { + let values = delimit_arg_values(param, &[env_value]); + bind_envs.positionals.insert(param.id(), values); + add_param_choice_fn(&mut bind_envs.choice_fns, param); + } + } + bind_envs + } + fn validate<'x>(&'x self, bind_envs: &BindEnvs<'a, 'x>) -> Option { + let cmds_len = self.cmds.len(); + let choices_fn_values = self.run_choices_fns(bind_envs).unwrap_or_default(); for level in 0..cmds_len { let flag_option_args = &self.flag_option_args[level]; let cmd = self.cmds[level]; + let flag_option_bind_envs = &bind_envs.flag_options[level]; let mut flag_option_map = IndexMap::new(); let mut missing_flag_options: IndexSet<&str> = cmd .flag_option_params .iter() - .filter(|v| v.required()) + .filter(|v| v.required() && !flag_option_bind_envs.contains_key(v.id())) .map(|v| v.id()) .collect(); + + let mut check_flag_option_bind_envs: IndexSet<&str> = + flag_option_bind_envs.keys().copied().collect(); for (i, (key, _, name)) in flag_option_args.iter().enumerate() { match (*key, name) { ("--help", _) | ("-help", _) | ("-h", None) | (_, Some("help")) => { @@ -555,6 +607,7 @@ impl<'a, 'b> Matcher<'a, 'b> { match *name { Some(name) => { missing_flag_options.swap_remove(name); + check_flag_option_bind_envs.swap_remove(name); flag_option_map.entry(name).or_insert(vec![]).push(i); } None => return Some(MatchError::UnknownArgument(level, key.to_string())), @@ -599,6 +652,40 @@ impl<'a, 'b> Matcher<'a, 'b> { } } } + + for name in check_flag_option_bind_envs { + if let Some(param) = cmd.flag_option_params.iter().find(|v| v.id() == name) { + let values = &flag_option_bind_envs[name]; + let mut is_valid = true; + let (min, _) = param.args_range(); + if param.is_flag() { + if !is_bool_value(values[0]) { + is_valid = false; + } + } else if min > 1 { + is_valid = false; + } + if is_valid { + if let Some(choices) = + get_param_choice(¶m.data.choice, &choices_fn_values) + { + for value in values.iter() { + if !choices.contains(&value.to_string()) { + is_valid = false; + } + } + } + } + if !is_valid { + return Some(MatchError::InvalidBindEnvironment( + level, + param.bind_env().unwrap_or_default(), + param.render_long_name(), + )); + } + } + } + if !missing_flag_options.is_empty() { let missing_flag_options: Vec = missing_flag_options .iter() @@ -673,11 +760,25 @@ impl<'a, 'b> Matcher<'a, 'b> { } } if positional_params_len > positional_values_len { - let missing_positionals: Vec<_> = last_cmd.positional_params[positional_values_len..] - .iter() - .filter(|param| param.required()) - .map(|v| v.render_notation()) - .collect(); + let mut missing_positionals = vec![]; + for param in &last_cmd.positional_params[positional_values_len..] { + if let Some(values) = bind_envs.positionals.get(param.id()) { + if let Some(choices) = get_param_choice(¶m.data.choice, &choices_fn_values) + { + for value in values.iter() { + if !choices.contains(&value.to_string()) { + return Some(MatchError::InvalidBindEnvironment( + level, + param.bind_env().unwrap_or_default(), + param.render_notation(), + )); + } + } + } + } else if param.required() { + missing_positionals.push(param.render_notation()) + } + } if !missing_positionals.is_empty() { return Some(MatchError::MissingRequiredArguments( level, @@ -715,12 +816,19 @@ impl<'a, 'b> Matcher<'a, 'b> { None } - fn run_choices_fns(&'a self) -> Option>> { + fn run_choices_fns<'x>( + &'x self, + bind_envs: &BindEnvs<'a, 'x>, + ) -> Option>> { + let fns: Vec<_> = { + let mut fns = self.choice_fns.clone(); + fns.extend(bind_envs.choice_fns.iter()); + fns.into_iter().collect() + }; let script_path = self.script_path.as_ref()?; let mut choices_fn_values = HashMap::new(); - let fns: Vec<&str> = self.choice_fns.iter().copied().collect(); let mut envs = HashMap::new(); - envs.insert("ARGC_OS".into(), env::consts::OS.to_string()); + envs.insert("ARGC_OS".into(), get_os()); let outputs = run_param_fns(script_path, &fns, self.args, envs)?; for (i, output) in outputs.into_iter().enumerate() { let choices = output @@ -843,6 +951,12 @@ impl<'a, 'b> Matcher<'a, 'b> { [possible values: {list}]"### ) } + MatchError::InvalidBindEnvironment(_level, env_name, param_name) => { + exit = 1; + format!( + r###"error: environment variable '{env_name}' has invalid value for param '{param_name}'"### + ) + } MatchError::InvalidEnvironment(_level, value, name, choices) => { exit = 1; let list = choices.join(", "); @@ -906,6 +1020,25 @@ impl<'a, 'b> Matcher<'a, 'b> { /// (value, description, nospace, color) pub(crate) type CompItem = (String, String, bool, CompColor); +#[derive(Debug)] +struct BindEnvs<'a, 'x> { + flag_options: Vec>, + positionals: BindEnvMap<'a, 'x>, + choice_fns: HashSet<&'a str>, +} + +impl<'a, 'x> BindEnvs<'a, 'x> { + fn new(len: usize) -> Self { + Self { + flag_options: vec![HashMap::new(); len], + positionals: HashMap::new(), + choice_fns: HashSet::new(), + } + } +} + +type BindEnvMap<'a, 'x> = HashMap<&'a str, Vec<&'x str>>; + fn find_subcommand<'a>( cmd: &'a Command, arg: &str, @@ -1237,10 +1370,14 @@ fn get_param_choice<'a, 'b: 'a>( } } -fn delimit_arg_values<'b, T: Param>(param: &T, values: &[&'b str]) -> Vec<&'b str> { +fn delimit_arg_values<'x, T: Param>(param: &T, values: &[&'x str]) -> Vec<&'x str> { if let Some(delimiter) = param.args_delimiter() { values.iter().flat_map(|v| v.split(delimiter)).collect() } else { values.to_vec() } } + +fn is_bool_value(value: &str) -> bool { + matches!(value, "true" | "false" | "0" | "1") +} diff --git a/src/param.rs b/src/param.rs index ea3a256..635e39f 100644 --- a/src/param.rs +++ b/src/param.rs @@ -1,8 +1,5 @@ -use crate::matcher::FlagOptionArg; -use crate::utils::{ - argc_var_name, escape_shell_words, is_choice_value_terminate, is_default_value_terminate, - to_cobol_case, MAX_ARGS, -}; +use crate::parser::{is_choice_value_terminate, is_default_value_terminate}; +use crate::utils::{argc_var_name, escape_shell_words, sanitize_var_name, to_cobol_case, MAX_ARGS}; use crate::ArgcValue; use anyhow::{bail, Result}; @@ -60,20 +57,20 @@ pub(crate) trait Param { fn default_value(&self) -> Option<&String> { self.data().default_value() } - fn bind_env(&self) -> Option> { - self.data().bind_env() + fn bind_env(&self) -> Option { + self.data().normalize_bind_env(self.id()) } } #[derive(Debug, PartialEq, Eq, Clone)] pub(crate) struct FlagOptionParam { pub(crate) data: ParamData, + pub(crate) id: String, pub(crate) is_flag: bool, pub(crate) short: Option, pub(crate) long_prefix: String, pub(crate) prefixed: bool, pub(crate) assigned: bool, - pub(crate) id: String, pub(crate) raw_notations: Vec, pub(crate) notations: Vec, pub(crate) inherit: bool, @@ -112,6 +109,9 @@ impl Param for FlagOptionParam { bail!("cannot combine delmiter and multiple notations") } } + if self.prefixed && self.bind_env().is_some() { + bail!("cannot bind env with prefixed options") + } Ok(()) } @@ -216,6 +216,7 @@ impl FlagOptionParam { assigned: self.assigned, default: self.data().default.clone(), choice: self.data().choice.clone(), + env: self.bind_env(), inherit: self.inherit, } } @@ -294,7 +295,7 @@ impl FlagOptionParam { list.join(" ") } - pub(crate) fn render_help_body(&self) -> String { + pub(crate) fn render_body(&self) -> String { let mut output = String::new(); if self.short.is_none() && self.long_prefix.len() == 1 && self.data.name.len() == 1 { output.push_str(&self.render_long_name()); @@ -320,29 +321,27 @@ impl FlagOptionParam { output } - pub(crate) fn to_argc_value(&self, args: &[FlagOptionArg]) -> Option { + pub(crate) fn to_argc_value(&self, args: &[(&str, &[&str])]) -> Option { let id = self.id().to_string(); if self.prefixed { let mut map: IndexMap> = IndexMap::new(); - for (arg, value, name) in args.iter() { - if name == &Some(id.as_str()) { - if let Some(arg_suffix) = self - .list_names() - .into_iter() - .find_map(|v| arg.strip_prefix(&v)) - { - let key = match arg_suffix.split_once('=') { - Some((arg_suffix, _)) => arg_suffix, - None => arg_suffix, - }; - if let Some(values) = map.get_mut(key) { - values.extend(value.iter().map(|v| v.to_string())); - } else { - map.insert( - key.to_string(), - value.iter().map(|v| v.to_string()).collect(), - ); - } + for (arg, value) in args { + if let Some(arg_suffix) = self + .list_names() + .into_iter() + .find_map(|v| arg.strip_prefix(&v)) + { + let key = match arg_suffix.split_once('=') { + Some((arg_suffix, _)) => arg_suffix, + None => arg_suffix, + }; + if let Some(values) = map.get_mut(key) { + values.extend(value.iter().map(|v| v.to_string())); + } else { + map.insert( + key.to_string(), + value.iter().map(|v| v.to_string()).collect(), + ); } } } @@ -352,16 +351,7 @@ impl FlagOptionParam { Some(ArgcValue::Map(id, map)) } } else { - let values: Vec<&[&str]> = args - .iter() - .filter_map(|(_, value, name)| { - if name == &Some(self.id()) { - Some(value.as_slice()) - } else { - None - } - }) - .collect(); + let values: Vec<&[&str]> = args.iter().map(|(_, value)| *value).collect(); if self.is_flag { if values.is_empty() { None @@ -451,6 +441,7 @@ pub struct FlagOptionValue { pub assigned: bool, pub default: Option, pub choice: Option, + pub env: Option, pub inherit: bool, } @@ -523,6 +514,7 @@ impl PositionalParam { terminated: self.terminated(), default: self.data().default.clone(), choice: self.data().choice.clone(), + env: self.bind_env(), } } @@ -570,6 +562,7 @@ pub struct PositionalValue { pub terminated: bool, pub default: Option, pub choice: Option, + pub env: Option, } #[derive(Debug, PartialEq, Eq, Clone)] @@ -604,7 +597,7 @@ impl Param for EnvParam { if !matches!(self.data().modifer, Modifier::Optional | Modifier::Required) { bail!("can only be a single value") } - if self.bind_env().is_some() { + if self.data.bind_env.is_some() { bail!("cannot bind another env") } Ok(()) @@ -642,7 +635,7 @@ impl EnvParam { } } - pub(crate) fn render_help_body(&self) -> String { + pub(crate) fn render_body(&self) -> String { let marker = if self.required() { "*" } else { "" }; format!("{}{}", self.id(), marker) } @@ -737,14 +730,10 @@ impl ParamData { } } - pub(crate) fn bind_env(&self) -> Option> { - self.bind_env.as_ref().map(|v| v.as_ref()) - } - pub(crate) fn normalize_bind_env(&self, id: &str) -> Option { - let bind_env = match self.bind_env()? { + let bind_env = match self.bind_env.as_ref()? { Some(v) => v.clone(), - None => id.to_uppercase(), + None => sanitize_var_name(id).to_uppercase(), }; Some(bind_env) } @@ -775,7 +764,7 @@ impl ParamData { } pub(crate) fn render_source_of_bind_env_and_describe(&self, parts: &mut Vec) { - if let Some(bind_env) = self.bind_env() { + if let Some(bind_env) = &self.bind_env { match bind_env { Some(v) => parts.push(format!("${v}")), None => parts.push("$$".into()), @@ -804,6 +793,9 @@ impl ParamData { output.push_str(&format!("[possible values: {}]", values.join(", "))); } if let Some(bind_env) = self.normalize_bind_env(id) { + if !output.is_empty() { + output.push(sep) + } output.push_str(&format!("[env: {bind_env}]")); } output diff --git a/src/parser.rs b/src/parser.rs index 604f85e..c754d94 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,7 +1,7 @@ use crate::param::{ ChoiceValue, DefaultValue, EnvParam, FlagOptionParam, Modifier, ParamData, PositionalParam, }; -use crate::utils::{is_choice_value_terminate, is_default_value_terminate}; +use crate::utils::is_special_var_char; use crate::Result; use anyhow::bail; use nom::{ @@ -734,8 +734,16 @@ fn create_err(input: &str, kind: ErrorKind) -> nom::Err> nom::Err::Error(nom::error::Error::new(input, kind)) } +pub(crate) fn is_choice_value_terminate(c: char) -> bool { + c == '|' || c == ']' +} + +pub(crate) fn is_default_value_terminate(c: char) -> bool { + c.is_whitespace() +} + fn is_name_char(c: char) -> bool { - c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | ':' | '@') + c.is_ascii_alphanumeric() || c == '_' || is_special_var_char(c) } fn is_env_name_char(c: char) -> bool { diff --git a/src/utils.rs b/src/utils.rs index 34612ad..533fd75 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -38,18 +38,10 @@ pub fn escape_shell_words(value: &str) -> String { shell_words::quote(value).to_string() } -pub fn is_choice_value_terminate(c: char) -> bool { - c == '|' || c == ']' -} - pub fn is_quote_char(c: char) -> bool { c == '\'' || c == '"' } -pub fn is_default_value_terminate(c: char) -> bool { - c.is_whitespace() -} - pub fn get_shell_path() -> anyhow::Result { match env::var("ARGC_SHELL_PATH") { Ok(v) => { @@ -80,6 +72,10 @@ pub fn get_shell_args(shell_path: &Path) -> Vec { } } +pub fn get_os() -> String { + env::consts::OS.to_string() +} + #[cfg(windows)] pub fn get_bash_path() -> Option { let bash_path = PathBuf::from("C:\\Program Files\\Git\\bin\\bash.exe"); @@ -192,14 +188,22 @@ pub fn expand_dotenv(value: &str) -> String { format!("[ -f {value} ] && set -o allexport && . {value} && set +o allexport") } +pub fn is_special_var_char(c: char) -> bool { + matches!(c, '-' | '.' | ':' | '@') +} + pub fn sanitize_var_name(id: &str) -> String { - id.replace(['-', '.', ':'], "_") + id.replace(is_special_var_char, "_") } pub fn argc_var_name(id: &str) -> String { format!("{VARIABLE_PREFIX}{}", sanitize_var_name(id)) } +pub fn all_envs() -> HashMap { + env::vars().collect() +} + pub fn load_dotenv(path: &str) -> Option> { let contents = fs::read_to_string(path).ok()?; let mut output = HashMap::new(); @@ -216,6 +220,10 @@ pub fn load_dotenv(path: &str) -> Option> { Some(output) } +pub fn is_true_value(value: &str) -> bool { + matches!(value, "true" | "1") +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/bind_env.rs b/tests/bind_env.rs new file mode 100644 index 0000000..0f08f08 --- /dev/null +++ b/tests/bind_env.rs @@ -0,0 +1,135 @@ +use rstest::rstest; + +#[rstest] +fn bind_env_flags_help() { + snapshot_bind_env!(args: ["flags", "-h"], envs: {}); +} + +#[rstest] +fn bind_env_flags() { + snapshot_bind_env!(args: ["flags"], envs: { + "FA1": "true", + "FB2": "false", + "FA": "true", + "FC": "true", + "FD": "true", + }); +} + +#[rstest] +fn bind_env_flags_bool_err() { + snapshot_bind_env!(args: ["flags"], envs: { + "FA1": "v1", + }); +} + +#[rstest] +fn bind_env_flags_bool_ok() { + snapshot_bind_env!(args: ["flags", "--fa1"], envs: { + "FA1": "v1", + }); +} + +#[rstest] +fn bind_env_options_help() { + snapshot_bind_env!(args: ["options", "-h"], envs: {}); +} + +#[rstest] +fn bind_env_options() { + snapshot_bind_env!(args: ["options"], envs: { + "OA1": "oa1", + "OA2": "oa2", + "OA": "oa3", + "OB": "ob", + "OC": "v1,v2", + "ODA": "oda", + "ODD": "odd", + "OCA": "a", + "OCC": "a", + "OFA": "abc", + "OFD": "abc,def", + "OXA": "oxa", + }); +} + +#[rstest] +fn bind_env_options_choice_err() { + snapshot_bind_env!(args: ["options"], envs: { + "OB": "ob", + "OCA": "oca", + }); +} + +#[rstest] +fn bind_env_options_choice_ok() { + snapshot_bind_env!(args: ["options", "--oca", "a"], envs: { + "OB": "ob", + "OCA": "oca", + }); +} + +#[rstest] +fn bind_env_options_choice_fn_err() { + snapshot_bind_env!(args: ["options"], envs: { + "OB": "ob", + "OFA": "ofa", + }); +} + +#[rstest] +fn bind_env_options_required_err() { + snapshot_bind_env!(args: ["options"], envs: {}); +} + +#[rstest] +fn bind_env_arg1() { + snapshot_bind_env!(args: ["cmd_arg1"], envs: { + "VAL": "v1", + }); +} + +#[rstest] +fn bind_env_arg2() { + snapshot_bind_env!(args: ["cmd_arg2"], envs: { + "VA": "v1", + }); +} + +#[rstest] +fn bind_env_arg_choice_err() { + snapshot_bind_env!(args: ["cmd_arg_with_choice"], envs: { + "VAL": "v1", + }); +} + +#[rstest] +fn bind_env_arg_choice_fn_err() { + snapshot_bind_env!(args: ["cmd_arg_with_choice_fn"], envs: { + "VAL": "v1", + }); +} + +#[rstest] +fn bind_env_multi_arg_with_choice_fn_and_comma_sep() { + snapshot_bind_env!(args: ["cmd_multi_arg_with_choice_fn_and_comma_sep"], envs: { + "VAL": "abc,def", + }); +} + +#[rstest] +fn bind_env_cmd_three_required_args() { + snapshot_bind_env!(args: ["cmd_three_required_args"], envs: { + "VAL1": "v1", + "VAL2": "v2", + "VAL3": "v3", + }); +} + +#[rstest] +fn bind_env_cmd_three_required_args_err() { + snapshot_bind_env!(args: ["cmd_three_required_args"], envs: { + "VAL1": "v1", + "VAL2": "v2", + }); +} diff --git a/tests/macros.rs b/tests/macros.rs index c374929..9874868 100644 --- a/tests/macros.rs +++ b/tests/macros.rs @@ -197,3 +197,33 @@ macro_rules! snapshot_env { "#)); }; } + +macro_rules! snapshot_bind_env { + ( + args: [$($arg:literal),*], + envs: {$($key:literal : $value:literal),*$(,)?} + + ) => { + let script_path = $crate::fixtures::locate_script("examples/bind-envs.sh"); + let args: Vec = vec![$($arg.to_string(),)*]; + let envs: Vec<(&str, &str)> = [$(($key, $value),)*].into_iter().collect(); + + let output = $crate::fixtures::run_script(&script_path, &args, &envs); + + // let build_output = { + // let build_script_dir = $crate::fixtures::tmpdir(); + // let source = std::fs::read_to_string(&script_path).unwrap(); + // let build_script_path = $crate::fixtures::build_script(&build_script_dir, &source); + // $crate::fixtures::run_script(&build_script_path, &args, &envs) + // }; + let build_output = ""; + + insta::assert_snapshot!(format!(r#" +# OUTPUT +{output} + +# BUILD_OUTPUT +{build_output} +"#)); + }; +} diff --git a/tests/snapshots/integration__bind_env__bind_env_arg1.snap b/tests/snapshots/integration__bind_env__bind_env_arg1.snap new file mode 100644 index 0000000..ae6b22c --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_arg1.snap @@ -0,0 +1,13 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +argc__args=([0]="bind-envs" [1]="cmd_arg1") +argc__fn=cmd_arg1 +argc__positionals=([0]="v1") +argc_val=v1 +cmd_arg1 v1 + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_arg2.snap b/tests/snapshots/integration__bind_env__bind_env_arg2.snap new file mode 100644 index 0000000..32e1b59 --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_arg2.snap @@ -0,0 +1,13 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +argc__args=([0]="bind-envs" [1]="cmd_arg2") +argc__fn=cmd_arg2 +argc__positionals=([0]="v1") +argc_val=v1 +cmd_arg2 v1 + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_arg_choice_err.snap b/tests/snapshots/integration__bind_env__bind_env_arg_choice_err.snap new file mode 100644 index 0000000..4c6800f --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_arg_choice_err.snap @@ -0,0 +1,9 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +error: environment variable 'VAL' has invalid value for param '[VAL]' + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_arg_choice_fn_err.snap b/tests/snapshots/integration__bind_env__bind_env_arg_choice_fn_err.snap new file mode 100644 index 0000000..4c6800f --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_arg_choice_fn_err.snap @@ -0,0 +1,9 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +error: environment variable 'VAL' has invalid value for param '[VAL]' + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args.snap b/tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args.snap new file mode 100644 index 0000000..86c4505 --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args.snap @@ -0,0 +1,15 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +argc__args=([0]="bind-envs" [1]="cmd_three_required_args") +argc__fn=cmd_three_required_args +argc__positionals=([0]="v1" [1]="v2" [2]="v3") +argc_val1=v1 +argc_val2=v2 +argc_val3=v3 +cmd_three_required_args v1 v2 v3 + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args_err.snap b/tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args_err.snap new file mode 100644 index 0000000..c644e3b --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args_err.snap @@ -0,0 +1,10 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +error: the following required arguments were not provided: + + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_flags.snap b/tests/snapshots/integration__bind_env__bind_env_flags.snap new file mode 100644 index 0000000..92f75e5 --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_flags.snap @@ -0,0 +1,16 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +argc__args=([0]="bind-envs" [1]="flags") +argc__fn=flags +argc__positionals=() +argc_fa1=1 +argc_fa3=1 +argc_fc=1 +argc_fd=1 +flags + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_flags_bool_err.snap b/tests/snapshots/integration__bind_env__bind_env_flags_bool_err.snap new file mode 100644 index 0000000..8b1e18d --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_flags_bool_err.snap @@ -0,0 +1,9 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +error: environment variable 'FA1' has invalid value for param '--fa1' + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_flags_bool_ok.snap b/tests/snapshots/integration__bind_env__bind_env_flags_bool_ok.snap new file mode 100644 index 0000000..27f4189 --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_flags_bool_ok.snap @@ -0,0 +1,13 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +argc__args=([0]="bind-envs" [1]="flags" [2]="--fa1") +argc__fn=flags +argc__positionals=() +argc_fa1=1 +flags + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_flags_help.snap b/tests/snapshots/integration__bind_env__bind_env_flags_help.snap new file mode 100644 index 0000000..d3ff883 --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_flags_help.snap @@ -0,0 +1,18 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +USAGE: bind-envs flags [OPTIONS] + +OPTIONS: + --fa1 [env: FA1] + --fa2 [env: FA2] + --fa3 [env: FA] + --fc... [env: FC] + --fd [env: FD] + -h, --help + + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_flags_ok.snap b/tests/snapshots/integration__bind_env__bind_env_flags_ok.snap new file mode 100644 index 0000000..b8d7fe4 --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_flags_ok.snap @@ -0,0 +1,21 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +USAGE: bind-envs + +COMMANDS: + flags + options + cmd_arg1 + cmd_arg2 + cmd_arg_with_default + cmd_arg_with_choice + cmd_arg_with_choice_fn + cmd_multi_arg_with_choice_fn_and_comma_sep + cmd_three_required_args + + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_multi_arg_with_choice_fn_and_comma_sep.snap b/tests/snapshots/integration__bind_env__bind_env_multi_arg_with_choice_fn_and_comma_sep.snap new file mode 100644 index 0000000..ab295b2 --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_multi_arg_with_choice_fn_and_comma_sep.snap @@ -0,0 +1,13 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +argc__args=([0]="bind-envs" [1]="cmd_multi_arg_with_choice_fn_and_comma_sep") +argc__fn=cmd_multi_arg_with_choice_fn_and_comma_sep +argc__positionals=([0]="abc" [1]="def") +argc_val=([0]="abc" [1]="def") +cmd_multi_arg_with_choice_fn_and_comma_sep abc def + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_options.snap b/tests/snapshots/integration__bind_env__bind_env_options.snap new file mode 100644 index 0000000..3b95fd6 --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_options.snap @@ -0,0 +1,24 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +argc__args=([0]="bind-envs" [1]="options") +argc__fn=options +argc__positionals=() +argc_oa1=oa1 +argc_oa2=oa2 +argc_oa3=oa3 +argc_ob=ob +argc_oc=([0]="v1" [1]="v2") +argc_oca=a +argc_occ=([0]="a") +argc_oda=oda +argc_odb=argc +argc_ofa=abc +argc_ofd=([0]="abc" [1]="def") +argc_oxa=([0]="oxa") +options + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_options_choice_err.snap b/tests/snapshots/integration__bind_env__bind_env_options_choice_err.snap new file mode 100644 index 0000000..6ac8741 --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_options_choice_err.snap @@ -0,0 +1,9 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +error: environment variable 'OCA' has invalid value for param '--oca' + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_options_choice_fn_err.snap b/tests/snapshots/integration__bind_env__bind_env_options_choice_fn_err.snap new file mode 100644 index 0000000..c75fc5c --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_options_choice_fn_err.snap @@ -0,0 +1,9 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +error: environment variable 'OFA' has invalid value for param '--ofa' + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_options_choice_ok.snap b/tests/snapshots/integration__bind_env__bind_env_options_choice_ok.snap new file mode 100644 index 0000000..d0d1cc4 --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_options_choice_ok.snap @@ -0,0 +1,16 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +argc__args=([0]="bind-envs" [1]="options" [2]="--oca" [3]="a") +argc__fn=options +argc__positionals=() +argc_ob=ob +argc_oca=a +argc_oda=a +argc_odb=argc +options + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_options_help.snap b/tests/snapshots/integration__bind_env__bind_env_options_help.snap new file mode 100644 index 0000000..3482412 --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_options_help.snap @@ -0,0 +1,25 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +USAGE: bind-envs options [OPTIONS] --ob + +OPTIONS: + --oa1 [env: OA1] + --oa2 [env: OA2] + --oa3 [env: OA] + --ob [env: OB] + --oc [OC]... [env: OC] + --oda [default: a] [env: ODA] + --odb [env: ODB] + --oca [possible values: a, b] [env: OCA] + --occ [OCC]... [possible values: a, b] [env: OCC] + --ofa [env: OFA] + --ofd [OFD]... [env: OFD] + --oxa [env: OXA] + -h, --help + + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_options_required_err.snap b/tests/snapshots/integration__bind_env__bind_env_options_required_err.snap new file mode 100644 index 0000000..be67aec --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_options_required_err.snap @@ -0,0 +1,10 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +error: the following required arguments were not provided: + --ob + + +# BUILD_OUTPUT diff --git a/tests/snapshots/integration__cli__export.snap b/tests/snapshots/integration__cli__export.snap index 2c9bb67..b32ccdc 100644 --- a/tests/snapshots/integration__cli__export.snap +++ b/tests/snapshots/integration__cli__export.snap @@ -29,6 +29,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -51,6 +52,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false } ], @@ -86,6 +88,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -110,6 +113,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -134,6 +138,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -158,6 +163,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -182,6 +188,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -206,6 +213,32 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, + "inherit": false + }, + { + "id": "of", + "long_name": "--of", + "short_name": null, + "describe": "multi-occurs + comma-separated list", + "is_flag": false, + "notations": [ + "OF" + ], + "required": false, + "multiple_values": true, + "multiple_occurs": true, + "args_range": [ + 1, + 1 + ], + "args_delimiter": ",", + "terminated": false, + "prefixed": false, + "assigned": false, + "default": null, + "choice": null, + "env": null, "inherit": false }, { @@ -230,6 +263,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -255,6 +289,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -280,6 +315,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -307,6 +343,7 @@ expression: stdout "value": "a" }, "choice": null, + "env": null, "inherit": false }, { @@ -334,6 +371,7 @@ expression: stdout "value": "_default_fn" }, "choice": null, + "env": null, "inherit": false }, { @@ -364,6 +402,7 @@ expression: stdout "b" ] }, + "env": null, "inherit": false }, { @@ -397,6 +436,7 @@ expression: stdout "b" ] }, + "env": null, "inherit": false }, { @@ -427,36 +467,7 @@ expression: stdout "b" ] }, - "inherit": false - }, - { - "id": "ocd", - "long_name": "--ocd", - "short_name": null, - "describe": "required + multi-occurs + choice", - "is_flag": false, - "notations": [ - "OCD" - ], - "required": true, - "multiple_values": true, - "multiple_occurs": true, - "args_range": [ - 1, - 1 - ], - "args_delimiter": null, - "terminated": false, - "prefixed": false, - "assigned": false, - "default": null, - "choice": { - "type": "Values", - "data": [ - "a", - "b" - ] - }, + "env": null, "inherit": false }, { @@ -487,6 +498,7 @@ expression: stdout true ] }, + "env": null, "inherit": false }, { @@ -517,6 +529,7 @@ expression: stdout false ] }, + "env": null, "inherit": false }, { @@ -547,6 +560,7 @@ expression: stdout true ] }, + "env": null, "inherit": false }, { @@ -577,6 +591,7 @@ expression: stdout true ] }, + "env": null, "inherit": false }, { @@ -601,6 +616,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -623,6 +639,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false } ], @@ -661,6 +678,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -683,6 +701,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -705,6 +724,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -727,6 +747,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -749,6 +770,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -771,6 +793,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false } ], @@ -809,6 +832,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -831,6 +855,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -853,6 +878,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -877,6 +903,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -901,6 +928,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -925,6 +953,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -955,6 +984,7 @@ expression: stdout "b" ] }, + "env": null, "inherit": false }, { @@ -985,6 +1015,7 @@ expression: stdout true ] }, + "env": null, "inherit": false }, { @@ -1007,6 +1038,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false } ], @@ -1047,6 +1079,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1071,6 +1104,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1095,6 +1129,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1117,6 +1152,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false } ], @@ -1157,6 +1193,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1181,6 +1218,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1205,6 +1243,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1229,6 +1268,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1253,6 +1293,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1277,6 +1318,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1301,6 +1343,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1326,6 +1369,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1351,6 +1395,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1378,6 +1423,7 @@ expression: stdout "value": "a" }, "choice": null, + "env": null, "inherit": false }, { @@ -1405,6 +1451,7 @@ expression: stdout "value": "_default_fn" }, "choice": null, + "env": null, "inherit": false }, { @@ -1435,6 +1482,7 @@ expression: stdout "b" ] }, + "env": null, "inherit": false }, { @@ -1468,6 +1516,7 @@ expression: stdout "b" ] }, + "env": null, "inherit": false }, { @@ -1498,6 +1547,7 @@ expression: stdout "b" ] }, + "env": null, "inherit": false }, { @@ -1528,6 +1578,7 @@ expression: stdout "b" ] }, + "env": null, "inherit": false }, { @@ -1558,6 +1609,7 @@ expression: stdout true ] }, + "env": null, "inherit": false }, { @@ -1588,6 +1640,7 @@ expression: stdout false ] }, + "env": null, "inherit": false }, { @@ -1618,6 +1671,7 @@ expression: stdout true ] }, + "env": null, "inherit": false }, { @@ -1648,6 +1702,7 @@ expression: stdout true ] }, + "env": null, "inherit": false }, { @@ -1672,6 +1727,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1694,6 +1750,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false } ], @@ -1732,6 +1789,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1754,6 +1812,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1776,6 +1835,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1798,6 +1858,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1820,6 +1881,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1842,6 +1904,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false } ], @@ -1882,6 +1945,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1906,6 +1970,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1930,6 +1995,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -1952,6 +2018,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false } ], @@ -1998,6 +2065,7 @@ expression: stdout true ] }, + "env": null, "inherit": false }, { @@ -2028,6 +2096,7 @@ expression: stdout true ] }, + "env": null, "inherit": false }, { @@ -2050,6 +2119,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false } ], @@ -2096,6 +2166,7 @@ expression: stdout "b" ] }, + "env": null, "inherit": false }, { @@ -2118,6 +2189,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false } ], @@ -2156,6 +2228,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -2178,6 +2251,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -2200,6 +2274,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -2222,6 +2297,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -2244,6 +2320,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -2268,6 +2345,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -2292,6 +2370,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -2316,6 +2395,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -2340,6 +2420,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -2365,6 +2446,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -2389,6 +2471,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -2420,6 +2503,7 @@ expression: stdout "z" ] }, + "env": null, "inherit": false }, { @@ -2450,6 +2534,7 @@ expression: stdout true ] }, + "env": null, "inherit": false }, { @@ -2480,6 +2565,7 @@ expression: stdout false ] }, + "env": null, "inherit": false }, { @@ -2510,6 +2596,7 @@ expression: stdout true ] }, + "env": null, "inherit": false }, { @@ -2534,6 +2621,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -2556,6 +2644,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false } ], @@ -2596,6 +2685,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -2620,6 +2710,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -2644,6 +2735,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false }, { @@ -2674,6 +2766,7 @@ expression: stdout true ] }, + "env": null, "inherit": false }, { @@ -2704,6 +2797,7 @@ expression: stdout true ] }, + "env": null, "inherit": false }, { @@ -2734,6 +2828,7 @@ expression: stdout true ] }, + "env": null, "inherit": false }, { @@ -2756,6 +2851,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false } ], @@ -2799,6 +2895,7 @@ expression: stdout "value": "val" }, "choice": null, + "env": null, "inherit": false }, { @@ -2826,6 +2923,7 @@ expression: stdout "value": "_default_fn" }, "choice": null, + "env": null, "inherit": false }, { @@ -2860,6 +2958,7 @@ expression: stdout "z" ] }, + "env": null, "inherit": false }, { @@ -2882,6 +2981,7 @@ expression: stdout "assigned": false, "default": null, "choice": null, + "env": null, "inherit": false } ], diff --git a/tests/snapshots/integration__spec__option_help.snap b/tests/snapshots/integration__spec__option_help.snap index 645808b..f4d0a8e 100644 --- a/tests/snapshots/integration__spec__option_help.snap +++ b/tests/snapshots/integration__spec__option_help.snap @@ -9,7 +9,7 @@ prog options -h command cat >&2 <<-'EOF' All kind of options -USAGE: prog options [OPTIONS] --oc --oe ... --ocd ... +USAGE: prog options [OPTIONS] --oc --oe ... OPTIONS: --oa @@ -18,6 +18,7 @@ OPTIONS: --oc required --od [OD]... multi-occurs --oe ... required + multi-occurs + --of [OF]... multi-occurs + comma-separated list --ona value notation --onb two-args value notations --onc unlimited-args value notations @@ -26,7 +27,6 @@ OPTIONS: --oca choice [possible values: a, b] --ocb choice + default [default: a] [possible values: a, b] --occ [OCC]... multi-occurs + choice [possible values: a, b] - --ocd ... required + multi-occurs + choice [possible values: a, b] --ofa choice from fn --ofb choice from fn + no validation --ofc [OFC]... multi-occurs + choice from fn @@ -40,7 +40,7 @@ exit 0 # RUN_OUTPUT All kind of options -USAGE: prog options [OPTIONS] --oc --oe ... --ocd ... +USAGE: prog options [OPTIONS] --oc --oe ... OPTIONS: --oa @@ -49,6 +49,7 @@ OPTIONS: --oc required --od [OD]... multi-occurs --oe ... required + multi-occurs + --of [OF]... multi-occurs + comma-separated list --ona value notation --onb two-args value notations --onc unlimited-args value notations @@ -57,7 +58,6 @@ OPTIONS: --oca choice [possible values: a, b] --ocb choice + default [default: a] [possible values: a, b] --occ [OCC]... multi-occurs + choice [possible values: a, b] - --ocd ... required + multi-occurs + choice [possible values: a, b] --ofa choice from fn --ofb choice from fn + no validation --ofc [OFC]... multi-occurs + choice from fn diff --git a/tests/tests.rs b/tests/tests.rs index 5c1a23a..b9784e7 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -9,6 +9,7 @@ pub use fixtures::locate_script; #[macro_use] mod macros; +mod bind_env; mod cli; mod compgen; mod details; From c1d29dfeb87a678abe3525ad698d87a35beda5e2 Mon Sep 17 00:00:00 2001 From: sigoden Date: Fri, 12 Apr 2024 06:54:05 +0000 Subject: [PATCH 5/6] implement bind env for argc-build --- docs/specification.md | 9 +- src/build.rs | 115 ++++++++++++++++-- src/matcher.rs | 49 +++++--- tests/env.rs | 12 +- tests/macros.rs | 61 +++++----- .../integration__bind_env__bind_env_arg1.snap | 5 + .../integration__bind_env__bind_env_arg2.snap | 5 + ...on__bind_env__bind_env_arg_choice_err.snap | 5 +- ..._bind_env__bind_env_arg_choice_fn_err.snap | 5 +- ...env__bind_env_cmd_three_required_args.snap | 7 ++ ..._bind_env_cmd_three_required_args_err.snap | 1 + ...integration__bind_env__bind_env_flags.snap | 8 ++ ...on__bind_env__bind_env_flags_bool_err.snap | 3 +- ...ion__bind_env__bind_env_flags_bool_ok.snap | 5 + ...ration__bind_env__bind_env_flags_help.snap | 9 ++ ...ulti_arg_with_choice_fn_and_comma_sep.snap | 5 + ...tegration__bind_env__bind_env_options.snap | 16 +++ ...bind_env__bind_env_options_choice_err.snap | 5 +- ...d_env__bind_env_options_choice_fn_err.snap | 5 +- ..._bind_env__bind_env_options_choice_ok.snap | 8 ++ ...tion__bind_env__bind_env_options_help.snap | 16 +++ ...nd_env__bind_env_options_required_err.snap | 2 + 22 files changed, 284 insertions(+), 72 deletions(-) diff --git a/docs/specification.md b/docs/specification.md index 080a23d..1b876f0 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -75,7 +75,6 @@ Define a positional argument. # @arg vb! required # @arg vc* multi-values # @arg vd+ multi-values + required -# @arg vf*, multi-values + comma-separated list # @arg vna value notation # @arg vda=a default # @arg vdb=`_default_fn` default from fn @@ -88,8 +87,8 @@ Define a positional argument. # @arg vfc*[`_choice_fn`] multi-values + choice from fn # @arg vfd*,[`_choice_fn`] multi-values + choice from fn + comma-separated list # @arg vxa~ capture all remaining args -# @arg ea $$ bind-env -# @arg eb $BE bind-named-env +# @arg vea $$ bind-env +# @arg veb $BE bind-named-env ``` ### `@option` @@ -124,8 +123,8 @@ Define an option argument. # @option --ofc*[`_choice_fn`] multi-occurs + choice from fn # @option --ofd*,[`_choice_fn`] multi-occurs + choice from fn + comma-separated list # @option --oxa~ capture all remaining args -# @option --ea $$ bind-env -# @option --eb $BE bind-named-env +# @option --oea $$ bind-env +# @option --oeb $BE bind-named-env ``` ### `@flag` diff --git a/src/build.rs b/src/build.rs index 2062d40..fd1a713 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1,12 +1,12 @@ use crate::{ command::Command, - param::{FlagOptionParam, Param}, + param::{FlagOptionParam, Param, PositionalParam}, utils::{escape_shell_words, expand_dotenv}, ChoiceValue, DefaultValue, }; use anyhow::Result; -const UTIL_FNS: [(&str, &str); 5] = [ +const UTIL_FNS: [(&str, &str); 6] = [ ( "_argc_take_args", r#" @@ -160,6 +160,22 @@ _argc_validate_choices() { fi done } +"#, + ), + ( + "_argc_check_bool", + r#" +_argc_check_bool() { + local env_name="$1" param_name=$2 + local env_value="${!env_name}" + if [[ "$env_value" == "true" ]] || [[ "$env_value" == "1" ]]; then + return 0 + elif [[ "$env_value" == "false" ]] || [[ "$env_value" == "0" ]]; then + return 1 + else + _argc_die "error: environment variable '$env_name' has invalid value for param '$param_name'" + fi +} "#, ), ]; @@ -447,6 +463,7 @@ fn build_parse(cmd: &Command, suffix: &str) -> String { ) }; + let flag_option_bind_envs = build_flag_option_bind_envs(cmd); let required_flag_options = build_required_flag_options(cmd); let handle = build_handle(cmd, suffix); @@ -479,7 +496,7 @@ _argc_parse{suffix}() {{ _argc_key="${{_argc_item%%=*}}" case "$_argc_key" in{combined_case} esac - done{required_flag_options} + done{flag_option_bind_envs}{required_flag_options} if [[ -n "$_argc_action" ]]; then $_argc_action else{handle} @@ -688,6 +705,9 @@ fn build_positionals(cmd: &Command) -> String { } else { String::new() }; + + let bind_env = build_poistional_bind_env(param); + let handle_nonexist = format!("{default}{required}"); let handle_nonexist = if !handle_nonexist.is_empty() { format!( @@ -700,7 +720,7 @@ fn build_positionals(cmd: &Command) -> String { format!( r#" IFS=: read -r values_index values_size <<<"${{_argc_match_positionals_values[{index}]}}" - if [[ -n "$values_index" ]]; then{variant}{choice}{handle_nonexist} + if [[ -n "$values_index" ]]; then{variant}{choice}{bind_env}{handle_nonexist} fi"# ) }) @@ -713,6 +733,85 @@ fn build_positionals(cmd: &Command) -> String { ) } +fn build_flag_option_bind_envs(cmd: &Command) -> String { + let mut output = vec![]; + for param in &cmd.flag_option_params { + if let Some(env_name) = param.bind_env() { + let var_name = param.var_name(); + let render_name = param.render_name_notations(); + let code = if param.is_flag() { + format!( + r#" + if [[ -z "${var_name}" ]] && [[ -n "${env_name}" ]]; then + if _argc_check_bool {env_name} "{render_name}"; then + {var_name}=1 + fi + fi"# + ) + } else { + let handle_bind_env = build_handle_bind_env(param, &render_name, 2); + format!( + r#" + if [[ -z "${var_name}" ]] && [[ -n "${env_name}" ]]; then{handle_bind_env} + fi"# + ) + }; + output.push(code); + } + } + output.join("") +} + +fn build_poistional_bind_env(param: &PositionalParam) -> String { + match param.bind_env() { + None => String::new(), + Some(env_name) => { + let handle_bind_env = build_handle_bind_env(param, ¶m.render_notation(), 3); + format!( + r#" + elif [[ -n "${env_name}" ]]; then{handle_bind_env} + argc__positionals+=("${{_argc_env_values[@]}}")"# + ) + } + } +} + +fn build_handle_bind_env(param: &T, render_name: &str, indent_level: usize) -> String { + let indent = build_indent(indent_level); + let env_name = param.bind_env().unwrap_or_default(); + let var_name = param.var_name(); + let split_env = match param.args_delimiter() { + Some(delimiter) => format!( + r#" +{indent}IFS="{delimiter}" read -r -a _argc_env_values <<<"${env_name}""# + ), + None => format!( + r#" +{indent}_argc_env_values=("${env_name}")"# + ), + }; + + let choice = build_choice( + "{_argc_env_values[@]}", + &format!(r#"environment variable `{env_name}` that bound to `{render_name}`"#), + param.choice(), + indent_level, + ); + + let variant = if param.multiple_values() { + format!( + r#" +{indent}{var_name}=("${{_argc_env_values[@]}}")"# + ) + } else { + format!( + r#" +{indent}{var_name}="${{_argc_env_values[0]}}""# + ) + }; + format!(r#"{indent}{split_env}{choice}{variant}"#) +} + fn build_required_flag_options(cmd: &Command) -> String { let required_flag_options: Vec<_> = cmd .flag_option_params @@ -829,8 +928,8 @@ fn build_envs(cmd: &Command) -> String { .join("") } -fn build_default(var_name: &str, value: Option<&DefaultValue>, indent: usize) -> String { - let indent = build_indent(indent); +fn build_default(var_name: &str, value: Option<&DefaultValue>, indent_level: usize) -> String { + let indent = build_indent(indent_level); match value { Some(value) => match value { DefaultValue::Value(value) => { @@ -878,6 +977,6 @@ fn build_choice( } } -fn build_indent(indent: usize) -> String { - " ".repeat(indent) +fn build_indent(indent_level: usize) -> String { + " ".repeat(indent_level) } diff --git a/src/matcher.rs b/src/matcher.rs index 3a01f71..f4c0903 100644 --- a/src/matcher.rs +++ b/src/matcher.rs @@ -64,7 +64,7 @@ pub(crate) enum MatchError { MissingRequiredEnvironments(Vec), NotMultipleArgument(usize, String), InvalidValue(usize, String, String, Vec), - InvalidBindEnvironment(usize, String, String), + InvalidBindEnvironment(usize, String, String, String, Vec), InvalidEnvironment(usize, String, String, Vec), MismatchValues(usize, String), NoFlagValue(usize, String), @@ -657,6 +657,7 @@ impl<'a, 'b> Matcher<'a, 'b> { if let Some(param) = cmd.flag_option_params.iter().find(|v| v.id() == name) { let values = &flag_option_bind_envs[name]; let mut is_valid = true; + let mut choice_values = vec![]; let (min, _) = param.args_range(); if param.is_flag() { if !is_bool_value(values[0]) { @@ -665,24 +666,30 @@ impl<'a, 'b> Matcher<'a, 'b> { } else if min > 1 { is_valid = false; } - if is_valid { - if let Some(choices) = - get_param_choice(¶m.data.choice, &choices_fn_values) - { - for value in values.iter() { - if !choices.contains(&value.to_string()) { - is_valid = false; - } - } - } - } if !is_valid { return Some(MatchError::InvalidBindEnvironment( level, + values[0].to_string(), param.bind_env().unwrap_or_default(), param.render_long_name(), + choice_values, )); } + if let Some(choices) = get_param_choice(¶m.data.choice, &choices_fn_values) + { + choice_values = choices.to_vec(); + for value in values.iter() { + if !choices.contains(&value.to_string()) { + return Some(MatchError::InvalidBindEnvironment( + level, + value.to_string(), + param.bind_env().unwrap_or_default(), + param.render_long_name(), + choice_values, + )); + } + } + } } } @@ -769,8 +776,10 @@ impl<'a, 'b> Matcher<'a, 'b> { if !choices.contains(&value.to_string()) { return Some(MatchError::InvalidBindEnvironment( level, + value.to_string(), param.bind_env().unwrap_or_default(), param.render_notation(), + choices.to_vec(), )); } } @@ -951,11 +960,19 @@ impl<'a, 'b> Matcher<'a, 'b> { [possible values: {list}]"### ) } - MatchError::InvalidBindEnvironment(_level, env_name, param_name) => { + MatchError::InvalidBindEnvironment(_level, value, env_name, name, choices) => { exit = 1; - format!( - r###"error: environment variable '{env_name}' has invalid value for param '{param_name}'"### - ) + if choices.is_empty() { + format!( + r###"error: environment variable `{env_name}` has invalid value for param '{name}'"### + ) + } else { + let list = choices.join(", "); + format!( + r###"error: invalid value `{value}` for environment variable `{env_name}` that bound to `{name}` + [possible values: {list}]"### + ) + } } MatchError::InvalidEnvironment(_level, value, name, choices) => { exit = 1; diff --git a/tests/env.rs b/tests/env.rs index c197a08..06bb38d 100644 --- a/tests/env.rs +++ b/tests/env.rs @@ -2,30 +2,30 @@ use rstest::rstest; #[rstest] fn env_help() { - snapshot_env!(args: ["-h"], envs: {}); + snapshot_meta_env!(["-h"], {}); } #[rstest] fn env_help_subcmd() { - snapshot_env!(args: ["run", "-h"], envs: {}); + snapshot_meta_env!(["run", "-h"], {}); } #[rstest] fn env_missing() { - snapshot_env!(args: [], envs: {}); + snapshot_meta_env!([], {}); } #[rstest] fn env_choice() { - snapshot_env!(args: [], envs: {"TEST_EB": "1", "TEST_ECA": "val"}); + snapshot_meta_env!([], {"TEST_EB": "1", "TEST_ECA": "val"}); } #[rstest] fn env_choice_fn() { - snapshot_env!(args: [], envs: {"TEST_EB": "1", "TEST_EFA": "val"}); + snapshot_meta_env!([], {"TEST_EB": "1", "TEST_EFA": "val"}); } #[rstest] fn env_run() { - snapshot_env!(args: [], envs: {"TEST_EB": "1"}); + snapshot_meta_env!([], {"TEST_EB": "1"}); } diff --git a/tests/macros.rs b/tests/macros.rs index 9874868..891d2b9 100644 --- a/tests/macros.rs +++ b/tests/macros.rs @@ -169,54 +169,51 @@ macro_rules! snapshot_compgen_shells { }; } -macro_rules! snapshot_env { +macro_rules! snapshot_meta_env { ( - args: [$($arg:literal),*], - envs: {$($key:literal : $value:literal),*} - + [$($arg:literal),*], + {$($key:literal : $value:literal),*$(,)?} ) => { - let script_path = $crate::fixtures::locate_script("examples/envs.sh"); - let args: Vec = vec![$($arg.to_string(),)*]; - let envs: Vec<(&str, &str)> = [$(($key, $value),)*].into_iter().collect(); - - let output = $crate::fixtures::run_script(&script_path, &args, &envs); - - let build_output = { - let build_script_dir = $crate::fixtures::tmpdir(); - let source = std::fs::read_to_string(&script_path).unwrap(); - let build_script_path = $crate::fixtures::build_script(&build_script_dir, &source); - $crate::fixtures::run_script(&build_script_path, &args, &envs) - }; + snapshot_env!( + args: [$($arg),*], + envs: {$($key : $value),*} + script_file: "examples/envs.sh" + ); + } +} - insta::assert_snapshot!(format!(r#" -# OUTPUT -{output} +macro_rules! snapshot_bind_env { + ( + args: [$($arg:literal),*], + envs: {$($key:literal : $value:literal),*$(,)?} -# BUILD_OUTPUT -{build_output} -"#)); + ) => { + snapshot_env!( + args: [$($arg),*], + envs: {$($key : $value),*} + script_file: "examples/bind-envs.sh" + ); }; } -macro_rules! snapshot_bind_env { +macro_rules! snapshot_env { ( args: [$($arg:literal),*], envs: {$($key:literal : $value:literal),*$(,)?} - + script_file: $script_file:literal ) => { - let script_path = $crate::fixtures::locate_script("examples/bind-envs.sh"); + let script_path = $crate::fixtures::locate_script($script_file); let args: Vec = vec![$($arg.to_string(),)*]; let envs: Vec<(&str, &str)> = [$(($key, $value),)*].into_iter().collect(); let output = $crate::fixtures::run_script(&script_path, &args, &envs); - // let build_output = { - // let build_script_dir = $crate::fixtures::tmpdir(); - // let source = std::fs::read_to_string(&script_path).unwrap(); - // let build_script_path = $crate::fixtures::build_script(&build_script_dir, &source); - // $crate::fixtures::run_script(&build_script_path, &args, &envs) - // }; - let build_output = ""; + let build_output = { + let build_script_dir = $crate::fixtures::tmpdir(); + let source = std::fs::read_to_string(&script_path).unwrap(); + let build_script_path = $crate::fixtures::build_script(&build_script_dir, &source); + $crate::fixtures::run_script(&build_script_path, &args, &envs) + }; insta::assert_snapshot!(format!(r#" # OUTPUT diff --git a/tests/snapshots/integration__bind_env__bind_env_arg1.snap b/tests/snapshots/integration__bind_env__bind_env_arg1.snap index ae6b22c..ba652b4 100644 --- a/tests/snapshots/integration__bind_env__bind_env_arg1.snap +++ b/tests/snapshots/integration__bind_env__bind_env_arg1.snap @@ -11,3 +11,8 @@ cmd_arg1 v1 # BUILD_OUTPUT +argc__args=([0]="prog" [1]="cmd_arg1") +argc__fn=cmd_arg1 +argc__positionals=([0]="v1") +argc_val=v1 +cmd_arg1 v1 diff --git a/tests/snapshots/integration__bind_env__bind_env_arg2.snap b/tests/snapshots/integration__bind_env__bind_env_arg2.snap index 32e1b59..691d605 100644 --- a/tests/snapshots/integration__bind_env__bind_env_arg2.snap +++ b/tests/snapshots/integration__bind_env__bind_env_arg2.snap @@ -11,3 +11,8 @@ cmd_arg2 v1 # BUILD_OUTPUT +argc__args=([0]="prog" [1]="cmd_arg2") +argc__fn=cmd_arg2 +argc__positionals=([0]="v1") +argc_val=v1 +cmd_arg2 v1 diff --git a/tests/snapshots/integration__bind_env__bind_env_arg_choice_err.snap b/tests/snapshots/integration__bind_env__bind_env_arg_choice_err.snap index 4c6800f..9940b5a 100644 --- a/tests/snapshots/integration__bind_env__bind_env_arg_choice_err.snap +++ b/tests/snapshots/integration__bind_env__bind_env_arg_choice_err.snap @@ -3,7 +3,10 @@ source: tests/bind_env.rs expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" --- # OUTPUT -error: environment variable 'VAL' has invalid value for param '[VAL]' +error: invalid value `v1` for environment variable `VAL` that bound to `[VAL]` + [possible values: x, y, z] # BUILD_OUTPUT +error: invalid value `v1` for environment variable `VAL` that bound to `[VAL]` + [possible values: x, y, z] diff --git a/tests/snapshots/integration__bind_env__bind_env_arg_choice_fn_err.snap b/tests/snapshots/integration__bind_env__bind_env_arg_choice_fn_err.snap index 4c6800f..7aaca3b 100644 --- a/tests/snapshots/integration__bind_env__bind_env_arg_choice_fn_err.snap +++ b/tests/snapshots/integration__bind_env__bind_env_arg_choice_fn_err.snap @@ -3,7 +3,10 @@ source: tests/bind_env.rs expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" --- # OUTPUT -error: environment variable 'VAL' has invalid value for param '[VAL]' +error: invalid value `v1` for environment variable `VAL` that bound to `[VAL]` + [possible values: abc, def, ghi] # BUILD_OUTPUT +error: invalid value `v1` for environment variable `VAL` that bound to `[VAL]` + [possible values: abc, def, ghi] diff --git a/tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args.snap b/tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args.snap index 86c4505..090d76f 100644 --- a/tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args.snap +++ b/tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args.snap @@ -13,3 +13,10 @@ cmd_three_required_args v1 v2 v3 # BUILD_OUTPUT +argc__args=([0]="prog" [1]="cmd_three_required_args") +argc__fn=cmd_three_required_args +argc__positionals=([0]="v1" [1]="v2" [2]="v3") +argc_val1=v1 +argc_val2=v2 +argc_val3=v3 +cmd_three_required_args v1 v2 v3 diff --git a/tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args_err.snap b/tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args_err.snap index c644e3b..10c3d23 100644 --- a/tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args_err.snap +++ b/tests/snapshots/integration__bind_env__bind_env_cmd_three_required_args_err.snap @@ -8,3 +8,4 @@ error: the following required arguments were not provided: # BUILD_OUTPUT +error: the required environments `` were not provided diff --git a/tests/snapshots/integration__bind_env__bind_env_flags.snap b/tests/snapshots/integration__bind_env__bind_env_flags.snap index 92f75e5..b0ef4da 100644 --- a/tests/snapshots/integration__bind_env__bind_env_flags.snap +++ b/tests/snapshots/integration__bind_env__bind_env_flags.snap @@ -14,3 +14,11 @@ flags # BUILD_OUTPUT +argc__args=([0]="prog" [1]="flags") +argc__fn=flags +argc__positionals=() +argc_fa1=1 +argc_fa3=1 +argc_fc=1 +argc_fd=1 +flags diff --git a/tests/snapshots/integration__bind_env__bind_env_flags_bool_err.snap b/tests/snapshots/integration__bind_env__bind_env_flags_bool_err.snap index 8b1e18d..f9b4f9d 100644 --- a/tests/snapshots/integration__bind_env__bind_env_flags_bool_err.snap +++ b/tests/snapshots/integration__bind_env__bind_env_flags_bool_err.snap @@ -3,7 +3,8 @@ source: tests/bind_env.rs expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" --- # OUTPUT -error: environment variable 'FA1' has invalid value for param '--fa1' +error: environment variable `FA1` has invalid value for param '--fa1' # BUILD_OUTPUT +error: environment variable 'FA1' has invalid value for param '--fa1' diff --git a/tests/snapshots/integration__bind_env__bind_env_flags_bool_ok.snap b/tests/snapshots/integration__bind_env__bind_env_flags_bool_ok.snap index 27f4189..1d101fb 100644 --- a/tests/snapshots/integration__bind_env__bind_env_flags_bool_ok.snap +++ b/tests/snapshots/integration__bind_env__bind_env_flags_bool_ok.snap @@ -11,3 +11,8 @@ flags # BUILD_OUTPUT +argc__args=([0]="prog" [1]="flags" [2]="--fa1") +argc__fn=flags +argc__positionals=() +argc_fa1=1 +flags diff --git a/tests/snapshots/integration__bind_env__bind_env_flags_help.snap b/tests/snapshots/integration__bind_env__bind_env_flags_help.snap index d3ff883..cab1f99 100644 --- a/tests/snapshots/integration__bind_env__bind_env_flags_help.snap +++ b/tests/snapshots/integration__bind_env__bind_env_flags_help.snap @@ -16,3 +16,12 @@ OPTIONS: # BUILD_OUTPUT +USAGE: prog flags [OPTIONS] + +OPTIONS: + --fa1 [env: FA1] + --fa2 [env: FA2] + --fa3 [env: FA] + --fc... [env: FC] + --fd [env: FD] + -h, --help diff --git a/tests/snapshots/integration__bind_env__bind_env_multi_arg_with_choice_fn_and_comma_sep.snap b/tests/snapshots/integration__bind_env__bind_env_multi_arg_with_choice_fn_and_comma_sep.snap index ab295b2..e1ed6be 100644 --- a/tests/snapshots/integration__bind_env__bind_env_multi_arg_with_choice_fn_and_comma_sep.snap +++ b/tests/snapshots/integration__bind_env__bind_env_multi_arg_with_choice_fn_and_comma_sep.snap @@ -11,3 +11,8 @@ cmd_multi_arg_with_choice_fn_and_comma_sep abc def # BUILD_OUTPUT +argc__args=([0]="prog" [1]="cmd_multi_arg_with_choice_fn_and_comma_sep") +argc__fn=cmd_multi_arg_with_choice_fn_and_comma_sep +argc__positionals=([0]="abc" [1]="def") +argc_val=([0]="abc" [1]="def") +cmd_multi_arg_with_choice_fn_and_comma_sep abc def diff --git a/tests/snapshots/integration__bind_env__bind_env_options.snap b/tests/snapshots/integration__bind_env__bind_env_options.snap index 3b95fd6..913207e 100644 --- a/tests/snapshots/integration__bind_env__bind_env_options.snap +++ b/tests/snapshots/integration__bind_env__bind_env_options.snap @@ -22,3 +22,19 @@ options # BUILD_OUTPUT +argc__args=([0]="prog" [1]="options") +argc__fn=options +argc__positionals=() +argc_oa1=oa1 +argc_oa2=oa2 +argc_oa3=oa3 +argc_ob=ob +argc_oc=([0]="v1" [1]="v2") +argc_oca=a +argc_occ=([0]="a") +argc_oda=oda +argc_odb=argc +argc_ofa=abc +argc_ofd=([0]="abc" [1]="def") +argc_oxa=([0]="oxa") +options diff --git a/tests/snapshots/integration__bind_env__bind_env_options_choice_err.snap b/tests/snapshots/integration__bind_env__bind_env_options_choice_err.snap index 6ac8741..bdbaa2a 100644 --- a/tests/snapshots/integration__bind_env__bind_env_options_choice_err.snap +++ b/tests/snapshots/integration__bind_env__bind_env_options_choice_err.snap @@ -3,7 +3,10 @@ source: tests/bind_env.rs expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" --- # OUTPUT -error: environment variable 'OCA' has invalid value for param '--oca' +error: invalid value `oca` for environment variable `OCA` that bound to `--oca` + [possible values: a, b] # BUILD_OUTPUT +error: invalid value `oca` for environment variable `OCA` that bound to `--oca ` + [possible values: a, b] diff --git a/tests/snapshots/integration__bind_env__bind_env_options_choice_fn_err.snap b/tests/snapshots/integration__bind_env__bind_env_options_choice_fn_err.snap index c75fc5c..e81f99f 100644 --- a/tests/snapshots/integration__bind_env__bind_env_options_choice_fn_err.snap +++ b/tests/snapshots/integration__bind_env__bind_env_options_choice_fn_err.snap @@ -3,7 +3,10 @@ source: tests/bind_env.rs expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" --- # OUTPUT -error: environment variable 'OFA' has invalid value for param '--ofa' +error: invalid value `ofa` for environment variable `OFA` that bound to `--ofa` + [possible values: abc, def, ghi] # BUILD_OUTPUT +error: invalid value `ofa` for environment variable `OFA` that bound to `--ofa ` + [possible values: abc, def, ghi] diff --git a/tests/snapshots/integration__bind_env__bind_env_options_choice_ok.snap b/tests/snapshots/integration__bind_env__bind_env_options_choice_ok.snap index d0d1cc4..84490a4 100644 --- a/tests/snapshots/integration__bind_env__bind_env_options_choice_ok.snap +++ b/tests/snapshots/integration__bind_env__bind_env_options_choice_ok.snap @@ -14,3 +14,11 @@ options # BUILD_OUTPUT +argc__args=([0]="prog" [1]="options" [2]="--oca" [3]="a") +argc__fn=options +argc__positionals=() +argc_ob=ob +argc_oca=a +argc_oda=a +argc_odb=argc +options diff --git a/tests/snapshots/integration__bind_env__bind_env_options_help.snap b/tests/snapshots/integration__bind_env__bind_env_options_help.snap index 3482412..7a9c4ef 100644 --- a/tests/snapshots/integration__bind_env__bind_env_options_help.snap +++ b/tests/snapshots/integration__bind_env__bind_env_options_help.snap @@ -23,3 +23,19 @@ OPTIONS: # BUILD_OUTPUT +USAGE: prog options [OPTIONS] --ob + +OPTIONS: + --oa1 [env: OA1] + --oa2 [env: OA2] + --oa3 [env: OA] + --ob [env: OB] + --oc [OC]... [env: OC] + --oda [default: a] [env: ODA] + --odb [env: ODB] + --oca [possible values: a, b] [env: OCA] + --occ [OCC]... [possible values: a, b] [env: OCC] + --ofa [env: OFA] + --ofd [OFD]... [env: OFD] + --oxa [env: OXA] + -h, --help diff --git a/tests/snapshots/integration__bind_env__bind_env_options_required_err.snap b/tests/snapshots/integration__bind_env__bind_env_options_required_err.snap index be67aec..f055814 100644 --- a/tests/snapshots/integration__bind_env__bind_env_options_required_err.snap +++ b/tests/snapshots/integration__bind_env__bind_env_options_required_err.snap @@ -8,3 +8,5 @@ error: the following required arguments were not provided: # BUILD_OUTPUT +error: the following required arguments were not provided: + --ob From 28156e6bf2d6a3d94e99ceb79ea57f6428e0bd13 Mon Sep 17 00:00:00 2001 From: sigoden Date: Fri, 12 Apr 2024 07:51:59 +0000 Subject: [PATCH 6/6] change syntax: bind-env is head of notations --- README.md | 1 + docs/specification.md | 12 ++--- examples/bind-envs.sh | 7 +++ src/param.rs | 53 ++++++++++++------- src/parser.rs | 14 ++--- tests/bind_env.rs | 12 +++++ ...ion__bind_env__bind_env_with_notation.snap | 20 +++++++ ...bind_env__bind_env_with_notation_help.snap | 25 +++++++++ 8 files changed, 112 insertions(+), 32 deletions(-) create mode 100644 tests/snapshots/integration__bind_env__bind_env_with_notation.snap create mode 100644 tests/snapshots/integration__bind_env__bind_env_with_notation_help.snap diff --git a/README.md b/README.md index bd63629..0efa849 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Argc lets you define your CLI through comments and focus on your specific code, - **Build bashscript**: Build a single standalone bashscript without argc dependency. - **Cross-shell autocompletion**: Generate completion scripts for bash, zsh, fish, powershell, and more. - **Man page**: Generate manage page documentation for your script. +- **Environment variables**: Document, validating and binding to option and positional arguments. - **Task runner**: An ideal task runner in Bash to automate the execution of predefined tasks with Argcfile.sh. - **Self documentation**: Comments with tags are CLI definitions, documentation, usage text. - **Cross platform**: A single executable file that can run on macOS, Linux, Windows, and BSD systems. diff --git a/docs/specification.md b/docs/specification.md index 1b876f0..b491d7e 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -66,8 +66,8 @@ Define a positional argument. > **Syntax**\ > `@arg` [_name_] [_modifier_]? [_param-value_]? -> [_notation_]? > [_bind-env_]? +> [_notation_]? > [_description_]? ```sh @@ -87,8 +87,8 @@ Define a positional argument. # @arg vfc*[`_choice_fn`] multi-values + choice from fn # @arg vfd*,[`_choice_fn`] multi-values + choice from fn + comma-separated list # @arg vxa~ capture all remaining args -# @arg vea $$ bind-env -# @arg veb $BE bind-named-env +# @arg vea $$ bind-env +# @arg veb $BE bind-named-env ``` ### `@option` @@ -97,8 +97,8 @@ Define an option argument. > **Syntax**\ > `@option` [_short_]? [_long_] [_modifier_]? [_param-value_]? -> [_notations_]? > [_bind-env_]? +> [_notations_]? > [_description_]? ```sh @@ -123,8 +123,8 @@ Define an option argument. # @option --ofc*[`_choice_fn`] multi-occurs + choice from fn # @option --ofd*,[`_choice_fn`] multi-occurs + choice from fn + comma-separated list # @option --oxa~ capture all remaining args -# @option --oea $$ bind-env -# @option --oeb $BE bind-named-env +# @option --oea $$ bind-env +# @option --oeb $BE bind-named-env ``` ### `@flag` diff --git a/examples/bind-envs.sh b/examples/bind-envs.sh index 80d90f1..93cf805 100644 --- a/examples/bind-envs.sh +++ b/examples/bind-envs.sh @@ -69,6 +69,13 @@ cmd_three_required_args() { _debug "$@"; } +# @cmd +# @option --OA $$ +# @arg val $$ +cmd_for_notation() { + _debug "$@"; +} + _debug() { ( set -o posix ; set ) | grep ^argc_ echo "$argc__fn" "$@" diff --git a/src/param.rs b/src/param.rs index 635e39f..a7d54e2 100644 --- a/src/param.rs +++ b/src/param.rs @@ -136,11 +136,22 @@ impl Param for FlagOptionParam { self.long_prefix, self.data.render_source_of_name_value(&name_suffix) )); + + if let Some(bind_env) = &self.data.bind_env { + match bind_env { + Some(v) => output.push(format!("${v}")), + None => output.push("$$".into()), + } + } + for raw_notation in &self.raw_notations { output.push(format!("<{}>", raw_notation)); } - self.data - .render_source_of_bind_env_and_describe(&mut output); + + if !self.data.describe.is_empty() { + output.push(self.data.describe.clone()); + } + output.join(" ") } } @@ -478,13 +489,20 @@ impl Param for PositionalParam { } fn render_source(&self) -> String { - let mut output = vec![]; - output.push(self.data.render_source_of_name_value("")); + let mut output = vec![self.data.render_source_of_name_value("")]; + + if let Some(bind_env) = &self.data.bind_env { + match bind_env { + Some(v) => output.push(format!("${v}")), + None => output.push("$$".into()), + } + } if let Some(raw_notation) = self.raw_notation.as_ref() { output.push(format!("<{}>", raw_notation)); } - self.data - .render_source_of_bind_env_and_describe(&mut output); + if !self.data.describe.is_empty() { + output.push(self.data.describe.clone()); + } output.join(" ") } } @@ -609,8 +627,15 @@ impl Param for EnvParam { fn render_source(&self) -> String { let mut output = vec![self.data.render_source_of_name_value("")]; - self.data - .render_source_of_bind_env_and_describe(&mut output); + if let Some(bind_env) = &self.data.bind_env { + match bind_env { + Some(v) => output.push(format!("${v}")), + None => output.push("$$".into()), + } + } + if !self.data.describe.is_empty() { + output.push(self.data.describe.clone()); + } output.join(" ") } } @@ -763,18 +788,6 @@ impl ParamData { output } - pub(crate) fn render_source_of_bind_env_and_describe(&self, parts: &mut Vec) { - if let Some(bind_env) = &self.bind_env { - match bind_env { - Some(v) => parts.push(format!("${v}")), - None => parts.push("$$".into()), - } - } - if !self.describe.is_empty() { - parts.push(self.describe.clone()); - } - } - pub(crate) fn render_describe(&self, describe: &str, id: &str) -> String { let mut output = describe.to_string(); let multiline = output.contains('\n'); diff --git a/src/parser.rs b/src/parser.rs index c754d94..fbe7bdd 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -248,11 +248,11 @@ fn parse_with_long_option_param(input: &str) -> nom::IResult<&str, FlagOptionPar parse_param_assign, parse_param_modifer, )), - parse_zero_or_many_value_notations, parse_zero_or_one_bind_env, + parse_zero_or_many_value_notations, parse_tail, )), - |((short, long_prefix), mut arg, value_names, bind_env, describe)| { + |((short, long_prefix), mut arg, bind_env, value_names, describe)| { arg.bind_env = bind_env; arg.describe = describe.to_string(); FlagOptionParam::new(arg, false, short, long_prefix, &value_names) @@ -276,11 +276,11 @@ fn parse_no_long_option_param(input: &str) -> nom::IResult<&str, FlagOptionParam parse_param_modifer, )), ), - parse_zero_or_many_value_notations, parse_zero_or_one_bind_env, + parse_zero_or_many_value_notations, parse_tail, )), - |(long_prefix, mut arg, value_names, bind_env, describe)| { + |(long_prefix, mut arg, bind_env, value_names, describe)| { arg.bind_env = bind_env; arg.describe = describe.to_string(); FlagOptionParam::new(arg, false, None, long_prefix, &value_names) @@ -318,11 +318,11 @@ fn parse_positional_param(input: &str) -> nom::IResult<&str, PositionalParam> { parse_param_assign, parse_param_modifer, )), - parse_zero_or_one_value_notation, parse_zero_or_one_bind_env, + parse_zero_or_one_value_notation, parse_tail, )), - |(mut arg, value_name, bind_env, describe)| { + |(mut arg, bind_env, value_name, describe)| { arg.bind_env = bind_env; arg.describe = describe.to_string(); PositionalParam::new(arg, value_name) @@ -940,6 +940,7 @@ mod tests { assert_parse_option_arg!("--foo <>"); assert_parse_option_arg!("--foo $$"); assert_parse_option_arg!("--foo $FOO"); + assert_parse_option_arg!("--foo $FOO "); } #[test] @@ -984,6 +985,7 @@ mod tests { assert_parse_option_arg!("-foo <>"); assert_parse_option_arg!("-foo $$"); assert_parse_option_arg!("-foo $FOO"); + assert_parse_option_arg!("-foo $FOO "); } #[test] diff --git a/tests/bind_env.rs b/tests/bind_env.rs index 0f08f08..752ef18 100644 --- a/tests/bind_env.rs +++ b/tests/bind_env.rs @@ -133,3 +133,15 @@ fn bind_env_cmd_three_required_args_err() { "VAL2": "v2", }); } + +#[rstest] +fn bind_env_with_notation() { + snapshot_bind_env!(args: ["cmd_for_notation"], envs: { + "OA": "oa", + "VAL": "v1", + }); +} +#[rstest] +fn bind_env_with_notation_help() { + snapshot_bind_env!(args: ["cmd_for_notation", "-h"], envs: {}); +} diff --git a/tests/snapshots/integration__bind_env__bind_env_with_notation.snap b/tests/snapshots/integration__bind_env__bind_env_with_notation.snap new file mode 100644 index 0000000..3c92ab5 --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_with_notation.snap @@ -0,0 +1,20 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +argc_OA=oa +argc__args=([0]="bind-envs" [1]="cmd_for_notation") +argc__fn=cmd_for_notation +argc__positionals=([0]="v1") +argc_val=v1 +cmd_for_notation v1 + + +# BUILD_OUTPUT +argc_OA=oa +argc__args=([0]="prog" [1]="cmd_for_notation") +argc__fn=cmd_for_notation +argc__positionals=([0]="v1") +argc_val=v1 +cmd_for_notation v1 diff --git a/tests/snapshots/integration__bind_env__bind_env_with_notation_help.snap b/tests/snapshots/integration__bind_env__bind_env_with_notation_help.snap new file mode 100644 index 0000000..19e8f5f --- /dev/null +++ b/tests/snapshots/integration__bind_env__bind_env_with_notation_help.snap @@ -0,0 +1,25 @@ +--- +source: tests/bind_env.rs +expression: "format!(r#\"\n# OUTPUT\n{output}\n\n# BUILD_OUTPUT\n{build_output}\n\"#)" +--- +# OUTPUT +USAGE: bind-envs cmd_for_notation [OPTIONS] [XYZ] + +ARGS: + [XYZ] [env: VAL] + +OPTIONS: + --OA [env: OA] + -h, --help + + + +# BUILD_OUTPUT +USAGE: prog cmd_for_notation [OPTIONS] [XYZ] + +ARGS: + [XYZ] [env: VAL] + +OPTIONS: + --OA [env: OA] + -h, --help