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

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

@@ -1,33 +1,11 @@
#!/bin/bash
#########################################################################
# #
# Argument parser for bash scripts #
# #
# Author: Anthony Axenov (Антон Аксенов) #
# Version: 1.5 #
# License: MIT #
# #
#########################################################################
# #
# 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) #
# #
#########################################################################
#!/usr/bin/env bash
# Argument parser for bash scripts
#
# Author: Anthony Axenov (Антон Аксенов)
# Version: 1.6
# License: MIT
# Description: https://git.axenov.dev/anthony/shell/src/branch/master/helpers/arg-parser
#purpose Little helper to check if string matches PCRE
#argument $1 - some string
@@ -49,46 +27,46 @@ grep_match() {
#usage To echo value: arg v
#usage To echo 1 if flag exists: arg f
arg() {
local need=${1:0:1} # argument to find (only first letter)
[ "$need" ] || {
echo "Argument is not specified!" >&2
exit 1
}
local isflag=$2 || 0 # should we find the value or just the presence of the $need?
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
local arg=${__MAIN_ARGS__[$idx]} # current argument
# skip $arg if it starts with '--', letter or digit
grep_match "$arg" "^(\w{1}|-{2})" && continue
# clear $arg from special and duplicate characters
# e.g. 'fas-)dfs' will become 'fasd'
local chars="$(printf "%s" "${arg}" | tr -s [${arg}] | tr -d "[:punct:][:blank:]")"
# now we can check if $need is one of $chars
if grep_match "-$need" "^-[$chars]$"; then # if it is
if [[ $isflag = 1 ]]; then # and we expect it as a flag
# then return '1' back into $retvar (if exists) or echo in stdout
[ "$retvar" ] && eval "$retvar='1'" || echo "1"
else # but if $arg is not a flag
# then get next argument as value of current one
local value="${__MAIN_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
else # otherwise throw error message into stderr (just in case)
echo "Argument '$arg' must have a correct value!" >&2
break
fi
fi
fi
[ "$1" ] || { echo "Argument name is not specified!" >&2 && exit 1; }
local arg_name="${1:0:1}" # first character of argument name to find
local is_flag="$2" || 0 # 1 if we need just find a flag, 0 to get a value
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 arg_found=0 # marker of found argument
for idx in "${!__RAW_ARGS__[@]}"; do # going through all args
local arg_search=${__RAW_ARGS__[idx]} # get current argument
# skip $arg_search if it starts with '--' or letter
grep_match "$arg_search" "^(\w|--)" && continue
# clear $arg_search from special and duplicate characters, e.g. 'fas-)dfs' will become 'fasd'
local arg_chars="$(printf "%s" "$arg_search" | tr -s [$arg_search] | tr -d "[:punct:][:blank:]")"
# if $arg_name is not one of $arg_chars the skip it
grep_match "-$arg_name" "^-[$arg_chars]$" || continue
arg_found=1
# then return '1'|'0' back into $value if we need flag or next arg value otherwise
[ "$is_flag" = 1 ] && value=1 || value="${__RAW_ARGS__[idx+1]}"
break
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
#argument $1 - argument (without leading dashes)
#argument $2 - is it flag? 1 if is, otherwise 0 or nothing
#argument $3 - variable to return value into
#argument $2 - (number) is it flag? 1 if is, otherwise 0 or nothing
#argument $3 - (string) variable to return value into
# (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
#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 1 if flag exists: arg f
argl() {
local need=$1 # argument to find
[ "$need" ] || {
echo "Argument is not specified!" >&2
exit 1
}
local isflag=$2 || 0 # should we find the value or just the presence of the $need?
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
local arg=${__MAIN_ARGS__[$idx]} # current argument
# if we expect $arg as a flag
if [[ $isflag = 1 ]]; then
# and if $arg has correct format (like '--flag')
if grep_match "$arg" "^--$need"; then
# then return '1' back into $retvar (if exists) or echo in stdout
[ "$retvar" = 0 ] && echo "1" || eval "$retvar=1"
break
fi
else # but if $arg is not a flag
# 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
[ "$1" ] || { echo "Argument name is not specified!" >&2 && exit 1; }
local arg_name="$1" # argument name to find
local is_flag="$2" || 0 # 1 if we need just find a flag, 0 to get a value
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 arg_found=0 # marker of found argument
for idx in "${!__RAW_ARGS__[@]}"; do # going through all args
local arg_search="${__RAW_ARGS__[idx]}" # get current argument
if [ "$arg_search" = "--$arg_name" ]; then # if current arg begins with two dashes
# then return '1' back into $value if we need flag or next arg value otherwise
[ "$is_flag" = 1 ] && value=1 || value="${__RAW_ARGS__[idx+1]}"
break # stop the loop
elif grep_match "$arg_search" "^--$arg_name=.+$"; then # check if $arg like '--foo=bar'
# then return '1' back into $value if we need flag or part from '=' to arg's end as value otherwise
[ "$is_flag" = 1 ] && value=1 || value="${arg_search#*=}"
break # stop the loop
fi
done
}
#purpose Get argument by its index
#argument $1 - (number) arg index
#argument $2 - (string) variable to return arg's name into
# (if not specified then it will be echo'ed in stdout)
#returns (string) arg name or nothing
#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"
[ "$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"
}
# 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.
# 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
# to the script when it was called:
__MAIN_ARGS__=($@)
# __RAW_ARGS__=("$@")
echo -e "\n1. Short args (vars):"
arg a 1 a # -a
arg v 0 v # -v v_value
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"
# arg a 1 flag_a
# echo "Flag -a has value '$flag_a'"
# echo "Flag -a has value '$(arg a 1)'"
echo -e "\n2. Short args (echo):"
echo "2.1 a=$(arg a 1)"
echo "2.2 v=$(arg v 0)"
echo "2.3 c=$(arg c 1)"
echo "2.4 z=$(arg z 1)"
# arg b 1 flag_b
# echo "Flag -b has value '$flag_b'"
# echo "Flag -b has value '$(arg b 1)'"
echo -e "\n3. Long args (vars):"
argl flag 1 flag # --flag
argl param1 0 param1 # --param1=test
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"
# arg c 1 flag_c
# echo "Flag -c has value '$flag_c'"
# echo "Flag -c has value '$(arg c 1)'"
echo -e "\n4. Long args (echo):"
echo "4.1 flag=$(argl flag 1)"
echo "4.2 param1=$(argl param1 0)"
echo "4.3 param2=$(argl param2 0)"
echo "4.4 bar=$(argl bar 1)"
# arg d 1 flag_d
# echo "Flag -d has value '$flag_d'"
# echo "Flag -d has value '$(arg d 1)'"
echo -e "\n5. Args by index:"
argn 1 first
echo "5.1 arg[1]=$first"
echo "5.2 arg[3]=$(argn 3)"
# argl flag1 1 flag_1
# echo "Flag --flag1 has value '$flag_1'"
# echo "Flag --flag1 has value '$(argl flag1 1)'"
# 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() {
echo -e "\n=== food() ==="
arg f 0 food
argl 'food' 0 food
[ $food ] && echo "Om nom nom! $food is very tasty" || echo "Uh oh" >&2
}
# arg e 0 arg_e
# echo "Arg -e has value '$arg_e'"
# echo "Arg -e has value '$(arg e 0)'"
hello() {
echo -e "\n=== hello() ==="
arg n 0 name
argl name 0 name
[ $name ] && echo "Hi, $name! How u r doin?" || echo "Hello, stranger..." >&2
}
# arg f 0 arg_f
# echo "Arg -f has value '$arg_f'"
# echo "Arg -f has value '$(arg f 0)'"
hello
food
# argl arg1 0 arg_1
# echo "Arg --arg1 has value '$arg_1'"
# echo "Arg --arg1 has value '$(argl arg1 0)'"
### OUTPUT ###
# Command to run:
# 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
# argl arg2 0 arg_2
# echo "Arg --arg2 has value '$arg_2'"
# echo "Arg --arg2 has value '$(argl arg2 0)'"