Bash, wie Optionen aus Parametern nach der Verarbeitung entfernt werden

9

Ich erinnere mich, dass ich irgendwo ein bashSkript gesehen habe, das die Liste der Positionsparameter, Analyseflags und Optionen mit Argumenten verwendet caseund shiftdurchgeht, wenn es auf sie trifft, und sie nach dem Parsen entfernt, um nur die bloßen Argumente zu belassen, die später vom Rest der verarbeitet werden Skript.

Wenn Sie beispielsweise die Befehlszeile von analysieren cp -R file1 -t /mybackup file2 -f, werden zunächst die Parameter durchlaufen, erkannt, dass der Benutzer angefordert hat, in Verzeichnisse von abzusteigen -R, das Ziel von anzugeben -t /mybackupund das Kopieren von zu erzwingen -f, und diese aus der Liste der Parameter entfernt das Programm, das file1 file2als verbleibende Argumente verarbeitet werden soll.

Aber ich scheine nicht in der Lage zu sein, mich an das Skript zu erinnern, das ich wann immer gesehen habe. Ich möchte das nur können. Ich habe auf verschiedenen Websites gegoogelt und eine Liste der relevanten Seiten angehängt, die ich untersucht habe.

Eine Frage auf dieser Website, die speziell nach "auftragsunabhängigen Optionen" gestellt wurde, aber sowohl die Einzelantwort als auch die Antwort auf die Frage, auf die sie gestellt wurde, berücksichtigt keine Fälle wie die oben genannten, in denen die Optionen mit normalen Argumenten gemischt sind, von denen ich annehme, dass dies die war Grund für die Person, auftragsunabhängige Optionen ausdrücklich zu erwähnen .

Da bashdas eingebaute getoptsArgument beim ersten Argument ohne Option zu enden scheint, scheint es als Lösung nicht ausreichend zu sein. Aus diesem Grund wird auf der Wooledge BashFAQ-Seite (siehe unten) erläutert, wie die Argumente neu angeordnet werden. Ich möchte jedoch vermeiden, mehrere Arrays zu erstellen, falls die Argumentliste ziemlich lang ist.

Da shiftes nicht unterstützt wird, einzelne Argumente aus der Mitte der Parameterliste zu entfernen, bin ich mir nicht sicher, wie ich das, was ich frage, auf einfache Weise implementieren kann.

Ich würde gerne hören, ob jemand Lösungen zum Entfernen von Argumenten aus der Mitte der Parameterliste hat, ohne ein ganz neues Array zu erstellen.

Seiten, die ich bereits gesehen habe:

Jamadagni
quelle
1
Sobald ich etwas Kompliziertes brauche, wechsle ich von Bash zu Perl.
Choroba

Antworten:

9

POSIXly sollte das Parsen nach Optionen am --oder beim ersten Argument ohne Option (oder ohne Option) beginnen, je nachdem, was zuerst eintritt. Also rein

cp -R file1 -t /mybackup file2 -f

dass die an file1, so cprekursiv alle kopieren soll file1, -t, /mybackupund file2in das -fVerzeichnis.

GNU getopt(3)(das GNU cpzum Parsen von Optionen verwendet (und hier verwenden Sie GNU, cpda Sie die GNU-spezifische -tOption verwenden)) $POSIXLY_CORRECTakzeptiert Optionen nach Argumenten , sofern die Umgebungsvariable nicht festgelegt ist. Es entspricht also tatsächlich dem Parsing von POSIX-Optionsstilen:

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

Die getoptsin die GNU-Shell ( bash) integrierte Shell verarbeitet nur den POSIX-Stil. Es werden auch keine langen Optionen oder Optionen mit optionalen Argumenten unterstützt.

Wenn Sie die Optionen auf die gleiche Weise wie GNU analysieren möchten cp, müssen Sie die GNU- getopt(3)API verwenden. Unter Linux können Sie dafür das erweiterte getoptDienstprogramm von verwenden util-linux( diese erweiterte Version des getoptBefehls wurde auch auf einige andere Unices wie FreeBSD portiert ).

Das getoptwird die Optionen in kanonischer Weise neu anordnen , die Sie es einfach mit einer analysieren erlaubt while/caseSchleife.

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

Sie würden es normalerweise verwenden als:

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 $*"

Beachten Sie auch, dass dadurch getoptOptionen genauso analysiert werden wie bei GNU cp. Insbesondere unterstützt es die langen Optionen (und gibt sie abgekürzt ein) und berücksichtigt die $POSIXLY_CORRECTUmgebungsvariablen (die beim Festlegen die Unterstützung für Optionen nach Argumenten deaktivieren) auf die gleiche Weise wie GNU cp.

Beachten Sie, dass die Verwendung von gdb und das Drucken der getopt_long()empfangenen Argumente beim Erstellen der folgenden Parameter hilfreich sein kann 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",
[...]

Dann können Sie verwenden getoptals:

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

Denken Sie daran, dass sich cpdie Liste der unterstützten Optionen von GNU von einer Version zur nächsten ändern kann und getoptnicht überprüft werden kann, ob Sie --sparsebeispielsweise der Option einen legalen Wert übergeben .

