Como definir funções bash semelhantes de uma só vez

10

Eu tenho essas funções em ~/.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
}

Como você vê, essas funções são muito semelhantes. Eu quero definir essas 3 funções ao mesmo tempo. Existe uma maneira de fazer isso?

meio Ambiente

bash --version
GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)
ferros e areias
fonte

Respostas:

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

As precauções usuais sobre evalaplicação.

Adrian Frühwirth
fonte
Isso não é comido pelo que for loop?eu quero dizer, variáveis ​​declaradas for loopgeralmente desaparecem - eu esperaria o mesmo de funções pelas mesmas razões.
mikeserv
O que te faz pensar isso? bash -c 'for i in 1; do :; done; echo $i'=> 1. Os typemostra claramente que as funções existem fora do âmbito do loop.
Adrian Frühwirth
1
@mikeserv Mesmo com basho escopo dinâmico de tudo o que você pode obter é uma localvariável local para o escopo de uma função inteira , as variáveis ​​definitivamente não "desaparecem" após um loop. De fato, como não há função envolvida aqui, nem mesmo é possível definir uma localvariável nesse caso.
Adrian Frühwirth
À direita - local no loop for - eles têm escopo local. Eles desaparecem assim que o shell do loop pai desaparece. Isso não acontece aqui?
mikeserv
Não, como acabei de explicar, não existe um conceito como "local para o loop for" nos scripts de shell, e minha postagem e o exemplo no meu comentário acima mostram claramente isso.
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."

A vontade acima, . source /dev/fd/3que é inserida na _gem_dec()função toda vez que é chamada como here-document. _gem_dec'starefa apenas pré-avaliada, recebe um parâmetro e a pré-avalia como bundle execalvo e como nome da função na qual é direcionada.

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

No caso acima, porém, não acho que possa haver qualquer risco.

Se o bloco de código acima for copiado em um .bashrcarquivo, o shell não apenas funcionará _guard(), _rspec()e_rake() será declarado no login, mas a _gem_dec()função também estará disponível para execução a qualquer momento, no prompt do shell (ou outro) e, portanto, novas funções de modelo podem seja declarado sempre que quiser com apenas:

_gem_dec $new_templated_function_name

E obrigado a @ Andrew por me mostrar que estes não seriam comidos por um for loop.

MAS COMO?

Eu uso o 3descritor de arquivo acima para manter stdin, stdout, and stderr, or <&0 >&1 >&2o hábito aberto - embora, como também seja o caso de algumas das outras precauções padrão que eu implemente aqui - porque a função resultante é muito simples, não é realmente necessário. É uma boa prática, no entanto. Ligar shift $#é outra dessas precauções desnecessárias.

Ainda assim, quando um arquivo é especificado como <inputou>output com [optional num]<fileou [optional num]>fileredireciona, o kernel o lê em um descritor de arquivo, que pode ser acessado através dos character devicearquivos especiais em /dev/fd/[0-9]*. Se o [optional num]especificador for omitido, 0<fileserá assumido para entrada e 1>filesaída. Considere isto:

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

E como a here-documenté apenas um meio de descrever um arquivo embutido dentro de um bloco de código, quando fazemos:

<<'HEREDOC'
[$CODE]
HEREDOC

Nós também podemos:

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

Com uma distinção muito importante . Se você não fizer "'\quote'"o <<"'\LIMITER"'de a here-document, o shell o avaliará $expansioncomo:

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

Portanto, para _gem_dec(), o 3<<-FUNC here-documenté avaliado como um arquivo na entrada, o mesmo que seria se fosse, 3<~/some.file exceto que, porque deixamos o FUNClimitador livre de aspas, ele é primeiro avaliado para $expansion.o importante: isso é entrada, significando existe apenas para, _gem_dec(),mas também é avaliado antes da _gem_dec()função ser executada, porque nosso shell precisa ler e avaliar seus dados $expansionsantes de entregá-los como entrada.

