bashopts.sh 28.9 KB
Newer Older
1 2 3 4 5 6 7 8 9
# Copyright 2017 Emeric Verschuur <emeric@mbedsys.org>
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
10
#
11
#   http://www.apache.org/licenses/LICENSE-2.0
12
#
13 14 15 16 17 18 19
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

Emeric Verschuur's avatar
Emeric Verschuur committed
20 21
set -e

22
BASHOPTS_VERSION=2.0.1
Emeric Verschuur's avatar
Emeric Verschuur committed
23

Emeric Verschuur's avatar
Emeric Verschuur committed
24
bashopts_exit_handle() {
Emeric Verschuur's avatar
Emeric Verschuur committed
25 26 27 28
  local err=$?
  set +o xtrace
  local code="${1:-1}"
  echo "Error in ${BASH_SOURCE[1]}:${BASH_LINENO[0]}. '${BASH_COMMAND}' exited with status $err"
29
  # Print out the stack trace described by $function_stack
Emeric Verschuur's avatar
Emeric Verschuur committed
30 31 32 33 34 35 36 37 38 39 40 41 42 43
  if [ ${#FUNCNAME[@]} -gt 2 ]
  then
    echo "Call tree:"
    for ((i=1;i<${#FUNCNAME[@]}-1;i++))
    do
      echo " $i: ${BASH_SOURCE[$i+1]}:${BASH_LINENO[$i]} ${FUNCNAME[$i]}(...)"
    done
  fi
  echo "Exiting with status ${code}"
  exit "${code}"
}

# trap ERR to provide an error handler whenever a command exits nonzero
#  this is a more verbose version of set -o errexit
44
trap 'bashopts_exit_handle' ERR
Emeric Verschuur's avatar
Emeric Verschuur committed
45 46 47 48
# setting errtrace allows our ERR trap handler to be propagated to functions,
#  expansions and subshells
set -o errtrace

49
# display a error (fatal)
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
bashopts_log() {
    if [ -n "$bashopts_log_handler" ]; then
        $bashopts_log_handler "$@"
        return;
    fi
    local level=$1;
    shift || bashopts_log C "Usage bashopts_log <level> message"
    case "${level,,}" in
        c|critical)
            >&2 printf "[CRIT] %s\n" "$@"
            exit 1
            ;;
        e|error)
            >&2 printf "[ERRO] %s\n" "$@"
            ;;
        w|warning)
            >&2 printf "[WARN] %s\n" "$@"
            ;;
        *)
            bashopts_log C "Invalid log level: $level"
            ;;
    esac
Emeric Verschuur's avatar
Emeric Verschuur committed
72 73
}

74
if [ ! "${BASH_VERSINFO[0]}" -ge 4 ]; then
75
    bashopts_log C "bashopts require BASH version 4 or greater"
76 77
fi

78
# extract the value part of a declaration ("the value")
79 80 81 82 83 84
bashopts_get_def() {
    declare | grep "^$1=" | sed -E 's/^[^=]+=//g'
    # NOTE: alternative but not working in some case...:
    # declare -p $1 | sed -E "s/^declare\\s[^=]*=//g"
}

85
# extract the full declaration (name="the value")
86 87 88 89 90 91
bashopts_get_def_full() {
    declare | grep "^$1="
    # NOTE: alternative but not working in some case...:
    # declare -p $1 | sed -E "s/^declare\\s[^=]*=/$1=/g"
}

92
# check and format an option name value
93 94
bashopts_check_opt_name() {
    if [[ "$1" =~ ^[a-zA-Z0-9_]+$ ]]; then
Emeric Verschuur's avatar
Emeric Verschuur committed
95 96 97
        echo $1
        return 0
    fi
98
    bashopts_log E "'$1' is not a valid variable name"
99
    return 1
Emeric Verschuur's avatar
Emeric Verschuur committed
100 101
}

102
# check and format a number value
103
bashopts_check_number() {
Emeric Verschuur's avatar
Emeric Verschuur committed
104 105 106
    if [ -z "$1" ]; then
        echo 0
        return 0
107
    elif [[ "$1" =~ ^-?[0-9]+([.][0-9]+)?$ ]]; then
Emeric Verschuur's avatar
Emeric Verschuur committed
108 109 110
        echo $1
        return 0
    fi
Emeric Verschuur's avatar
Emeric Verschuur committed
111
    bashopts_log E "Option $op: '$1' is not a valid number"
112
    return 1
Emeric Verschuur's avatar
Emeric Verschuur committed
113 114
}

115
# check and format a boolean value
116
bashopts_check_boolean() {
117 118
    case "${1,,}" in
        ''|f|false|0)
Emeric Verschuur's avatar
Emeric Verschuur committed
119
            echo "false"
120
            return 0
Emeric Verschuur's avatar
Emeric Verschuur committed
121
            ;;
122
        t|true|1)
Emeric Verschuur's avatar
Emeric Verschuur committed
123
            echo "true"
