design-patterns bash shell

design patterns - Patrones de diseño o mejores prácticas para scripts de shell



design-patterns bash (9)

¿Alguien sabe de algún recurso que hable sobre mejores prácticas o patrones de diseño para scripts de shell (sh, bash, etc.)?


Eche un vistazo a la guía avanzada Bash-Scripting Guide para obtener mucha sabiduría sobre scripting de shell, no solo Bash, tampoco.

No escuche a las personas que le dicen que mire otros lenguajes posiblemente más complejos. Si el scripting de shell cumple con sus necesidades, úselo. Quieres funcionalidad, no fantasía. Los nuevos idiomas proporcionan nuevas habilidades valiosas para su currículum, pero eso no ayuda si tiene trabajo que debe hacerse y usted ya conoce el shell.

Como se dijo, no hay muchas "mejores prácticas" o "patrones de diseño" para las secuencias de comandos shell. Los diferentes usos tienen diferentes pautas y sesgos, como cualquier otro lenguaje de programación.


Escribí scripts de shell bastante complejos y mi primera sugerencia es "do not". La razón es que es bastante fácil cometer un pequeño error que obstaculiza su secuencia de comandos, o incluso lo hace peligroso.

Dicho esto, no tengo otros recursos para pasar, sino mi experiencia personal. Esto es lo que normalmente hago, que es excesivo, pero tiende a ser sólido, aunque muy detallado.

Invocación

haga que su script acepte opciones largas y cortas. tenga cuidado porque hay dos comandos para analizar las opciones, getopt y getopts. Usa getopt ya que te enfrentas a menos problemas.

CommandLineOptions__config_file="" CommandLineOptions__debug_level="" getopt_results=`getopt -s bash -o c:d:: --long config_file:,debug_level:: -- "$@"` if test $? != 0 then echo "unrecognized option" exit 1 fi eval set -- "$getopt_results" while true do case "$1" in --config_file) CommandLineOptions__config_file="$2"; shift 2; ;; --debug_level) CommandLineOptions__debug_level="$2"; shift 2; ;; --) shift break ;; *) echo "$0: unparseable option $1" EXCEPTION=$Main__ParameterException EXCEPTION_MSG="unparseable option $1" exit 1 ;; esac done if test "x$CommandLineOptions__config_file" == "x" then echo "$0: missing config_file parameter" EXCEPTION=$Main__ParameterException EXCEPTION_MSG="missing config_file parameter" exit 1 fi

Otro punto importante es que un programa siempre debe devolver cero si se completa con éxito, distinto de cero si algo salió mal.

Llamadas de función

Puede invocar funciones en bash, solo recuerde definirlas antes de la llamada. Las funciones son como scripts, solo pueden devolver valores numéricos. Esto significa que debe inventar una estrategia diferente para devolver valores de cadena. Mi estrategia es usar una variable llamada RESULTADO para almacenar el resultado, y devolver 0 si la función se completó limpiamente. Además, puede generar excepciones si devuelve un valor diferente de cero, y luego establecer dos "variables de excepción" (mía: EXCEPCIÓN y EXCEPCIÓN_MSG), la primera contiene el tipo de excepción y la segunda un mensaje legible para humanos.

Cuando llamas a una función, los parámetros de la función se asignan a los vars especiales $ 0, $ 1, etc. Te sugiero que los pongas en nombres más significativos. declarar las variables dentro de la función como locales:

function foo { local bar="$0" }

Situaciones propensas a errores

En bash, a menos que declare lo contrario, una variable no establecida se utiliza como una cadena vacía. Esto es muy peligroso en caso de error tipográfico, ya que la variable mal tipada no se informará, y se evaluará como vacía. utilizar

set -o nounset

para evitar que esto suceda Tenga cuidado, porque si lo hace, el programa abortará cada vez que evalúe una variable indefinida. Por esta razón, la única forma de verificar si una variable no está definida es la siguiente:

if test "x${foo:-notset}" == "xnotset" then echo "foo not set" fi

Puede declarar las variables como de solo lectura:

readonly readonly_var="foo"

Modularización

Puede lograr la modularización "tipo pitón" si usa el siguiente código:

