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:
* 🇷🇺 [axenov.dev/bash-args](https://axenov.dev/bash-args/)
@@ -7,8 +87,8 @@ More info:
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)
bash 5.0.17(1)-release (x86_64-pc-linux-gnu) and later
zsh 5.8 (x86_64-ubuntu-linux-gnu) and later
```
## Version history
@@ -26,4 +106,6 @@ v1.4 - new function argn()
- some text corrections
v1.5 - arg(), grep_match(): fixed searching for -e argument
- grep_match(): redirect output into /dev/null
v1.6 - removed useless argn()
- arg() and argl() refactored and now support values with whitespaces
```

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)'"