nutools/lib/ulib/base.args

450 lines
18 KiB
Bash

##@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=(-c:,--config: .)
# parse_args_check "$@" || return
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()
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"'
}