bash comment supprimer les options des paramètres après le traitement

9

Je me souviens avoir vu quelque part un bashscript utilisant caseet shiftparcourant la liste des paramètres de position, analysant les drapeaux et les options avec des arguments lorsqu'il les rencontre, et les supprimant après l'analyse pour ne laisser que les arguments nus, qui sont ensuite traités par le reste de la scénario.

Par exemple, en analysant la ligne de commande de cp -R file1 -t /mybackup file2 -f, il parcourrait d'abord les paramètres, reconnaîtrait que l'utilisateur a demandé de descendre dans les répertoires par -R, spécifié la cible par -t /mybackupet pour forcer la copie par -f, et supprimait ceux de la liste des paramètres, laissant le programme à traiter file1 file2comme arguments restants.

Mais je ne semble pas être capable de me souvenir / découvrir quel que soit le script que j'ai vu à chaque fois. J'aimerais juste pouvoir faire ça. J'ai fait des recherches sur divers sites et j'ai joint une liste des pages pertinentes que j'ai examinées.

Une question sur ce site Web portait spécifiquement sur les "options indépendantes de l'ordre", mais la réponse unique et la réponse à la question à laquelle elle a été dupliquée ne prennent pas en compte des cas comme celui ci-dessus où les options sont mélangées avec des arguments normaux, ce qui, je présume, était le raison pour laquelle la personne mentionne spécifiquement les options indépendantes de la commande .

Étant donné que bashla fonction intégrée getoptssemble s'arrêter au premier argument de non-option, elle ne semble pas être une solution suffisante. C'est pourquoi la page de Wooledge BashFAQ (voir ci-dessous) explique comment réorganiser les arguments. Mais j'aimerais éviter de créer plusieurs tableaux au cas où la liste d'arguments serait assez longue.

Étant donné shiftque ne prend pas en charge le saut d'arguments individuels au milieu de la liste des paramètres, je ne sais pas quelle est la manière simple de mettre en œuvre ce que je demande.

J'aimerais savoir si quelqu'un a des solutions pour supprimer les arguments du milieu de la liste des paramètres sans créer un tout nouveau tableau.

Pages que j'ai déjà vues:

jamadagni
la source
1
Une fois que j'ai besoin de quelque chose de compliqué, je passe de bash à Perl.
choroba

Réponses:

9

POSIX, l'analyse des options doit s'arrêter au --ou au premier argument sans option (ou sans option) selon la première éventualité. Donc dans

cp -R file1 -t /mybackup file2 -f

qui est en file1, donc cpdoivent copier récursive tous file1, -t, /mybackupet file2dans le -frépertoire.