124
            return 0
Emeric Verschuur's avatar
Emeric Verschuur committed
125 126
            ;;
        *)
Emeric Verschuur's avatar
Emeric Verschuur committed
127
            bashopts_log E "Option $op: '$1' is not a valid boolean value"
128
            return 1
Emeric Verschuur's avatar
Emeric Verschuur committed
129 130 131
            ;;
    esac
}
Emeric Verschuur's avatar
Emeric Verschuur committed
132

133 134 135 136 137 138
# check and format a string value
bashopts_check_string() {
    echo "$1"
    return 0
}

Emeric Verschuur's avatar
Emeric Verschuur committed
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
# check and format a enumeration value
bashopts_check_enumeration() {
    local line expr values
    while read -r line; do
        expr="^($line)\$"
        if [[ "$1" =~ $expr ]]; then
            echo "${line##*|}"
            return 0
        fi
        values+=("'${line##*|}'")
    done <<< "${2:-${bashopts_optprop_enum_values[$op]}}"
    bashopts_log E "Option $op: Invalid value '$1' (accepted values are: ${values[*]})"
    return 1
}

154 155 156 157 158 159
# check nothing
bashopts_check_nothing() {
    echo "$1"
    return 0
}

160
# declare the options property arrays
Emeric Verschuur's avatar
Emeric Verschuur committed
161
for f in name default expression short_opt long_opt description type enum_values method check setting interactive req_value; do
162
    eval declare -x -A bashopts_optprop_$f
163 164
done

165 166 167
# declare the associative array: arg name => option name
declare -x -A bashopts_arg2op
# option list in the declaration order
Emeric Verschuur's avatar
Emeric Verschuur committed
168
bashopts_optlist=()
169
# commands list (from global tool_name [args] [commands] [-- optional extra args])
Emeric Verschuur's avatar
Emeric Verschuur committed
170
bashopts_commands=()
171
# extra arguments list (from global tool_name [args] [commands] [-- optional extra args])
172
bashopts_extra_args=()
173
# tool name (from global tool_name [args] [commands] [-- optional extra args])
174
bashopts_tool_name=$0
175

176 177
# STEP 1: setup
bashopts_setup() {
178 179
    local arg arglist no_default_opts non_interactive disable_interactive
    if ! arglist=$(getopt -o "n:d:u:s:yxp" -n "$0 " -- "$@"); then
180
        bashopts_log C "Usage bashopts_setup:" \
181 182 183 184
            "        -n <val>  Tool name" \
            "        -d <val>  Tool description" \
            "        -u <val>  Tool usage description" \
            "        -s <val>  setting file path" \
185 186
            "        -y        Set non interactive mode as the default mode" \
            "        -x        Disable entirely interactive mode" \
187
            "        -p        Force value storage even if the value is equal to the default one"
188 189
    fi
    eval set -- "$arglist";
190
    # Store the global bashopts properties
191 192 193 194 195 196 197 198
    while true; do
        arg=$1
        shift
        case "$arg" in
            -n) bashopts_tool_name=$1;           shift;;
            -d) bashopts_tool_description=$1;    shift;;
            -u) bashopts_tool_usage=$1;          shift;;
            -s) bashopts_tool_settings_path=$1;  shift;;
199 200
            -y) non_interactive="true";;
            -x) disable_interactive="true";;
201
            -p) bashopts_tool_settings_force_write="true";;
202
            --) break;;
203
            *)  bashopts_log C "Fatal error";;
204 205 206
        esac
    done
    if [ -z "$bashopts_tool_name" ]; then
207
        bashopts_log C "Undefined tool name"
208 209
    fi
    if [ -z "$bashopts_tool_description" ]; then
210
        bashopts_log C "Undefined tool description"
211
    fi
Emeric Verschuur's avatar
Emeric Verschuur committed
212
    bashopts_tool_usage=${bashopts_tool_usage:-"$bashopts_tool_name [options and commands] [-- [extra args]]"}
213
    # add the default options
214
    bashopts_declare -n __BASHOPTS_DISPLAY_HELP__ -l help -o h -d "Display this help"
215
    if [ "$disable_interactive" == "true" ]; then
216
        BASHOPTS_INTERACTIVE="false"
217 218
    else
        if [ "$non_interactive" == "true" ]; then
219
            bashopts_declare -n BASHOPTS_INTERACTIVE -l interactive -o i -d "Interactive mode"
220
        else
221
            bashopts_declare -n BASHOPTS_NON_INTERACTIVE -l non-interactive -o n -d "Non interactive mode"
222
        fi
223 224 225 226
    fi
}

