Commit dbfcaa37 authored by Emeric Verschuur's avatar Emeric Verschuur
Browse files

add bashopts_process_option and process option rewrite

parent 51305b79
Loading
Loading
Loading
Loading
Loading
+15 −13
Original line number Diff line number Diff line
@@ -8,7 +8,7 @@ There are mainly 5 steps:
- Setup bashopts with ```bashopts_setup``` function
- Declare options/arguments with ```bashopts_declare``` (one call per option)
- Parse arguments with ```bashopts_parse_args```
- Process arguments with ```bashopts_process_args```
- Process arguments with ```bashopts_process_opts```

### Load the library
Load all the function from the ```bashopts.sh``` file and you can add the line ```trap 'bashopts_exit_handle' ERR``` to display backtrace on error
@@ -26,7 +26,9 @@ bashopts_setup option list:
 - ```-d <val>```: Tool description
 - ```-u <val>```: Tool usage (optional)
 - ```-s <val>```: Define a setting file path (optional)
 - ```-f```: Force edition for interactive options (default: a prompt is displayed only if not given via the command line)
 - ```-y```: Set non interactive mode as the default mode (optional)
 - ```-x```: Disable entirely interactive mode (optional)
 - ```-p```: Force value storage even if the value is equal to the default one (optional)
 
Code example:
```bash
@@ -71,7 +73,7 @@ bashopts_parse_args "$@"
```

### Argument processing
Process all the options with ```bashopts_process_args```
Process all the options with ```bashopts_process_opts```

This function will fill all options values using the following order:
- Value from the command line
@@ -81,9 +83,9 @@ This function will fill all options values using the following order:
- The default option value (may be defined with the ```bashopts_declare -v```)


bashopts_process_args usage:
bashopts_process_opts usage:
```bash
bashopts_process_args
bashopts_process_opts
```

### Option exploitation
@@ -105,20 +107,20 @@ bashopts_process_args
trap 'bashopts_exit_handle' ERR

# Initialize the library
bashopts_setup -n "$0" -d "This is myapp tool description displayed on help message" -s "$HOME/.config/myapprc"
bashopts_setup -n "example.sh" -d "This is myapp tool description displayed on help message" -s "$HOME/.config/myapprc"

# Declare the options
bashopts_declare -n first_name -l first -o f -d "First name" -t string -i -s -r
bashopts_declare -n last_name -l last -o l -d "Last name" -t string -i -s -r
bashopts_declare -n display_name -l display-name -t string -d "Display name" -e "\$first_name \$last_name"
bashopts_declare -n display_name -l display-name -t string -d "Display name" -e "\"\$first_name \$last_name\""
bashopts_declare -n age -l number -d "Age" -t number
bashopts_declare -n email_list -t string -m add -l email -d "Email adress"

# Parse arguments
bashopts_parse_args "$@"

# Process argument
bashopts_process_args
# Process options
bashopts_process_opts

# Display the values
echo
@@ -139,17 +141,17 @@ $ ./example.sh --help
Output:
```
NAME:
    ./example.sh - This is myapp tool description displayed on help message
    example.sh - This is myapp tool description displayed on help message

USAGE:
    [options and commands] [-- [extra args]]
    example.sh [options and commands] [-- [extra args]]

OPTIONS:
    -h,--help                          Display this help
    -n,--non-interactive true          Non interactive mode - [$bashopts_non_interactive] (type:boolean, default:false)
    -n,--non-interactive false         Non interactive mode - [$bashopts_non_interactive] (type:boolean, default:false)
    -f,--first "John"                  First name - [$first_name] (type:string, default:"")
    -l,--last "Smith"                  Last name - [$last_name] (type:string, default:"")
    --display-name "John Smith"        Display name - [$display_name] (type:string, default:"$first_name $last_name")
    --display-name "John Smith"        Display name - [$display_name] (type:string, default:"\"$first_name $last_name\"")
    --number 0                         Age - [$age] (type:number, default:0)
    --email                            Email adress - [$email_list] (type:string, default:"")
```
+218 −153
Original line number Diff line number Diff line
@@ -44,8 +44,8 @@ trap 'bashopts_exit_handle' ERR
#  expansions and subshells
set -o errtrace

