Patrones de diseño o mejores prácticas para scripts de shell [cerrado]


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

Author: nbro, 2008-09-17

9 answers

Escribí scripts de shell bastante complejos y mi primera sugerencia es "no". La razón es que es bastante fácil cometer un pequeño error que dificulta su script, o incluso hacerlo peligroso.

Dicho esto, no tengo otros recursos para pasarte 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. ten cuidado porque hay dos comandos para analizar opciones, getopt y getopts. Use getopt para enfrentar 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 a funciones

Puede llamar a funciones en bash, solo recuerde definirlas antes de la llamada. Las funciones son como scripts, solo pueden devolver valores numéricos. Esto significa que usted tiene que inventar un diferente estrategia para devolver valores de cadena. Mi estrategia es usar una variable llamada RESULT 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" (mine: EXCEPTION y EXCEPTION_MSG), la primera contiene el tipo de excepción y la segunda un mensaje legible por humanos.

Cuando se llama a una función, los parámetros de la función se asignan a los vars especiales 0 0, $1 etc. Le sugiero que los ponga en nombres más significativos. declare 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 sin establecer se usa como una cadena vacía. Esto es muy peligroso en caso de error tipográfico, ya que la variable mal escrita no será reportada, y será evaluada como vacía. use

set -o nounset

Para evitar que esto suceda. Sin embargo, 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 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 readonly:

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 "AModule / ModuleFile"

Que será se buscó en SHELL_LIBRARY_PATH. Como siempre importa en el espacio de nombres global, recuerde poner un prefijo a todas sus funciones y variables, de lo contrario corre el riesgo de conflictos de nombres. Utilizo doble guión bajo como punto de Python.

También, ponga esto como lo primero 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 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, puede hacer "Programación orientada a Singleton": tiene 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íos (análogos a las variables miembro) una función init (constructor) y funciones miembro, como en este ejemplo code

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

Captura y manipulación de señales

Encontré esto útil para atrapar y manejar salvedad.

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 y sugerencias

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

Ni siquiera considere trabajar con tcsh. no soporta funciones, y es horrible en general.

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

 201
Author: Stefano Borini,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-04-11 11:37:30

Echa un vistazo a Advanced Bash-Scripting Guide para obtener mucha sabiduría sobre shell scripting, no solo Bash, tampoco.

No escuches a la gente diciéndote que mires otros lenguajes, posiblemente más complejos. Si el script de shell satisface sus necesidades, use eso. 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 hacer y ya conoce shell.

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

 21
Author: jtimberman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-17 02:44:13

Shell script es un lenguaje diseñado para manipular archivos y procesos. Si bien es ideal 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 recreando nueva lógica en el script de shell.

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

 18
Author: pixelbeat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-17 23:57:14

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

 11
Author: Fhoxh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-04-10 22:50:37

Fácil: utilice python en lugar de scripts de shell. Obtienes un aumento de casi 100 veces en la legibilidad, sin tener que complicar nada que no necesites, y conservas la capacidad de evolucionar partes de tu script en funciones, objetos, objetos persistentes (zodb), objetos distribuidos (pyro) casi sin ningún código adicional.

 8
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-17 00:02:48

Use set-e para no avanzar después de errores. Intenta hacerlo compatible con sh sin depender de bash si quieres que se ejecute en not-linux.

 8
Author: user10392,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-17 00:05:01

Sepa cuándo usarlo. Para comandos de pegado rápidos y sucios, está bien. Si necesita tomar más que pocas decisiones no triviales, bucles, cualquier cosa, vaya a Python, Perl y modularizar.

El mayor problema con shell es a menudo que el resultado final solo se ve como 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.

 8
Author: Paweł Hajdan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-17 19:30:13

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

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

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

Intento evitar funciones desde tienden a hacer el guión más complicado. (Perl fue creado para ese propósito.)

Para asegurarse de que el script es portable, pruebe no solo con #!/bin / sh, pero también use #!/ bin / ash,#!/ bin / dash, etc. Pronto descubrirás el código específico de Bash.

 6
Author: Willem,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-17 20:33:48

O la cita anterior similar a lo que dijo Joao:

"Use perl. Usted querrá saber bash, pero no usarlo."

Lamentablemente olvidé quién dijo eso.

Y sí en estos días recomendaría python sobre perl.

 -1
Author: Sarien,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-17 00:04:52