Stéphane Chazelas
quelle
@all: Danke für deine Antworten. @Stephane: Gibt es einen Grund, warum Sie while [ "$#" -gt 0 ]io nur verwenden while (($#))? Ist es nur, um einen Bashismus zu vermeiden?
Jamadagni
Ja, aber es (($#))ist eher ein Kshismus .
Stéphane Chazelas
1

Jedes Mal getopts, wenn ein Argument verarbeitet wird, wird nicht erwartet, dass die Shell-Variable $OPTINDauf die nächste Zahl in der Argumentliste gesetzt wird, die verarbeitet werden soll, und es wird eine andere als 0 zurückgegeben. Wenn der $OPTINDWert 1 festgelegt ist, getoptswird POSIX angegeben, um a zu akzeptieren neue Argumentliste. Dies überwacht also nur die getoptsRückkehr, speichert Inkremente eines Zählers plus $OPTINDfür jede fehlgeschlagene Rückgabe, verschiebt die fehlgeschlagenen Argumente und setzt $OPTINDjeden fehlgeschlagenen Versuch zurück. Sie können es wie opts "$@"folgt verwenden - obwohl Sie die caseSchleife anpassen oder in einer Variablen speichern und diesen Abschnitt in ändern möchten 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

Während der Ausführung wird $argsauf jedes Argument gesetzt, das getoptsnicht behandelt wurde ... also ...

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"

AUSGABE

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

Dies funktioniert in bash, dash, zsh, ksh93, mksh... na ja, ich beenden an dieser Stelle versuchen. In jeder Schale bekam es auch $[Rtf]flagund $targ. Der Punkt ist, dass alle Zahlen für die Argumente, getoptsdie nicht verarbeitet werden wollten, erhalten blieben.

Das Ändern des Optionsstils machte ebenfalls keinen Unterschied. Es hat funktioniert wie -Rf -t/mybackupoder -R -f -t /mybackup. Es funktionierte in der Mitte der Liste, am Ende der Liste oder am Anfang der Liste ...

Der beste Weg ist jedoch, ein --Ende der Optionen in Ihre Arg-Liste aufzunehmen und dies shift "$(($OPTIND-1))"am Ende eines getoptsVerarbeitungslaufs zu tun . Auf diese Weise entfernen Sie alle verarbeiteten Parameter und behalten das Ende der Argumentliste bei.

Eine Sache, die ich gerne mache, ist, lange Optionen in kurze zu übersetzen - und das mache ich auf sehr ähnliche Weise, weshalb diese Antwort leicht kam - bevor ich renne 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
quelle
Ihre lange Optionskonvertierung würde auch Nichtoptionen (als cp -t --file foooder cp -- --file foo) konvertieren und würde nicht mit abgekürzten ( --fi...) eingegebenen Optionen oder mit der --target=/destSyntax fertig werden .
Stéphane Chazelas
@ StéphaneChazelas - das lange Konvertieren ist nur ein Beispiel - es ist offensichtlich nicht sehr gut aufgebaut. Ich habe das getoptsDing durch eine viel einfachere Funktion ersetzt.
Mikesserv
@ StéphaneChazelas - bitte schauen Sie noch einmal. Während der lange Optionskommentar gerechtfertigt bleibt, ist der erste - und die damit verbundene Abwertung - nicht mehr so, wie ich denke. Ich denke auch, dass dies opts()einen Einfluss auf Ihre eigene Antwort hat, da es opts()in einer einzigen Schleife arbeitet - jedes Argument nur einmal berührt - und dies zuverlässig (soweit ich das beurteilen kann) , portabel und ohne eine einzige Unterschale.
Mikesserv
Das Zurücksetzen von OPTIND ist das Starten einer weiteren getopts-Schleife. In der Tat, dass mehrere Befehlszeilen / Sätze von Optionen ist das Parsen ( -R, -t /mybackup, -f). Ich werde meine Abstimmung vorerst $abeibehalten, da sie immer noch verschleiert ist, Sie nicht initialisiert verwenden und args= opts...wahrscheinlich argsnach der optsRückkehr in vielen Shells (einschließlich Bash) nicht gesetzt (oder auf den vorherigen Wert gesetzt) bleiben.
Stéphane Chazelas
@ StéphaneChazelas - das schließt ein bash- ich hasse das. Wenn die Funktion eine aktuelle Shell- Funktion ist, sollte sie meiner Meinung nach beibehalten werden. Ich Adressierung diese Dinge - wie die aktuelle Funktion, mit mehr Tests, robust gemacht werden könnte , alle Fälle zu handhaben , und sogar Flag Argument mit seiner vorhergehenden Option, aber ich stimme nicht , dass es verschleiert . Wie geschrieben, ist es das einfachste und direkteste Mittel, um seine Aufgabe zu erfüllen, das ich mir vorstellen kann. testDann shiftmacht es wenig Sinn, wenn Sie es in jedem fehlgeschlagenen shiftFall tun sollten return. Es ist nicht beabsichtigt, etwas anderes zu tun.
Mikeserv