# display a error (fatal) error
bashopts_error() {
# display a error (fatal)
bashopts_critical() {
    local l
    >&2 echo -n "[ERROR] "
    for l in "$@"; do
@@ -54,7 +54,16 @@ bashopts_error() {
    exit 1
}

# display a warning (non fatal) error
# display a error (non fatal)
bashopts_error() {
    local l
    >&2 echo -n "[ERROR] "
    for l in "$@"; do
        >&2 echo -e "$l"
    done
}

# display a warning (non fatal)
bashopts_warning() {
    local l
    >&2 echo -n "[WARNING] "
@@ -67,16 +76,6 @@ bashopts_regex_escape () {
    echo $1 | sed 's/[][()\.^$\/?*+]/\\&/g'
}

# check if the value is a boolean
bashopts_isboolean() {
    [[ $1 =~ ^(true|false)$ ]] || return 1
}

# check if the value is a number
bashopts_isnumber() {
    [[ $1 =~ ^-?[0-9]+([.][0-9]+)?$ ]] || return 1
}

# extract the value part of a declaration ("the value")
bashopts_get_def() {
    declare | grep "^$1=" | sed -E 's/^[^=]+=//g'
@@ -92,43 +91,60 @@ bashopts_get_def_full() {
}

# check and format an option name value
bashopts_chkvarname() {
    if [[ $1 =~ ^[a-zA-Z0-9_]+$ ]]; then
bashopts_check_opt_name() {
    if [[ "$1" =~ ^[a-zA-Z0-9_]+$ ]]; then
        echo $1
        return 0
    fi
    bashopts_error "$1 is not a valid variable name"
    bashopts_error "'$1' is not a valid variable name"
    return 1
}

# check and format a number value
bashopts_chknum() {
bashopts_check_number() {
    if [ -z "$1" ]; then
        echo 0
        return 0
    elif [[ $1 =~ ^-?[0-9]+([.][0-9]+)?$ ]]; then
    elif [[ "$1" =~ ^-?[0-9]+([.][0-9]+)?$ ]]; then
        echo $1
        return 0
    fi
    bashopts_error "$1 is not a valid number"
    bashopts_error "'$1' is not a valid number"
    return 1
}

# check and format a boolean value
bashopts_chkbool() {
bashopts_check_boolean() {
    case "$1" in
        ''|f|false|F|FALSE|0)
        ''|f|false|F|FALSE|False|0)
            echo "false"
            return 0
            ;;
        t|true|T|TRUE|1)
        t|true|T|TRUE|True|1)
            echo "true"
            return 0
            ;;
        *)
            bashopts_error "$1 is not a valid boolean value"
            bashopts_error "'$1' is not a valid boolean value"
            return 1
            ;;
    esac
}

# check and format a string value
bashopts_check_string() {
    echo "$1"
    return 0
}

# check nothing
bashopts_check_nothing() {
    echo "$1"
    return 0
}

# declare the options property arrays
for f in name default expression short_opt long_opt description type method setting interactive req_value; do
for f in name default expression short_opt long_opt description type method check setting interactive req_value; do
    eval declare -x -A bashopts_optprop_$f
done

@@ -147,7 +163,7 @@ bashopts_tool_name=$0
bashopts_setup() {
    local arg arglist no_default_opts non_interactive disable_interactive
    if ! arglist=$(getopt -o "n:d:u:s:yxp" -n "$0 " -- "$@"); then
        bashopts_error "Usage bashopts_setup:" \
        bashopts_critical "Usage bashopts_setup:" \
            "        -n <val>  Tool name" \
            "        -d <val>  Tool description" \
            "        -u <val>  Tool usage description" \
@@ -155,7 +171,6 @@ bashopts_setup() {
            "        -y        Set non interactive mode as the default mode" \
            "        -x        Disable entirely interactive mode" \
            "        -p        Force value storage even if the value is equal to the default one"
        return 1
    fi
    eval set -- "$arglist";
    # Store the global bashopts properties
@@ -171,14 +186,14 @@ bashopts_setup() {
            -x) disable_interactive="true";;
            -p) bashopts_tool_settings_force_write="true";;
            --) break;;
            *)  bashopts_error "Fatal error";;
            *)  bashopts_critical "Fatal error";;
        esac
    done
    if [ -z "$bashopts_tool_name" ]; then
        bashopts_error "Undefined tool name"
        bashopts_critical "Undefined tool name"
    fi
    if [ -z "$bashopts_tool_description" ]; then
        bashopts_error "Undefined tool description"
        bashopts_critical "Undefined tool description"
    fi
    bashopts_tool_usage=${bashopts_tool_usage:-"$bashopts_tool_name [options and commands] [-- [extra args]]"}
    # add the default options