Vamos fazer, guard,por exemplo:

_gem_dec guard

Então, primeiro o shell precisa lidar com a entrada, o que significa ler:

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

No descritor de arquivo 3 e avaliando-o para expansão do shell. Se nesse momento você executou:

cat /dev/fd/3

Ou:

cat <&3

Como os dois comandos são equivalentes, você verá *:

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

... antes de qualquer código na função ser executado. Esta é a função <input, afinal. Para mais exemplos, veja minha resposta para uma pergunta diferente aqui .

(* Tecnicamente, isso não é completamente verdade. Como eu uso uma liderança -dashantes da here-doc limiter, as opções acima seriam justificadas à esquerda. Mas eu usei o -dashque eu pude <tab-insert>para facilitar a leitura em primeiro lugar, para não tirar o <tab-inserts>antes oferecendo a você para ler ...)

A parte mais legal sobre isso é a citação - observe que as '"aspas permanecem e apenas as \aspas foram retiradas. Provavelmente, é por esse motivo, mais do que qualquer outro, que se você precisar avaliar duas vezes um shell $expansion, recomendarei o here-documentporque as aspas são muito mais fáceis do que eval.

De qualquer forma, agora o código acima é exatamente como um arquivo alimentado, como 3<~/heredoc.fileapenas esperando a _gem_dec()função continuar e aceitar sua entrada /dev/fd/3.

Então, quando começamos, _gem_dec()a primeira coisa que faço é lançar todos os parâmetros posicionais, porque nosso próximo passo é uma expansão de shell avaliada duas vezes e não quero que nenhum dos contidos $expansionsseja interpretado como qualquer um dos meus $1 $2 $3...parâmetros atuais . Então eu:

shift $#

shiftdescarta quantos positional parametersvocê especificar e começa $1com o que resta. Então, se eu liguei _gem_dec one two threepara os alertas _gem_dec's $1 $2 $3parâmetros posicionais seria one two threeea contagem posicional corrente total, ou $#seria 3. Se eu então chamado shift 2,os valores de oneetwo seria shifted longe, o valor de $1se mudar para threee $#iria expandir a 1. Assim, shift $#apenas joga todos eles fora. Fazer isso é estritamente preventivo e é apenas um hábito que desenvolvi depois de fazer esse tipo de coisa por um tempo. Aqui está (subshell)um pouco espalhado por uma questão de clareza:

