Last active
September 11, 2023 12:30
-
-
Save anthonyaxenov/d53c4385b7d1466e0affeb56388b1005 to your computer and use it in GitHub Desktop.
[SHELL] Argument parser for bash scripts without getopt or getopts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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) # | |
# # | |
######################################################################### | |
# # | |
# Version history: # | |
# v1.0 - initial # | |
# v1.1 - arg(): improved skipping uninteresting args # | |
# - arg(): check next arg to be valid value # | |
# v1.2 - removed all 'return' statements # | |
# - arg(): error message corrected # | |
# - new examples # | |
# v1.3 - argl(): improved flag check # | |
# - some text corrections # | |
# 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 # | |
# # | |
######################################################################### | |
#purpose Little helper to check if string matches PCRE | |
#argument $1 - some string | |
#argument $2 - regex | |
#exitcode 0 - string valid | |
#exitcode 1 - string is not valid | |
grep_match() { | |
printf "%s" "$1" | grep -qP "$2" >/dev/null | |
} | |
#purpose Find short argument or its value | |
#argument $1 - (string) argument (without leading dashes; only first letter will be processed) | |
#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') | |
#usage To find flag into var: arg f 1 myvar or flag=$(arg 'f') | |
#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) | |
local args=(${MAIN_ARGS[0]}) # args we need are stored in 1st element of MAIN_ARGS | |
for ((idx=0; idx<${#args[@]}; ++idx)) do # going through args | |
local arg=${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 $3 (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="${args[$idx+1]}" | |
# check if it is valid value | |
if grep_match "$value" "^\w+$"; then | |
# and return it back back into $3 (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 | |
done | |
} | |
#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 | |
# (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') | |
#usage To find flag into var: arg f 1 myvar or flag=$(arg 'f') | |
#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) | |
local args=(${MAIN_ARGS[0]}) # args we need are stored in 1st element of MAIN_ARGS | |
for ((idx=0; idx<${#args[@]}; ++idx)) do | |
local arg=${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 $3 (if exists) or echo in stdout | |
[ ! $retvar = 0 ] && eval "$retvar=1" || echo "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 $3 (if exists) or echo in stdout | |
[ ! $retvar = 0 ] && eval "$retvar=${arg#*=}" || echo "${arg#*=}" | |
break | |
fi | |
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" | |
fi | |
} | |
# 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. | |
# first we must save the original arguments passed | |
# to the script when it was called: | |
MAIN_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" | |
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)" | |
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" | |
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)" | |
echo -e "\n5. Args by index:" | |
argn 1 first | |
echo "5.1 arg[1]=$first" | |
echo "5.2 arg[3]=$(argn 3)" | |
# Well, now we will try to get global args inside different functions | |
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 | |
} | |
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 | |
} | |
hello | |
food | |
### 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment