Cómo definir una función bash similar a la vez

10

Tengo estas funciones en ~/.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 ves estas funciones son muy similares. Quiero definir estas 3 funciones a la vez. ¿Hay alguna manera de hacerlo?

medio ambiente

bash --version
GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)
hierro y
fuente

Respuestas:

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

Las precauciones habituales sobre evalaplicar.

Adrian Frühwirth
fuente
No se come por lo que for loop?quiero decir, las variables declaradas en for loopgeneral generalmente desaparecen; esperaría lo mismo de las funciones por las mismas razones.
mikeserv
¿Qué te hace pensar que? bash -c 'for i in 1; do :; done; echo $i'=> 1. El typemuestra claramente que las funciones existen fuera del alcance del bucle.
Adrian Frühwirth
1
@mikeserv Incluso con bashel alcance dinámico de todo lo que puede obtener es una localvariable local al alcance de una función completa , las variables definitivamente no "desaparecen" después de un ciclo. De hecho, dado que no hay una función involucrada aquí, ni siquiera es posible definir una localvariable en este caso.
Adrian Frühwirth
Derecha, local para el bucle for, tienen alcance local. Desaparecen tan pronto como lo hace su shell de bucle principal. ¿Esto no sucede aquí?
mikeserv
No, como acabo de explicar, no existe un concepto como "local para el bucle for" en las secuencias de comandos de shell y mi publicación y el ejemplo en mi comentario anterior muestran claramente eso.
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."

La voluntad anterior . source /dev/fd/3que se introduce en la _gem_dec()función cada vez que se llama como un here-document. _gem_dec'súnico trabajo previamente evaluado es recibir un parámetro y evaluarlo previamente como el bundle execobjetivo y como el nombre de la función en la que se dirige.

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

Sin embargo, en el caso anterior, no creo que pueda haber ningún riesgo.

Si el bloque de código anterior se copia en un .bashrcarchivo, no solo las funciones de shell _guard(), _rspec()y_rake() se declararán al iniciar sesión, sino que la _gem_dec()función también estará disponible para su ejecución en cualquier momento en el indicador de shell (o de otro modo) y así las nuevas funciones con plantilla pueden ser declarado cuando lo desee con solo:

_gem_dec $new_templated_function_name

Y gracias a @Andrew por mostrarme que estos no serían comidos por un for loop.

¿PERO CÓMO?

Sin embargo, utilizo el 3descriptor de archivo anterior para mantenerme stdin, stdout, and stderr, or <&0 >&1 >&2abierto por hábito, como también es el caso de algunas de las otras precauciones predeterminadas que implemento aquí, porque la función resultante es tan simple que realmente no es necesaria. Sin embargo, es una buena práctica. Llamar shift $#es otra de esas precauciones innecesarias.

Aún así, cuando un archivo se especifica como <inputo>output con [optional num]<fileo [optional num]>fileredirección, el núcleo lo lee en un descriptor de archivo, al que se puede acceder a través de los character devicearchivos especiales en /dev/fd/[0-9]*. Si [optional num]se omite el especificador, 0<filese supone para entrada y 1>filesalida. Considera esto:

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

Y como a here-documentes solo un medio para describir un archivo en línea dentro de un bloque de código, cuando lo hacemos:

<<'HEREDOC'
[$CODE]
HEREDOC

También podríamos hacer:

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

Con una distinción muy importante . Si no "'\quote'"lo <<"'\LIMITER"'hace, here-documententonces el shell lo evaluará para el shell $expansioncomo:

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

Entonces, para _gem_dec(), el 3<<-FUNC here-documentse evalúa como un archivo en la entrada, de la misma manera que lo sería si lo fuera, 3<~/some.file excepto que debido a que dejamos el FUNClimitador sin comillas, primero se evalúa para $expansion.Lo importante sobre esto es que es entrada, lo que significa solo existe para _gem_dec(),pero también se evalúa antes de _gem_dec()que se ejecute la función porque nuestro shell tiene que leer y evaluar su $expansionsantes de entregarla como entrada.