# STEP 2: add options
227
bashopts_declare() {
Emeric Verschuur's avatar
Emeric Verschuur committed
228 229
    local arg arglist options options_enum_values
    if ! arglist=$(getopt -o "n:v:x:o:l:d:t:e:m:k:rsi" -n "$0 " -- "$@"); then
230
        bashopts_log C "Usage bashopts_declare:" \
Emeric Verschuur's avatar
Emeric Verschuur committed
231 232
            "        -n <val>  Name" \
            "        -v <val>  Default value" \
Emeric Verschuur's avatar
Emeric Verschuur committed
233
            "        -x <val>  Bash expression: like default but this expression is computed and can contain variables and other bash expression" \
Emeric Verschuur's avatar
Emeric Verschuur committed
234 235 236
            "        -o <val>  Short option" \
            "        -l <val>  Long option" \
            "        -d <val>  Description" \
Emeric Verschuur's avatar
Emeric Verschuur committed
237 238
            "        -t <val>  Value type: string, enumeration, number, boolean (default)" \
            "        -e <val>  Enum element: restrict accepted values with a list of '-e <element>' options (you have to set one '-e <val>' by elements)" \
Emeric Verschuur's avatar
Emeric Verschuur committed
239
            "        -m <val>  Method: set (DEFAULT: simple value), add (list with several values)" \
240
            "        -k <val>  Custom check method (bash function)" \
241
            "        -r        Value required" \
242
            "        -i        Enable interactive edition" \
243
            "        -s        Store in setting"
Emeric Verschuur's avatar
Emeric Verschuur committed
244 245 246
    fi
    eval set -- "$arglist";
    declare -A options
247
    # parse all the parameters
Emeric Verschuur's avatar
Emeric Verschuur committed
248 249 250 251
    while true; do
        arg=$1
        shift
        case "$arg" in
252
            -n) options[name]=$(bashopts_check_opt_name $1 || exit 1); shift;;
Emeric Verschuur's avatar
Emeric Verschuur committed
253 254 255 256 257 258 259 260 261
            -v) options[default]=$1;        shift;;
            -x) options[expression]=$1;     shift;;
            -o) options[short_opt]=$1;      shift;;
            -l) options[long_opt]=$1;       shift;;
            -d) options[description]=$1;    shift;;
            -t) options[type]=$1;           shift;;
            -e) options_enum_values+=($1);  shift;;
            -m) options[method]=$1;         shift;;
            -k) options[check]=$1;          shift;;
262
            -s) options[setting]="true";;
263
            -i) options[interactive]="true";;
Emeric Verschuur's avatar
Emeric Verschuur committed
264 265
            -r) options[req_value]="true";;
            --) break;;
266
            *)  bashopts_log C "Fatal error";;
Emeric Verschuur's avatar
Emeric Verschuur committed
267 268
        esac
    done
269
    # Check incompatible -v and -r options
270
    if [ -n "${options[default]}" ] && [ "${options[req_value]}" == "true" ]; then
271
        bashopts_log C "bashopts_declare: -r and -v options cannot be activated at the same time"
272
    fi
273
    # format the type and check/format the default value
Emeric Verschuur's avatar
Emeric Verschuur committed
274 275
    case "${options[type],,}" in
        ''|b|bool|boolean)
276 277
            options[type]="boolean"
            ;;
Emeric Verschuur's avatar
Emeric Verschuur committed
278 279 280 281 282 283 284 285
        e|enum|enumeration)
            options[type]="enumeration"
            if [ ${#options_enum_values[@]} -lt 2 ]; then
                bashopts_log C "bashopts_declare: ${options[name]} enumeration need at least two elements (two '-e <val>' calls at least)"
            fi
            options[enum_values]="$(IFS=$'\n'; echo "${options_enum_values[*]}")"
            ;;
        s|str|string)
Emeric Verschuur's avatar
Emeric Verschuur committed
286
            options[type]="string"
Emeric Verschuur's avatar
Emeric Verschuur committed
287
            ;;
Emeric Verschuur's avatar
Emeric Verschuur committed
288
        n|num|number)
Emeric Verschuur's avatar
Emeric Verschuur committed
289
            options[type]="number"
Emeric Verschuur's avatar
Emeric Verschuur committed
290
            ;;
Emeric Verschuur's avatar
Emeric Verschuur committed
291
        *)
292
            bashopts_log C "Invalid type ${options[type]}"
Emeric Verschuur's avatar
Emeric Verschuur committed
293 294
            ;;
    esac
Emeric Verschuur's avatar
Emeric Verschuur committed
295 296 297 298 299
    # Check for incompatibility with old version (-e opt moved to -x)
    if [ "${options[type]}" != "enumeration" ] && [ ${#options_enum_values[@]} -gt 0 ]; then
        bashopts_log C "bashopts_declare: The former '-e' option is now moved to '-x'" \
            " => the new '-e' is reserved for enumeration elements"
    fi
300 301 302 303
    # Setup check value method
    if ! [[ -v options[check] ]]; then
        options[check]="bashopts_check_${options[type]}"
    fi
304
    # format the option method
305
    case "${options[method],,}" in
Emeric Verschuur's avatar
Emeric Verschuur committed
306
        ''|s|set)
307
            # default: simple value - override
Emeric Verschuur's avatar
Emeric Verschuur committed
308
            options[method]="set"
309 310
            if [ "${options[type]}" != "string" ] || [[ -v options[default] ]]; then
                # Check the default value format
311 312 313 314
                if ! options[default]="$(${options[check]} "${options[default]}" "${options[enum_values]}")"; then
                    bashopts_log W "Invalid default value for ${options[name]} option, this value will stay unset"
                    unset options[default]
                fi
315
            fi
Emeric Verschuur's avatar
Emeric Verschuur committed
316
            ;;
Emeric Verschuur's avatar
Emeric Verschuur committed
317
        a|add)