set -o nounset function getScriptAbsoluteDir { # @description used to get the script path # @param $1 the script $0 parameter local script_invoke_path="$1" local cwd=`pwd` # absolute path ? if so, the first character is a / if test "x${script_invoke_path:0:1}" = ''x/'' then RESULT=`dirname "$script_invoke_path"` else RESULT=`dirname "$cwd/$script_invoke_path"` fi } script_invoke_path="$0" script_name=`basename "$0"` getScriptAbsoluteDir "$script_invoke_path" script_absolute_dir=$RESULT function import() { # @description importer routine to get external functionality. # @description the first location searched is the script directory. # @description if not found, search the module in the paths contained in $SHELL_LIBRARY_PATH environment variable # @param $1 the .shinc file to import, without .shinc extension module=$1 if test "x$module" == "x" then echo "$script_name : Unable to import unspecified module. Dying." exit 1 fi if test "x${script_absolute_dir:-notset}" == "xnotset" then echo "$script_name : Undefined script absolute dir. Did you remove getScriptAbsoluteDir? Dying." exit 1 fi if test "x$script_absolute_dir" == "x" then echo "$script_name : empty script path. Dying." exit 1 fi if test -e "$script_absolute_dir/$module.shinc" then # import from script directory . "$script_absolute_dir/$module.shinc" elif test "x${SHELL_LIBRARY_PATH:-notset}" != "xnotset" then # import from the shell script library path # save the separator and use the '':'' instead local saved_IFS="$IFS" IFS='':'' for path in $SHELL_LIBRARY_PATH do if test -e "$path/$module.shinc" then . "$path/$module.shinc" return fi done # restore the standard separator IFS="$saved_IFS" fi echo "$script_name : Unable to find module $module." exit 1 }

luego puede importar archivos con la extensión .shinc con la siguiente sintaxis

importar "AModule / ModuleFile"

Que se buscará en SHELL_LIBRARY_PATH. Como siempre importa en el espacio de nombres global, recuerde incluir todas las funciones y variables con un prefijo adecuado, de lo contrario corre el riesgo de conflictos de nombres. Uso doble guion bajo como punto de python.

Además, pon esto como lo primero en tu módulo

# avoid double inclusion if test "${BashInclude__imported+defined}" == "defined" then return 0 fi BashInclude__imported=1

Programación orientada a objetos

En bash, no puedes hacer programación orientada a objetos, a menos que construyas un sistema bastante complejo de asignación de objetos (pensé en eso, es factible, pero insano). En la práctica, puede hacer "programación orientada a Singleton": tiene una instancia de cada objeto y solo uno.

Lo que hago es: definir un objeto en un módulo (ver la entrada de modularización). Luego defino variables vars (análogas a las variables miembro) una función init (constructor) y funciones de miembros, como en este código de ejemplo

# avoid double inclusion if test "${Table__imported+defined}" == "defined" then return 0 fi Table__imported=1 readonly Table__NoException="" readonly Table__ParameterException="Table__ParameterException" readonly Table__MySqlException="Table__MySqlException" readonly Table__NotInitializedException="Table__NotInitializedException" readonly Table__AlreadyInitializedException="Table__AlreadyInitializedException" # an example for module enum constants, used in the mysql table, in this case readonly Table__GENDER_MALE="GENDER_MALE" readonly Table__GENDER_FEMALE="GENDER_FEMALE" # private: prefixed with p_ (a bash variable cannot start with _) p_Table__mysql_exec="" # will contain the executed mysql command p_Table__initialized=0 function Table__init { # @description init the module with the database parameters # @param $1 the mysql config file # @exception Table__NoException, Table__ParameterException EXCEPTION="" EXCEPTION_MSG="" EXCEPTION_FUNC="" RESULT="" if test $p_Table__initialized -ne 0 then EXCEPTION=$Table__AlreadyInitializedException EXCEPTION_MSG="module already initialized" EXCEPTION_FUNC="$FUNCNAME" return 1 fi local config_file="$1" # yes, I am aware that I could put default parameters and other niceties, but I am lazy today if test "x$config_file" = "x"; then EXCEPTION=$Table__ParameterException EXCEPTION_MSG="missing parameter config file" EXCEPTION_FUNC="$FUNCNAME" return 1 fi p_Table__mysql_exec="mysql --defaults-file=$config_file --silent --skip-column-names -e " # mark the module as initialized p_Table__initialized=1 EXCEPTION=$Table__NoException EXCEPTION_MSG="" EXCEPTION_FUNC="" return 0 } function Table__getName() { # @description gets the name of the person # @param $1 the row identifier # @result the name EXCEPTION="" EXCEPTION_MSG="" EXCEPTION_FUNC="" RESULT="" if test $p_Table__initialized -eq 0 then EXCEPTION=$Table__NotInitializedException EXCEPTION_MSG="module not initialized" EXCEPTION_FUNC="$FUNCNAME" return 1 fi id=$1 if test "x$id" = "x"; then EXCEPTION=$Table__ParameterException EXCEPTION_MSG="missing parameter identifier" EXCEPTION_FUNC="$FUNCNAME" return 1 fi local name=`$p_Table__mysql_exec "SELECT name FROM table WHERE id = ''$id''"` if test $? != 0 ; then EXCEPTION=$Table__MySqlException EXCEPTION_MSG="unable to perform select" EXCEPTION_FUNC="$FUNCNAME" return 1 fi RESULT=$name EXCEPTION=$Table__NoException EXCEPTION_MSG="" EXCEPTION_FUNC="" return 0 }

Atrapando y manejando señales

Encontré esto útil para atrapar y manejar excepciones.

function Main__interruptHandler() { # @description signal handler for SIGINT echo "SIGINT caught" exit } function Main__terminationHandler() { # @description signal handler for SIGTERM echo "SIGTERM caught" exit } function Main__exitHandler() { # @description signal handler for end of the program (clean or unclean). # probably redundant call, we already call the cleanup in main. exit } trap Main__interruptHandler INT trap Main__terminationHandler TERM trap Main__exitHandler EXIT function Main__main() { # body } # catch signals and exit trap exit INT TERM EXIT Main__main "$@"

Consejos

Si algo no funciona por alguna razón, intente reordenar el código. El orden es importante y no siempre es intuitivo.

ni siquiera consideres trabajar con tcsh. no admite funciones, y es horrible en general.

Espero que ayude, aunque tenga en cuenta. Si tiene que usar el tipo de cosas que escribí aquí, significa que su problema es demasiado complejo para ser resuelto con shell. usa otro idioma Tuve que usarlo debido a factores humanos y legado.


Fácil: use python en lugar de scripts de shell. Obtiene un aumento de casi 100 veces en la legibilidad, sin tener que complicar nada que no necesite, y preservando la capacidad de convertir partes de su script en funciones, objetos, objetos persistentes (zodb), objetos distribuidos (pyro) casi sin ningún código extra



O la cita anterior similar a lo que dijo Joao:

"Usa Perl. Querrás saber bash pero no usarlo".

Lamentablemente, olvidé quién dijo eso.

Y sí, estos días recomendaría Python sobre Perl.


Para encontrar algunas "mejores prácticas", observe cómo las distribuciones de Linux (por ejemplo, Debian) escriben sus guiones de inicio (generalmente se encuentran en /etc/init.d)

La mayoría de ellos carecen de "bash-ismos" y tienen una buena separación de configuración, archivos de biblioteca y formato de origen.

Mi estilo personal es escribir un shell-shellscript que define algunas variables predeterminadas, y luego intenta cargar ("fuente") un archivo de configuración que puede contener nuevos valores.

Intento evitar las funciones, ya que tienden a complicar el guión. (Perl fue creado para ese propósito)

Para asegurarse de que el script sea portable, pruebe no solo con #! / Bin / sh, sino también use #! / Bin / ash, #! / Bin / dash, etc. Pronto detectará el código específico de Bash.


shell script es un lenguaje diseñado para manipular archivos y procesos. Si bien es genial para eso, no es un lenguaje de propósito general, por lo tanto, siempre intente pegar la lógica de las utilidades existentes en lugar de volver a crear una nueva lógica en el script de shell.

Aparte de ese principio general, he recopilado algunos errores comunes del script de shell .


use set -e para no arar después de los errores. Intenta hacer que sea compatible sh sin depender de bash si quieres que se ejecute en no-linux.


Sepa cuándo usarlo. Para comandos de pegado rápidos y sucios, está bien. Si necesita tomar más de unas pocas decisiones no triviales, bucles, cualquier cosa, opte por Python, Perl y modularice .

El mayor problema con el caparazón suele ser que el resultado final parece una gran bola de barro, 4000 líneas de bash y crece ... y no puedes deshacerte de él porque ahora todo tu proyecto depende de ello. Por supuesto, comenzó en 40 líneas de bello bash.