Jak zdefiniować podobną funkcję bash na raz

10

Mam te funkcje w ~/.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
}

Jak widzisz, te funkcje są bardzo podobne. Chcę zdefiniować te 3 funkcje jednocześnie. Czy istnieje sposób, aby to zrobić?

środowisko

bash --version
GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)
żelazko piaskowe
źródło

Odpowiedzi:

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[@]}" "$@"
}

Obowiązują zwykłe przestrogi dotyczące eval.

Adrian Frühwirth
źródło
Nie for loop?oznacza to, że to znaczy, że zmienne zadeklarowane w for loopzasadzie znikają - oczekiwałbym tego samego od funkcji z tych samych powodów.
mikeserv
Dlaczego tak sądzisz? bash -c 'for i in 1; do :; done; echo $i'=> 1. Te typewyraźnie pokazuje, że istnieją funkcje poza zakresem pętli.
Adrian Frühwirth
1
@ mikeserv Nawet przy bashdynamicznym określaniu zakresu wszystko, co możesz uzyskać, to localzmienna lokalna dla zakresu całej funkcji , zmienne zdecydowanie nie „znikają” po pętli. W rzeczywistości, ponieważ nie ma tu żadnej funkcji, zdefiniowanie localzmiennej w tym przypadku nie jest nawet możliwe .
Adrian Frühwirth
Po prawej - lokalnie do pętli for - mają zasięg lokalny. Znikają, gdy tylko powłoka nadrzędnej pętli przestanie działać. Czy to się tu nie zdarza?
mikeserv
Nie, jak właśnie wyjaśniłem, nie ma takiej koncepcji jak „lokalna dla pętli for” w skryptach powłoki, a mój post i przykład w moim komentarzu powyżej wyraźnie to pokazują.
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."

Powyższa wola, . source /dev/fd/3która jest wprowadzana do _gem_dec()funkcji za każdym razem, gdy jest wywoływana jako here-document. _gem_dec'szadanie wstępnie ocenione, ma otrzymać jeden parametr i wstępnie ocenić go zarówno jako bundle execcel, jak i nazwę funkcji, w której jest on docelowy.

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

W powyższym przypadku nie sądzę, aby istniało jakiekolwiek ryzyko.

Jeżeli powyższy kod blok jest kopiowany do .bashrcpliku, nie tylko funkcje powłoki _guard(), _rspec()i_rake() być zadeklarowane podczas logowania, ale _gem_dec()funkcja będzie dostępna do realizacji w dowolnym momencie w swojej skorupie prompt (lub inaczej) i tak nowy szablonowe funkcje mogą zadeklaruj, kiedy tylko chcesz:

_gem_dec $new_templated_function_name

I dzięki @Andrew za pokazanie mi, że nie zostaną zjedzeni przez for loop.

ALE JAK?

Używam 3powyższego deskryptora pliku, aby trzymać stdin, stdout, and stderr, or <&0 >&1 >&2się z dala od przyzwyczajenia - chociaż, podobnie jak w przypadku kilku innych domyślnych środków ostrożności, które tu wdrażam - ponieważ wynikowa funkcja jest tak prosta, że ​​tak naprawdę nie jest konieczna. Jest to jednak dobra praktyka. Dzwonienie shift $#to kolejna z tych niepotrzebnych środków ostrożności.

Mimo to, gdy plik jest określony jako <inputalbo>output z [optional num]<filelub [optional num]>fileprzekierowanie jądro odczytuje go w deskryptorze pliku, które mogą być dostępne za pośrednictwem character devicespecjalnych plików /dev/fd/[0-9]*. Jeśli [optional num]specyfikator zostanie pominięty, wówczas 0<fileprzyjmuje się dla danych wejściowych i 1>filewyjściowych. Rozważ to:

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

A ponieważ a here-documentjest jedynie sposobem na opisanie pliku wewnątrz bloku kodu, kiedy:

<<'HEREDOC'
[$CODE]
HEREDOC

Równie dobrze moglibyśmy:

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

Z jednym bardzo ważnym rozróżnieniem. Jeśli nie tematyce następnie powłoka oceni go do muszli , takich jak:"'\quote'"<<"'\LIMITER"'here-document$expansion

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

Tak więc, dla _gem_dec(),3<<-FUNC here-document jest oceniany jako plik na wejściu, tak samo jak byłoby, gdyby to było 3<~/some.file wyjątkiem , że ponieważ mamy opuścić FUNCogranicznik darmo cytatów, jest to pierwszy oceniano $expansion.Ważną rzeczą jest to, że jest to wejście, znaczenie istnieje _gem_dec(),tylko dlatego, że jest również analizowany przed uruchomieniem _gem_dec()funkcji, ponieważ nasza powłoka musi ją odczytać i ocenić $expansionsprzed przekazaniem jej jako danych wejściowych.

Zróbmy guard,na przykład:

