args-parser refactor

This commit is contained in:
2025-01-17 19:07:08 +08:00
parent 53d5a31a30
commit 45499ca5df
2 changed files with 189 additions and 217 deletions

View File

@@ -1,4 +1,84 @@
# Argument parser for bash scripts # Argument parser for bash scripts v1.6
## Usage
```shell
# 1. add these lines after shebang:
__RAW_ARGS__=("$@")
source args.sh
# 2. read arguments as flags:
arg a 1 flag_a
echo "Flag -a has value '$flag_a'"
echo "Flag -a has value '$(arg a 1)'"
arg b 1 flag_b
echo "Flag -b has value '$flag_b'"
echo "Flag -b has value '$(arg b 1)'"
arg c 1 flag_c
echo "Flag -c has value '$flag_c'"
echo "Flag -c has value '$(arg c 1)'"
arg d 1 flag_d
echo "Flag -d has value '$flag_d'"
echo "Flag -d has value '$(arg d 1)'"
argl flag1 1 flag_1
echo "Flag --flag1 has value '$flag_1'"
echo "Flag --flag1 has value '$(argl flag1 1)'"
argl flag2 1 flag_2
echo "Flag --flag2 has value '$flag_2'"
echo "Flag --flag2 has value '$(argl flag2 1)'"
# 3. and/or read arguments' values:
arg a 0 arg_a
echo "Arg -a has value '$arg_a'"
echo "Arg -a has value '$(arg a 0)'"
arg b 0 arg_b
echo "Arg -b has value '$arg_b'"
echo "Arg -b has value '$(arg b 0)'"
argl arg1 0 arg_1
echo "Arg --arg1 has value '$arg_1'"
echo "Arg --arg1 has value '$(argl arg1 0)'"
argl arg2 0 arg_2
echo "Arg --arg2 has value '$arg_2'"
echo "Arg --arg2 has value '$(argl arg2 0)'"
```
## How it works
1. Short arguments can be specified contiguously or separately
and their order does not matter, but before each of them
(or the first of them) one leading dash must be specified.
> Valid combinations: `-a -b -c`, `-cba`, `-b -azc "value of z"`
2. Short arguments can have values and if are - value must go
next to argument itself.
> Valid combinations: `-ab avalue`, `-ba avalue`, `-a avalue -b`
3. Long arguments cannot be combined like short ones and each
of them must be specified separately with two leading dashes.
> Valid combinations: `--foo --bar`, `--bar --foo`
4. Long arguments can have a value which must be specified after `=`.
> Valid combinations: `--foo value --bar`, `--bar --foo=value`
5. If arg value may contain space then value must be "double-quoted".
6. You can use arg() or argl() to check presence of any arg, no matter
if it has value or not.
More info: More info:
* 🇷🇺 [axenov.dev/bash-args](https://axenov.dev/bash-args/) * 🇷🇺 [axenov.dev/bash-args](https://axenov.dev/bash-args/)
@@ -7,8 +87,8 @@ More info:
Tested in Ubuntu 20.04.2 LTS in: Tested in Ubuntu 20.04.2 LTS in:
``` ```
bash 5.0.17(1)-release (x86_64-pc-linux-gnu) bash 5.0.17(1)-release (x86_64-pc-linux-gnu) and later
zsh 5.8 (x86_64-ubuntu-linux-gnu) zsh 5.8 (x86_64-ubuntu-linux-gnu) and later
``` ```
## Version history ## Version history
@@ -26,4 +106,6 @@ v1.4 - new function argn()
- some text corrections - some text corrections
v1.5 - arg(), grep_match(): fixed searching for -e argument v1.5 - arg(), grep_match(): fixed searching for -e argument
- grep_match(): redirect output into /dev/null - grep_match(): redirect output into /dev/null
v1.6 - removed useless argn()
- arg() and argl() refactored and now support values with whitespaces
``` ```

316
helpers/arg-parser/args.sh Normal file → Executable file
View File

@@ -1,33 +1,11 @@
#!/bin/bash #!/usr/bin/env bash
#########################################################################
# # # Argument parser for bash scripts
# Argument parser for bash scripts # #
# # # Author: Anthony Axenov (Антон Аксенов)
# Author: Anthony Axenov (Антон Аксенов) # # Version: 1.6
# Version: 1.5 # # License: MIT
# License: MIT # # Description: https://git.axenov.dev/anthony/shell/src/branch/master/helpers/arg-parser
# #
#########################################################################
# #
# With 'getopt' you cannot combine different #
# arguments for different nested functions. #
# #
# 'getopts' does not support long arguments with #
# values (like '--foo=bar'). #
# #
# These functions supports different arguments and #
# their combinations: #
# -a -b -c #
# -a avalue -b bvalue -c cvalue #
# -cab bvalue #
# --arg #
# --arg=value -ab -c cvalue --foo #
# #
# Tested in Ubuntu 20.04.2 LTS in: #
# bash 5.0.17(1)-release (x86_64-pc-linux-gnu) #
# zsh 5.8 (x86_64-ubuntu-linux-gnu) #
# #
#########################################################################
#purpose Little helper to check if string matches PCRE #purpose Little helper to check if string matches PCRE
#argument $1 - some string #argument $1 - some string
@@ -49,46 +27,46 @@ grep_match() {
#usage To echo value: arg v #usage To echo value: arg v
#usage To echo 1 if flag exists: arg f #usage To echo 1 if flag exists: arg f
arg() { arg() {
local need=${1:0:1} # argument to find (only first letter) [ "$1" ] || { echo "Argument name is not specified!" >&2 && exit 1; }
[ "$need" ] || { local arg_name="${1:0:1}" # first character of argument name to find
echo "Argument is not specified!" >&2 local is_flag="$2" || 0 # 1 if we need just find a flag, 0 to get a value
exit 1 local var_name="$3" || 0 # variable name to return value into or 0 to echo it in stdout
} local value= # initialize empty value to check if we found one later
local isflag=$2 || 0 # should we find the value or just the presence of the $need? local arg_found=0 # marker of found argument
local retvar=$3 || 0 # var to return value into (if 0 then value will be echo'ed in stdout)
for ((idx=0; idx<${#__MAIN_ARGS__[@]}; ++idx)) do # going through args for idx in "${!__RAW_ARGS__[@]}"; do # going through all args
local arg=${__MAIN_ARGS__[$idx]} # current argument local arg_search=${__RAW_ARGS__[idx]} # get current argument
# skip $arg if it starts with '--', letter or digit
grep_match "$arg" "^(\w{1}|-{2})" && continue # skip $arg_search if it starts with '--' or letter
# clear $arg from special and duplicate characters grep_match "$arg_search" "^(\w|--)" && continue
# e.g. 'fas-)dfs' will become 'fasd'
local chars="$(printf "%s" "${arg}" | tr -s [${arg}] | tr -d "[:punct:][:blank:]")" # clear $arg_search from special and duplicate characters, e.g. 'fas-)dfs' will become 'fasd'
# now we can check if $need is one of $chars local arg_chars="$(printf "%s" "$arg_search" | tr -s [$arg_search] | tr -d "[:punct:][:blank:]")"
if grep_match "-$need" "^-[$chars]$"; then # if it is
if [[ $isflag = 1 ]]; then # and we expect it as a flag # if $arg_name is not one of $arg_chars the skip it
# then return '1' back into $retvar (if exists) or echo in stdout grep_match "-$arg_name" "^-[$arg_chars]$" || continue
[ "$retvar" ] && eval "$retvar='1'" || echo "1" arg_found=1
else # but if $arg is not a flag
# then get next argument as value of current one # then return '1'|'0' back into $value if we need flag or next arg value otherwise
local value="${__MAIN_ARGS__[$idx+1]}" [ "$is_flag" = 1 ] && value=1 || value="${__RAW_ARGS__[idx+1]}"
# check if it is valid value
if grep_match "$value" "^[[:graph:]]+$"; then
# and return it back back into $retvar (if exists) or echo in stdout
[ "$retvar" ] && eval "$retvar='$value'" || echo "$value"
break break
else # otherwise throw error message into stderr (just in case)
echo "Argument '$arg' must have a correct value!" >&2
break
fi
fi
fi
done done
[ "$is_flag" = 1 ] && [ -z "$value" ] && value=0;
# if value we found is empty or looks like another argument then exit with error message
if [ "$arg_found" = 1 ] && ! grep_match "$value" "^[[:graph:]]+$" || grep_match "$value" "^--?\w+$"; then
echo "ERROR: Argument '-$arg_name' must have correct value!" >&2 && exit 1
fi
# return '$value' back into $var_name (if exists) or echo in stdout
[ "$var_name" ] && eval "$var_name='$value'" || echo "$value"
} }
#purpose Find long argument or its value #purpose Find long argument or its value
#argument $1 - argument (without leading dashes) #argument $1 - argument (without leading dashes)
#argument $2 - is it flag? 1 if is, otherwise 0 or nothing #argument $2 - (number) is it flag? 1 if is, otherwise 0 or nothing
#argument $3 - variable to return value into #argument $3 - (string) variable to return value into
# (if not specified then it will be echo'ed in stdout) # (if not specified then it will be echo'ed in stdout)
#returns (string) 1 (if $2 == 1), value (if correct and if $2 != 1) or nothing #returns (string) 1 (if $2 == 1), value (if correct and if $2 != 1) or nothing
#usage To get value into var: arg v 0 myvar or myvalue=$(arg 'v') #usage To get value into var: arg v 0 myvar or myvalue=$(arg 'v')
@@ -96,171 +74,83 @@ arg() {
#usage To echo value: arg v #usage To echo value: arg v
#usage To echo 1 if flag exists: arg f #usage To echo 1 if flag exists: arg f
argl() { argl() {
local need=$1 # argument to find [ "$1" ] || { echo "Argument name is not specified!" >&2 && exit 1; }
[ "$need" ] || { local arg_name="$1" # argument name to find
echo "Argument is not specified!" >&2 local is_flag="$2" || 0 # 1 if we need just find a flag, 0 to get a value
exit 1 local var_name="$3" || 0 # variable name to return value into or 0 to echo it in stdout
} local value= # initialize empty value to check if we found one later
local isflag=$2 || 0 # should we find the value or just the presence of the $need? local arg_found=0 # marker of found argument
local retvar=$3 || 0 # var to return value into (if 0 then value will be echo'ed in stdout)
for ((idx=0; idx<${#__MAIN_ARGS__[@]}; ++idx)) do # going through args for idx in "${!__RAW_ARGS__[@]}"; do # going through all args
local arg=${__MAIN_ARGS__[$idx]} # current argument local arg_search="${__RAW_ARGS__[idx]}" # get current argument
# if we expect $arg as a flag
if [[ $isflag = 1 ]]; then if [ "$arg_search" = "--$arg_name" ]; then # if current arg begins with two dashes
# and if $arg has correct format (like '--flag') # then return '1' back into $value if we need flag or next arg value otherwise
if grep_match "$arg" "^--$need"; then [ "$is_flag" = 1 ] && value=1 || value="${__RAW_ARGS__[idx+1]}"
# then return '1' back into $retvar (if exists) or echo in stdout break # stop the loop
[ "$retvar" = 0 ] && echo "1" || eval "$retvar=1" elif grep_match "$arg_search" "^--$arg_name=.+$"; then # check if $arg like '--foo=bar'
break # then return '1' back into $value if we need flag or part from '=' to arg's end as value otherwise
fi [ "$is_flag" = 1 ] && value=1 || value="${arg_search#*=}"
else # but if $arg is not a flag break # stop the loop
# check if $arg has correct format (like '--foo=bar')
if grep_match "$arg" "^--$need=.+$"; then # if it is
# then return part from '=' to arg's end as value back into $retvar (if exists) or echo in stdout
[ "$retvar" = 0 ] && echo "${arg#*=}" || eval "$retvar=${arg#*=}"
break
fi
fi fi
done done
}
#purpose Get argument by its index [ "$is_flag" = 1 ] && [ -z "$value" ] && value=0;
#argument $1 - (number) arg index
#argument $2 - (string) variable to return arg's name into # if value we found is empty or looks like another argument then exit with error message
# (if not specified then it will be echo'ed in stdout) if [ "$arg_found" = 1 ] && ! grep_match "$value" "^[[:graph:]]+$" || grep_match "$value" "^--?\w+$"; then
#returns (string) arg name or nothing echo "ERROR: Argument '--$arg_name' must have correct value!" >&2 && exit 1;
#usage To get arg into var: argn 1 myvar or arg=$(argn 1)
#usage To echo in stdout: argn 1
argn() {
local idx=$1 # argument index
[ $idx ] || {
error "Argument index is not specified!"
exit 1
}
local retvar=$2 || 0 # var to return value into (if 0 then value will be echo'ed in stdout)
local args=(${__MAIN_ARGS[0]}) # args we need are stored in 1st element of __MAIN_ARGS
local arg=${args[$idx]} # current argument
if [ $arg ]; then
[ ! $retvar = 0 ] && eval "$retvar=$arg" || echo "$arg"
fi fi
# return '$value' back into $var_name (if exists) or echo in stdout
[ "$var_name" ] && eval "$var_name='$value'" || echo "$value"
} }
# Keep in mind: ################################
# 1. Short arguments can be specified contiguously or separately
# and their order does not matter, but before each of them
# (or the first of them) one leading dash must be specified.
# Valid combinations: '-a -b -c', '-cba', '-b -ac'
# 2. Short arguments can have values and if are - value must go
# next to argument itself.
# Valid combinations: '-ab avalue', '-ba avalue', '-a avalue -b'
# 3. Long arguments cannot be combined like short ones and each
# of them must be specified separately with two leading dashes.
# Valid combinations: '--foo --bar', '--bar --foo'
# 4. Long arguments can have a value which must be specified after '='.
# Valid combinations: '--foo=value --bar', '--bar --foo=value'
# 5. Values cannot contain spaces even in quotes both for short and
# long args, otherwise first word will return as value.
# 6. You can use arg() or argl() to check presence of any arg, no matter
# if it has value or not.
### USAGE ###
# This is simple examples which you can play around with. # This is simple examples which you can play around with.
# 1. uncomment code below
# 2. call thi sscript to see what happens:
# /args.sh -abcd --flag1 --flag2 -e evalue -f fvalue --arg1=value1 --arg2 value2
# first we must save the original arguments passed # __RAW_ARGS__=("$@")
# to the script when it was called:
__MAIN_ARGS__=($@)
echo -e "\n1. Short args (vars):" # arg a 1 flag_a
arg a 1 a # -a # echo "Flag -a has value '$flag_a'"
arg v 0 v # -v v_value # echo "Flag -a has value '$(arg a 1)'"
arg c 1 c # -c
arg z 1 z # -z (not exists)
echo "1.1 a=$a"
echo "1.2 v=$v"
echo "1.3 c=$c"
echo "1.4 z=$z"
echo -e "\n2. Short args (echo):" # arg b 1 flag_b
echo "2.1 a=$(arg a 1)" # echo "Flag -b has value '$flag_b'"
echo "2.2 v=$(arg v 0)" # echo "Flag -b has value '$(arg b 1)'"
echo "2.3 c=$(arg c 1)"
echo "2.4 z=$(arg z 1)"
echo -e "\n3. Long args (vars):" # arg c 1 flag_c
argl flag 1 flag # --flag # echo "Flag -c has value '$flag_c'"
argl param1 0 param1 # --param1=test # echo "Flag -c has value '$(arg c 1)'"
argl param2 0 param2 # --param2=password
argl bar 1 bar # --bar (not exists)
echo "3.1 flag=$flag"
echo "3.2 param1=$param1"
echo "3.3 param2=$param2"
echo "3.4 bar=$bar"
echo -e "\n4. Long args (echo):" # arg d 1 flag_d
echo "4.1 flag=$(argl flag 1)" # echo "Flag -d has value '$flag_d'"
echo "4.2 param1=$(argl param1 0)" # echo "Flag -d has value '$(arg d 1)'"
echo "4.3 param2=$(argl param2 0)"
echo "4.4 bar=$(argl bar 1)"
echo -e "\n5. Args by index:" # argl flag1 1 flag_1
argn 1 first # echo "Flag --flag1 has value '$flag_1'"
echo "5.1 arg[1]=$first" # echo "Flag --flag1 has value '$(argl flag1 1)'"
echo "5.2 arg[3]=$(argn 3)"
# Well, now we will try to get global args inside different functions # argl flag2 1 flag_2
# echo "Flag --flag2 has value '$flag_2'"
# echo "Flag --flag2 has value '$(argl flag2 1)'"
food() { # arg e 0 arg_e
echo -e "\n=== food() ===" # echo "Arg -e has value '$arg_e'"
arg f 0 food # echo "Arg -e has value '$(arg e 0)'"
argl 'food' 0 food
[ $food ] && echo "Om nom nom! $food is very tasty" || echo "Uh oh" >&2
}
hello() { # arg f 0 arg_f
echo -e "\n=== hello() ===" # echo "Arg -f has value '$arg_f'"
arg n 0 name # echo "Arg -f has value '$(arg f 0)'"
argl name 0 name
[ $name ] && echo "Hi, $name! How u r doin?" || echo "Hello, stranger..." >&2
}
hello # argl arg1 0 arg_1
food # echo "Arg --arg1 has value '$arg_1'"
# echo "Arg --arg1 has value '$(argl arg1 0)'"
### OUTPUT ### # argl arg2 0 arg_2
# echo "Arg --arg2 has value '$arg_2'"
# Command to run: # echo "Arg --arg2 has value '$(argl arg2 0)'"
# bash args.sh -va asdf --flag --param1=paramvalue1 -c --param2="somevalue2 sdf" --name="John" -f Seafood
# 1. Short args (vars):
# 1.1 a=1
# 1.2 v=v_value
# 1.3 c=1
# 1.4 z=
#
# 2. Short args (echo):
# 2.1 a=1
# 2.2 v=v_value
# 2.3 c=1
# 2.4 z=
#
# 3. Long args (vars):
# 3.1 longflag=1
# 3.2 param1=test
# 3.3 param2=password
# 3.4 barflag=
#
# 4. Long args (echo):
# 4.1 longflag=1
# 4.2 param1=test
# 4.3 param2=password
# 4.4 barflag=
#
# 5. Args by index:
# 5.1 arg[1]=asdf
# 5.2 arg[3]=--param1=paramvalue1
#
# === hello() ===
# Hi, John! How u r doin?
#
# === food() ===
# Om nom nom! Seafood is very tasty