318
            # array value - add
Emeric Verschuur's avatar
Emeric Verschuur committed
319
            options[method]="add"
Emeric Verschuur's avatar
Emeric Verschuur committed
320 321
            ;;
        *)
322
            bashopts_log C "Invalid method ${options[method]}"
Emeric Verschuur's avatar
Emeric Verschuur committed
323 324
            ;;
    esac
325
    # Check option name
326
    if [[ -v bashopts_optprop_name[${options[name]}] ]]; then
327
        bashopts_log C "Dupplicate option name '${options[name]}'"
Emeric Verschuur's avatar
Emeric Verschuur committed
328
    fi
329
    # check the short option
Emeric Verschuur's avatar
Emeric Verschuur committed
330 331
    if [[ -v options[short_opt] ]]; then
        if ! [[ ${options[short_opt]} =~ ^[a-zA-Z0-9_-]$ ]]; then
332
            bashopts_log C "Invalid short option ${options[short_opt]}"
Emeric Verschuur's avatar
Emeric Verschuur committed
333
        fi
334
        if [[ -v bashopts_arg2op[-${options[short_opt]}] ]]; then
335
            bashopts_log C "Dupplicate short option '${options[short_opt]}'"
Emeric Verschuur's avatar
Emeric Verschuur committed
336
        fi
337
        bashopts_arg2op[-${options[short_opt]}]=${options[name]}
Emeric Verschuur's avatar
Emeric Verschuur committed
338
    fi
339
    # check the long option
340 341
    if [[ -v options[long_opt] ]]; then
        if ! [[ ${options[long_opt]} =~ ^[a-zA-Z0-9_-]{2,}$ ]]; then
342
            bashopts_log C "Invalid long option ${options[long_opt]}"
343
        fi
344
        if [[ -v bashopts_arg2op[--${options[long_opt]}] ]]; then
345
            bashopts_log C "Dupplicate long option '${options[long_opt]}'"
346
        fi
347
        bashopts_arg2op[--${options[long_opt]}]=${options[name]}
348
    fi
349
    # store the option properties
350
    for f in ${!options[@]}; do
351
        eval "bashopts_optprop_$f[${options[name]}]='${options[$f]//\'/\'\\\'\'}'"
Emeric Verschuur's avatar
Emeric Verschuur committed
352 353 354 355
    done
    bashopts_optlist+=(${options[name]})
}

Emeric Verschuur's avatar
Emeric Verschuur committed
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
bashopts_get_valid_value_list() {
    local op
    case "$1" in
        -*)
            op=${bashopts_arg2op[$1]}
            ;;
        *)
            op=$1
            ;;
    esac
    case "${bashopts_optprop_type[$op]}" in
        boolean)
            echo -e "true\nfalse"
            ;;
        enumeration)
            while read -r line; do
                echo "\"${line##*|}\""
            done <<< "${bashopts_optprop_enum_values[$op]}"
            ;;
    esac
}

378
# maximum of two values
Emeric Verschuur's avatar
Emeric Verschuur committed
379 380 381 382
bashopts_math_max() {
    echo $(($1>$2?$1:$2))
}

383
# minimum of two values
Emeric Verschuur's avatar
Emeric Verschuur committed
384 385 386 387
bashopts_math_min() {
    echo $(($1<$2?$1:$2))
}

388
# join array element
Emeric Verschuur's avatar
Emeric Verschuur committed
389 390
bashopts_join_by() {
    local IFS="$1"
391
    shift || bashopts_log C "Usage: bashopts_join_by <character> [elt1 [elt2...]]"
Emeric Verschuur's avatar
Emeric Verschuur committed
392 393 394
    echo "$*"
}