_gem_dec guard

Więc najpierw powłoka musi obsłużyć dane wejściowe, co oznacza odczyt:

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

W deskryptor pliku 3 i ocenianie go pod kątem rozszerzenia powłoki. Jeśli w tej chwili prowadziłeś:

cat /dev/fd/3

Lub:

cat <&3

Ponieważ oba są równoważnymi poleceniami, zobaczysz *:

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

... przedtem w ogóle wykonuje się kod w funkcji. Jest to funkcja użytkownika <input, po wszystkim. Aby uzyskać więcej przykładów, zobacz moją odpowiedź na inne pytanie tutaj .

(* Technicznie nie jest to do końca prawdą. Ponieważ używam wiodącego -dashprzed here-doc limiterpowyższym, powyższe byłoby usprawiedliwione do lewej strony. Ale użyłem -dashtak, żebym mógł przede wszystkim <tab-insert>dla czytelności, więc nie zamierzam usuwać <tab-inserts>wcześniej oferując ci do przeczytania ...)

Najmilszą częścią tego jest cytowanie - zauważ, że '"cytaty pozostały i tylko \cytaty zostały usunięte. Prawdopodobnie z tego powodu bardziej niż jakiegokolwiek innego, jeśli będziesz musiał dwukrotnie ocenić powłokę $expansion, polecę to, here-documentponieważ cytaty są znacznie łatwiejsze niż eval.

W każdym razie, teraz powyższy kod jest dokładnie tak, jak plik wprowadzony, jak 3<~/heredoc.filetylko czekanie na uruchomienie _gem_dec()funkcji i zaakceptowanie jej danych wejściowych /dev/fd/3.

Więc kiedy zaczynamy, _gem_dec()najpierw rzucam wszystkie parametry pozycyjne, ponieważ naszym następnym krokiem jest podwójnie ocenione rozszerzenie powłoki i nie chcę, aby którykolwiek z zawartych elementów $expansionsbył interpretowany jako jeden z moich bieżących $1 $2 $3...parametrów. Więc ja:

shift $#

shiftodrzuca tyle, positional parametersile określisz i zaczyna od $1tego, co pozostało. Tak więc, gdybym wywołał _gem_dec one two threew linii poleceń _gem_dec's $1 $2 $3parametry pozycyjne byłyby, one two threea całkowita bieżąca liczba pozycyjna, lub $#wynosiłaby 3. Gdybym wtedy wywołał shift 2,wartości oneitwo zostałby shiftusunięty , wartość $1zmieniłaby się na threei $#rozszerzyłaby się do 1. Więc shift $#po prostu wyrzuca je wszystkie. Robienie tego jest ściśle zapobiegawcze i jest tylko nawykiem, który rozwinąłem po zrobieniu tego rodzaju rzeczy przez jakiś czas. Oto (subshell)trochę dla jasności:

