Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: parameters binding environment variables #312

Merged
merged 6 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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