395
# dump an option value by its name
Emeric Verschuur's avatar
Emeric Verschuur committed
396 397
bashopts_dump_value() {
    local op=$1
398
    shift || bashopts_log C "Usage: bashopts_dump_value op_name"
399
    [[ -v "$op" ]] || return 0
Emeric Verschuur's avatar
Emeric Verschuur committed
400 401 402 403 404 405 406 407
    if [ "${bashopts_optprop_method[$op]}" == "set" ]; then
        if [ "${bashopts_optprop_type[$op]}" == "string" ]; then
            echo -n "\"${!op//\"/\\\"}\""
        else
            echo -n "${!op}"
        fi
        return 0
    fi
Emeric Verschuur's avatar
Emeric Verschuur committed
408
    eval set -- \"\${${op}[@]}\"
Emeric Verschuur's avatar
Emeric Verschuur committed
409 410 411 412 413 414
    echo -n "["
    if [ "${bashopts_optprop_type[$op]}" == "string" ]; then
        echo -n "\"${1//\"/\\\"}\""
    else
        echo -n "${1}"
    fi
Emeric Verschuur's avatar
Emeric Verschuur committed
415 416
    shift
    while [ -n "$1" ]; do
Emeric Verschuur's avatar
Emeric Verschuur committed
417 418 419 420 421
        if [ "${bashopts_optprop_type[$op]}" == "string" ]; then
            echo -n ", \"${1//\"/\\\"}\""
        else
            echo -n ", ${1}"
        fi
Emeric Verschuur's avatar
Emeric Verschuur committed
422
        shift
Emeric Verschuur's avatar
Emeric Verschuur committed
423 424 425 426
    done
    echo -n "]"
}

