Skip to content

Commit

Permalink
implement bind env for argc-build
Browse files Browse the repository at this point in the history
  • Loading branch information
sigoden committed Apr 12, 2024
1 parent 823f229 commit c1d29df
Show file tree
Hide file tree
Showing 22 changed files with 284 additions and 72 deletions.
9 changes: 4 additions & 5 deletions docs/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <PATH> value notation
# @arg vda=a default
# @arg vdb=`_default_fn` default from fn
Expand All @@ -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`
Expand Down Expand Up @@ -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`
Expand Down
115 changes: 107 additions & 8 deletions src/build.rs
Original file line number Diff line number Diff line change
@@ -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#"
Expand Down Expand Up @@ -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
}
"#,
),
];
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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!(
Expand All @@ -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"#
)
})
Expand All @@ -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, &param.render_notation(), 3);
format!(
r#"
elif [[ -n "${env_name}" ]]; then{handle_bind_env}
argc__positionals+=("${{_argc_env_values[@]}}")"#
)
}
}
}

fn build_handle_bind_env<T: Param>(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
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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)
}
49 changes: 33 additions & 16 deletions src/matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub(crate) enum MatchError {
MissingRequiredEnvironments(Vec<String>),
NotMultipleArgument(usize, String),
InvalidValue(usize, String, String, Vec<String>),
InvalidBindEnvironment(usize, String, String),
InvalidBindEnvironment(usize, String, String, String, Vec<String>),
InvalidEnvironment(usize, String, String, Vec<String>),
MismatchValues(usize, String),
NoFlagValue(usize, String),
Expand Down Expand Up @@ -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]) {
Expand All @@ -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(&param.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(&param.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,
));
}
}
}
}
}

Expand Down Expand Up @@ -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(),
));
}
}
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 6 additions & 6 deletions tests/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"});
}
Loading

0 comments on commit c1d29df

Please sign in to comment.