So definieren Sie eine ähnliche Bash-Funktion auf einmal

10

Ich habe diese Funktionen in ~/.bashrc:

function guard() {
    if [ -e 'Gemfile' ]; then
    bundle exec guard "$@"
    else
    command guard "$@"
    fi
}
function rspec() {
    if [ -e 'Gemfile' ]; then
    bundle exec rspec "$@"
    else
    command rspec "$@"
    fi
}
function rake() {
    if [ -e 'Gemfile' ]; then
        bundle exec rake "$@"
    else
        command rake "$@"
    fi
}

Wie Sie sehen, sind diese Funktionen sehr ähnlich. Ich möchte diese 3 Funktionen gleichzeitig definieren. Gibt es einen Weg, es zu schaffen?

Umgebung

bash --version
GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)
Eisensand
quelle

Antworten:

8
$ cat t.sh
#!/bin/bash

for func in guard rspec rake; do
        eval "
        ${func}() {
                local foo=(command ${func})
                [ -e 'Gemfile' ] && foo=(bundle exec ${func})
                \"\${foo[@]}\" \"\$@\"
        }
        "
done

type guard rspec rake

.

$ ./t.sh
guard is a function
guard ()
{
    local foo=(command guard);
    [ -e 'Gemfile' ] && foo=(bundle exec guard);
    "${foo[@]}" "$@"
}
rspec is a function
rspec ()
{
    local foo=(command rspec);
    [ -e 'Gemfile' ] && foo=(bundle exec rspec);
    "${foo[@]}" "$@"
}
rake is a function
rake ()
{
    local foo=(command rake);
    [ -e 'Gemfile' ] && foo=(bundle exec rake);
    "${foo[@]}" "$@"
}

Es evalgelten die üblichen Vorsichtsmaßnahmen .

Adrian Frühwirth
quelle
Wird es nicht von dem gegessen, was for loop?ich meine, Variablen, die in einem for loopallgemein deklariert sind, verschwinden - ich würde das gleiche von Funktionen aus den gleichen Gründen erwarten.
Mikeserv
Was bringt dich dazu, das zu denken? bash -c 'for i in 1; do :; done; echo $i'=> 1. Das typezeigt deutlich, dass die Funktionen außerhalb des Bereichs der Schleife existieren.
Adrian Frühwirth
1
@mikeserv Selbst mit bashdem dynamischen Scoping können Sie nur eine localVariable erhalten, die lokal für den Umfang einer ganzen Funktion ist. Variablen "verschwinden" definitiv nicht nach einer Schleife. Da hier keine Funktion beteiligt ist, ist es localin diesem Fall nicht einmal möglich, eine Variable zu definieren .
Adrian Frühwirth
Richtig - lokal für die for-Schleife - sie haben einen lokalen Gültigkeitsbereich. Sie verschwinden, sobald ihre übergeordnete Loop-Shell dies tut. Passiert das hier nicht?
Mikeserv
Nein, wie ich gerade erklärt habe, gibt es in Shell-Skripten kein Konzept wie "local to the for loop", und mein Beitrag und das Beispiel in meinem obigen Kommentar zeigen dies deutlich.
Adrian Frühwirth
7
_gem_dec() { shift $# ; . /dev/fd/3
} 3<<-FUNC
    _${1}() { [ ! -e 'Gemfile' ] && { 
        command $1 "\$@" ; return \$?
        } || bundle exec $1 "\$@"
    }
FUNC
for func in guard rspec rake ; do _gem_dec $func ; done
echo "_guard ; _rspec ; _rake are all functions now."

Der obige Wille, . source /dev/fd/3der der _gem_dec()Funktion jedes Mal zugeführt wird, wenn sie als vorab ausgewerteter here-document. _gem_dec'sJob aufgerufen wird, besteht darin, einen Parameter zu empfangen und ihn sowohl als bundle execZiel als auch als Namen der Funktion vorab auszuwerten, auf die er abzielt.

NOTE: . sourcing shell expansions results in twice-evaluated variables - just like eval. It can be risky.

Im obigen Fall glaube ich jedoch nicht, dass ein Risiko bestehen kann.

Wenn der obige Codeblock in eine .bashrcDatei kopiert wird, werden nicht nur die Shell-Funktionen _guard(), _rspec()und_rake() beim Anmelden deklariert, sondern die _gem_dec()Funktion kann auch jederzeit an Ihrer Shell-Eingabeaufforderung (oder auf andere Weise) ausgeführt werden, sodass neue Vorlagenfunktionen verfügbar sind deklariert werden, wann immer Sie möchten mit nur:

_gem_dec $new_templated_function_name

Und danke an @Andrew, der mir gezeigt hat, dass diese nicht von einem gefressen werden for loop.

ABER WIE?