427
# display the formated help
Emeric Verschuur's avatar
Emeric Verschuur committed
428
bashopts_diplay_help() {
Emeric Verschuur's avatar
Emeric Verschuur committed
429
    local elts optargs_max_len=8 val ncol line
Emeric Verschuur's avatar
Emeric Verschuur committed
430
    declare -A optargs
431 432 433
    ncol=$(tput cols 2> /dev/null || true)
    ncol=${ncol:-160}
    local value_max_len=$(( $ncol / 4 ))
434
    # compute the good arguments comumn size
Emeric Verschuur's avatar
Emeric Verschuur committed
435 436
    for op in "${bashopts_optlist[@]}"; do
        elts=()
Emeric Verschuur's avatar
Emeric Verschuur committed
437
        unset val
Emeric Verschuur's avatar
Emeric Verschuur committed
438
        if ! [[ $op =~ ^__.*__$ ]] && [[ -v $op ]]; then
439
            val=" $(bashopts_dump_value $op | tr -d '\n')"
Emeric Verschuur's avatar
Emeric Verschuur committed
440
        fi
441 442
        if [[ -v bashopts_optprop_short_opt[$op] ]]; then elts+=("-${bashopts_optprop_short_opt[$op]}"); fi
        if [[ -v bashopts_optprop_long_opt[$op] ]]; then elts+=("--${bashopts_optprop_long_opt[$op]}"); fi
443
        optargs[$op]="$(bashopts_join_by , ${elts[@]})${val:0:${value_max_len}}"
Emeric Verschuur's avatar
Emeric Verschuur committed
444 445
        optargs_max_len=$(bashopts_math_max $optargs_max_len ${#optargs[$op]})
    done
Emeric Verschuur's avatar
Emeric Verschuur committed
446
    optargs_max_len=$(bashopts_math_min $optargs_max_len $(( $ncol / 3 )) )
447
    # display global info
448
    echo
449
    echo "NAME:"
450
    echo "    $bashopts_tool_name - $bashopts_tool_description"
451 452
    echo
    echo "USAGE:"
453
    echo -e "    $bashopts_tool_usage"
Emeric Verschuur's avatar
Emeric Verschuur committed
454
    echo
455
    echo "OPTIONS:"
Emeric Verschuur's avatar
Emeric Verschuur committed
456
    for op in "${bashopts_optlist[@]}"; do
Emeric Verschuur's avatar
Emeric Verschuur committed
457
        # display arguments, value if available, description, and additional info if available
458 459 460 461 462 463 464 465 466 467
        printf "    %-${optargs_max_len}s    ${bashopts_optprop_description[$op]}" "${optargs[$op]}"
        if ! [[ $op =~ ^__.*__$ ]]; then
            # display additional information the each properties
            # discarding special options like --help
            echo -n " - [\$$op] (type:${bashopts_optprop_type[$op]}"
            if [[ -v bashopts_optprop_expression[$op] ]]; then
                printf ", default: \"%.${value_max_len}s\"" "$(tr -d '\n' <<< "${bashopts_optprop_expression[$op]//\"/\\\"}")"
            elif [[ -v bashopts_optprop_default[$op] ]]; then
                if [[ "${bashopts_optprop_type[$op]}" =~ ^(string|enumeration)$ ]]; then
                    printf ", default: \"%.${value_max_len}s\"" "$(tr -d '\n' <<< "${bashopts_optprop_default[$op]//\"/\\\"}")"
468
                else
469
                    printf ", default: %.${value_max_len}s" "$(tr -d '\n' <<< "${bashopts_optprop_default[$op]}")"
470
                fi
471 472
            else
                elts=")"
473
            fi
474 475 476 477 478 479 480 481 482 483 484
            if [ "${bashopts_optprop_type[$op]}" == "enumeration" ]; then
                echo -n ", accepted values:$(
                    while read -r line; do
                        echo -n " '${line##*|}'"
                    done <<< "${bashopts_optprop_enum_values[$op]}"
                )"
            fi
            echo ")"
        else
            echo ""
        fi
485
    done
Emeric Verschuur's avatar
Emeric Verschuur committed
486 487 488
    test "$1" != "-e" || exit $2
}

489 490 491 492 493
# Enable help display on option process
bashopts_diplay_help_delayed() {
    __BASHOPTS_DISPLAY_HELP__="true"
}

494
# display all otions values and properties
Emeric Verschuur's avatar
Emeric Verschuur committed
495 496
bashopts_diplay_summary() {
    local elts desc_max_len=0 val dval
497
    local value_max_len=$(( $ncol / 4 ))
Emeric Verschuur's avatar
Emeric Verschuur committed
498 499 500 501 502 503
    declare -A optargs
    for op in "${bashopts_optlist[@]}"; do
        desc_max_len=$(bashopts_math_max $desc_max_len ${#bashopts_optprop_description[$op]})
    done
    for op in "${bashopts_optlist[@]}"; do
        if ! [[ $op =~ ^__.*__$ ]]; then
504
            printf "* %-${desc_max_len}.${value_max_len}s : $(bashopts_dump_value $op | tr -d '\n')\n" "${bashopts_optprop_description[$op]}"
Emeric Verschuur's avatar
Emeric Verschuur committed
505 506
        fi
    done
507 508
}

509
# STEP 3: parse arg
510 511
bashopts_parse_args() {
    local op arg val args is_arg short_opts long_opts
512

513
    # split argument into two arrays: normal and extra arguments
514 515 516 517 518 519 520 521 522 523 524
    is_arg=1
    args=()
    for arg in "$@"; do
        if [ $is_arg -eq 1 ]; then
            if [ "$arg" == "--" ]; then is_arg=0; continue; fi
            args+=("$arg")
        else
            bashopts_extra_args+=("$arg")
        fi
    done

525
    # build the long and short getopt option list from the options
526 527 528
    short_opts=""
    long_opts=()
    for op in "${bashopts_optlist[@]}"; do
529
        if [[ -v bashopts_optprop_short_opt[$op] ]]; then
530
            short_opts="${short_opts}${bashopts_optprop_short_opt[$op]}:$(test "${bashopts_optprop_type[$op]}" != "boolean" || echo ":")"
531
        fi
532
        if [[ -v bashopts_optprop_long_opt[$op] ]]; then
533
            long_opts+=("${bashopts_optprop_long_opt[$op]}:$(test "${bashopts_optprop_type[$op]}" != "boolean" || echo ":")")
Emeric Verschuur's avatar
Emeric Verschuur committed
534
        fi
535
    done
536
    long_opts=$(bashopts_join_by , ${long_opts[@]})
537

538
    # call the getopt
539
    if ! args=$(getopt -o $short_opts -l "$long_opts" -n "$bashopts_tool_name" -- "${args[@]}"); then
540 541 542 543
        >&2 bashopts_diplay_help
        exit 1
    fi
    eval set -- "$args";
544

545
    # store the arguments value part
546 547 548 549 550
    while true; do
        arg=$1
        shift
        case $arg in
            --)
551
                # end of the argument part
552 553 554
                break
                ;;
            -*)
Emeric Verschuur's avatar
Emeric Verschuur committed
555
                val="$1"
556
                shift
557
                op=${bashopts_arg2op[$arg]}
558 559 560 561 562 563 564 565 566
                if [ -z "$val" ]; then
                    if [ "${bashopts_optprop_type[$op]}" == "boolean" ]; then
                        # boolean argument with no value is considered as true
                        val="true"
                    else
                        # empty value tell to unset the value or clear the array
                        unset $op
                        continue
                    fi
567
                fi
568
                val="$(${bashopts_optprop_check[$op]} "$val")" || exit 1
569
                case "${bashopts_optprop_method[$op]}" in
570
                    set)
571
                        # normal case: override the value
572
                        eval "$op=$(declare -p val | sed -E 's/^declare\s[^=]*=//g')"
573 574
                        ;;
                    add)
575
                        # array case: add the value
576
                        eval "$op+=($(declare -p val | sed -E 's/^declare\s[^=]*=//g'))"
577 578 579 580
                        ;;
                esac
                ;;
            *)
581
                bashopts_log C "Fatal error: args"
582 583 584
                ;;
        esac
    done
585 586

    # store the command part
587
    bashopts_commands=("$@")
588 589
}

590
# display an array: [val1, val2, ...]
Emeric Verschuur's avatar
Emeric Verschuur committed
591 592
bashopts_dump_array() {
    local type=$1
593
    shift || bashopts_log C "Usage: bashopts_dump_array type elt1 [elt2...]"
Emeric Verschuur's avatar
Emeric Verschuur committed
594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611
    echo -n "["
    if [ "$type" == "string" ]; then
        echo -n "\"${1//\"/\\\"}\""
    else
        echo -n "${1}"
    fi
    shift || true
    while [ -n "$1" ]; do
        if [ "$type" == "string" ]; then
            echo -n ", \"${1//\"/\\\"}\""
        else
            echo -n ", ${1}"
        fi
        shift
    done
    echo -n "]"
}