( 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

W każdym razie następnym krokiem jest magia. Jeśli pojawi . ~/some.shsię monit powłoki, wszystkie funkcje i zmienne środowiskowe zadeklarowane w programie ~/some.shbędą mogły być wywoływane po zachęcie powłoki. Tak samo jest tutaj, z wyjątkiem my plik specjalny dla naszego deskryptor pliku, lub - co jest gdzie nasz plik w linii został pathed - i mamy zadeklarowane naszą funkcję. I tak to działa.. sourcecharacter device. /dev/fd/3here-document

_guard

Teraz robi wszystko, co _guardpowinna zrobić twoja funkcja.

Uzupełnienie:

Świetny sposób na zapisanie pozycji:

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

EDYTOWAĆ:

Kiedy po raz pierwszy odpowiedziałem na to pytanie, bardziej skupiłem się na problemie zadeklarowania powłoki function()zdolnej do zadeklarowania innych funkcji, które pozostaną w bieżącym $ENVetapie prasowania powłoki, niż na tym, co pytający zrobiłby z tymi stałymi funkcjami. Od tego czasu zdałem sobie sprawę, że moje pierwotnie zaproponowane rozwiązanie, w którym 3<<-FUNCprzyjęło formę:

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

Nie będzie prawdopodobnie pracował zgodnie z oczekiwaniami do pytającego, bo specjalnie zmienił nazwę deklaratywnego funkcja jest od $1celu _${1}, który, jeśli nazywa się tak jak _gem_dec guardna przykład, spowodowałoby _gem_decdeklarowania funkcji o nazwie _guarda nie tylko guard.

Uwaga: Takie zachowanie jest dla mnie kwestią przyzwyczajenia - zazwyczaj działam z założeniem, że funkcje powłoki powinny zajmować tylko własne,_namespaceaby uniknąć włamania się donamespacepowłokicommands.

Nie jest to jednak powszechny nawyk, o czym świadczy użycie commandwezwania przez pytającego $1.

Dalsze badanie prowadzi mnie do wniosku, że:

Pytający chce, aby funkcje powłoki o nazwie, guard, rspec, or rakektóre po wywołaniu skompilują na nowo rubyfunkcję o tej samej nazwie, ifco plik Gemfileistnieje w $PATH OR if Gemfile , nie istnieje, funkcja powłoki powinna wykonać rubyfunkcję o tej samej nazwie.

Nie działałoby to wcześniej, ponieważ zmieniłem również $1wezwanie commanddo przeczytania:

command _${1}

Co nie spowodowałoby wykonania rubyfunkcji skompilowanej przez funkcję powłoki jako:

bundle exec $1

Mam nadzieję, że zobaczysz (jak ostatecznie to zrobiłem) , że wydaje się, że pytający commandw ogóle używa tylko pośrednio, namespaceponieważ commandwoli wywołać plik wykonywalny w $PATHfunkcji powłoki o tej samej nazwie.

Jeśli moja analiza jest poprawna (jak mam nadzieję pytający to potwierdzi), to:

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

Powinien lepiej spełniać te warunki, z wyjątkiem tego, że wywołanie guardw wierszu będzie tylko próbowało wykonać plik wykonywalny w $PATHnazwie, guardpodczas gdy wywołanie _guardw wierszu sprawdzi, czy Gemfile'sistnieje i odpowiednio się skompiluje lub uruchomi guardplik wykonywalny w $PATH. W ten sposób namespacejest chroniony i, przynajmniej tak, jak go postrzegam, zamiary pytającego są nadal spełnione.

W rzeczywistości, zakładając, że nasza funkcja powłoki _${1}()i plik wykonywalny ${PATH}/${1}to jedyne dwa sposoby, w jakie nasza powłoka może interpretować wywołanie albo, $1albo _${1}użycie commandfunkcji w ogóle jest teraz całkowicie zbędne. Mimo to pozwoliłem temu pozostać, ponieważ nie lubię popełniać tego samego błędu dwa razy ... w każdym razie z rzędu.

Jeśli jest to nie do przyjęcia dla pytającego, a on / ona wolałaby _całkowicie się pozbyć , to w obecnej formie edycja _underscorewyjścia powinna być wszystkim, co pytający musi zrobić, aby spełnić jego / jej wymagania, tak jak je rozumiem.

Oprócz tej zmiany zredagowałem również funkcję, aby używać &&i / lub|| warunek zwarcia powłoki zamiast oryginalnej if/thenskładni. W ten sposób commandinstrukcja jest oceniana w ogóle tylko wtedy, gdy jej Gemfilenie ma $PATH. Ta modyfikacja wymaga jednak dodania, return $?aby upewnić się, że bundleinstrukcja nie zostanie uruchomiona, jeśli zdarzenie Gemfilenie istnieje, ale ruby $1funkcja zwraca wartość inną niż 0.

Na koniec należy zauważyć, że to rozwiązanie implementuje tylko przenośne konstrukcje powłoki. Innymi słowy, powinno to dawać identyczne wyniki w dowolnej powłoce, która twierdzi, że jest zgodna z POSIX. Choć oczywiście byłoby dla mnie nonsensem twierdzenie, że każdy system zgodny z POSIX musi obsługiwać ruby bundledyrektywę, przynajmniej imperatywne powłoki wywołujące go powinny zachowywać się tak samo, niezależnie od tego, czy wywołująca powłoka jest, shczy też dash. Również powyższe prace będą zgodnie z oczekiwaniami (zakładając, przynajmniej w połowie-sane shoptsw każdym razie) w obu bashi zsh.

mikeserv
źródło
Wstawiam twój kod ~/.bashrci wywołuję . ~/.bashrc, a następnie te trzy funkcje są wykonywane. Może zachowanie różni się w zależności od środowiska, więc do pytania dodałem swoje środowisko. Poza tym nie mogłem zrozumieć, dlaczego _guard ; _rspec ; _rakepotrzebna jest ostatnia linia . Szukałem shifti deskryptor pliku, wygląda na to, że są one poza moim obecnym zrozumieniem.
ironsand
Po prostu umieściłem to, aby pokazać, że można je wywołać. Przepraszam - włączyłem echo. Możesz więc wywoływać je jako funkcje, jak wykazałeś.
mikeserv
@Tetsu - czy ma to teraz lepszy sens?
mikeserv
Przeczytałem twoją odpowiedź 3 razy, ale szczerze mówiąc, potrzebuję więcej wiedzy, aby zrozumieć wyjaśnienie. Mimo że jestem ci bardzo wdzięczny, przeczytam ją ponownie, gdy zdobędę więcej doświadczenia.
ironsand
@Tetsu Być może teraz jest to jaśniejsze ...? Myślę, że zdałem sobie sprawę, a teraz poprawiłem, błąd, który popełniłem wcześniej. Daj mi znać, jeśli chcesz.
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
źródło