En el trabajo, escribo scripts de bash con frecuencia. Mi supervisor ha sugerido que todo el script se divida en funciones, similar al siguiente ejemplo:
#!/bin/bash
# Configure variables
declare_variables() {
noun=geese
count=three
}
# Announce something
i_am_foo() {
echo "I am foo"
sleep 0.5
echo "hear me roar!"
}
# Tell a joke
walk_into_bar() {
echo "So these ${count} ${noun} walk into a bar..."
}
# Emulate a pendulum clock for a bit
do_baz() {
for i in {1..6}; do
expr $i % 2 >/dev/null && echo "tick" || echo "tock"
sleep 1
done
}
# Establish run order
main() {
declare_variables
i_am_foo
walk_into_bar
do_baz
}
main
¿Hay alguna razón para hacer esto además de la "legibilidad", que creo que podría estar igualmente bien establecida con algunos comentarios más y un poco de espacio entre líneas?
¿Hace que el script se ejecute de manera más eficiente (en realidad esperaría lo contrario, si es que lo hace), o hace que sea más fácil modificar el código más allá del potencial de legibilidad antes mencionado? ¿O es realmente solo una preferencia estilística?
Tenga en cuenta que, aunque el guión no lo demuestra así, el "orden de ejecución" de las funciones en nuestros scripts actuales tiende a ser muy lineal - walk_into_bar
depende de las cosas que i_am_foo
ha hecho, y do_baz
actúa sobre la materia creada por walk_into_bar
- así que estar poder intercambiar arbitrariamente el orden de ejecución no es algo que generalmente estaríamos haciendo. Por ejemplo, de repente no querrás poner declare_variables
después walk_into_bar
, eso rompería las cosas.
Un ejemplo de cómo escribiría el script anterior sería:
#!/bin/bash
# Configure variables
noun=geese
count=three
# Announce something
echo "I am foo"
sleep 0.5
echo "hear me roar!"
# Tell a joke
echo "So these ${count} ${noun} walk into a bar..."
# Emulate a pendulum clock for a bit
for i in {1..6}; do
expr $i % 2 >/dev/null && echo "tick" || echo "tock"
sleep 1
done
fuente
main()
en la parte superior y agregomain "$@"
en la parte inferior para llamarlo. Eso le permite ver la lógica del script de alto nivel primero cuando la abre.local
: esto proporciona un alcance variable que es increíblemente importante en cualquier script no trivial.Respuestas:
Comencé a usar este mismo estilo de programación bash después de leer la publicación del blog de Kfir Lavi "Defensive Bash Programming" . Da bastantes buenas razones, pero personalmente considero que estas son las más importantes:
los procedimientos se vuelven descriptivos: es mucho más fácil descubrir qué debe hacer una parte particular del código. En lugar de un muro de código, verá "Oh, la
find_log_errors
función lee ese archivo de registro en busca de errores". Compárelo con la búsqueda de un montón de líneas awk / grep / sed que usan dios sabe qué tipo de expresiones regulares en medio de un guión largo: no tiene idea de lo que está haciendo allí a menos que haya comentarios.puede depurar funciones encerrándolas en
set -x
yset +x
. Una vez que sepa que el resto del código funciona bien, puede usar este truco para centrarse en depurar solo esa función específica. Claro, puede encerrar partes del guión, pero ¿qué pasa si es una porción larga? Es más fácil hacer algo como esto:uso de impresión con
cat <<- EOF . . . EOF
. Lo he usado varias veces para que mi código sea mucho más profesional. Además,parse_args()
con lagetopts
función es bastante conveniente. Nuevamente, esto ayuda con la legibilidad, en lugar de incluir todo en el script como un muro de texto gigante. También es conveniente reutilizarlos.Y obviamente, esto es mucho más legible para alguien que conoce C o Java, o Vala, pero tiene una experiencia bash limitada. En cuanto a la eficiencia, no hay mucho de lo que puede hacer: bash en sí no es el lenguaje más eficiente y la gente prefiere perl y python cuando se trata de velocidad y eficiencia. Sin embargo, puede
nice
una función:En comparación con las llamadas agradables en todas y cada una de las líneas de código, esto disminuye la gran cantidad de tipeo Y puede usarse convenientemente cuando desea que solo una parte de su script se ejecute con menor prioridad.
Ejecutar funciones en segundo plano, en mi opinión, también ayuda cuando quieres tener un montón de declaraciones para ejecutar en segundo plano.
Algunos de los ejemplos en los que he usado este estilo:
fuente
local
y llamando todo a través de lamain()
función. Esto hace que las cosas sean mucho más manejables y puede evitar una situación potencialmente desordenada.La legibilidad es una cosa. Pero hay más en la modularización que solo esto. (La semi-modularización es quizás más correcta para las funciones).
En las funciones, puede mantener algunas variables locales, lo que aumenta la confiabilidad y disminuye la posibilidad de que las cosas se estropeen.
Otro profesional de las funciones es la reutilización . Una vez que se codifica una función, se puede aplicar varias veces en el script. También puede portarlo a otro script.
Su código ahora puede ser lineal, pero en el futuro puede ingresar en el ámbito de subprocesamiento múltiple o procesamiento múltiple en el mundo Bash. Una vez que aprenda a hacer cosas en funciones, estará bien equipado para el paso hacia el paralelo.
Un punto más para agregar. Como Etsitpab Nioliv nota en el comentario a continuación, es fácil redirigir desde las funciones como una entidad coherente. Pero hay un aspecto más de las redirecciones con funciones. A saber, las redirecciones se pueden establecer a lo largo de la definición de la función. P.ej.:
Ahora las llamadas a funciones no necesitan redireccionamientos explícitos.
Esto puede evitar muchas repeticiones, lo que nuevamente aumenta la confiabilidad y ayuda a mantener las cosas en orden.
Ver también
fuente
source
o. scriptname.sh
, y use esas funciones como si estuvieran en su nuevo script.En mi comentario, mencioné tres ventajas de las funciones:
Son más fáciles de probar y verificar la corrección.
Las funciones se pueden reutilizar (obtener) fácilmente en futuros scripts
A tu jefe le gustan.
Y, nunca subestimes la importancia del número 3.
Me gustaría abordar un problema más:
Para obtener el beneficio de dividir el código en funciones, uno debe tratar de hacer que las funciones sean lo más independientes posible. Si
walk_into_bar
requiere una variable que no se usa en otro lugar, entonces esa variable debe definirse y hacerse local parawalk_into_bar
. El proceso de separar el código en funciones y minimizar sus interdependencias debería hacer que el código sea más claro y simple.Idealmente, las funciones deberían ser fáciles de probar individualmente. Si, debido a las interacciones, no son fáciles de probar, entonces esa es una señal de que podrían beneficiarse de la refactorización.
fuente
;-)
Divide el código en funciones por la misma razón que lo haría para C / C ++, python, perl, ruby o cualquier código de lenguaje de programación. La razón más profunda es la abstracción: encapsula las tareas de nivel inferior en primitivas (funciones) de nivel superior para que no tenga que preocuparse por cómo se hacen las cosas. Al mismo tiempo, el código se vuelve más legible (y mantenible), y la lógica del programa se vuelve más clara.
Sin embargo, mirando su código, me resulta bastante extraño tener una función para declarar variables; Esto realmente me hace levantar una ceja.
fuente
main
función / método, entonces?Si bien estoy totalmente de acuerdo con la reutilización , legibilidad y besar delicadamente a los jefes, pero hay otra ventaja de las funciones en bash : alcance variable . Como muestra LDP :
No veo esto muy a menudo en los scripts de shell del mundo real, pero parece una buena idea para scripts más complejos. La reducción de la cohesión ayuda a evitar errores en los que está tropezando con una variable esperada en otra parte del código.
La reutilización a menudo significa crear una biblioteca común de funciones e incorporar
source
esa biblioteca a todos sus scripts. Esto no los ayudará a correr más rápido, pero te ayudará a escribirlos más rápido.fuente
local
, pero creo que la mayoría de las personas que escriben scripts divididos en funciones siguen el principio de diseño. Usignlocal
solo hace que sea más difícil introducir errores.local
hace que las variables estén disponibles para funcionar y sus hijos, por lo que es realmente bueno tener una variable que se pueda transmitir desde la función A, pero que no esté disponible para la función B, que puede querer tener una variable con el mismo nombre pero con un propósito diferente. Así que eso es bueno para definir el alcance, y como dijo Voo - menos erroresUna razón completamente diferente de las que ya se dan en otras respuestas: una razón por la que a veces se utiliza esta técnica, donde la única declaración de definición de no función en el nivel superior es una llamada
main
, es asegurarse de que el script no haga nada desagradable accidentalmente si el script está truncado El script puede truncarse si se canaliza desde el proceso A al proceso B (el shell), y el proceso A finaliza por cualquier motivo antes de que haya terminado de escribir el script completo. Esto es especialmente probable que suceda si el proceso A recupera el script desde un recurso remoto. Si bien por razones de seguridad no es una buena idea, es algo que se hace y algunos scripts se han modificado para anticipar el problema.fuente
main()
patrón es habitual en Python, donde se usaif __name__ == '__main__': main()
al final del archivo.import
el script actual sin ejecutarsemain
. Supongo que se podría poner un guardia similar en un script bash.Un proceso requiere una secuencia. La mayoría de las tareas son secuenciales. No tiene sentido meterse con el pedido.
Pero lo más importante de la programación, que incluye secuencias de comandos, es la prueba. Pruebas, pruebas, pruebas. ¿Qué scripts de prueba tiene actualmente para validar la exactitud de sus scripts?
Tu jefe está tratando de guiarte de ser un guionista a ser un programador. Esta es una buena dirección para entrar. A las personas que vienen después de ti les gustará.
PERO. Siempre recuerda tus raíces orientadas al proceso. Si tiene sentido tener las funciones ordenadas en la secuencia en la que normalmente se ejecutan, haga eso, al menos como primer paso.
Más adelante, verá que algunas de sus funciones están manejando entradas, otras salidas, otras procesando, otras modelando datos y otras manipulando datos, por lo que puede ser inteligente agrupar métodos similares, tal vez incluso moverlos a archivos separados .
Más tarde aún, puede darse cuenta de que ahora ha escrito bibliotecas de pequeñas funciones auxiliares que utiliza en muchos de sus scripts.
fuente
Los comentarios y el espacio no pueden acercarse a la legibilidad de las funciones, como demostraré. Sin funciones, no se puede ver el bosque por los árboles: grandes problemas se esconden entre muchas líneas de detalle. En otras palabras, las personas no pueden enfocarse simultáneamente en los detalles finos y en el panorama general. Eso podría no ser obvio en un guión corto; siempre que sea breve, puede ser lo suficientemente legible. Sin embargo, el software se hace más grande, no más pequeño, y ciertamente es parte de todo el sistema de software de su empresa, que seguramente es mucho más grande, probablemente millones de líneas.
Considera si te di instrucciones como esta:
Cuando llegaste a la mitad, o incluso al 5%, habrías olvidado cuáles fueron los primeros pasos. No podrías detectar la mayoría de los problemas, porque no podías ver el bosque por los árboles. Comparar con funciones:
Eso es ciertamente mucho más comprensible, sin importar cuántos comentarios pueda poner en la versión secuencial línea por línea. También hace que sea mucho más probable que notes que olvidaste hacer el café, y probablemente olvidaste sit_down () al final. Cuando su mente está pensando en los detalles de las expresiones regulares grep y awk, no puede estar pensando en el panorama general: "¿y si no hay café preparado"?
Las funciones principalmente le permiten ver el panorama general y notar que olvidó preparar el café (o que alguien podría preferir el té). En otro momento, en un estado mental diferente, le preocupa la implementación detallada.
También hay otros beneficios discutidos en otras respuestas, por supuesto. Otro beneficio que no se indica claramente en las otras respuestas es que las funciones proporcionan una garantía que es importante para prevenir y corregir errores. Si descubre que alguna variable $ foo en la función adecuada walk_to () estaba mal, sabrá que solo tiene que mirar las otras 6 líneas de esa función para encontrar todo lo que podría haber sido afectado por ese problema, y todo lo que podría han hecho que esté mal. Sin funciones (apropiadas), cualquier cosa y todo en todo el sistema podría ser una causa de que $ foo sea incorrecto, y cualquier cosa y todo podría verse afectado por $ foo. Por lo tanto, no puede arreglar $ foo de manera segura sin volver a examinar cada línea del programa. Si $ foo es local para una función,
fuente
bash
sintaxis. Sin embargo, es una pena; No creo que haya una manera de pasar información a funciones como esa. (es decir,pour();
<coffee
). Se parece más ac++
ophp
(creo).Algunos truismos relevantes sobre la programación:
Los comentarios comienzan como una brecha para no poder expresar sus ideas claramente en el código *, y empeoran (o simplemente se equivocan) con el cambio. Por lo tanto, si es posible, exprese conceptos, estructuras, razonamiento, semántica, flujo, manejo de errores y cualquier otra cosa pertinente para la comprensión del código como código.
Dicho esto, las funciones de Bash tienen algunos problemas que no se encuentran en la mayoría de los idiomas:
local
palabra clave da como resultado la contaminación del espacio de nombres global.local foo="$(bar)"
resultados en la pérdida del código de salida debar
."$@"
significa en diferentes contextos.* Lo siento si esto ofende, pero después de usar comentarios durante algunos años y desarrollarlos sin ellos ** durante más años, está bastante claro cuál es superior.
** Todavía es necesario usar comentarios para la licencia, documentación de API y similares.
fuente
local foo=""
Entonces el establecimiento de ellos utilizando la ejecución de comandos para actuar en el resultado ...foo="$(bar)" || { echo "bar() failed"; return 1; }
. Esto nos saca de la función rápidamente cuando no se puede establecer un valor requerido. Las llaves son necesarias para asegurar quereturn 1
solo se ejecute en caso de falla.El tiempo es dinero
Hay otras buenas respuestas que arrojan luz sobre las razones técnicas para escribir de forma modular un guión, potencialmente largo, desarrollado en un entorno de trabajo, desarrollado para ser utilizado por un grupo de personas y no solo para su propio uso.
Quiero centrarme en una expectativa: en un entorno laboral "el tiempo es dinero" . Por lo tanto, la ausencia de errores y el rendimiento de su código se evalúan junto con la capacidad de lectura , la capacidad de prueba , el mantenimiento, la refactorización, la reutilización ...
Escribir en "módulos" un código disminuirá el tiempo de lectura necesario no solo para el codificador en sí, sino incluso el tiempo utilizado por los evaluadores o por el jefe. Además, tenga en cuenta que el tiempo de un jefe generalmente se paga más que el tiempo de un codificador y que su jefe evaluará la calidad de su trabajo.
Además, escribir en "módulos" independientes un código (incluso un script bash) le permitirá trabajar en "paralelo" con otro componente de su equipo, acortando el tiempo de producción general y utilizando, en el mejor de los casos, la experiencia del single, para revisar o reescribir una parte con sin efectos secundarios en los demás, para reciclar el código que acaba de escribir "tal cual"para otro programa / script, para crear bibliotecas (o bibliotecas de fragmentos), para reducir el tamaño general y la probabilidad relacionada de errores, para depurar y probar minuciosamente cada parte ... y, por supuesto, se organizará en la sección lógica de su programa / script y mejorar su legibilidad. Todo lo que ahorrará tiempo y dinero. El inconveniente es que debe atenerse a los estándares y comentar sus funciones (que, sin embargo, debe hacer en un entorno de trabajo).
Cumplir con un estándar ralentizará su trabajo al principio, pero acelerará el trabajo de todos los demás (y también el suyo) después. De hecho, cuando la colaboración crece en número de personas involucradas, esto se convierte en una necesidad inevitable. Entonces, por ejemplo, incluso si creo que las variables globales tienen que definirse globalmente y no en una función, puedo entender un estándar que las inicializa en una función llamada
declare_variables()
llamada siempre en la primera línea de lamain()
...Por último, pero no menos importante, no subestimes la posibilidad en los editores de código fuente modernos de mostrar u ocultar rutinas selectivamente separadas ( plegado de código ). Esto mantendrá el código compacto y enfocará al usuario ahorrando nuevamente tiempo.
Aquí arriba puedes ver cómo se despliega solo la
walk_into_bar()
función. Incluso de los otros tenían 1000 líneas de largo cada uno, aún podía mantener bajo control todo el código en una sola página. Tenga en cuenta que se pliega incluso la sección donde va a declarar / inicializar las variables.fuente
Aparte de las razones dadas en otras respuestas:
fuente
Otra razón que a menudo se pasa por alto es el análisis sintáctico de bash:
Este script obviamente contiene un error de sintaxis y bash no debería ejecutarlo, ¿verdad? Incorrecto.
Si envolviéramos el código en una función, esto no sucedería:
fuente