Patrones de diseño o mejores prácticas para scripts de shell [cerrado]
-
09-06-2019 - |
Pregunta
¿Alguien conoce algún recurso que hable sobre mejores prácticas o patrones de diseño para scripts de shell (sh, bash, etc.)?
Solución
Escribí scripts de shell bastante complejos y mi primera sugerencia es "no hacerlo".La razón es que es bastante fácil cometer un pequeño error que obstaculice su script o incluso lo haga peligroso.
Dicho esto, no tengo otros recursos para pasarte más que mi experiencia personal.Esto es lo que hago normalmente, que es excesivo, pero tiende a ser sólido, aunque muy verboso.
Invocación
haga que su script acepte opciones largas y cortas.Tenga cuidado porque hay dos comandos para analizar opciones, getopt y getopts.Utilice getopt cuando tenga 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 correctamente y un valor distinto de cero si algo salió mal.
Llamadas a funciones
Puedes llamar funciones en bash, solo recuerda 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 utilizar 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 establece dos "variables de excepción" (la mía:EXCEPTION y EXCEPTION_MSG), el primero contiene el tipo de excepción y el segundo un mensaje legible por humanos.
Cuando llamas a una función, los parámetros de la función se asignan a las variables especiales $0, $1, etc.Te sugiero que les pongas 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 configurada se usa como una cadena vacía.Esto es muy peligroso en caso de error tipográfico, ya que la variable mal escrita no se informará y se evaluará como vacía.usar
set -o nounset
para evitar que esto suceda.Sin embargo, tenga cuidado, porque si hace esto, el programa abortará cada vez que evalúe una variable indefinida.Por este motivo, la única forma de comprobar si una variable no está definida es la siguiente:
if test "x${foo:-notset}" == "xnotset"
then
echo "foo not set"
fi
Puede declarar variables como de solo lectura:
readonly readonly_var="foo"
Modularización
Puede lograr una modularización "similar a Python" si utiliza 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 "Módulo/Archivo de módulo"
El cual se buscará en SHELL_LIBRARY_PATH.Como siempre importa en el espacio de nombres global, recuerde anteponer a todas sus funciones y variables un prefijo adecuado; de lo contrario, corre el riesgo de que haya conflictos de nombres.Utilizo doble guión 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 una locura).En la práctica, sin embargo, puedes hacer "programación orientada a Singleton":tienes una instancia de cada objeto, y solo una.
Lo que hago es:Defino un objeto en un módulo (ver la entrada de modularización).Luego defino vars vacías (análogas a las variables miembro), una función init (constructor) y funciones miembro, 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
}
Atrapamiento y manejo de señales.
Esto me resultó útil para detectar 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 algún motivo, intente reordenar el código.El orden es importante y no siempre intuitivo.
Ni siquiera consideres trabajar con tcsh.no admite funciones y es horrible en general.
Espero que te ayude, aunque ten en cuenta.Si tiene que usar el tipo de cosas que escribí aquí, significa que su problema es demasiado complejo para resolverlo con Shell.utilizar otro idioma.Tuve que usarlo debido a factores humanos y legado.
Otros consejos
Échale un vistazo al Guía avanzada de secuencias de comandos Bash para obtener mucha sabiduría sobre scripts de shell, no solo sobre Bash.
No escuches a la gente que te dice que busques otros lenguajes, posiblemente más complejos.Si los scripts de shell satisfacen sus necesidades, utilícelos.Quieres funcionalidad, no elegancia.Los nuevos idiomas proporcionan nuevas habilidades valiosas para su currículum, pero eso no ayuda si tiene trabajo que hacer y ya conoce Shell.
Como se indicó, no existen muchas "mejores prácticas" o "patrones de diseño" para las secuencias de comandos de shell.Los diferentes usos tienen diferentes pautas y prejuicios, como cualquier otro lenguaje de programación.
El script de shell es un lenguaje diseñado para manipular archivos y procesos.Si bien es excelente para eso, no es un lenguaje de propósito general, así que siempre trate de pegar la lógica de las utilidades existentes en lugar de recrear una nueva lógica en el script de shell.
Aparte de ese principio general, he recopilado algunos errores comunes en el script de shell.
Hubo una gran sesión en OSCON este año (2008) sobre este tema: http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf
Fácil:Utilice Python en lugar de scripts de Shell.Obtiene un aumento de casi 100 veces en legibilidad, sin tener que complicar nada que no necesita y preservando la capacidad de evolucionar partes de su script en funciones, objetos, objetos persistentes (zodb), objetos distribuidos (pyro) casi sin ningún código adicional.
use set -e para no seguir adelante después de los errores.Intente hacerlo compatible con sh sin depender de bash si desea que se ejecute en un sistema que no sea Linux.
Sepa cuándo usarlo. Para pegar comandos rápidos y sucios, está bien.Si necesita tomar más que unas cuantas decisiones no triviales, bucles o cualquier cosa, opte por Python, Perl y modularizar.
El mayor problema con Shell es a menudo que el resultado final parece una gran bola de barro, 4000 líneas de bash y creciendo...y no puedes deshacerte de él porque ahora todo tu proyecto depende de ello.Por supuesto, comenzó en 40 líneas de hermosa fiesta.
Para encontrar algunas "mejores prácticas", observe cómo las distribuciones de Linux (p. ej.Debian) escriben sus scripts de inicio (generalmente se encuentran en /etc/init.d)
La mayoría de ellos no tienen "bash-ismos" y tienen una buena separación de ajustes de configuración, archivos de biblioteca y formato fuente.
Mi estilo personal es escribir un master-shellscript que define algunas variables predeterminadas y luego intenta cargar ("obtener") un archivo de configuración que puede contener nuevos valores.
Intento evitar funciones ya que tienden a complicar el script.(Perl fue creado para ese propósito).
Para asegurarse de que el script sea portátil, pruebe no solo con #!/bin/sh, sino también use #!/bin/ash, #!/bin/dash, etc.Pronto descubrirás el código específico de Bash.
O la cita anterior similar a lo que dijo Joao:
"Usa Perl.Querrás conocer bash pero no usarlo".
Lamentablemente olvidé quién dijo eso.
Y sí, hoy en día recomendaría Python en lugar de Perl.