diff --git a/helpers/arg-parser/README.md b/helpers/arg-parser/README.md index 612f5a9..9cb84b0 100644 --- a/helpers/arg-parser/README.md +++ b/helpers/arg-parser/README.md @@ -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 ``` diff --git a/helpers/arg-parser/args.sh b/helpers/arg-parser/args.sh old mode 100644 new mode 100755 index 06aaacc..c1ad9f6 --- a/helpers/arg-parser/args.sh +++ b/helpers/arg-parser/args.sh @@ -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)'"