├── LICENSE ├── README.md ├── bash_args_parser.bash └── test └── test.bash /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Thomas Buckley-Houston 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BASH Arguments Parser by @tombh 2 | 3 | A quick and convenient way to support arguments in BASH. There are already some other 4 | projects that do exactly this, but of course they weren't quite _exactly_ how I want 5 | it. 6 | 7 | Just one small file: copy, paste, `source bash_args_parser.bash` and enjoy 🤓 8 | 9 | ## Usage: 10 | ```bash 11 | declare -A args=( 12 | [summary]="A short description of what this command or function does" 13 | [0:foo]="A foo argument, it's positional and required" 14 | [--bar:flag]="A boolean option that doesn't take a value" 15 | [--foobar]="An option that requires a value" 16 | ) 17 | BAPt_parse_arguments args "$@" 18 | 19 | echo "foo is ${args[foo]}. bar is ${args[foo]}. foobar is ${args[foobar]}" 20 | ``` 21 | 22 | `--help` and usage docs are automatically added, eg: 23 | ``` 24 | Usage: example foo [OPTIONS] 25 | 26 | A short description of what this command or function does 27 | 28 | Arguments: 29 | foo A foo argument, it's positional and required 30 | 31 | Options: 32 | --help Show help 33 | --bar A boolean option that doesn't take a value 34 | --foobar An option that requires a value 35 | ``` 36 | 37 | Also supports: 38 | * Passing through arguments with an `any` key. 39 | * More detailed usage with a `details` key. 40 | * Can also be used, without changes, inside a function. 41 | ```sh 42 | function better_ls { 43 | local args 44 | declare -A args=( 45 | [summary]="List folder contents" 46 | [any]="The normal \`ls\` arguments" 47 | [details]="$( 48 | cat <<-EOM 49 | Look at all this room! 50 | 51 | Finally I can relax 😎 52 | EOM 53 | )" 54 | ) 55 | BAPt_parse_arguments args "$@" 56 | 57 | if [[ ${args[any]} =~ work ]]; then 58 | echo "Sorry, you need to be relaxing" 59 | exit 60 | else 61 | "$(ls "${args[any]}")" 62 | fi 63 | } 64 | ``` 65 | 66 | ## Testing 67 | 68 | See: https://github.com/pgrange/bash_unit 69 | -------------------------------------------------------------------------------- /bash_args_parser.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | # Bash Arguments Parser by tombh (BAPt) 4 | # 5 | # Usage: 6 | # ``` 7 | # declare -A args=( 8 | # [summary]="A short description of what this command or function does" 9 | # [0:foo]="A foo argument, it's positional and required" 10 | # [--bar:flag]="A boolean option that doesn't take a value" 11 | # [--foobar]="An option that requires a value" 12 | # ) 13 | # BAPt_parse_arguments args "$@" 14 | # 15 | # echo "foo is ${args[foo]}. bar is ${args[foo]}. foobar is ${args[foobar]}" 16 | # ``` 17 | 18 | __BAPt_ERROR_PREFIX='Error parsing arguments' 19 | __BAPt_SCRIPT_NAME=$(basename "$0") 20 | 21 | function BAPt_parse_arguments { 22 | local -n arg_defs="$1" 23 | shift 24 | local parent_args=("$@") 25 | local usage option_definitions parsed parts positionals options 26 | arg_defs+=([--help:flag]="Show help") 27 | 28 | # Why build $usage now rather than as and when it's needed? 29 | # $arg_defs, which is needded to build the usage string, is an associative array 30 | # which are hard to copy. So it's easier to maintain a copy of the prebuilt usage 31 | # rather than a copy of $arg_defs. 32 | usage=$(__BAPt_build_usage arg_defs) 33 | 34 | if [[ ${parent_args[*]} = "--help" ]]; then 35 | __BAPt_show_usage 0 36 | fi 37 | 38 | if [[ -n ${arg_defs[any]} ]]; then 39 | if [[ -z ${parent_args[*]} ]]; then 40 | echo "$__BAPt_ERROR_PREFIX: arguments expected" 1>&2 41 | echo 1>&2 42 | __BAPt_show_usage 1 43 | fi 44 | return 0 45 | fi 46 | 47 | __BAPt_parse 48 | } 49 | 50 | function __BAPt_parse { 51 | option_definitions=$(__BAPt_get_option_definitions) 52 | if ! parsed=$( 53 | getopt \ 54 | -n "$__BAPt_ERROR_PREFIX" \ 55 | --longoptions "$option_definitions" \ 56 | -- _ "${parent_args[@]}" 57 | ); then 58 | echo 59 | __BAPt_show_usage 1 60 | fi 61 | 62 | parts="$(echo "$parsed" | sed 's/ -- /\n/' | sed 's/ --$/\n/')" 63 | parts1=$(echo "$parts" | sed -n 1p) 64 | parts2=$(echo "$parts" | sed -n 2p) 65 | [[ $parts2 = "''" ]] && parts2="" 66 | 67 | if ! __BAPt_parse_positional_args "$parts2"; then 68 | __BAPt_show_usage 1 69 | fi 70 | 71 | if ! __BAPt_parse_options "$parts1"; then 72 | __BAPt_show_usage 1 73 | fi 74 | } 75 | 76 | function __BAPt_show_usage { 77 | local exit_code=$1 78 | if [[ $exit_code -gt 0 ]]; then 79 | echo "$usage" >&2 80 | exit "$exit_code" 81 | else 82 | echo "$usage" 83 | exit 0 84 | fi 85 | } 86 | 87 | function __BAPt_build_usage { 88 | local calling_function=${FUNCNAME[2]} 89 | local widest usage command_name positionals=() options=() description line arg_list=() 90 | 91 | widest=$(__BAPt_find_widest) 92 | 93 | __BAPt_extract_positionals_and_options 94 | 95 | if [[ -n $calling_function ]]; then 96 | command_name=$(basename "$calling_function") 97 | else 98 | command_name=$(basename "$__BAPt_SCRIPT_NAME") 99 | fi 100 | 101 | echo "Usage: $command_name ${arg_list[*]} [OPTIONS]" 102 | 103 | if [[ -n ${arg_defs[summary]} ]]; then 104 | echo 105 | echo "${arg_defs[summary]}" 106 | fi 107 | 108 | if [[ ${#positionals} -gt 0 ]]; then 109 | echo 110 | echo "Arguments:" 111 | for line in "${positionals[@]}"; do 112 | echo "$line" 113 | done 114 | fi 115 | 116 | if [[ ${#options} -gt 0 ]]; then 117 | echo 118 | echo "Options:" 119 | for line in "${options[@]}"; do 120 | echo "$line" 121 | done 122 | fi 123 | 124 | if [[ -n ${arg_defs[details]} ]]; then 125 | echo 126 | echo "${arg_defs[details]}" 127 | fi 128 | } 129 | 130 | function __BAPt_extract_positionals_and_options { 131 | for key in "${!arg_defs[@]}"; do 132 | description="${arg_defs[$key]}" 133 | if [[ $key =~ ^[0-9]: ]]; then 134 | name=${key/*:/} 135 | index=${key//:*/} 136 | else 137 | name=${key//:flag/} 138 | fi 139 | line=$(printf "%-${widest}s %s\n" " $name" "$description") 140 | if [[ $key =~ ^[0-9]: ]]; then 141 | arg_list[$index]="$name" 142 | positionals[$index]=$line 143 | fi 144 | if [[ $key = any ]]; then 145 | arg_list[0]="[ARGUMENTS]" 146 | positionals[0]=$line 147 | fi 148 | if [[ $key =~ ^-- ]]; then 149 | options+=("$line") 150 | fi 151 | done 152 | } 153 | 154 | function __BAPt_find_widest { 155 | local widest=0 156 | for key in "${!arg_defs[@]}"; do 157 | key=${key//:flag/} 158 | width=${#key} 159 | if [[ $width -gt $widest ]]; then 160 | widest=$width 161 | fi 162 | done 163 | echo "$((widest + 2))" 164 | } 165 | 166 | # Convert an associative array into a `getopt`-compatible options definition 167 | # Eg, from: 168 | # ([--foo]="bar" [--boolme]="") 169 | # to: 170 | # "foo:,boolme" 171 | function __BAPt_get_option_definitions { 172 | local value option_defs_string option_defs_array=() 173 | 174 | for key in "${!arg_defs[@]}"; do 175 | if [[ ! $key =~ ^-- ]]; then 176 | continue 177 | fi 178 | value="${arg_defs[$key]}" 179 | key="${key//--/}" 180 | if [[ ! $key =~ ":flag" ]]; then 181 | key="$key:" 182 | else 183 | key="${key//:flag/}" 184 | fi 185 | option_defs_array+=("$key") 186 | done 187 | option_defs_string=$(__BAPt_join_by ',' "${option_defs_array[@]}") 188 | 189 | echo "$option_defs_string" 190 | } 191 | 192 | function __BAPt_parse_positional_args { 193 | local parsed=$1 194 | declare -a "positionals=($parsed)" 195 | 196 | local index name arity=0 197 | for key in "${!arg_defs[@]}"; do 198 | if [[ $key =~ ^[0-9]: ]]; then 199 | index=${key//:*/} 200 | name=${key/*:/} 201 | arg_defs["$name"]="${positionals[index]}" 202 | unset 'arg_defs['"$key"']' 203 | arity=$((arity + 1)) 204 | fi 205 | done 206 | 207 | if [[ ${#positionals[@]} -ne $arity ]]; then 208 | echo "$__BAPt_ERROR_PREFIX: Expected $arity got ${#positionals[@]}" >&2 209 | echo >&2 210 | return 1 211 | fi 212 | } 213 | 214 | function __BAPt_parse_options { 215 | local parsed=$1 216 | local value name 217 | declare -a "options=($parsed)" 218 | 219 | local index=0 220 | for item in "${options[@]}"; do 221 | if [[ $item =~ ^-- ]]; then 222 | name=${item/*--/} 223 | if [[ -n ${arg_defs[$item]} ]]; then 224 | value=${options[(($index + 1))]} 225 | else 226 | value=true 227 | fi 228 | arg_defs["$name"]="$value" 229 | unset 'arg_defs['"$item"']' 230 | fi 231 | index=$((index + 1)) 232 | done 233 | } 234 | 235 | function __BAPt_join_by { 236 | local delimeter=${1-} field=${2-} 237 | if shift 2; then 238 | printf %s "$field" "${@/#/$delimeter}" 239 | fi 240 | } 241 | -------------------------------------------------------------------------------- /test/test.bash: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | setup_suite() { 4 | source '../bash_args_parser.bash' 5 | } 6 | 7 | example() { 8 | local args 9 | declare -A args=( 10 | [summary]="A short description of what this command or function does" 11 | [0:foo]="A foo argument, it's positional and required" 12 | [--bar]="A boolean option that doesn't take a value" 13 | [--foobar:flag]="An option that requires a value" 14 | ) 15 | BAPt_parse_arguments args "$@" 16 | 17 | echo "foo is ${args[foo]}. bar is ${args[bar]}. foobar is ${args[foobar]}" 18 | } 19 | 20 | 21 | test_happy_path() { 22 | result=$(example hello --bar world --foobar 2>&1) 23 | assert_matches "foo is hello" "$result" 24 | assert_matches "bar is world" "$result" 25 | assert_matches "foobar is true" "$result" 26 | } 27 | 28 | test_failures() { 29 | assert_fail example --bar world --foobar 2>&1 30 | result=$(example --bar world --foobar 2>&1) 31 | assert_matches "Expected 1 got 0" "$result" 32 | result=$(example hello --bar 2>&1) 33 | assert_matches "option '--bar' requires an argument" "$result" 34 | } 35 | --------------------------------------------------------------------------------