Emeric Verschuur's avatar
Emeric Verschuur committed
612 613 614 615 616 617 618 619 620
bashopts_read_json_array() {
    local line
    while read -r line; do
        eval "$1+=($line)"
    done <<< "$(jq '.[]' <<< "$2")" && return 0 || \
    bashopts_log E "Invalid JSON array"
    return 1
}

621
# Process a specified option
622 623 624
bashopts_process_option() {
    local dval tval ival op arg arglist check val_req edit_req
    if ! arglist=$(getopt -o "n:k:r" -n "bashopts_process_option " -- "$@"); then
625
        bashopts_log C "Usage bashopts_process_opt" \
626 627 628
            "        -n <val>  property name" \
            "        -k <val>  override value check function" \
            "        -r        At least one value required"
629
    fi
630 631 632 633 634 635 636 637 638 639
    eval set -- "$arglist";
    # parse all the parameters
    while true; do
        arg=$1
        shift
        case "$arg" in
            -n) op=$1; shift;;
            -k) check=$1; shift;;
            -r) val_req="true";;
            --) break;;
640
            *)  bashopts_log C "Fatal error";;
641 642 643
        esac
    done
    test -n "$op" || \
644
        bashopts_log C "bashopts_process_option: missing -n option"
645 646 647
    if [ -z "$check" ]; then
        check="${bashopts_optprop_check[$op]}"
    fi
648 649 650
    if [ "${bashopts_optprop_req_value[$op]}" == "true" ]; then
        val_req="true"
    fi
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667

    # eval or get default value
    if [[ -v bashopts_optprop_expression[$op] ]]; then
        eval "dval=${bashopts_optprop_expression[$op]}"
    elif [ "${bashopts_optprop_method[$op]}" == "add" ]; then
        dval=()
    else
        dval="${bashopts_optprop_default[$op]}"
    fi
    # Init edit_req
    edit_req=${bashopts_optprop_interactive[$op]}
    if [[ -v $op ]]; then
        # Extract value from option name
        eval "tval=$(bashopts_get_def $op)"
        # Edition no more really required if already defined
        edit_req="false"
    elif [ "${bashopts_optprop_setting[$op]}" == "true" ] \
Emeric Verschuur's avatar
Emeric Verschuur committed
668
        && [ -f "$(readlink -m "$bashopts_tool_settings_path")" ] \
669 670 671 672 673 674 675
        && grep -E -q "^$op=" $bashopts_tool_settings_path; then
        eval "tval=$(grep -E "^${op}=" $bashopts_tool_settings_path | sed -E "s/^[^=]+=//g")"
    fi
    if [[ -v tval ]]; then
        # Check current value(s)
        for (( i=0; i<${#tval[@]}; i++)); do
            if ! $check "${tval[$i]}" > /dev/null; then
676
                if [ "$BASHOPTS_INTERACTIVE" != "true" ]; then
677
                    bashopts_log C "Non interactive mode: Exit due to one or more error"
678 679 680 681
                fi
                # (re)enable edition
                edit_req="true"
                break
682
            fi
683
        done
Emeric Verschuur's avatar
Emeric Verschuur committed
684
    elif [ "$val_req" == "true" ] && [ "$__BASHOPTS_DISPLAY_HELP__" != "true" ]; then
685
        bashopts_log E "At least one value required"
686
        if [ "$BASHOPTS_INTERACTIVE" != "true" ]; then
687
            bashopts_log C "Non interactive mode: Exit due to one or more error"
688 689 690 691 692 693 694 695 696 697
        fi
        # (re)enable edition
        edit_req="true"
    fi
    if ! [[ -v tval ]] || [ "$edit_req" == "true" ]; then
        if [[ ! -v tval ]] && [ -n "$dval" ]; then
            # set default value
            eval "tval=$(bashopts_get_def dval)"
        fi
        if [ "$edit_req" == "true" ]; then
698
            if [ "$BASHOPTS_INTERACTIVE" == "true" ]; then
699
                # interactive edition
700
                while true; do
Emeric Verschuur's avatar
Emeric Verschuur committed
701 702 703 704 705 706 707 708 709 710 711 712 713
                    # Display the property description
                    echo "* ${bashopts_optprop_description[$op]}$(
                        # Add possible value list for enumeration type
                        if [ "${bashopts_optprop_type[$op]}" == "enumeration" ]; then
                            echo -n " (accepted values:$(
                                while read -r line; do
                                    echo -n " '${line##*|}'"
                                done <<< "${bashopts_optprop_enum_values[$op]}"
                            )"
                            echo -n ")"
                        fi
                    )"
                    # Add info for array properties
Emeric Verschuur's avatar
Emeric Verschuur committed
714 715 716
                    if [ "${bashopts_optprop_method[$op]}" == "add" ]; then
                        echo " -> List property format: 'single val.' or BASH array '(v1 v2 v3)' or JSON array '[v1, v2, v3]'"
                    fi
Emeric Verschuur's avatar
Emeric Verschuur committed
717
                    echo -n "    $(bashopts_dump_array {bashopts_optprop_type[$op]} "${tval[@]}"): "
Emeric Verschuur's avatar
Emeric Verschuur committed
718
                    read ival || bashopts_log C "Unexpected error, aborting..."
719 720 721
                    if [ -n "$ival" ]; then
                        if [ "${bashopts_optprop_method[$op]}" == "add" ]; then
                            # array value
Emeric Verschuur's avatar
Emeric Verschuur committed
722 723 724 725 726 727 728 729 730 731 732 733 734 735 736
                            tval=()
                            case "${ival:0:1}" in
                                '[')
                                    bashopts_read_json_array tval "$ival" || continue
                                    ;;
                                '(')
                                    if ! eval "tval=$ival" 2>/dev/null; then
                                        bashopts_log E "Invalid BASH array"
                                        continue
                                    fi
                                    ;;
                                *)
                                    tval+=("$ival")
                                    ;;
                            esac