@@ -197,8 +212,8 @@ bashopts_setup() {
# STEP 2: add options
bashopts_declare() {
    local arg arglist options
    if ! arglist=$(getopt -o "n:v:e:o:l:d:t:m:rsi" -n "$0 " -- "$@"); then
        bashopts_error "Usage bashopts_declare:" \
    if ! arglist=$(getopt -o "n:v:e:o:l:d:t:m:k:rsi" -n "$0 " -- "$@"); then
        bashopts_critical "Usage bashopts_declare:" \
            "        -n <val>  Name" \
            "        -v <val>  Default value" \
            "        -e <val>  Bash expression: like default but this expression is computed and can contain variables and other bash expression" \
@@ -207,10 +222,10 @@ bashopts_declare() {
            "        -d <val>  Description" \
            "        -t <val>  Value type: string (default), number, boolean" \
            "        -m <val>  Method: set (DEFAULT: simple value), add (list with several values)" \
            "        -k <val>  Custom check method (bash function)" \
            "        -r        Value required" \
            "        -i        Enable interactive edition" \
            "        -s        Store in setting"
        return 1
    fi
    eval set -- "$arglist";
    declare -A options
@@ -219,7 +234,7 @@ bashopts_declare() {
        arg=$1
        shift
        case "$arg" in
            -n) options[name]=$(bashopts_chkvarname $1); shift;;
            -n) options[name]=$(bashopts_check_opt_name $1 || exit 1); shift;;
            -v) options[default]=$1;     shift;;
            -e) options[expression]=$1;  shift;;
            -o) options[short_opt]=$1;   shift;;
@@ -227,18 +242,18 @@ bashopts_declare() {
            -d) options[description]=$1; shift;;
            -t) options[type]=$1;        shift;;
            -m) options[method]=$1;      shift;;
            -k) options[check]=$1;       shift;;
            -s) options[setting]="true";;
            -i) options[interactive]="true";;
            -r) options[req_value]="true";;
            --) break;;
            *)  bashopts_error "Fatal error";;
            *)  bashopts_critical "Fatal error";;
        esac
    done
    # format the type and check/format the default value
    case "${options[type]}" in
        ''|bool|boolean)
            options[type]="boolean"
            options[default]=$(bashopts_chkbool ${options[default]})
            ;;
        str|string)
            options[type]="string"
@@ -247,12 +262,17 @@ bashopts_declare() {
        num|number)
            options[type]="number"
            options[req_value]="true"
            options[default]=$(bashopts_chknum ${options[default]})
            ;;
        *)
            bashopts_error "Invalid type ${options[type]}"
            bashopts_critical "Invalid type ${options[type]}"
            ;;
    esac
    # Setup check value method
    if ! [[ -v options[check] ]]; then
        options[check]="bashopts_check_${options[type]}"
    fi
    # Check the default value format
    options[default]="$(${options[check]} "${options[default]}")" || exit 1
    # format the option method
    case "${options[method]}" in
        ''|s|set)
@@ -264,30 +284,30 @@ bashopts_declare() {
            options[method]="add"
            ;;
        *)
            bashopts_error "Invalid method ${options[method]}"
            bashopts_critical "Invalid method ${options[method]}"
            ;;
    esac
    # Check option name
    if [[ -v bashopts_optprop_name[${options[name]}] ]]; then
        bashopts_error "Dupplicate option name '${options[name]}'"
        bashopts_critical "Dupplicate option name '${options[name]}'"
    fi
    # check the short option
    if [[ -v options[short_opt] ]]; then
        if ! [[ ${options[short_opt]} =~ ^[a-zA-Z0-9_-]$ ]]; then
            bashopts_error "Invalid short option ${options[short_opt]}"
            bashopts_critical "Invalid short option ${options[short_opt]}"
        fi
        if [[ -v bashopts_arg2op[-${options[short_opt]}] ]]; then
            bashopts_error "Dupplicate short option '${options[short_opt]}'"
            bashopts_critical "Dupplicate short option '${options[short_opt]}'"
        fi
        bashopts_arg2op[-${options[short_opt]}]=${options[name]}
    fi
    # check the long option
    if [[ -v options[long_opt] ]]; then
        if ! [[ ${options[long_opt]} =~ ^[a-zA-Z0-9_-]{2,}$ ]]; then
            bashopts_error "Invalid long option ${options[long_opt]}"
            bashopts_critical "Invalid long option ${options[long_opt]}"
        fi
        if [[ -v bashopts_arg2op[--${options[long_opt]}] ]]; then
            bashopts_error "Dupplicate long option '${options[long_opt]}'"
            bashopts_critical "Dupplicate long option '${options[long_opt]}'"
        fi
        bashopts_arg2op[--${options[long_opt]}]=${options[name]}
    fi
