2008-09-17 10 views

Respuesta

190

Escribí guiones 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 pasarlo, pero mi experiencia personal. Esto es lo que normalmente hago, que es exagerado, pero tiende a ser sólido, aunque muy detallado.

Invocación

hacen su script acepta 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:: -- "[email protected]"` 

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 realiza correctamente, no nulo si algo salió mal.

Las llamadas a funciones

Puede llamar a funciones en bash, sólo recuerda a definirlos 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 llama a una función, los parámetros de la función se asignan a los valores especiales $ 0, $ 1, etc. Le sugiero que los ponga en nombres más significativos. declarar las variables dentro de la función como locales: situaciones propensas

function foo { 
    local bar="$0" 
} 

error

En bash, a menos que se declare lo contrario, una variable unset 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. use

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 manera de comprobar si una variable no está definida es el siguiente:

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

Se puede declarar como variables de sólo lectura:

readonly readonly_var="foo" 

modularización

Usted puede lograr modularización tipo "pitón" 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 
} 

A continuación, puede importar archivos con la extensión .shinc con la siguiente sintaxis

importación "AModule/modulefile"

Cuál será buscado 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.

También, ponga esto como primera cosa en su 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 se puede hacer la programación orientada a objetos, a menos que tenga un sistema bastante complejo de asignación de objetos (pensé en eso, es factible, pero loco). 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 vars vacíos (análogos a las variables miembro) una función init (constructor) y funciones miembro, como en este ejemplo de código

# 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 
} 

señales de manipulación

Trapping y me encontré con 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 "[email protected]" 

Consejos y sugerencias

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

ni siquiera consideran 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.

+6

Guau, y pensé que iba a ser excesivo en bash ... Tiendo a usar funciones aisladas y subcapas de abuso (por lo tanto, sufro cuando la velocidad es de alguna manera relevante). No hay variables globales alguna vez, ni dentro ni fuera (para preservar los restos de cordura). Todas las devoluciones a través de stdout o salida de archivo. set -u/set -e (conjunto muy malo -e se vuelve inútil tan pronto como primero si, y la mayoría de mi código a menudo está allí). Argumentos de función tomados con [local something = "$ 1"; shift] (permite un fácil reordenamiento al refactorizar). Después de un script de 3000 líneas bash, tiendo a escribir incluso los más pequeños de esta manera ... – Eugene

+5

un científico loco – Prospero

+0

pequeñas correcciones para la modularización: 1 necesita una devolución después de . "$ script_absolute_dir/$ module.shinc" para evitar advertencias faltantes. 2 debe establecer IFS = "$ saved_IFS" antes de su regreso al encontrar el módulo en $ SHELL_LIBRARY_PATH – Duff

8

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 necesita, y conservando la capacidad de convertir partes de su script en funciones, objetos, objetos persistentes (zodb), objetos distribuidos (pyro) casi sin cualquier código adicional.

+7

se contradice diciendo "sin tener que complicarse" y luego enumera las diversas complejidades que cree que agregan valor, mientras que en la mayoría de los casos se utilizan como monstruos feos en lugar de utilizarse para simplificar los problemas y la implementación. – Evgeny

+3

esto implica un gran inconveniente, sus scripts no serán portátiles en sistemas donde python no está presente – astropanic

+1

Me doy cuenta de que esto fue respondido en '08 (ahora son dos días antes de '12); sin embargo, para quienes analicen estos años más tarde, advertiría a cualquiera que no le dé la espalda a idiomas como Python o Ruby, ya que es más probable que esté disponible y, si no, es un comando (o un par de clics) lo que se instalará. . Si necesita mayor portabilidad, piense en escribir su programa en Java, ya que tendrá dificultades para encontrar una máquina que no tenga una JVM disponible. –

-1

O la cita más antigua similar a lo que dijo Joao:

"Utilice el Perl Usted querrá saber golpe, pero no lo use."

Tristemente olvidé quién dijo eso.

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

8

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

20

Eche un vistazo a Advanced Bash-Scripting Guide para obtener mucha sabiduría sobre las secuencias de comandos shell, no solo Bash, tampoco.

No escuche a las personas que le dicen que mire otros idiomas, 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 indicó, 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.

+7

Tenga en cuenta que para secuencias de comandos de complejidad incluso leve, NO es una mejor práctica. La codificación no se trata solo de hacer que algo funcione. Se trata de construirlo de manera rápida, fácil y confiable, reutilizable y fácil de leer y mantener (especialmente para otros). Los scripts de Shell no se adaptan bien a ningún nivel. Los lenguajes más robustos son mucho más simples para proyectos con cualquier lógica. – drifter

8

saber cuándo usarlo. Para comandos de pegado rápidos y sucios juntos, está bien. Si necesita tomar más de algunas decisiones no triviales, bucles, cualquier cosa, vaya a Python, Perl y modularize.

El problema más grande con la concha 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 beautiful bash.

6

para encontrar algunos "buenas prácticas", mira qué distro de Linux (por ejemplo Debian) escribir sus guiones init-(que normalmente se encuentran en /etc/init.d)

La mayoría de ellos tienen "Bash-ismos" y tiene una buena separación de configuración, archivos de biblioteca y formato de fuente.

Mi estilo personal es escribir un master-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 se creó para ese fin.)

Para asegurarse de que la secuencia de comandos es portátil, pruebe no solo con #!/Bin/sh, sino también use #!/Bin/ash, #!/Bin/dash, etc. .Verás el código específico de Bash lo suficientemente pronto.

17

shell script es un lenguaje diseñado para manipular archivos y procesos. Si bien es genial para eso, no es un lenguaje de uso general, así que siempre intente pegar la lógica de las utilidades existentes en lugar de recreando una nueva lógica en el script de shell.

Aparte de ese principio general, he recogido algunos common shell script mistakes.

Cuestiones relacionadas