GNU getopt(3)cependant (que GNU cputilise pour analyser les options (et ici vous utilisez GNU cppuisque vous utilisez l' -toption spécifique à GNU )), à moins que la $POSIXLY_CORRECTvariable d'environnement ne soit définie, accepte les options après les arguments. Il est donc en fait équivalent à l'analyse de style d'option POSIX:

cp -R -t /mybackup -f -- file1 file2

Le getoptsshell intégré, même dans le shell GNU ( bash), ne gère que le style POSIX. Il ne prend pas non plus en charge les options longues ou les options avec des arguments facultatifs.

Si vous souhaitez analyser les options de la même manière que GNU cp, vous devrez utiliser l' getopt(3)API GNU . Pour cela, si sous Linux, vous pouvez utiliser l' getoptutilitaire amélioré de util-linux( cette version améliorée de la getoptcommande a également été portée sur d'autres Unices comme FreeBSD ).

Cela getoptréorganisera les options de manière canonique, ce qui vous permettra de les analyser simplement avec une while/caseboucle.

$ getopt -n "$0" -o t:Rf -- -Rf file1 -t/mybackup file2
 -R -f -t '/mybackup' -- 'file1' 'file2'

Vous l'utiliseriez généralement comme:

parsed_options=$(
  getopt -n "$0" -o t:Rf -- "$@"
) || exit
eval "set -- $parsed_options"
while [ "$#" -gt 0 ]; do
  case $1 in
    (-[Rf]) shift;;
    (-t) shift 2;;
    (--) shift; break;;
    (*) exit 1 # should never be reached.
  esac
done
echo "Now, the arguments are $*"

Notez également que cela getoptanalysera les options de la même manière que GNU cp. En particulier, il prend en charge les options longues (et les saisit en abrégé) et honore les $POSIXLY_CORRECTvariables d'environnement (qui, une fois définies, désactivent la prise en charge des options après les arguments) de la même manière que GNU cp.

Notez que l'utilisation de gdb et l'impression des arguments getopt_long()reçus peuvent aider à construire les paramètres pour getopt(1):

(gdb) bt
#0  getopt_long (argc=2, argv=0x7fffffffdae8, options=0x4171a6 "abdfHilLnprst:uvxPRS:T", long_options=0x417c20, opt_index=0x0) at getopt1.c:64
(gdb) set print pretty on
(gdb) p *long_options@40
$10 = {{
    name = 0x4171fb "archive",
    has_arg = 0,
    flag = 0x0,
    val = 97
  }, {
    name = 0x417203 "attributes-only",
[...]

Ensuite, vous pouvez utiliser getoptcomme:

getopt -n cp -o abdfHilLnprst:uvxPRS:T -l archive... -- "$@"

N'oubliez pas que cpla liste des options prises en charge par GNU peut changer d'une version à la suivante et qu'elle getoptne pourra pas vérifier si vous transmettez une valeur légale à l' --sparseoption par exemple.

Stéphane Chazelas
la source
@all: merci pour vos réponses. @Stephane: y a-t-il une raison pour laquelle vous utilisez while [ "$#" -gt 0 ]juste io while (($#))? Est-ce juste pour éviter un bashisme?
jamadagni
Oui, mais (($#))c'est plus un kshisme .
Stéphane Chazelas
1

Ainsi, chaque fois qu'il getoptstraite un argument, il ne s'attend pas à ce qu'il définit la variable shell $OPTINDau numéro suivant dans la liste d'arguments qu'il doit traiter et renvoie autre que 0. Si $OPTINDest défini sur une valeur de 1, getoptsest spécifié par POSIX pour accepter un nouvelle liste d'arguments. Ainsi, cela ne fait que surveiller le getoptsretour, enregistre les incréments d'un compteur plus $OPTINDpour tout retour échoué, éloigne les arguments échoués et réinitialise $OPTINDchaque essai échoué. Vous pouvez l'utiliser comme opts "$@"- bien que vous souhaitiez personnaliser la caseboucle ou bien l'enregistrer dans une variable et changer cette section en eval $case.

opts() while getopts :Rt:f opt || {
             until command shift "$OPTIND" || return 0
                   args=$args' "${'"$((a=${a:-0}+$OPTIND))"'}"'
                   [ -n "${1#"${1#-}"}" ]
             do OPTIND=1; done 2>/dev/null; continue
};     do case "$opt"  in
             R) Rflag=1      ;;
             t) tflag=1      ;
                targ=$OPTARG ;;
             f) fflag=1      ;;
       esac; done

Lors de son exécution, il définit $argstous les arguments qui getoptsn'ont pas été traités ... alors ...

set -- -R file1 -t /mybackup file2 -f
args= opts "$@"; eval "set -- $args"
printf %s\\n "$args"
printf %s\\n "$@"         
printf %s\\n "$Rflag" "$tflag" "$fflag" "$targ"

PRODUCTION

 "${2}" "${5}"
file1
file2
1
1
1
/mybackup

Cela fonctionne bash, dash, zsh, ksh93, mksh... eh bien, je cesser d' essayer à ce moment - là. Dans chaque coquille, il a également obtenu $[Rtf]flaget $targ. Le fait est que tous les nombres des arguments qui getoptsne voulaient pas être traités sont restés.

Changer le style des options n'a fait aucune différence non plus. Cela a fonctionné comme -Rf -t/mybackupou -R -f -t /mybackup. Cela a fonctionné au milieu de la liste, à la fin de la liste ou en tête de liste ...

Pourtant, le meilleur moyen est simplement de coller un --pour la fin des options sur votre liste d'arguments, puis de le faire shift "$(($OPTIND-1))"à la fin d'un getoptscycle de traitement. De cette façon, vous supprimez tous les paramètres traités et conservez l'extrémité arrière de la liste des arguments.

Une chose que j'aime faire est de traduire les options longues en courtes - et je le fais de manière très similaire, c'est pourquoi cette réponse est venue facilement - avant de courir getopts.

i=0
until [ "$((i=$i+1))" -gt "$#" ]
do case "$1"                   in
--Recursive) set -- "$@" "-R"  ;;
--file)      set -- "$@" "-f"  ;;
--target)    set -- "$@" "-t"  ;;
*)           set -- "$@" "$1"  ;;
esac; shift; done
mikeserv
la source
Votre conversion d'options longues convertirait également des non-options (en tant que cp -t --file fooou cp -- --file foo) et ne gérerait pas les options saisies en abrégé ( --fi...), ni la --target=/destsyntaxe.
Stéphane Chazelas
@ StéphaneChazelas - la longue conversion n'est qu'un exemple - elle n'a évidemment pas été très bien construite. J'ai remplacé la getoptschose par une fonction beaucoup plus simple.
mikeserv
@ StéphaneChazelas - veuillez regarder à nouveau. Bien que le long commentaire sur l'option reste justifié, le premier - et le vote négatif qui l'accompagne - ne l'est plus, comme je pense. En outre, je pense que cela opts()a une certaine incidence sur votre propre réponse, car il opts()fonctionne dans une seule boucle - en touchant chaque argument mais une fois - et le fait de manière fiable (aussi près que je peux dire) , de manière portable et sans un seul sous-shell.
mikeserv
réinitialiser OPTIND est ce que j'appelle démarrer une autre boucle getopts. En effet, c'est l' analyse de plusieurs lignes de commande / ensembles d'options ( -R, -t /mybackup, -f). Je vais garder mon vote bas pour l'instant car il est toujours obscurci, vous utilisez $anon initialisé et args= opts...laissera probablement argsnon défini (ou défini sur ce qu'il était auparavant) après les optsretours dans de nombreux shells (y compris bash).
Stéphane Chazelas
@ StéphaneChazelas - cela inclut bash- je déteste ça. À mon avis, si la fonction est une fonction shell actuelle qui devrait être conservée. J'aborde ces choses - car la fonction actuelle, avec plus de tests, pourrait être robuste pour gérer tous les cas, et même pour signaler l' argument avec son option précédente, mais je ne suis pas d'accord pour dire qu'elle est obscurcie . Tel qu'il est écrit, c'est le moyen le plus simple et le plus direct d'accomplir sa tâche auquel je puisse penser. testing puis shifting n'a pas de sens quand dans tous les shiftcas d' échec vous devriez return. Il n'est pas prévu de faire autrement.
mikeserv