Skip to content

Commit

Permalink
feat: parameters binding environment variables (#312)
Browse files Browse the repository at this point in the history
* feat: support param bind-env

* update specification.md

* modified parser and param

* implement bind env

* implement bind env for argc-build

* change syntax: bind-env is head of notations
  • Loading branch information
sigoden authored Apr 12, 2024
1 parent 2f17ebe commit 60278d9
Show file tree
Hide file tree
Showing 39 changed files with 1,373 additions and 226 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions docs/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Define a positional argument.

> **<sup>Syntax</sup>**\
> `@arg` [_name_] [_modifier_]<sup>?</sup> [_param-value_]<sup>?</sup>
> [_bind-env_]<sup>?</sup>
> [_notation_]<sup>?</sup>
> [_description_]<sup>?</sup>
Expand All @@ -86,6 +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 <PATH> bind-named-env
```

### `@option`
Expand All @@ -94,6 +97,7 @@ Define an option argument.

> **<sup>Syntax</sup>**\
> `@option` [_short_]<sup>?</sup> [_long_] [_modifier_]<sup>?</sup> [_param-value_]<sup>?</sup>
> [_bind-env_]<sup>?</sup>
> [_notations_]<sup>?</sup>
> [_description_]<sup>?</sup>
Expand All @@ -104,6 +108,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 <PATH> value notation
# @option --onb <FILE> <FILE> two-args value notations
# @option --onc <CMD> <FILE+> unlimited-args value notations
Expand All @@ -118,6 +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 <PATH> bind-named-env
```

### `@flag`
Expand All @@ -126,13 +133,16 @@ Define a flag argument. Flag is a special option that does not accept any value.

> **<sup>Syntax</sup>**\
> `@flag` [_short_]<sup>?</sup> [_long_]`*`<sup>?</sup>
> [_bind-env_]<sup>?</sup>
> [_description_]<sup>?</sup>
```sh
# @flag --fa
# @flag -b --fb short
# @flag -c short only
# @flag --fd* multi-occurs
# @flag --ea $$ bind-env
# @flag --eb $BE bind-named-env
```

### `@env`
Expand Down Expand Up @@ -290,6 +300,13 @@ A-Z a-z 0-9 `!` `#` `$` `%` `*` `+` `,` `.` `/` `:` `=` `?` `@` `[` `]` `^` `_`

`,` `:` `@` `|` `/`

### bind-env

Flags/options bind environment variables

- `$$`: bind environment variable whose name is derived from the corresponding param name
- `$`[_NAME_]: bind environment variable whose name is *NAME*

## description

Plain text, can be multiple lines.
Expand All @@ -313,6 +330,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
Expand Down
94 changes: 94 additions & 0 deletions examples/bind-envs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# @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 "$@";
}

# @cmd
# @option --OA $$ <XYZ>
# @arg val $$ <XYZ>
cmd_for_notation() {
_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" "$@")"
2 changes: 1 addition & 1 deletion examples/options.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <PATH> value notation
# @option --onb <FILE> <FILE> two-args value notations
# @option --onc <CMD> <FILE+> unlimited-args value notations
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/bin/argc/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 _};
Expand Down Expand Up @@ -214,7 +214,7 @@ fn run_compgen(mut args: Vec<String>) -> 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..];
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)
}
Loading

0 comments on commit 60278d9

Please sign in to comment.