Hagamos guard,por ejemplo:

_gem_dec guard

Primero, el shell tiene que manejar la entrada, lo que significa leer:

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

En el descriptor de archivo 3 y evaluándolo para la expansión del shell. Si en este momento corriste:

cat /dev/fd/3

O:

cat <&3

Como ambos son comandos equivalentes, verías *:

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

... antes de que se ejecute ningún código en la función. Esta es la función <input, después de todo. Para más ejemplos, vea mi respuesta a una pregunta diferente aquí .

(* Técnicamente, esto no es del todo cierto. Debido a que utilizo una guía -dashantes de la here-doc limiter, todo lo anterior estaría justificado a la izquierda. Pero utilicé -dashlo que pude <tab-insert>para facilitar la lectura en primer lugar, así que no voy a quitar el <tab-inserts>antes ofreciéndolo para leer ...)

La mejor parte de esto es la cita: tenga en cuenta que las '"citas permanecen y solo las \citas se eliminaron. Es probablemente por esta razón más que cualquier otra que si tiene que evaluar dos veces un shell $expansion, lo recomendaré here-documentporque las citas son mucho más fáciles que eval.

De todos modos, ahora el código anterior es exactamente como un archivo alimentado como 3<~/heredoc.filesimplemente esperando que la _gem_dec()función se ponga en marcha y acepte su entrada /dev/fd/3.

Entonces, cuando comenzamos, lo _gem_dec()primero que hago es arrojar todos los parámetros posicionales, porque nuestro siguiente paso es una expansión de shell dos veces evaluada y no quiero que ninguno de los contenidos $expansionsse interprete como ninguno de mis $1 $2 $3...parámetros actuales . Asique:

shift $#

shiftdescarta tantos positional parameterscomo especifiques y comienza $1con lo que queda. Entonces, si llamé _gem_dec one two threeen la _gem_dec's $1 $2 $3posición, los parámetros posicionales serían one two threey el recuento posicional actual total, o $#sería 3. Si luego llamé a shift 2,los valores de oneytwo sería shifteditado , el valor de $1cambiaría threey $#se expandiría a 1. Así que shift $#solo los tira a todos a la basura. Hacer esto es estrictamente precautorio y es solo un hábito que he desarrollado después de hacer este tipo de cosas por un tiempo. Aquí está (subshell)un poco extendido para mayor claridad:

