##@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 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 name_="${2%%=*}=" [ -n "$reset_" ] && eval "$name_=" else name_="$2" [ -n "$reset_" ] && eval "$name_=()" fi flag_=: else option_="$optdesc_" name_="$2" [ -n "$reset_" ] && eval "$name_=" 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 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_" return 1 fi } 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[@]}" parse_opts "${PRETTYOPTS[@]}" "${args[@]}" @ args -- "$@" && return 0 eerror "$args" return 1 } 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[@]}" parse_opts "${PRETTYOPTS[@]}" "${args[@]}" @ args -- "$@" || die "$args" } 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"' }