@@ -311,14 +331,14 @@ bashopts_math_min() {
# join array element
bashopts_join_by() {
    local IFS="$1"
    shift || bashopts_error "Usage: bashopts_join_by <character> [elt1 [elt2...]]"
    shift || bashopts_critical "Usage: bashopts_join_by <character> [elt1 [elt2...]]"
    echo "$*"
}

# dump an option value by its name
bashopts_dump_value() {
    local op=$1
    shift || bashopts_error "Usage: bashopts_dump_value op_name"
    shift || bashopts_critical "Usage: bashopts_dump_value op_name"
    [[ -v "$op" ]] || return 0
    if [ "${bashopts_optprop_method[$op]}" == "set" ]; then
        if [ "${bashopts_optprop_type[$op]}" == "string" ]; then
@@ -460,28 +480,17 @@ bashopts_parse_args() {
                val="$1"
                shift
                op=${bashopts_arg2op[$arg]}
                if [ -z "$val" ] && [ "${bashopts_optprop_type[$op]}" != "boolean" ]; then
                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
                case "${bashopts_optprop_type[$op]}" in
                    boolean)
                        if [ -z "$val" ]; then
                            # boolean argument with no value is considered as true
                            val="true"
                fi
                        # check and format boolean value
                        val=$(bashopts_chkbool $val)
                        ;;
                    string)
                        # nothing to do
                        ;;
                    number)
                        # check and format number value
                        val=$(bashopts_chknum $val)
                        ;;
                esac
                val="$(${bashopts_optprop_check[$op]} "$val")" || exit 1
                case "${bashopts_optprop_method[$op]}" in
                    set)
                        # normal case: override the value
@@ -494,7 +503,7 @@ bashopts_parse_args() {
                esac
                ;;
            *)
                bashopts_error "Fatal error: args"
                bashopts_critical "Fatal error: args"
                ;;
        esac
    done