Ich verwende den 3obigen Dateideskriptor, um mich stdin, stdout, and stderr, or <&0 >&1 >&2aus Gewohnheit offen zu halten - wie es auch bei einigen anderen Standardvorkehrungen der Fall ist, die ich hier implementiere -, weil die resultierende Funktion so einfach ist, dass sie wirklich nicht notwendig ist. Es ist jedoch eine gute Praxis. Das Anrufen shift $#ist eine weitere dieser unnötigen Vorsichtsmaßnahmen.

Wenn eine Datei als <inputoder>output mit [optional num]<fileoder [optional num]>fileumgeleitet wird, liest der Kernel sie in einen Dateideskriptor ein, auf den über die character devicespeziellen Dateien in zugegriffen werden kann /dev/fd/[0-9]*. Wenn der Bezeichner [optional num]weggelassen wird, 0<filewird für die Eingabe und 1>filefür die Ausgabe angenommen. Bedenken Sie:

l='line %d\n' ; printf "$l" 1 2 3 4 5 6 >/dev/fd/1
> line 1
> line 2
> line 3
> line 4
> line 5
> line 6

( printf "$l" 4 5 6 >/dev/fd/3 ; printf "$l" 1 2 3 ) >/tmp/sample 3>/tmp/sample2

( cat /tmp/sample2 ) </tmp/sample
> line 4
> line 5
> line 6

( cat /dev/fd/0 ) </tmp/sample
> line 1
> line 2
> line 3

( cat /dev/fd/3 ) </tmp/sample 3</tmp/sample2
> line 4
> line 5
> line 6

Und weil a here-documentnur ein Mittel ist, um eine Datei in einem Codeblock inline zu beschreiben, wenn wir Folgendes tun:

<<'HEREDOC'
[$CODE]
HEREDOC

Wir könnten genauso gut tun:

echo '[$CODE]' >/dev/fd/0

Mit einem sehr wichtigen Unterschied. Wenn nicht "'\quote'"die <<"'\LIMITER"'eines here-documentdann wird die Schale für Schale bewerten $expansionwie:

echo "[$CODE]" >/dev/fd/0

Also, für _gem_dec(), die 3<<-FUNC here-documentals Datei auf Eingabe ausgewertet wird, gleich wie es wäre, wenn es waren 3<~/some.file außer dass , weil wir das verlassen FUNCLimiter frei von Kursen, zunächst für ausgewertet $expansion.Das Wichtigste dabei ist , dass es eingegeben wird, Bedeutung Es existiert nur für, _gem_dec(),aber es wird auch ausgewertet, bevor die _gem_dec()Funktion ausgeführt wird, da unsere Shell sie lesen und auswerten muss, $expansionsbevor sie als Eingabe übergeben wird.

Machen wir guard,zum Beispiel:

_gem_dec guard

Also muss die Shell zuerst die Eingabe verarbeiten, was bedeutet, dass sie liest:

3<<-FUNC
    _${1}() { [ ! -e 'Gemfile' ] && { 
        command $1 "\$@" ; return \$?
        } || bundle exec $1 "\$@"
    }
FUNC

In Dateideskriptor 3 und Auswertung für die Shell-Erweiterung. Wenn Sie zu diesem Zeitpunkt gelaufen sind:

cat /dev/fd/3

Oder:

cat <&3

Da beide äquivalente Befehle sind, sehen Sie *:

_guard() { [ ! -e 'Gemfile' ] && { 
    command guard "$@" ; return $?
    } || bundle exec guard "$@"
}

... bevor überhaupt ein Code in der Funktion ausgeführt wird. Dies ist die Funktion des <input, nachdem alle. Weitere Beispiele finden Sie in meiner Antwort auf eine andere Frage hier .

(* Technisch ist dies nicht ganz richtig. Weil ich ein führender verwenden , -dashbevor das here-doc limiterwürde das vor allem linksbündig werden. Aber ich habe das -dashso konnte ich <tab-insert>in erster Linie für die Lesbarkeit so werde ich das nicht strippen <tab-inserts>vor Biete es dir zum Lesen an ...)

Das Schönste daran ist das Zitieren - beachten Sie, dass die '"Anführungszeichen erhalten bleiben und nur die \Anführungszeichen entfernt wurden. Es ist wahrscheinlich mehr als jeder andere Grund, dass $expansionich das empfehlen werde , wenn Sie eine Shell zweimal bewerten müssen, here-documentda die Anführungszeichen viel einfacher sind als eval.

Wie auch immer, jetzt ist der obige Code genau wie eine eingespeiste Datei, als würde man 3<~/heredoc.filenur darauf warten, dass die _gem_dec()Funktion startet und ihre Eingabe akzeptiert /dev/fd/3.

