##@cooked comments # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
## Analyse des arguments de la ligne de commande
##@cooked nocomments
##@include base.core
##@include base.string
##@include base.array
uprovide base.args
urequire base.core base.string base.array #XXX maj de cette liste

function __po_parse_optdescs() {
    # Parser et normaliser la description des options valides. A l'issue de
    # l'appel de cette méthode, 3 tableaux sont initialisés, dont chaque
    # position correspond à une option:
    # - options_ contient la liste des options valides e.g. (-o --longo...)
    # - names_ contient la liste des variables qu'il faut mettre à jour pour ces
    # options e.g. (name name...)
    # - flags_ contient la liste des flags pour les options: '' pour une option
    # simple, ':' pour option avec argument obligatoire, '::' pour option avec
    # argument facultatif, '$' pour une commande à lancer au lieu d'une variable
    # à mettre à jour.
    # De plus, les variables suivantes sont initialisées:
    # - destargs_ obtient la variable qui doit être initialisée avec le reste
    # des arguments.
    # - opts_ et longopts_ sont les arguments à utiliser pour la commande getopt
    # du package util-linux
    # Retourner le nombre d'arguments à shifter
    local -a optdescs_
    local optdesc_ option_ name_ flag_ value_
    local reset_
    local shift_
    local i_ count_

    let shift_=0
    while [ -n "$1" ]; do
        if [ "$1" == -- ]; then
            shift
            let shift_=$shift_+1
            break
        elif [ "$1" == "%" ]; then
            reset_=1
            shift
            let shift_=$shift_+1
        elif [ "$1" == "-" -o "$1" == "+" ]; then
            # annuler les précédentes options + ou -
            if [ "${opts_#+}" != "$opts_" ]; then
                opts_="${opts_#+}"
            elif  [ "${opts_#-}" != "$opts_" ]; then
                opts_="${opts_#-}"
            fi
            # puis rajouter l'option
            opts_="$1$opts_"
            shift
            let shift_=$shift_+1
        elif [ "$1" == "@" ]; then
            destargs_="$2"
            shift; shift
            let shift_=$shift_+2
        elif [[ "$1" == --* ]] || [[ "$1" == -* ]]; then
            array_split optdescs_ "$1" ","
            if [ "$2" == . ]; then
                local autoname_=
                for optdesc_ in "${optdescs_[@]}"; do
                    if [ ${#optdesc_} -gt ${#autoname_} ]; then
                        autoname_="$optdesc_"
                    fi
                done
                while [ -n "$autoname_" -a "${autoname_#-}" != "$autoname_" ]; do autoname_="${autoname_#-}"; done
                while [ -n "$autoname_" -a "${autoname_%:}" != "$autoname_" ]; do autoname_="${autoname_%:}"; done
                autoname_="${autoname_//-/_}"
                shift; shift
                set -- dummy "$autoname_" "$@"
            fi
            for optdesc_ in "${optdescs_[@]}"; do
                if [[ "$2" == \$* ]]; then
                    name_="$2"
                    if [[ "$optdesc_" == *:: ]]; then
                        option_="${optdesc_%::}"
                        flag_='::$'
                    elif [[ "$optdesc_" == *: ]]; then
                        option_="${optdesc_%:}"
                        flag_=':$'
                    else
                        option_="$optdesc_"
                        flag_='$'
                    fi
                elif [[ "$optdesc_" == *:: ]]; then
                    option_="${optdesc_%::}"
                    if [[ "$2" == *=* ]]; then
                        # la valeur mentionnée est toujours ignorée. le '=' à la
                        # fin est utilisé par __po_process_options() ci-dessous.
                        name_="${2%%=*}="
                        [ -n "$reset_" ] && eval "$name_"
                    else
                        name_="$2"
                        [ -n "$reset_" ] && eval "$name_=()"
                    fi
                    flag_=::
                elif [[ "$optdesc_" == *: ]]; then
                    option_="${optdesc_%:}"
                    if [[ "$2" == *=* ]]; then
                        # la valeur mentionnée est toujours ignorée. le '=' à la
                        # fin est utilisé par __po_process_options() ci-dessous.
                        name_="${2%%=*}="
                        [ -n "$reset_" ] && eval "$name_"
                    else
                        name_="$2"
                        [ -n "$reset_" ] && eval "$name_=()"
                    fi
                    flag_=:
                else
                    option_="$optdesc_"
                    name_="$2"
                    [ -n "$reset_" ] && eval "${2%%=*}="
                    flag_=
                fi

                if i_="$(array_find options_ "$option_")"; then
                    # supprimer l'ancienne occurence
                    options_=("${options_[@]:0:$i_}" "${options_[@]:$(($i_ + 1))}")
                    names_=("${names_[@]:0:$i_}" "${names_[@]:$(($i_ + 1))}")
                    flags_=("${flags_[@]:0:$i_}" "${flags_[@]:$(($i_ + 1))}")
                fi
                options_=("${options_[@]}" "$option_")
                names_=("${names_[@]}" "$name_")
                flags_=("${flags_[@]}" "$flag_")
            done
            shift; shift
            let shift_=$shift_+2
        else
            break
        fi
    done

    i_=0
    count_=${#options_[*]}
    while [ $i_ -lt $count_ ]; do
        option_="${options_[$i_]}"
        flag_="${flags_[$i_]}"
        i_=$(($i_ + 1))

        # pour construire longopts_ et opts_, enlever $ de flag
        flag_="${flag_%$}"
        if [[ "$option_" == --* ]]; then
            longopts_="${longopts_:+$longopts_,}${option_#--}$flag_"
        elif [[ "$option_" == -* ]]; then
            opts_="$opts_${option_#-}$flag_"
        fi
    done

    return $shift_
}
function __po_check_options() {
    # vérifier la validité des options mentionnées dans les arguments
    # Si les options sont valides, retourner 0 et afficher la chaine des
    # arguments traitées.
    # Sinon, retourner 1 et initialiaser la variable $destargs_ avec le message
    # d'erreur.
    local -a getopt_args_
    getopt_args_=(-o "$opts_" ${longopts_:+-l "$longopts_"} -- "$@")
    local args_
    if args_="$(getopt -q "${getopt_args_[@]}")"; then
        recho "$args_"
        return 0
    else
        # relancer la commande pour avoir le message d'erreur
        LANG=C getopt "${getopt_args_[@]}" 2>&1 1>/dev/null
        return 1
    fi
}
function __po_process_options() {
    # Traiter les options
    while [ -n "$1" ]; do
        if [ "$1" == -- ]; then
            shift
            break
        elif [[ "$1" == -* ]]; then
            local i_
            let i_=0
            for option_ in "${options_[@]}"; do
                [ "$1" == "${options_[$i_]}" ] && break
                let i_=$i_+1
            done
            name_="names_[$i_]"; name_="${!name_}"
            flag_="flags_[$i_]"; flag_="${!flag_}"
            function inc@ { eval "let $1=\$$1+1"; }
            function res@ { setv "$1" "${value_:-$2}"; }
            function add@ { array_add "$1" "${value_:-$2}"; }
            if [ -z "$name_" ]; then
                # option non reconnue. ce cas aurait dû être traité par
                # __po_check_options.
                ewarn "$1: option non reconnue, elle sera ignorée"
            elif [[ "$flag_" == *\$ ]]; then
                if [[ "$flag_" == :* ]]; then
                    value_="$2"; shift
                    function set@ { res@ "$@"; }
                else
                    value_=
                    function set@ { inc@ "$@"; }
                fi
                eval "${name_#\$}"
            elif [ "$flag_" == "" ]; then
                if [[ "$name_" == *=* ]]; then
                    setv "${name_%%=*}" "${name_#*=}"
                else
                    inc@ "$name_"
                fi
            elif [ "$flag_" == ":" -o "$flag_" == "::" ]; then
                value_="$2"; shift
                if [ "${name_%=}" != "$name_" ]; then
                    setv "${name_%=}" "$value_"
                elif [[ "$name_" == *=* ]]; then
                    setv "${name_%%=*}" "${name_#*=}"
                else
                    array_add "$name_" "$value_"
                fi
            fi
        else
            break
        fi
        shift
    done
    unset -f inc@ res@ add@ set@
    [ -n "$destargs_" ] &&
    set_array "$destargs_" @ "$@"
    return 0
}
function parse_opts() {
# Analyser des arguments. Cette fonction doit être appelée avec une description
# des options à analyser, suivie des arguments proprement dits. En fonction des
# options rencontrées, certaines variables sont mises à jour.
# Les arguments de cette fonction sont donc de la forme 'optdescs -- args'

# optdescs est une suite d'arguments d'une des formes suivantes: 'opt,opt... var',
# 'opt,opt... $cmd', '@ var', '+', '-', '%'
# - Dans la forme 'opt var[=value]', opt est une description d'option, var un
# nom de variable à mettre à jour, et value une valeur éventuelle pour les
# options sans argument. Si plusieurs options sont mentionnées, séparées par des
# virgules, alors tous les options partagent les mêmes paramètres.
# opt peut être de la forme '-o' ou '--longopt' pour des options sans
# arguments. Dans ce cas, var obtient le nombre de fois que l'option est
# mentionnée ('' pour aucune mention, '1' pour une seule mention, etc.), sauf si
# on utilise la forme var=value, auquel cas la variable obtient la valeur value,
# et le nombre d'occurences de l'option n'est pas compté.
# Pour faciliter la lecture:
#     '--opt .' est équivalent à '--opt opt'
#     '--opt: .' est équivalent à '--opt: opt='
# Avec la forme '-o:' ou '--longopt:', l'option prend un argument obligatoire.
# Avec la forme '-o::' ou '--longopt::', l'option prend un argument facultatif
# (dans ce cas, la valeur de l'option sur la ligne de commande doit
# obligatoirement être collée à l'option.)
# Si ces options sont mentionnées plusieurs fois sur la ligne de commande, alors
# la variable de destination est un tableau qui contient toutes les valeurs.
# Le traitement de la valeur d'une variable dépend de la forme utilisée.
# - Avec une option sans argument, le comportement est celui décrit ci-dessus.
# - Avec une option qui prend des arguments, la forme '-o: var' considère que
# var est un tableau qui contiendra toutes les valeurs mentionnées dans les
# options. Avec la forme '-o: var=', la variable n'est pas un tableau et
# contient toujours la dernière valeur spécifiée.
# - Dans la forme 'opt $cmd', la commande cmd est executée avec eval *dès* que
# l'option est rencontrée. La variable option_ contient l'option, e.g. '-o' ou
# '--longopt'. Le cas échéant, la variable value_ contient la valeur de
# l'option. La fonction 'set@ NAME' met à jour la variable NAME, soit en lui
# donnant la valeur $value_, soit en l'incrémentant, suivant le type d'option.
# La fonction 'inc@ NAME' incrémente la variable NAME, 'res@ NAME [VALUE]'
# initialise la variable à la valeur VALUE, 'add@ NAME [VALUE]' met à jour le
# tableau NAME, en lui ajoutant la valeur VALUE. Par défaut, VALUE vaut $value_
# - Dans la forme '@ var', var est un nom de tableau qui est initialisé avec le
# reste des arguments.
# - Avec les caractères '-' ou '+', l'on influe sur la méthode d'analyse. Par
# défaut, les options sont valides n'importe ou sur la ligne de commande. Avec
# '+', l'analyse s'arrête au premier argument qui n'est pas une option. Avec
# '-', les options sont valides n'importe ou sur la ligne de commande, mais les
# arguments ne sont pas réordonnés, et apparaissent dans l'ordre de leur
# mention.
# - Toutes les variables mentionnées après le caractère '%' sont
# initialisées. Elle sont garanties d'être vides
# Si opt est définie plusieurs fois, la dernière définition est celle qui est
# retenue, e.g. dans l'exemple suivant, l'option -o prend une valeur et met à
# jour la variable second:
#     parse_opts -o,--longo first=1 -o: second= ....

# Si une erreur se produit pendant l'analyse, retourner 1. Si '@ var' est
# spécifié, insérer le texte de l'erreur comme unique élément du tableau var.
# Une suggestion d'utilisation est donc celle-ci:
#     parse_opts ... @ args -- "$@" && set -- "${args[@]}" || die "$args"

    # D'abord, parser et normaliser les options
    # options_ contient la liste des options (-o --longo...)
    # names_ contient la liste des variables qu'il faut mettre à jour (name name...)
    # flags_ contient la liste des flags pour les options: '' pour une option
    # simple, ':' pour option avec argument obligatoire, '::' pour option avec
    # argument facultatif
    [ -z "$__ULIB_NO_DISABLE_SET_X" ] && [[ $- == *x* ]] && { set +x; local __ULIB_PARSE_OPTS_SET_X=1; } # désactiver set -x pour cette fonction

    local -a options_ names_ flags_ destargs_
    local opts_ longopts_
    __po_parse_optdescs "$@" || shift $?
    local args_
    if args_="$(__po_check_options "$@")"; then
        eval "set -- $args_"
        __po_process_options "$@"
    else
        [ -n "$destargs_" ] && setv "$destargs_" "$args_"
        [ -n "$__ULIB_SET_X" ] && set -x
        return 1
    fi

    [ -n "$__ULIB_PARSE_OPTS_SET_X" ] && set -x; return 0
}

function parse_args_check() {
# Simplifier l'utilisation de parse_opts(). En entrée, le tableau args doit être
# initialisé avec la liste des options. En sortie, ce tableau contient la liste
# des arguments restant sur la ligne de commande. En cas d'erreur, retourner 1.
# Exemple d'utilisation:
#   args=(...)
#   parse_args_check "$@" || return; set -- "${args[@]}"
    [ -z "$__ULIB_NO_DISABLE_SET_X" ] && [[ $- == *x* ]] && { set +x; local __ULIB_PARSE_ARGS_SET_X=1; } # désactiver set -x pour cette fonction
    if parse_opts "${PRETTYOPTS[@]}" "${args[@]}" @ args -- "$@"; then
        [ -n "$__ULIB_PARSE_ARGS_SET_X" ] && set -x
        return 0
    else
        eerror "$args"
        [ -n "$__ULIB_PARSE_ARGS_SET_X" ] && set -x
        return 1
    fi
}
function parse_args() {
# Simplifier l'utilisation de parse_opts(). En entrée, le tableau args doit être
# initialisé avec la liste des options. En sortie, ce tableau contient la liste
# des arguments restant sur la ligne de commande. En cas d'erreur, quitter le
# script avec die()
# Exemple d'utilisation:
#   args=(...)
#   parse_args_check "$@"; set -- "${args[@]}"
    [ -z "$__ULIB_NO_DISABLE_SET_X" ] && [[ $- == *x* ]] && { set +x; local __ULIB_PARSE_ARGS_SET_X=1; } # désactiver set -x pour cette fonction
    parse_opts "${PRETTYOPTS[@]}" "${args[@]}" @ args -- "$@" || die "$args"
    [ -n "$__ULIB_PARSE_ARGS_SET_X" ] && set -x; return 0
}

HELP_DESC=
HELP_USAGE=
HELP_OPTIONS=
function __genparse_shortopt() {
    local LC_COLLATE=C
    local shortopt="${1//[^A-Z]}"
    shortopt="$(strlower "${shortopt:0:1}")"
    [ -n "$shortopt" ] && echo "$shortopt"
}
function genparse() {
# Afficher une ligne de commande à évaluer pour simplifier l'utilisation de
# parse_opts(). Une fonction display_help() par défaut est définie et les
# options appropriées de parse_opts sont utilisées pour reconnaître les options
# spécifiées par les arguments.
# Cette fonction peut être utilisée de cette manière:
#     HELP_DESC=...
#     HELP_ARG_DESC=... # pour chaque arg
#     eval "$(genparse [args...])"
# D'autres variables peuvent être définies: HELP_USAGE, HELP_OPTIONS,
# HELP_ARG_OPTION. Consulter le source pour connaitre leur utilisation
# Les arguments de cette fonction sont de la forme 'sansarg' pour une option
# simple qui ne prend pas d'argument ou 'avecarg=[default-value]' pour une
# option qui prend un argument. Les options générées sont des options
# longues. En l'occurence, les options générées sont respectivement '--sansarg'
# et '--avecarg:'
# Les variables et les options sont toujours en minuscule. Pour les variables,
# le caractère '-' est remplacé par '_'. Si une option contient une lettre en
# majuscule, l'option courte correspondante à cette lettre sera aussi reconnue.
# Par exemple, la commande suivante:
#     genparse Force enCoding=utf-8 input= long-Option=
# affichera ceci:
#     function display_help() {
#         [ -n "$HELP_USAGE" ] || HELP_USAGE="USAGE
#         $scriptname [options]"
#         [ -n "$HELP_OPTIONS" ] || HELP_OPTIONS="OPTIONS
#     ${HELP_FORCE_OPTION:-    -f, --force${HELP_FORCE_DESC:+
#             $HELP_FORCE_DESC}}
#     ${HELP_ENCODING_OPTION:-    -c, --encoding VALUE${HELP_ENCODING_DESC:+
#             ${HELP_ENCODING_DESC}}}
#     ${HELP_INPUT_OPTION:-    --input VALUE${HELP_INPUT_DESC:+
#             ${HELP_INPUT_DESC}}}
#     ${HELP_LONG_OPTION_OPTION:-    -o, --long-option VALUE${HELP_LONG_OPTION_DESC:+
#             ${HELP_LONG_OPTION_DESC}}}"
#         uecho "${HELP_DESC:+$HELP_DESC
#
#     }$HELP_USAGE${HELP_OPTIONS:+
#
#     $HELP_OPTIONS}"
#     }
#
#     force=
#     encoding=utf-8
#     input=""
#     long_option=""
#     parse_opts "${PRETTYOPTS[@]}" \
#         --help '$exit_with display_help' \
#         -f,--force force=1 \
#         -c:,--encoding: encoding= \
#         --input: input= \
#         -o:,--long-option: long_option= \
#         @ args -- "$@" && set -- "${args[@]}" || die "$args"

    local -a names descs vars options
    local i desc var option name uname value shortopt

    # analyser les arguments
    for var in "$@"; do
        if [[ "$var" == *=* ]]; then
            splitvar "$var" name value
            shortopt="$(__genparse_shortopt "$name")"
            option="$(strlower "$name")"
            name="${option//-/_}"
            array_add names "$name"
            array_add descs "${shortopt:+-$shortopt, }--$option VALUE"
            array_add vars "$(echo_setv "$name" "$value")"
            array_add options "${shortopt:+-$shortopt:,}--$option: $name="
        else
            name="$var"
            shortopt="$(__genparse_shortopt "$name")"
            option="$(strlower "$name")"
            name="${option//-/_}"
            array_add names "$name"
            array_add descs "${shortopt:+-$shortopt, }--$option"
            array_add vars "$name="
            array_add options "${shortopt:+-$shortopt,}--$option $name=1"
        fi
    done

    # afficher la commande parse_opts
    echo -n 'function display_help() {
    [ -n "$HELP_USAGE" ] || HELP_USAGE="USAGE
    $scriptname'
    [ -n "$descs" ] && echo -n ' [options]'
    echo '"'
    if [ -n "$descs" ]; then
        echo -n '    [ -n "$HELP_OPTIONS" ] || HELP_OPTIONS="OPTIONS'
        i=0
        while [ $i -lt ${#descs[*]} ]; do
            name="${names[$i]}"
            uname="$(strupper "$name")"
            desc="${descs[$i]}"
            echo -n "
\${HELP_${uname}_OPTION:-    $desc\${HELP_${uname}_DESC:+
        \${HELP_${uname}_DESC//
/
        }}}"
            i=$(($i + 1))
        done
        echo '"'
    fi
    echo '    uecho "${HELP_DESC:+$HELP_DESC

}$HELP_USAGE${HELP_OPTIONS:+

$HELP_OPTIONS}"
}
'
    for var in "${vars[@]}"; do
        echo "$var"
    done
    echo 'parse_opts "${PRETTYOPTS[@]}" \'
    echo '    --help '\''$exit_with display_help'\'' \'
    for option in "${options[@]}"; do
        echo "    $option \\"
    done
    echo '    @ args -- "$@" && set -- "${args[@]}" || die "$args"'
}