( 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

De qualquer forma, o próximo passo é onde a mágica acontece. Se você . ~/some.shsolicitar no shell, todas as funções e variáveis ​​de ambiente declaradas ~/some.shserão chamadas no prompt do shell. O mesmo acontece aqui, exceto . source o character devicearquivo especial para o nosso descritor de arquivo, ou . /dev/fd/3- que é onde o here-documentarquivo in-line foi localizado - e declaramos nossa função. E é assim que funciona.

_guard

Agora faz o que sua _guardfunção deve fazer.

Termo aditivo:

Uma ótima maneira de dizer salve suas posições:

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

EDITAR:

Quando respondi pela primeira vez a essa pergunta, concentrei-me mais no problema de declarar um shell function()capaz de declarar outras funções que persistissem no $ENVferro atual do shell do que no que o solicitante faria com as referidas funções persistentes. Desde então, percebi que minha solução oferecida originalmente na qual 3<<-FUNCassumia a forma:

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

Provavelmente não teria funcionado como esperado para o autor da pergunta, porque alterei especificamente o nome da função declarativa $1para a _${1}qual, se chamado como _gem_dec guardpor exemplo, resultaria na _gem_decdeclaração de uma função nomeada _guardem oposição a just guard.

Nota: Esse comportamento é uma questão de hábito para mim - normalmente opero na presunção de que as funções do shell devem ocupar apenas as suas_namespacepara evitar a intrusão nopróprionamespaceshellcommands.

Porém, este não é um hábito universal, como é evidenciado no uso de quem commandpede $1.

Um exame mais aprofundado me leva a acreditar no seguinte:

O autor da questão quer funções shell nomeados guard, rspec, or rakeque, quando chamado, irá compilar novamente uma rubyfunção com o mesmo nome ifdo arquivo Gemfileexiste no $PATH OR if Gemfile não existe, a função shell deve executar a rubyfunção com o mesmo nome.

Isso não teria funcionado anteriormente, porque também alterei o $1chamado commandpara ler:

command _${1}

O que não resultaria na execução da rubyfunção que a função shell compilou como:

bundle exec $1

Espero que você possa ver (como acabei vendo ) que parece que o solicitante está usando apenas commandpara especificar indiretamente, namespaceporque commandpreferirá chamar um arquivo executável em $PATHuma função shell com o mesmo nome.

Se minha análise estiver correta (como espero que o solicitante confirme), então isto:

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

Deve atender melhor a essas condições, com a exceção de que a chamada guardno prompt apenas tentará executar um arquivo executável em $PATHnamed, guardenquanto a chamada _guardno prompt verificará a Gemfile'sexistência e compilará de acordo ou executará o guardexecutável no $PATH. Dessa maneira, namespaceé protegida e, pelo menos como eu a percebo, a intenção do solicitante ainda é cumprida.

De fato, presumindo que nossa função de shell _${1}()e o executável ${PATH}/${1}sejam as duas únicas maneiras pelas quais nosso shell pode interpretar uma chamada para $1ou _${1}então o uso de commandna função é agora totalmente redundante. Ainda assim, deixei como não gosto de cometer o mesmo erro duas vezes ... de qualquer maneira.

Se isso for inaceitável para o solicitante e ele preferir acabar com o processo _totalmente, então, em sua forma atual, editar a _underscoresaída deve ser tudo o que o solicitante precisa fazer para atender aos requisitos que eu os entendo.

Além dessa alteração, também editei a função para usar &&e / ou|| shell condicionais de curto-circuito , em vez da if/thensintaxe original . Desta forma, a commanddeclaração só é avaliado em tudo , se Gemfilenão está na $PATH. Essa modificação requer a adição de, return $?no entanto, para garantir que a bundleinstrução não seja executada caso o evento Gemfilenão exista, mas a ruby $1função retornará algo diferente de 0.

Por fim, devo observar que esta solução implementa apenas construções de shell portáteis. Em outras palavras, isso deve produzir resultados idênticos em qualquer shell que reivindique compatibilidade com POSIX. Embora, obviamente, seja absurdo eu afirmar que todo sistema compatível com POSIX deve lidar com a ruby bundlediretiva, pelo menos os imperativos do shell que o invocam devem se comportar da mesma maneira, independentemente de o shell de chamada ser shou dash. Além disso, o acima funcionará conforme o esperado (presumindo pelo menos meio caminhoshopts normal ) em ambos bashe zsh.

mikeserv
fonte
Eu insiro seu código ~/.bashrce chamo . ~/.bashrc, então essas três funções são executadas. Talvez o comportamento seja diferente por ambiente, então eu adicionei meu ambiente à pergunta. Além disso, eu não conseguia entender por que a última linha _guard ; _rspec ; _rakeé necessária. Eu pesquisei sobre shifte descritor de arquivo, parece que estes estão além do meu entendimento atual.
ironsand
Eu apenas coloquei isso lá para mostrar que eles eram chamados. Desculpe - eu fiz eco agora. Então você pode chamá-los como funções, como demonstrou.
mikeserv
@ Tetsu - faz mais sentido agora?
mikeserv
Li sua resposta três vezes, mas honestamente dizendo que preciso de mais conhecimento para entender a explicação. Mesmo que eu esteja muito agradecido, lerei novamente quando tiver mais experiência.
ironsand
@Tetsu Talvez seja mais claro agora ...? Acho que percebi e já corrigi um erro que cometi anteriormente. Por favor me avise, se você quiser.
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
fonte