Wenn wir also anfangen, _gem_dec()werfe ich als erstes alle Positionsparameter, da unser nächster Schritt eine zweimal evaluierte Shell-Erweiterung ist und ich nicht möchte, dass einer der enthaltenen Parameter $expansionsals einer meiner aktuellen $1 $2 $3...Parameter interpretiert wird. Also ich:

shift $#

shiftverwirft so viele, positional parameterswie Sie angeben, und beginnt $1mit dem, was übrig bleibt. Also , wenn ich rief _gem_dec one two threean der Eingabeaufforderung _gem_dec's $1 $2 $3Positionsparameter sei one two threeund die Gesamtstrom Positionsanzahl oder $#3 sein würde , wenn ich rufe dann shift 2,die Werte oneundtwo würde shifted weg, den Wert $1ändern würde threeund $#würde erweitern , um 1. So shift $#einfach wirft sie alle weg. Dies zu tun ist streng vorsorglich und nur eine Gewohnheit, die ich entwickelt habe, nachdem ich so etwas für eine Weile getan habe. Hier ist es der (subshell)Klarheit halber etwas ausgebreitet:

( set -- one two three ; echo "$1 $2 $3" ; echo $# )
> one two three
> 3

( set -- one two three ; shift 2 ; echo "$1 $2 $3" ; echo $# )
> three
> 1

( set -- one two three ; shift $# ; echo "$1 $2 $3" ; echo $# )
>
> 0

Wie auch immer, der nächste Schritt ist, wo die Magie passiert. Wenn Sie sich . ~/some.shan der Shell-Eingabeaufforderung befinden, können alle in deklarierten Funktionen und Umgebungsvariablen ~/some.shan Ihrer Shell-Eingabeaufforderung aufgerufen werden. Das Gleiche gilt hier, außer dass wir . source die character devicespezielle Datei für unseren Dateideskriptor oder . /dev/fd/3- wo unsere here-documentInline-Datei gespeichert wurde - und unsere Funktion deklariert haben. Und so funktioniert es.

_guard

Jetzt macht alles, was Ihre _guardFunktion tun soll.

Nachtrag:

Eine gute Möglichkeit zu sagen, speichern Sie Ihre Positionals:

f() { . /dev/fd/3
} 3<<-ARGS
    args='${args:-"$@"}'
ARGS

BEARBEITEN:

Als ich diese Frage zum ersten Mal beantwortete, konzentrierte ich mich mehr auf das Problem, eine Shell function()zu deklarieren, die andere Funktionen deklarieren kann, die in der aktuellen Shell- $ENVBügelung bestehen bleiben, als darauf, was der Fragesteller mit diesen persistenten Funktionen tun würde. Seitdem habe ich festgestellt, dass meine ursprünglich angebotene Lösung 3<<-FUNCdie Form hatte:

3<<-FUNC
    _${1}() { 
        if [ -e 'Gemfile' ]; then
            bundle exec $1 "\$@"
        else 
            command _${1} "\$@"
    }
FUNC

Hätte wahrscheinlich nicht gearbeitet wie für die Fragesteller erwartet , weil ich die deklarativen Funktion Namen von speziell verändert $1zu _${1}denen, wenn wie genannt _gem_dec guardzum Beispiel würde in der _gem_decDeklaration eine Funktion namens _guardim Gegensatz zu nur guard.

Hinweis: Ein solches Verhalten ist für mich eine Gewohnheit - ich gehe normalerweise davon aus, dass Shell-Funktionen nur ihre eigenenFunktionen einnehmen sollten_namespace, um ein Eindringen in dieeigentlichenamespaceShell zuvermeidencommands.

Dies ist jedoch keine universelle Gewohnheit, wie aus der Verwendung des Fragestellers commandzum Anrufen hervorgeht $1.

Eine weitere Untersuchung lässt mich Folgendes glauben:

Der Fragesteller möchte Shell-Funktionen mit dem Namen guard, rspec, or rake, die beim Aufrufen eine rubyFunktion mit demselben Namen neu kompilieren, in der ifdie Datei Gemfileexistiert, $PATH ODER if Gemfile nicht existiert. Die Shell-Funktion sollte die rubyFunktion mit demselben Namen ausführen .

Das hätte vorher nicht geklappt, weil ich auch den $1von commandgelesenen geändert habe:

command _${1}

Was nicht zur Ausführung der rubyFunktion geführt hätte, die die Shell-Funktion kompiliert hat als:

bundle exec $1

Ich hoffe, Sie können sehen (wie ich es letztendlich getan habe), dass der Fragesteller anscheinend nur commandindirekt spezifiziert, namespaceweil er commandes vorzieht, eine ausführbare Datei $PATHüber eine gleichnamige Shell-Funktion aufzurufen .

Wenn meine Analyse korrekt ist (wie ich hoffe, dass der Fragesteller dies bestätigt), dann ist dies:

_${1}() { [ ! -e 'Gemfile' ] && { 
    command $1 "\$@" ; return \$?
    } || bundle exec $1 "\$@"
}

Sollte diese Bedingungen besser erfüllen, mit der Ausnahme, dass beim Aufrufen guardder Eingabeaufforderung nur versucht wird, eine ausführbare Datei in $PATHnamed auszuführen, guardwährend beim Aufrufen _guardder Eingabeaufforderung die Gemfile'sExistenz überprüft und entsprechend kompiliert oder die guardausführbare Datei in ausgeführt wird $PATH. Auf diese Weise namespacewird geschützt und zumindest so, wie ich es wahrnehme, wird die Absicht des Fragestellers immer noch erfüllt.

In der Tat, unsere Shell - Funktion vorausgesetzt , _${1}()und die ausführbaren Datei ${PATH}/${1}sind die nur zwei Möglichkeiten , um unsere Schale einen Anruf entweder interpretieren könnte $1oder _${1}dann die Verwendung von commandin der Funktion überhaupt völlig überflüssig gemacht wird nun. Trotzdem habe ich es bleiben lassen, da ich nicht zweimal den gleichen Fehler machen möchte ... sowieso hintereinander.

Wenn dies für den Fragesteller nicht akzeptabel ist und er / sie es vorziehen würde, das Problem _vollständig _underscorezu beseitigen, sollte in der aktuellen Form die Bearbeitung des Outs alles sein, was der Fragesteller tun muss, um seine / ihre Anforderungen zu erfüllen, so wie ich sie verstehe.

Abgesehen von dieser Änderung habe ich bearbeitet auch die Funktion zu verwenden &&und / oder|| Schale Kurzschluss conditionals anstatt die ursprüngliche if/thenSyntax. Auf diese Art und Weise die commandAnweisung wird nur dann ausgewertet , überhaupt , wenn Gemfilenicht in ist $PATH. Diese Änderung erfordert jedoch das Hinzufügen von, return $?um sicherzustellen, dass die bundleAnweisung nicht ausgeführt wird, falls das Ereignis Gemfilenicht vorhanden ist, die ruby $1Funktion jedoch etwas anderes als 0 zurückgibt .

Zuletzt sollte ich beachten, dass diese Lösung nur tragbare Shell-Konstrukte implementiert. Mit anderen Worten, dies sollte zu identischen Ergebnissen in jeder Shell führen, die POSIX-Kompatibilität beansprucht. Während es für mich natürlich Unsinn wäre zu behaupten, dass jedes POSIX-kompatible System mit der ruby bundleDirektive umgehen muss , sollten sich zumindest die Shell-Imperative, die es aufrufen, gleich verhalten, unabhängig davon, ob die aufrufende Shell shoder ist dash. Auch das oben Genannte shoptswird in beiden und wie erwartet funktionieren (vorausgesetzt, es ist sowieso mindestens halbwegs gesund ) .bashzsh

mikeserv
quelle
Ich gebe Ihren Code ein ~/.bashrcund rufe auf . ~/.bashrc, dann werden diese drei Funktionen ausgeführt. Vielleicht unterscheidet sich das Verhalten je nach Umgebung, deshalb habe ich der Frage meine Umgebung hinzugefügt. Außerdem konnte ich nicht verstehen, warum die letzte Zeile _guard ; _rspec ; _rakebenötigt wird. Ich habe nach shiftund Dateideskriptor gesucht , es sieht so aus, als ob diese über mein derzeitiges Verständnis hinausgehen.
Eisen und
Ich habe das nur dort abgelegt, um zu zeigen, dass sie abrufbar sind. Entschuldigung - ich habe jetzt ein Echo eingefügt. Sie können sie also als Funktionen aufrufen, wie Sie gezeigt haben.
Mikeserv
@Tetsu - macht es jetzt einen besseren Sinn?
Mikeserv
Ich habe Ihre Antwort dreimal durchgelesen, aber ehrlich gesagt, ich brauche mehr Wissen, um die Erklärung zu verstehen. Obwohl ich Ihnen sehr dankbar bin, werde ich es noch einmal lesen, wenn ich mehr Erfahrung habe.
Eisen und
@ Tetsu Vielleicht ist es jetzt klarer ...? Ich glaube, ich habe einen Fehler, den ich zuvor gemacht habe, erkannt und jetzt korrigiert. Bitte lassen Sie es mich wissen, wenn Sie möchten.
Mikeserv
2
function threeinone () {
    local var="$1"
    if [ $# -ne 1 ]; then
        return 1
    fi
    if ! [ "$1" = "guard" -o "$1" = "rspec" -o "$1" = "rake" ]; then
        return 1
    fi
    shift
    if [ -e 'Gemfile' ]; then
        bundle exec "$var" "$@"
    else
        command "$var" "$@"
    fi
}

threeinone guard
threeinone rspec
threeinone rake
Hauke ​​Laging
quelle