737 738 739 740
                        else
                            # non array/normal value
                            tval=$ival
                        fi
Emeric Verschuur's avatar
Emeric Verschuur committed
741 742
                    elif [ "${bashopts_optprop_method[$op]}" == "add" ] && ! [[ -v tval ]]; then
                        tval=()
743 744 745
                    fi
                    # check format
                    if [ "${#tval[@]}" -eq 0 ] && [ "$val_req" == "true" ]; then
746
                        bashopts_log E "At least one value required"
747 748
                        unset tval
                        continue
749 750
                    fi
                    if [ "${bashopts_optprop_method[$op]}" == "add" ]; then
751
                        # array value
752 753 754 755 756 757 758 759 760
                        for (( i=0; i<${#tval[@]}; i++)); do
                            if ! tval[$i]="$($check "${tval[$i]}")"; then
                                unset tval
                                break
                            fi
                        done
                    else
                        # non array/normal value
                        if ! tval="$($check "$tval")"; then
761 762 763
                            unset tval
                        fi
                    fi
Emeric Verschuur's avatar
Emeric Verschuur committed
764
                    if declare -p tval > /dev/null 2>&1; then
765
                        # edit OK, break
766 767
                        break
                    fi
768
                    # otherwise, loop...
769
                done
770 771
            fi
        fi
772 773 774 775 776 777 778 779
    fi
    if [[ -v tval ]]; then
        eval "$op=$(bashopts_get_def tval)"
    fi
    if [ "${bashopts_optprop_setting[$op]}" == "true" ]; then
        if [ -n "$bashopts_tool_settings_path" ]; then
            # vrite the value to the setting file
            (
780 781
                test -d "$(dirname $bashopts_tool_settings_path)" || \
                    mkdir -p "$(dirname $bashopts_tool_settings_path)"
782 783 784 785 786 787 788 789 790 791
                if [ -f "$bashopts_tool_settings_path" ]; then
                    # remove old value
                    sed -i "/^$op=/d" $bashopts_tool_settings_path
                fi
                if [ "$bashopts_tool_settings_force_write" == "true" ] \
                    || [ "$(bashopts_get_def $op)" != "$(bashopts_get_def dval)" ]; then
                    # append the new value to the file if the value is not the default or 
                    # force_write is true
                    echo "$(bashopts_get_def_full $op)" >> $bashopts_tool_settings_path
                fi
792
            ) || bashopts_log W "Please check the settings file"
793
        else
794
            bashopts_log W "No settings file specified"
795
        fi
796
    fi
797 798 799
    if [ "$op" == "BASHOPTS_NON_INTERACTIVE" ] && ! [[ -v BASHOPTS_INTERACTIVE ]]; then
        if [ "$BASHOPTS_NON_INTERACTIVE" == "true" ]; then
            BASHOPTS_INTERACTIVE="false"
800
        else
801
            BASHOPTS_INTERACTIVE="true"
802
        fi
803 804 805 806 807 808
    fi
}

# STEP 4: process arg
bashopts_process_opts() {
    local op
809 810
    if [ "$__BASHOPTS_DISPLAY_HELP__" == "true" ]; then
        BASHOPTS_INTERACTIVE="false"
811 812 813
    fi
    for op in "${bashopts_optlist[@]}"; do
        bashopts_process_option -n $op
814
    done
815
    if [ "$__BASHOPTS_DISPLAY_HELP__" == "true" ]; then
816 817 818
        bashopts_diplay_help
        exit 0
    fi
Emeric Verschuur's avatar
Emeric Verschuur committed
819
}
820

821
# Export all option variables
822 823 824 825 826 827
bashopts_export_opts() {
    for op in "${bashopts_optlist[@]}"; do
        if [[ -v $op ]]; then
            export $op
        fi
    done
828
}