This commit is contained in:
2024-08-16 15:54:54 +00:00
parent 7e8e5ff2a8
commit cccb969400
2 changed files with 1119 additions and 0 deletions

273
README.md Normal file
View File

@@ -0,0 +1,273 @@
# Sh args parser
This is POSIX-compliant code for parsing shell script arguments. It helps you focus on writing the script logic by separating argument parsing and basic validation.
## Contents
1. [Installation](#installation)
2. [Usage](#usage)
3. [Syntax conventions](#argument-syntax-conventions)
4. [Configuration](#configuration)
## Installation
Include the `parser.sh` file in your script using [`dot`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#dot) or [`source`](https://www.gnu.org/software/bash/manual/bash.html#index-source).
```bash
#!/usr/bin/env sh
. parser.sh
```
Or copy the contents of the `parser.sh` file into your script.
## Usage
The parser provides functions for working with arguments. They become available after passing all script arguments to the parser using `_parse`:
```bash
#!/usr/bin/env sh
. parser.sh
# In most cases, all script arguments are passed to _parse: $@.
_parse --foo bar baz
_is_used_option '--foo' && echo "Option 'foo' is used"
_get_option_args_count '--foo'
_get_option_arg '--foo' 2
```
```
Option 'foo' is used
2
baz
```
### What's next?
1. [Learn about the argument syntax.](#argument-syntax-conventions)
2. [Configure the parser to suit your needs.](#configuration)
3. [Map commands.](#_map_command)
4. [Map options.](#_map_option)
5. [Learn about the functions for working with parsing results.](#functions-for-working-with-parsing-results)
## Argument Syntax Conventions
The argument syntax is based on, but does not fully comply with, the conventions used in POSIX <sup>[1](https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html), [2](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html)</sup>.
### General
- Commands, options, and positional arguments are separated by spaces.
- All script arguments are case-sensitive.
### Commands
A command typically represents the core action performed by the script: `run`, `build`, `issue`, etc. If the script is designed to perform a single action, a command is generally not required.
- A script may or may not accept a command.
- If the script accepts a command, it must be the first argument.
- A command can contain Latin letters, digits, `-`, and `_`.
### Positional Arguments
- Positional arguments are placed after the command, if present.
- The presence and number of positional arguments are determined by the script's logic.
- After the special argument --, all arguments are considered positional. This allows passing arguments that start with a dash and might be confused with options.
### Options
Options are script parameters. Each option has at least one alias by which it can be specified in the command line. Options often have both short and long aliases, such as `-h` and `--help`.
- Short aliases start with `-` and consist of a single Latin letter;
- Long aliases start with `--` and consist of Latin letters, digits, `-`, and `_`;
- Short aliases can be used individually or combined. For example, `-b -a -r` is equivalent to `-bar`;
- Options may have arguments. The presence and number of arguments are determined by the specific option and the script's logic.
## Configuration
Configuration is defined by assigning values to variables before using the parser. For example:
```bash
_accept_command=true
```
Configuration variables must be assigned before calling `_parse`.
| Name | Default Value | Description |
| --- | --- | --- |
| _accept_command | `auto` | `any` — The script accepts a command.<br>`none` — The script does not accept a command.<br>`mapped_only` — The script accepts only a mapped command.<br>`auto` — The script accepts a command if at least one has been mapped. |
| _accept_options | `auto` | `any` — The script accepts any options.<br>`none` — The script does not accept any options.<br>`mapped_only` — The script accepts only mapped options.<br>`auto` — The script accepts only mapped options if at least one has been mapped. Otherwise, it accepts any options. |
| _default_max_positional_args | | Maximum number of positional arguments. A different value can be set for each command (see [_map_command](#_map_command)). |
| _default_min_positional_args | `0` | Minimum number of positional arguments. A different value can be set for each command (see [_map_command](#_map_command)). |
| _mapping_key_value_delimiter | `=` | Key-value delimiter for options mapping (see [_map_option](#_map_option)). |
| _mapping_values_delimiter | `,` | Values delimiter for options mapping (see [_map_option](#_map_option)). |
| _option_duplicates_allowed | `false` | `true` — Multiple uses of options are allowed.<br>`false` — Multiple uses of options are not allowed.|
| _option_key_value_delimiter | `' '` | Option alias-args delimiter. |
| _option_values_delimiter | `' '` | Option values delimiter. |
| _option_variable_default_value | `true` | Default value assigned to the option variable (see [_map_option](#_map_option)). |
| _options_combination_allowed | `true` | `true` — Combining short option aliases into combinations is allowed.<br>`false` — Combining short option aliases is not allowed. |
| _options_combination_args_allowed | `true` | `true` — Passing arguments to the last option in a combination is allowed.<br>`false` — Passing arguments to the last option in a combination is not allowed. |
| _positional_args_placement | `any` | `any` — Positional arguments can be placed anywhere, including mixed with options.<br>`before_options` — Positional arguments are placed before options.<br>`after_options` — Positional arguments are placed after options. |
## Functions
#### _parse
Parses the provided arguments. Typically, takes all command-line arguments `$@` as input.
```bash
# Parser configuration, command and option mapping above
_parse $@
# Script logic below
```
### Functions for command and option mapping
Mapping functions are used to specify the available commands and options of the script, as well as for their basic validation. Mapping functions must be used before calling `_parse`.
#### _map_command
Maps a command. Takes key-value pairs as arguments. Key and values list are separated by `_mapping_key_value_delimiter`.
| Key | Description |
| --- | --- |
| name | Command name. Required key. |
| description | Command description. Currently not used. |
| min_args | Minimum number of positional arguments when using the command. |
| max_args | Maximum number of positional arguments when using the command. |
```bash
_map_command \
name=foo \
min_args=1 \
max_args=2
```
### _map_option
Maps an option. Takes key-value pairs as arguments. Key and values list are separated by `_mapping_key_value_delimiter`. Values of the same key are separated by `_mapping_values_delimiter`.
| Key | Description |
| --- | --- |
| aliases | List of option aliases. Required key. |
| description | Option description. Currently not used. |
| min_args | Minimum number of option arguments. |
| max_args | Maximum number of option arguments. |
| variable | Name of the variable that will store the value when the option is used. |
| variable_value | Value that will be assigned to the option variable when used. |
| required | If the `required` flag is present, the option will be considered mandatory. |
```bash
_map_option \
aliases=-f,--foo \
min_args=1 \
max_args=10 \
required
```
### Functions for working with parsing results
#### _get_command
Outputs the used command.
```bash
_accept_command=true
_parse foo bar baz
_get_command
```
```
foo
```
#### _is_used_command
Checks if the command passed as the first argument was used.
```bash
_accept_command=true
_parse foo bar baz
_is_used_command foo && echo 'Command "foo" was used'
_is_used_command bar && echo 'Command "bar" was used'
```
```
Command "foo" was used
```
#### _get_positional_args_count
Outputs the number of positional arguments.
```bash
_parse foo bar baz
_get_positional_args_count
```
```
3
```
#### _get_positional_arg
Outputs the positional argument at the index passed as the first argument. Indexes start from 1.
```bash
_parse foo bar baz
_get_positional_arg 3
```
```
baz
```
#### _is_used_option
Checks if the option with the alias passed as the first argument was used.
```bash
_parse foo --bar -abc
_is_used_option foo && echo 'Option "--foo" was used'
_is_used_option --bar && echo 'Option "--bar" was used'
_is_used_option bar && echo 'Option "bar" was used'
_is_used_option -c && echo 'Option "-c" was used'
```
```
Option "--bar" was used
Option "-c" was used
```
#### _get_option_args_count
Outputs the number of arguments for the option with the alias passed as the first argument.
```bash
_parse -o foo bar baz
_get_option_args_count -o
```
```
3
```
#### _get_option_arg
Outputs the argument for the option with the alias passed as the first argument, at the index provided as the second argument. Indexes start from 1.
```bash
_parse -o foo bar baz
_get_option_arg -o 3
```
```
baz
```
## TODO
- Bug: required + variable/variable_value при разметке опций не имеет смысла, т.к. переменная будет константой;
- Add examples;
- Implement "Help" generation;
- Add tests.
## License
This project is licensed under the terms of the MIT license.

846
parser.sh Executable file
View File

@@ -0,0 +1,846 @@
#!/usr/bin/env sh
_accept_command=auto # auto, none, any, mapped_only
_accept_options=auto # auto, none, any, mapped_only
_default_max_positional_args=""
_default_min_positional_args=0
_mapping_key_value_delimiter="="
_mapping_values_delimiter=","
_option_duplicates_allowed=true
_option_key_value_delimiter=" "
_option_variable_default_value=true
_option_values_delimiter=" "
_options_combination_allowed=true
_options_combination_args_allowed=true
_positional_args_placement=any # any, before_options, after_options
_mapped_commands_count=0
_mapped_options_count=0
_positional_args_count=0
_err() {
echo "$1"
exit 1
}
_math() {
printf "%s" "$(($@))"
}
_is() {
[ "$1" = "true" ]
}
_is_not() {
[ "$1" != "true" ]
}
_is_int() {
case $1 in
[0-9])
return 0
;;
[1-9][0-9]*)
return 0
;;
*)
return 1
;;
esac
}
_assign() {
if ! _is_valid_var_name "$1"; then
_err "Invalid variable name: $1."
fi
_value=$2
_escaped_value=""
while [ -n "$_value" ]; do
_part=${_value%%\'*}
_escaped_value="$_escaped_value$_part"
_value=${_value#"$_part"}
if [ -n "$_value" ]; then
_escaped_value="$_escaped_value'\''"
_value=${_value#\'}
fi
done
eval "$1='$_escaped_value'"
}
_var_value() {
if _is_valid_var_name "$1"; then
eval "echo \"\$$1\""
else
_err "Invalid variable name: $1."
fi
}
_starts_with() {
case "$1" in
"$2"*)
return 0
;;
*)
return 1
;;
esac
}
_get_mapping_entry_key() {
echo "${1%%["${_mapping_key_value_delimiter}"]*}"
}
_get_mapping_entry_value() {
case "$1" in
*"$_mapping_key_value_delimiter"*)
echo "${1#*"${_mapping_key_value_delimiter}"}"
;;
*)
;;
esac
}
_str_contains() {
case "$1" in
*"$2"*)
return 0
;;
*)
return 1
;;
esac
}
_aliases_list_contains() {
_str_contains \
"${_mapping_values_delimiter}$1${_mapping_values_delimiter}" \
"${_mapping_values_delimiter}$2${_mapping_values_delimiter}"
}
_is_valid_var_name() {
case "$1" in
[!a-zA-Z_]*)
return 1
;;
*[!a-zA-Z0-9_]*)
return 1
;;
*)
return 0
;;
esac
}
_command_required() {
[ "$_accept_command" = "none" ] && return 1
[ "$_accept_command" != "auto" ] && return 0
_i=1
while [ "$_i" -le "$_mapped_commands_count" ]; do
_command_auto_mapped=$(_var_value "_commands_${_i}_auto")
if _is_not "$_command_auto_mapped"; then
return 0
fi
_i=$(_math "$_i + 1")
done
return 1
}
_map_command() {
_command_index=$(_math "$_mapped_commands_count + 1")
_mapping_command_prefix="_commands_${_command_index}"
while [ "$#" -gt 0 ]; do
_map_entry="$1"
if _str_contains "$_map_entry" "$_mapping_key_value_delimiter"; then
_map_key=$(_get_mapping_entry_key "$_map_entry")
_map_value=$(_get_mapping_entry_value "$_map_entry")
if [ -z "$_map_key" ] || [ -z "$_map_value" ]; then
_err "Invalid command #${_command_index} mapping entry: $_map_entry."
fi
else
_map_key="$_map_entry"
_map_value=true
fi
# Per-key command mapping validation.
if [ "$_map_key" = "description" ]; then
_validate_command_description "$_map_value"
elif [ "$_map_key" = "max_args" ]; then
_validate_command_max_args "$_map_value"
elif [ "$_map_key" = "min_args" ]; then
_validate_command_min_args "$_map_value"
elif [ "$_map_key" = "name" ]; then
_validate_command_name "$_map_value"
else
_err "Invalid command #${_command_index} mapping key: $_map_key."
fi
_assign "${_mapping_command_prefix}_${_map_key}" "$_map_value"
shift
done
# Post-mapping command validation.
if [ -z "$(_var_value "${_mapping_command_prefix}_name")" ]; then
_err "Missing command #${_command_index} name."
fi
_mapped_commands_count=$_command_index
}
_map_command_auto() {
_command_index=$(_math "$_mapped_commands_count + 1")
_mapping_command_prefix="_commands_${_command_index}"
_validate_command_name "$1"
_assign "${_mapping_command_prefix}_name" "$1"
_assign "${_mapping_command_prefix}_auto" "true"
_mapped_commands_count=$_command_index
}
_map_option() {
_option_index=$(_math "$_mapped_options_count + 1")
_mapping_option_prefix="_options_${_option_index}"
while [ "$#" -gt 0 ]; do
_map_entry="$1"
if _str_contains "$_map_entry" "$_mapping_key_value_delimiter"; then
_map_key=$(_get_mapping_entry_key "$_map_entry")
_map_value=$(_get_mapping_entry_value "$_map_entry")
if [ -z "$_map_key" ] || [ -z "$_map_value" ]; then
_err "Invalid option #${_option_index} mapping entry: $_map_entry."
fi
else
_map_key="$_map_entry"
_map_value=""
fi
# Per-key option mapping validation.
if [ "$_map_key" = "aliases" ]; then
_validate_option_aliases "$_map_value"
elif [ "$_map_key" = "variable" ]; then
_validate_option_variable "$_map_value"
elif [ "$_map_key" = "variable_value" ]; then
_validate_option_variable_value "$_map_value"
elif [ "$_map_key" = "description" ]; then
_validate_option_description "$_map_value"
elif [ "$_map_key" = "max_args" ]; then
_validate_option_max_args "$_map_value"
elif [ "$_map_key" = "min_args" ]; then
_validate_option_min_args "$_map_value"
elif [ "$_map_key" = "required" ]; then
_validate_option_required "$_map_value"
_map_value=true
else
_err "Invalid option #${_option_index} mapping key: $_map_key."
fi
_assign "${_mapping_option_prefix}_${_map_key}" "$_map_value"
shift
done
# Post-mapping option validation.
if [ -z "$(_var_value "${_mapping_option_prefix}_aliases")" ]; then
_err "Missing option #${_option_index} aliases."
fi
_mapped_options_count=$_option_index
}
_map_option_auto() {
_option_index=$(_math "$_mapped_options_count + 1")
_mapping_option_prefix="_options_${_option_index}"
_validate_option_aliases "$1"
_assign "${_mapping_option_prefix}_aliases" "$1"
_assign "${_mapping_option_prefix}_auto" "true"
_mapped_options_count=$_option_index
}
_validate_command_description() {
if [ -n "$(_var_value "${_mapping_command_prefix}_description")" ]; then
_err "Command #${_command_index} description is already mapped."
fi
}
_validate_command_max_args() {
if [ -n "$(_var_value "${_mapping_command_prefix}_max_args")" ]; then
_err "Command #${_command_index} max args is already mapped."
fi
if [ -z "$1" ]; then
_err "Command #${_command_index} max args cannot be empty."
fi
if ! _is_int "$1"; then
_err "Command #${_command_index} max args is invalid: $1. Must be a non-negative integer."
fi
}
_validate_command_min_args() {
if [ -n "$(_var_value "${_mapping_command_prefix}_min_args")" ]; then
_err "Command #${_command_index} min_args is already mapped."
fi
if [ -z "$1" ]; then
_err "Command #${_command_index} min args cannot be empty."
fi
if ! _is_int "$1"; then
_err "Command #${_command_index} min args is invalid: $1. Must be a non-negative integer."
fi
}
_validate_command_name() {
_existing_command_name=$(_var_value "${_mapping_command_prefix}_name")
if [ -n "$_existing_command_name" ]; then
_err "Command #${_command_index} name is already mapped: $_existing_command_name."
fi
# Check if command name not empty.
if [ -z "$1" ]; then
_err "Command #${_command_index} name is required."
fi
# Check if command name is valid.
case "$1" in
[!a-zA-Z0-9]*)
_err "Command #${_command_index} name is invalid: $1. Must start with an alphanumeric character."
;;
*[!a-z0-9\-_]*)
_err "Command #${_command_index} name is invalid: $1. Must be alphanumeric, hyphen and underscore only."
;;
*)
;;
esac
# Check if command name is unique.
_i=1
while [ "$_i" -le "$_mapped_commands_count" ]; do
if [ "$1" = "$(_var_value "_commands_${_i}_name")" ]; then
_err "Command #${_command_index} already mapped: $1."
fi
_i=$(_math "$_i + 1")
done
}
_validate_option_aliases() {
if [ -n "$(_var_value "${_mapping_option_prefix}_aliases")" ]; then
_err "Option #${_option_index} aliases are already mapped."
fi
_old_ifs="$IFS"
IFS="$_mapping_values_delimiter"
_current_option_aliases_list="$_mapping_values_delimiter"
for _option_alias in $1; do
if _aliases_list_contains "$_current_option_aliases_list" "$_option_alias"; then
_err "Option #${_option_index} alias duplicate: $_option_alias."
fi
case "$_option_alias" in
--*)
_validate_long_option_alias "${_option_alias#??}"
;;
-*)
_validate_short_option_alias "${_option_alias#?}"
;;
*)
_err "Option #${_option_index} alias is invalid: $_option_alias. Must start with: -- or -."
;;
esac
_current_option_aliases_list="${_current_option_aliases_list}${_option_alias}${_mapping_values_delimiter}"
done
IFS="$_old_ifs"
}
_validate_long_option_alias() {
if [ -z "$1" ]; then
_err "Option #${_option_index} alias cannot be empty."
fi
case "$1" in
[!a-zA-Z0-9]*)
_err "Option #${_option_index} alias is invalid: $1. Must start with an alphanumeric character."
;;
*[!a-z0-9\-_]*)
_err "Option #${_option_index} alias is invalid: $1. Must be alphanumeric, hyphen and underscore only."
;;
*)
;;
esac
_validate_option_alias_uniqueness "--$1"
}
_validate_short_option_alias() {
if [ -z "$1" ]; then
_err "Option #${_option_index} alias cannot be empty."
fi
case "$1" in
[!a-zA-Z])
_err "Option #${_option_index} alias is invalid: $1. Must be a single latin letter."
;;
*)
;;
esac
_validate_option_alias_uniqueness "-$1"
}
_validate_option_alias_uniqueness() {
_i=1
while [ "$_i" -le "$_mapped_options_count" ]; do
if _aliases_list_contains "$(_var_value "_options_${_i}_aliases")" "$1"; then
_err "Option #${_option_index} alias is already mapped for option #${_i}: $1."
fi
_i=$(_math "$_i + 1")
done
}
_validate_option_variable() {
if [ -n "$(_var_value "${_mapping_option_prefix}_variable")" ]; then
_err "Option #${_option_index} variable is already mapped."
fi
if [ -z "$1" ]; then
_err "Option #${_option_index} variable cannot be empty."
fi
if ! _is_valid_var_name "$1"; then
_err "Option #${_option_index} variable is invalid: $1. Must be a valid variable name."
fi
_i=1
while [ "$_i" -le "$_mapped_options_count" ]; do
if [ "$1" = "$(_var_value "_options_${_i}_variable")" ]; then
_err "Option #${_option_index} variable is already mapped for option #${_i}: $1."
fi
_i=$(_math "$_i + 1")
done
}
_validate_option_variable_value() {
if [ -n "$(_var_value "${_mapping_option_prefix}_variable_value")" ]; then
_err "Option #${_option_index} variable value is already mapped."
fi
if [ -z "$1" ]; then
_err "Option #${_option_index} variable value cannot be empty."
fi
}
_validate_option_description() {
if [ -n "$(_var_value "${_mapping_option_prefix}_description")" ]; then
_err "Option #${_option_index} description is already mapped."
fi
if [ -z "$1" ]; then
_err "Option #${_option_index} description cannot be empty."
fi
}
_validate_option_max_args() {
if [ -n "$(_var_value "${_mapping_option_prefix}_max_args")" ]; then
_err "Option #${_option_index} max args are already mapped."
fi
if [ -z "$1" ]; then
_err "Option #${_option_index} max args cannot be empty."
fi
if ! _is_int "$1"; then
_err "Option #${_option_index} max args is invalid: $1. Must be a non-negative integer."
fi
if (_is "$(_var_value "${_mapping_option_prefix}_required")") && [ "$1" -eq 0 ]; then
_err "Option #${_option_index} must be removed as constant. It is required without arguments."
fi
}
_validate_option_min_args() {
if [ -n "$(_var_value "${_mapping_option_prefix}_min_args")" ]; then
_err "Option #${_option_index} min args are already mapped."
fi
if [ -z "$1" ]; then
_err "Option #${_option_index} min args cannot be empty."
fi
if ! _is_int "$1"; then
_err "Option #${_option_index} min args is invalid: $1. Must be a non-negative integer."
fi
}
_validate_option_required() {
if [ -n "$(_var_value "${_mapping_option_prefix}_required")" ]; then
_err "Option #${_option_index} required is already mapped."
fi
if [ -n "$1" ]; then
_err "Option #${_option_index} required is invalid: $1. Must be used as a flag without value."
fi
_option_max_args=$(_var_value "${_mapping_option_prefix}_max_args")
if [ -n "$_option_max_args" ] && [ "$_option_max_args" -eq 0 ]; then
_err "Option #${_option_index} must be removed as constant. It is required without arguments."
fi
}
_get_mapped_option_index_by_alias() {
_i=1
while [ "$_i" -le "$_mapped_options_count" ]; do
if _aliases_list_contains "$(_var_value "_options_${_i}_aliases")" "$1"; then
echo "$_i"
return 0
fi
_i=$(_math "$_i + 1")
done
return 1
}
_get_mapped_command_index_by_name() {
_i=1
while [ "$_i" -le "$_mapped_commands_count" ]; do
if [ "$(_var_value "_commands_${_i}_name")" = "$1" ]; then
echo "$_i"
return 0
fi
_i=$(_math "$_i + 1")
done
return 1
}
_parse() {
_option_index=0
_used_command_index=0
if _command_required; then
if [ -z "$1" ]; then
_err "Command is required."
fi
if _starts_with "$1" "-"; then
_err "Command is required. Found option instead: $1."
fi
_use_command "$1"
shift
fi
if [ "$_accept_options" = "none" ]; then
_parse_positional_args "$@"
else
while [ $# -gt 0 ]; do
case "$1" in
--)
shift
_parse_positional_args "$@"
break
;;
--*)
_parse_option "$1"
;;
-*)
_parse_options_combination "$1"
;;
*)
if [ "$_option_index" -eq 0 ] || [ "$_option_key_value_delimiter" != " " ]; then
_parse_positional_args "$1"
else
_parse_option_args "$1"
fi
;;
esac
shift
done
fi
_i=1
while [ "$_i" -le "$_mapped_options_count" ]; do
_option_used=$(_var_value "_options_${_i}_used")
if (_is "$(_var_value "_options_${_i}_required")") && (_is_not "$_option_used"); then
_err "Option is required: $(_var_value "_options_${_i}_aliases")."
fi
_option_min_args=$(_var_value "_options_${_i}_min_args")
if (_is "$_option_used") && [ -n "$_option_min_args" ] && [ "$_option_min_args" -gt 0 ]; then
_option_args_count=$(_var_value "_options_${_i}_args_count")
if [ -z "$_option_args_count" ] || [ "$_option_args_count" -lt "$_option_min_args" ]; then
_err "At least $_option_min_args argument(s) required for option: $(_var_value "_options_${_i}_used_alias")."
fi
fi
_i=$(_math "$_i + 1")
done
if [ "$_used_command_index" -ne 0 ]; then
_min_positional_args=$(_var_value "_commands_${_used_command_index}_min_args")
fi
if [ -z "$_min_positional_args" ]; then
_min_positional_args=$_default_min_positional_args
fi
if [ "$_min_positional_args" -gt 0 ] && [ "$_positional_args_count" -lt "$_min_positional_args" ]; then
if [ "$_used_command_index" -eq 0 ]; then
_err "At least $_min_positional_args argument(s) required."
else
_err "At least $_min_positional_args argument(s) required for command: $(_var_value "_commands_${_used_command_index}_name")."
fi
fi
}
_parse_positional_args() {
if [ "$_positional_args_placement" = "before_options" ] && [ -n "$_option_index" ]; then
_err "Positional arguments must be placed before options: $1."
fi
if [ -z "$_command_index" ]; then
_max_args_count="$_default_max_positional_args"
else
_max_args_count=$(_var_value "_commands_${_command_index}_max_args")
fi
while [ $# -gt 0 ]; do
_positional_arg_index=$(_math "$_positional_args_count + 1")
if [ -n "$_max_args_count" ] && [ "$_positional_arg_index" -gt "$_max_args_count" ]; then
if [ -z "$_command_index" ]; then
_err "Maximum $_max_args_count positional argument(s) allowed."
else
_err "Maximum $_max_args_count positional argument(s) allowed for command: $(_var_value "_commands_${_command_index}_name")."
fi
fi
_assign "_positional_args_${_positional_arg_index}" "$1"
_positional_args_count=$_positional_arg_index
shift
done
}
_parse_option() {
_option_alias="${1%%"$_option_key_value_delimiter"*}"
_use_option "$_option_alias"
if [ "$_option_key_value_delimiter" != " " ] && [ "$_option_alias" != "$1" ]; then
_parse_option_args "${1#*"$_option_key_value_delimiter"}"
fi
}
_parse_options_combination() {
_options_combination=${1%%"$_option_key_value_delimiter"*}
if [ ${#_options_combination} = 1 ]; then
_err "Unknown empty option: $1."
fi
if [ "$_options_combination" != "$1" ] && (_is_not "$_options_combination_allowed"); then
_err "Short options combination is not allowed: $1."
fi
_i=2
while [ "$_i" -le "${#_options_combination}" ]; do
_use_option "-$(printf '%s' "$_options_combination" | cut -c "$_i")"
_i=$(_math "$_i + 1")
done
if [ "$_option_key_value_delimiter" != " " ] && [ "$_options_combination" != "$1" ]; then
_option_args=${1#*"$_option_key_value_delimiter"}
if [ ${#_options_combination} -gt 2 ] && (_is_not "$_options_combination_args_allowed"); then
_err "Arguments after short options combination are not allowed: $_option_args."
fi
_parse_option_args "$_option_args"
fi
if [ ${#_options_combination} -gt 2 ] && (_is_not "$_options_combination_allowed"); then
_option_index=0
fi
}
_use_option() {
if [ "$_positional_args_placement" = "after_options" ] && [ "$_positional_args_count" -gt 0 ]; then
_err "Positional arguments must be placed before options: $_positional_args_1."
fi
_option_index=$(_get_mapped_option_index_by_alias "$1")
if [ -z "$_option_index" ]; then
if [ "$_accept_options" = "mapped_only" ]; then
_err "Unknown option: $1."
else
_map_option_auto "$1"
_option_index=$_mapped_options_count
fi
elif (_is_not "$_option_duplicates_allowed") && (_is "$(_var_value "_options_${_option_index}_used")"); then
_err "Option is already used: $1."
fi
_assign "_options_${_option_index}_used" "true"
_assign "_options_${_option_index}_used_alias" "$1"
_option_variable=$(_var_value "_options_${_option_index}_variable")
if [ -n "$_option_variable" ]; then
_option_variable_value=$(_var_value "_options_${_option_index}_variable_value")
if [ -z "$_option_variable_value" ]; then
_option_variable_value="$_option_variable_default_value"
fi
_assign "$_option_variable" "$_option_variable_value"
fi
}
_parse_option_args() {
_old_ifs=$IFS
IFS=$_option_values_delimiter
for _option_arg in $1; do
_parse_option_arg "$_option_arg"
done
IFS=$_old_ifs
}
_parse_option_arg() {
_option_arg_index=$(_math "$(_var_value "_options_${_option_index}_args_count") + 1")
_max_args_count=$(_var_value "_options_${_option_index}_max_args")
if [ -n "$_max_args_count" ] && [ "$_option_arg_index" -gt "$_max_args_count" ]; then
_err "Maximum $_max_args_count argument(s) allowed for option: $(_var_value "_options_${_option_index}_used_alias")."
fi
_assign "_options_${_option_index}_args_${_option_arg_index}" "$1"
_assign "_options_${_option_index}_args_count" "$_option_arg_index"
}
_use_command() {
_command_index=$(_get_mapped_command_index_by_name "$1")
if [ -z "$_command_index" ]; then
if [ "$_accept_command" = "mapped_only" ]; then
_err "Unknown command: $1."
fi
_map_command_auto "$1"
_command_index=$_mapped_commands_count
fi
_used_command_index=$_command_index
}
_get_command() {
_var_value "_commands_${_used_command_index}_name"
}
_is_used_command() {
_command_index=$(_get_mapped_command_index_by_name "$1")
[ -n "$_command_index" ] && [ -n "$_used_command_index" ] && [ "$_command_index" -eq "$_used_command_index" ]
}
_get_positional_args_count() {
echo "$_positional_args_count"
}
_get_positional_arg() {
if [ -z "$1" ]; then
_err "Missing positional argument index."
fi
if ! _is_int "$1" || [ "$1" -lt 1 ]; then
_err "Invalid positional argument index: $1. Must be a positive integer."
fi
if [ "$_positional_args_count" -ge "$1" ]; then
_var_value "_positional_args_$1"
fi
}
_is_used_option() {
_option_index=$(_get_mapped_option_index_by_alias "$1")
[ -n "$_option_index" ] && _is "$(_var_value "_options_${_option_index}_used")"
}
_get_option_args_count() {
_option_index=$(_get_mapped_option_index_by_alias "$1")
if [ -n "$_option_index" ]; then
_args_count=$(_var_value "_options_${_option_index}_args_count")
fi
echo "${_args_count:=0}"
}
_get_option_arg() {
if [ -z "$1" ]; then
_err "Missing option alias."
fi
if [ -z "$2" ]; then
_err "Missing option argument index."
fi
if ! _is_int "$2" || [ "$2" -lt 1 ]; then
_err "Invalid option argument index: $1. Must be a positive integer."
fi
_option_index=$(_get_mapped_option_index_by_alias "$1")
if [ -n "$_option_index" ]; then
_args_count=$(_var_value "_options_${_option_index}_args_count")
if [ -n "$_args_count" ] && [ "$_args_count" -ge "$2" ]; then
_var_value "_options_${_option_index}_args_$2"
fi
fi
}