( 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 todos modos, el siguiente paso es donde sucede la magia. Si . ~/some.shen el indicador de comandos de la shell, todas las funciones y variables de entorno declaradas en ~/some.shese momento serían invocables en el indicador de shell. Lo mismo es cierto aquí, excepto que tenemos . source el character devicearchivo especial para nuestro descriptor de archivo o . /dev/fd/3, que es donde nuestro here-documentarchivo en línea ha sido modificado, y hemos declarado nuestra función. Y así es como funciona.

_guard

Ahora hace lo _guardque se supone que debe hacer tu función.

Apéndice:

Una excelente manera de decir guardar tus posicionales:

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

EDITAR:

Cuando respondí por primera vez a esta pregunta, me concentré más en el problema de declarar un caparazón function()capaz de declarar otras funciones que persistirían en el $ENVplanchado actual del caparazón que en lo que el autor de la pregunta haría con dichas funciones persistentes. Desde entonces me di cuenta de que mi solución original ofrecida en la que 3<<-FUNCtomó la forma:

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

Probablemente no han funcionado como se esperaba para el autor de la pregunta porque Alteré específicamente el nombre de la función declarativa de $1a _${1}que, de ser llamado así _gem_dec guardpor ejemplo, daría lugar a _gem_decdeclarar una función llamada _guarden lugar de sólo guard.

Nota: Tal comportamiento es una cuestión de hábito para mí: normalmente opero bajo la presunción de que las funciones de shell deberían ocupar solo las suyas_namespacepara evitar su intrusión en elnamespaceshellcommands.

Sin embargo, este no es un hábito universal, como se evidencia en el uso que hace el autor de la commandllamada $1.

Un examen más detallado me lleva a creer lo siguiente:

El autor de la pregunta desea que se nombren las funciones de shell guard, rspec, or rakeque, cuando se llama, compilarán de nuevo una rubyfunción del mismo nombre en ifel que el archivo Gemfileexiste $PATH o if Gemfile no existe, la función de shell debería ejecutar la rubyfunción del mismo nombre.

Esto no habría funcionado anteriormente porque también alteré el $1llamado por commandleer:

command _${1}

Lo que no habría resultado en la ejecución de la rubyfunción que la función de shell compiló como:

bundle exec $1

Espero que puedan ver (como eventualmente lo hice) que parece que el autor de la pregunta solo está usando commanden absoluto para especificar indirectamente namespaceporque commandpreferirá llamar a un archivo ejecutable $PATHsobre una función de shell del mismo nombre.

Si mi análisis es correcto (como espero que lo confirme) , esto:

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

Debería satisfacer mejor esas condiciones con la excepción de que llamar guardal indicador solo intentará ejecutar un archivo ejecutable en $PATHnamed guardmientras que llamar _guardal indicador verificará la Gemfile'sexistencia y compilará en consecuencia o ejecutará el guardejecutable en $PATH. De esta manera namespaceestá protegido y, al menos como lo percibo, la intención del autor de la pregunta aún se cumple.

De hecho, suponiendo que nuestra función de shell _${1}()y el ejecutable ${PATH}/${1}son las dos únicas formas en que nuestro shell podría interpretar una llamada a cualquiera $1o _${1}luego, el uso de commandla función ahora se vuelve completamente redundante. Aún así, he dejado que permanezca, ya que no me gusta cometer el mismo error dos veces ... seguidas de todos modos.

Si esto es inaceptable para el autor de la pregunta y él / ella preferiría eliminarlo por _completo, entonces, en su forma actual, editar la _underscoresalida debería ser todo lo que el solicitante debe hacer para cumplir con sus requisitos tal como los entiendo.

Aparte de ese cambio, también he editado la función para usar &&y / o|| condicionales de cortocircuito de shell en lugar de la if/thensintaxis original . De esta manera el commandcomunicado sólo se evalúa en absoluto si Gemfileno está en $PATH. Sin embargo, esta modificación requiere la adición return $?para garantizar que la bundleinstrucción no se ejecute en el caso de Gemfileque no exista, pero la ruby $1función devuelve algo que no sea 0.

Por último, debo señalar que esta solución implementa solo construcciones de shell portátiles. En otras palabras, esto debería producir resultados idénticos en cualquier shell que reclame compatibilidad POSIX. Si bien, por supuesto, sería una tontería para mí afirmar que todos los sistemas compatibles con POSIX deben manejar la ruby bundledirectiva, al menos los imperativos de shell que lo invocan deben comportarse de la misma manera, independientemente de si el shell de llamada es sho no dash. Además, lo anterior funcionará como se espera (suponiendo al menos medio sano de shoptstodos modos) en ambos bashy zsh.

mikeserv
fuente
Pongo su código ~/.bashrcy llamo . ~/.bashrc, luego se ejecutan estas tres funciones. Tal vez el comportamiento difiere según el entorno, así que agregué mi entorno a la pregunta. Además, no podía entender por qué _guard ; _rspec ; _rakese necesita la última línea . Busqué sobre shiftel descriptor de archivos, parece que estos están más allá de mi comprensión actual.
ironsand
Solo puse eso allí para mostrar que eran invocables. Lo siento, puse un eco ahora. Entonces puedes llamarlos como funciones, como has demostrado.
mikeserv
@Tetsu: ¿tiene más sentido ahora?
mikeserv
Leí tu respuesta 3 veces, pero honestamente dije que necesito más conocimiento para entender la explicación. Aunque te estoy muy agradecido, lo volveré a leer cuando tenga más experiencia.
ironsand
@Tetsu ¿Quizás ahora está más claro ...? Creo que me di cuenta, y ahora he corregido, un error que cometí anteriormente. Por favor, házmelo saber, si quieres.
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
fuente