@@ -506,7 +515,7 @@ bashopts_parse_args() {
# display an array: [val1, val2, ...]
bashopts_dump_array() {
    local type=$1
    shift || bashopts_error "Usage: bashopts_dump_array type elt1 [elt2...]"
    shift || bashopts_critical "Usage: bashopts_dump_array type elt1 [elt2...]"
    echo -n "["
    if [ "$type" == "string" ]; then
        echo -n "\"${1//\"/\\\"}\""
@@ -525,76 +534,117 @@ bashopts_dump_array() {
    echo -n "]"
}

# STEP 4: process arg
bashopts_process_args() {
    local dval ival
    if [ "$__bashopts_display_help__" == "true" ]; then
        bashopts_interactive="false"
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
        bashopts_critical "Usage bashopts_process_opt" \
            "        -n <val>  property name" \
            "        -k <val>  override value check function" \
            "        -r        At least one value required"
    fi
    for op in "${bashopts_optlist[@]}"; do
        unset dval
    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;;
            *)  bashopts_critical "Fatal error";;
        esac
    done
    test -n "$op" || \
        bashopts_critical "bashopts_process_option: missing -n option"
    if [ -z "$check" ]; then
        check="${bashopts_optprop_check[$op]}"
    fi

    # 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
        if ! [[ -v $op ]]; then
            unset tval
            # read the value from file
            if ! [[ -v $op ]] && [ "${bashopts_optprop_setting[$op]}" == "true" ] \
        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" ] \
        && [ -f "$(readlink -f $bashopts_tool_settings_path)" ] \
        && 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
                if [ "$bashopts_interactive" != "true" ]; then
                    bashopts_critical "Non interactive mode: Exit due to one or more error"
                fi
                # (re)enable edition
                edit_req="true"
                break
            fi
        done
    elif [ "$val_req" == "true" ]; then
        bashopts_error "At least one value required"
        if [ "$bashopts_interactive" != "true" ]; then
            bashopts_critical "Non interactive mode: Exit due to one or more error"
        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 [ "${bashopts_optprop_interactive[$op]}" == "true" ] && [ "$bashopts_interactive" == "true" ]; then
        if [ "$edit_req" == "true" ]; then
            if [ "$bashopts_interactive" == "true" ]; then
                # interactive edition
                while true; do
                    echo "* ${bashopts_optprop_description[$op]}"
                    echo -n "    $(bashopts_dump_array {bashopts_optprop_type[$op]} "${tval[@]}"): "
                    read ival
                    if [ -z "$ival" ]; then
                        # keep the previous/default value
                        break;
                    fi
                    read ival || return 1
                    if [ -n "$ival" ]; then
                        if [ "${bashopts_optprop_method[$op]}" == "add" ]; then
                            # array value
                            if ! eval "tval=$ival" 2>/dev/null; then
                            echo "'$ival' is not a valid BASH array (format: '( \"val 1\" \"val2\" \"val3...\" )')"
                                bashopts_error "'$ival' must be written in BASH array format: '( \"val 1\" \"val2\" \"val3...\" )'"
                                unset tval
                                continue
                            fi
                        else
                        # non array/norman value
                            # non array/normal value
                            tval=$ival
                        fi
                    # check boolean/number (array or not)
                    case "${bashopts_optprop_type[$op]}" in
                        boolean)
                            for v in "${tval[@]}"; do
                                if ! bashopts_isboolean $v; then
                                    echo "'$v' is not a valid boolean value"
                    fi
                    # check format
                    if [ "${#tval[@]}" -eq 0 ] && [ "$val_req" == "true" ]; then
                        bashopts_error "At least one value required"
                    fi
                    if [ "${bashopts_optprop_method[$op]}" == "add" ]; then
                        # array value
                        for (( i=0; i<${#tval[@]}; i++)); do
                            if ! tval[$i]="$($check "${tval[$i]}")"; then
                                unset tval
                                break
                            fi
                        done
                            ;;
                        number)
                            for v in "${tval[@]}"; do
                                if ! bashopts_isnumber $v; then
                                    echo "'$v' is not a valid number value"
                    else
                        # non array/normal value
                        if ! tval="$($check "$tval")"; then
                            unset tval
                            break
                        fi
                            done
                            ;;
                    esac
                    fi
                    if [[ -v tval ]]; then
                        # edit OK, break
                        break
@@ -602,10 +652,11 @@ bashopts_process_args() {
                    # otherwise, loop...
                done
            fi
        fi
    fi
    if [[ -v tval ]]; then
        eval "$op=$(bashopts_get_def tval)"
    fi
        fi
    if [ "${bashopts_optprop_setting[$op]}" == "true" ]; then
        if [ -n "$bashopts_tool_settings_path" ]; then
            # vrite the value to the setting file
@@ -632,9 +683,23 @@ bashopts_process_args() {
            bashopts_interactive="true"
        fi
    fi
}

# STEP 4: process arg
bashopts_process_opts() {
    local op
    if [ "$__bashopts_display_help__" == "true" ]; then
        bashopts_interactive="false"
    fi
    for op in "${bashopts_optlist[@]}"; do
        bashopts_process_option -n $op
    done
    if [ "$__bashopts_display_help__" == "true" ]; then
        bashopts_diplay_help
        exit 0
    fi
}

bashopts_process_args() {
    bashopts_process_opts "$@"
}
+3 −3
Original line number Diff line number Diff line
@@ -12,15 +12,15 @@ bashopts_setup -n "example.sh" -d "This is myapp tool description displayed on h
# Declare the options
bashopts_declare -n first_name -l first -o f -d "First name" -t string -i -s -r
bashopts_declare -n last_name -l last -o l -d "Last name" -t string -i -s -r
bashopts_declare -n display_name -l display-name -t string -d "Display name" -e "\$first_name \$last_name"
bashopts_declare -n display_name -l display-name -t string -d "Display name" -e "\"\$first_name \$last_name\""
bashopts_declare -n age -l number -d "Age" -t number
bashopts_declare -n email_list -t string -m add -l email -d "Email adress"

# Parse arguments
bashopts_parse_args "$@"

# Process argument
bashopts_process_args
# Process options
bashopts_process_opts

# Display the values
echo
+110 −19

File changed.

Preview size limit exceeded, changes collapsed.