2009-01-09 28 views
20

¿Cuáles serían sus sugerencias para una buena plantilla de script bash/ksh para usar como estándar para todos los scripts creados recientemente?plantillas de script de shell

Normalmente comienzo (después de #! Line) con un encabezado comentado con un nombre de archivo, sinopsis, uso, valores devueltos, autor (es), changelog y encajaría en líneas de 80 caracteres.

todas las líneas de documentación Comienzo con símbolos de doble hash: "##", por lo que puedo grep para ellos fácilmente y los nombres locales de var se anteponen con "__".

¿alguna otra mejor práctica? ¿consejos? ¿convenciones de nombres? ¿Qué pasa con los códigos de retorno?

gracias

edición: comentarios sobre el control de versiones: nos dieron SVN bien, pero otra sección en la empresa tiene un acuerdo de recompra por separado y este es su guión - ¿cómo sé a quién contactar con Q de si hay no @ información de autor? Las entradas javadocs-similares tienen algún mérito incluso en el contexto del shell, en mi humilde opinión, pero podría estar equivocado. gracias por las respuestas

+4

Autor (es)? Registro de cambios? ¡Obtenga un sistema de control de versiones! – derobert

Respuesta

18

Me extiendo la respuesta de Norman a 6 líneas, y el último de los que está en blanco:

#!/bin/ksh 
# 
# @(#)$Id$ 
# 
# Purpose 

La tercera línea es una cadena de identificación del control de versiones - en realidad es un híbrido con un CCSC marcador '@(#)' que puede ser identificado por el programa (SCCS) what y una cadena de versión RCS que se expande cuando el archivo se coloca en RCS, el VCS predeterminado que uso para mi uso privado. El programa RCS ident toma la forma expandida de $Id$, que podría verse como $Id: mkscript.sh,v 2.3 2005/05/20 21:06:35 jleffler Exp $. La quinta línea me recuerda que el script debe tener una descripción de su propósito en la parte superior; Sustituyo la palabra con una descripción real de la secuencia de comandos (que es por lo que no hay dos puntos después de ella, por ejemplo).

Después de eso, esencialmente no hay nada estándar para un script de shell. Hay fragmentos estándar que aparecen, pero no un fragmento estándar que aparece en cada script. (Mi discusión asume que los guiones están escritos en notación de shell Bourne, Korn o POSIX (Bash). Hay una discusión por separado sobre por qué alguien que pone un derivado de C Shell después del símbolo #! está viviendo en pecado.)

Por ejemplo, el código aparece en una forma u otra cada vez que un script crea) los archivos intermedios (temporales:

tmp=${TMPDIR:-/tmp}/prog.$$ 
trap "rm -f $tmp.?; exit 1" 0 1 2 3 13 15 

...real work that creates temp files $tmp.1, $tmp.2, ... 

rm -f $tmp.? 
trap 0 
exit 0 

La primera línea elige un directorio temporal, por defecto a/tmp si el usuario no lo hizo especifique una alternativa ($ TMPDIR es ampliamente reconocido y estandarizado por POSIX). Luego crea un prefijo de nombre de archivo que incluye la identificación del proceso. Esta no es una medida de seguridad; es una medida de simultaneidad simple que evita que varias instancias de la secuencia de comandos pisoteen los datos de la otra parte. (Para mayor seguridad, use nombres de archivos no predecibles en un directorio que no sea público.) La segunda línea garantiza que los comandos 'rm' y 'exit' se ejecuten si el shell recibe cualquiera de las señales SIGHUP (1), SIGINT (2), SIGQUIT (3), SIGPIPE (13) o SIGTERM (15). El comando 'rm' elimina cualquier archivo intermedio que coincida con la plantilla; El comando exit garantiza que el estado sea distinto de cero, lo que indica algún tipo de error. El 'trap' de 0 significa que el código también se ejecuta si el shell se cierra por algún motivo: cubre el descuido en la sección marcada como 'trabajo real'. El código al final luego elimina cualquier archivo temporal que haya sobrevivido, antes de levantando la trampa al salir, y finalmente sale con un estado de cero (correcto). Claramente, si desea salir con otro estado, puede - simplemente asegúrese de configurarlo en una variable antes de ejecutar las líneas rm y trap, y luego use exit $exitval.

lo general el uso de la siguiente para eliminar la ruta y el sufijo de la secuencia de comandos, por lo que puede utilizar $arg0 al informar errores:

arg0=$(basename $0 .sh) 

I a menudo utilizan una función de shell para informar de errores:

error() 
{ 
    echo "$arg0: $*" 1>&2 
    exit 1 
} 

Si solo hay una o quizás dos salidas de error, no me molesto con la función; si hay más, lo hago porque simplifica la codificación. También creo funciones más o menos elaboradas llamadas usage para dar el resumen de cómo usar el comando, nuevamente, solo si hay más de un lugar donde se usaría.

Otro fragmento bastante estándar es un bucle análisis de las opciones, utilizando el getopts incorporada en el shell:

vflag=0 
out= 
file= 
Dflag= 
while getopts hvVf:o:D: flag 
do 
    case "$flag" in 
    (h) help; exit 0;; 
    (V) echo "$arg0: version $Revision$ ($Date$)"; exit 0;; 
    (v) vflag=1;; 
    (f) file="$OPTARG";; 
    (o) out="$OPTARG";; 
    (D) Dflag="$Dflag $OPTARG";; 
    (*) usage;; 
    esac 
done 
shift $(expr $OPTIND - 1) 

o:

shift $(($OPTIND - 1)) 

Las comillas alrededor de los espacios de la manija "$ OPTARG" en argumentos. El Dflag es acumulativo, pero la notación utilizada aquí pierde el seguimiento de los espacios en los argumentos. Existen formas (no estándar) de evitar ese problema también.

La notación del primer cambio funciona con cualquier caparazón (o lo haría si usaba retrocesos en lugar de '$(...)'. El segundo funciona en conchas modernas, incluso podría haber una alternativa con corchetes en lugar de paréntesis, pero esto funciona así que no me he molestado en averiguar qué es eso.

Un truco final por ahora es que a menudo tengo la versión GNU y una versión no GNU de los programas, y quiero poder elegir qué Yo suelo.Muchos de mis guiones, por lo tanto, utilizar variables tales como:

: ${PERL:=perl} 
: ${SED:=sed} 

Y luego, cuando necesito invocar Perl o sed, la secuencia de comandos utiliza $PERL o $SED. Esto me ayuda cuando algo se comporta de manera diferente (puedo elegir la versión operativa) o mientras desarrollo el guión (puedo agregar opciones extra de solo depuración al comando sin modificar el guión).

+0

Hola @Jonathan, ¿cuál es la notación ": $ {VAR: = file}"? Gracias de antemano – tmow

+1

@tmow: La notación '$ {VAR: = file}' significa que si '$ VAR' está establecido en un valor no vacío, entonces use ese valor, pero si' $ VAR' no está configurado o está establece en la cadena vacía, luego usa el valor 'file', y establece' $ VAR' en ese valor. Por lo tanto, es un poco como (pero mucho más corto que): '[-z" $ VAR "] && VAR = archivo; echo $ VAR'. –

+0

thx mucho, ¡es realmente útil! – tmow

3

Yo sugeriría

#!/bin/ksh 

y eso es todo. ¿Comentarios de bloques pesados ​​para scripts de shell? Me dan los pelos de punta.

Sugerencias:

  1. la documentación debe ser información o código, no comentarios. Por lo menos una función usage(). Eche un vistazo a cómo ksh y las otras herramientas AST se documentan con opciones de menú en cada comando. (No se puede vincular porque el sitio web está caído.)

  2. Declara variables locales con typeset. Para eso es para eso. No hay necesidad de guiones bajos desagradables.

7

Cualquier código que va a ser puesto en libertad en la naturaleza debe tener el siguiente encabezado corto:

# Script to turn lead into gold 
# Copyright (C) 2009 Joe Q Hacker - All Rights Reserved 
# Permission to copy and modify is granted under the foo license 
# Last revised 1/1/2009 

Mantener un registro de cambios va en las cabeceras de código es un recuerdo de cuando los sistemas de control de versiones eran terriblemente incómoda. Una última fecha de modificación muestra a alguien la antigüedad del script.

Si va a confiar en bashisms, use #!/Bin/bash, not/bin/sh, ya que sh es la invocación POSIX de cualquier shell. Incluso si/bin/sh apunta a bash, muchas funciones se desactivarán si lo ejecuta a través de/bin/sh. La mayoría de las distribuciones de Linux no tomarán scripts que dependan de bashisms, intenten ser portátiles.

Para mí, los comentarios en shell scripts son una especie de tonto menos que lean algo como:

# I am not crazy, this really is the only way to do this 

Shell scripting es tan simple que (a menos que su escritura una manifestación para enseñar a alguien cómo hacerlo) el código casi siempre se obvia a sí mismo.

A algunos proyectiles no les gusta ser alimentados con variables tipeadas 'locales'. Creo que hasta hoy Busybox (una carcasa de rescate común) es uno de ellos. En su lugar, GLOBALS_OBVIOUS es mucho más fácil de leer, especialmente cuando se depura a través de/bin/sh -x ./script.sh.

Mi preferencia personal es dejar que la lógica hable por sí misma y minimizar el trabajo para el analizador. Por ejemplo, mucha gente podría escribir:

if [ $i = 1 ]; then 
    ... some code 
fi 

donde me gustaría simplemente:

[ $i = 1 ] && { 
    ... some code 
} 

Del mismo modo, alguien podría escribir:

if [ $i -ne 1 ]; then 
    ... some code 
fi 

... donde me gustaría:

[ $i = 1 ] || { 
    ... some code 
} 

La única vez que utilizo if/th convencional en/else es si hay un else-if para lanzar en la mezcla.

Un ejemplo terriblemente loco de muy buen código de shell portátil se puede estudiar con solo ver el script 'configurar' en la mayoría de los paquetes de software libre que usan autoconf. Digo loco porque tiene 6300 líneas de código que atienden a todos los sistemas conocidos por el hombre que tienen un shell como UNIX. No quieres ese tipo de abotagamiento, pero es interesante estudiar algunos de los diversos ataques de portabilidad dentro ... como ser amable con aquellos que podrían apuntar/bin/sh a zsh :)

El único otro consejo que puedo dar es ver su expansión en el aquí-docs, es decir

cat <<EOF> foo.sh 
    printf "%s was here" "$name" 
EOF 

... va a ampliar $ nombre, cuando es probable que desee salir de la variable en su lugar.Resuelva esto a través de:

printf "%s was here" "\$name" 

que dejará $ nombre como variable, en lugar de expandirlo.

También recomiendo aprender a usar trampas para atrapar señales ... y hacer uso de esos controladores como código repetitivo. Es muy útil decirle a un script que se ejecute que reduzca la velocidad con un simple SIGUSR1 :)

La mayoría de los programas nuevos que escribo (que están orientados a herramientas/línea de comandos) comienzan como scripts de shell, es una excelente manera de crear prototipos de herramientas UNIX.

También le puede interesar el compilador de script SHC shell, check it out here.

+3

Si no desea expandir aquí los documentos, use << 'EOF' para suprimir la expansión. Solo use barras diagonales inversas cuando desee expandir algunas cosas y algunas cosas no expandidas. –

1

En general, tengo algunas convenciones que me gusta mantener para cada script que escribo. Escribo todos los scripts con la suposición de que otras personas podrían leerlos.

empiezo cada guión con mi cabecera,

#!/bin/bash 
# [ID LINE] 
## 
## FILE: [Filename] 
## 
## DESCRIPTION: [Description] 
## 
## AUTHOR: [Author] 
## 
## DATE: [XX_XX_XXXX.XX_XX_XX] 
## 
## VERSION: [Version] 
## 
## USAGE: [Usage] 
## 

uso ese formato de fecha, para facilitar la grep/búsqueda. Uso los '[' refuerzos para indicar el texto que las personas necesitan para ingresar. si ocurren fuera de un comentario, intento comenzar con '# ['. De esta manera, si alguien los pega como está, no se confundirá con la entrada o un comando de prueba. Verifique la sección de uso en una página man, para ver este estilo como un ejemplo.

Cuando quiero comentar una línea de código, uso un solo '#'. Cuando estoy haciendo un comentario como una nota, uso un doble '##'. El /etc/nanorc usa esa convención también. Encuentro útil, para diferenciar un comentario que se eligió para no ejecutar; Versos de un comentario que fue creado como una nota.

Todas mis variables de shell, prefiero hacer en MAYÚSCULAS. Intento mantener entre 4 y 8 caracteres, a menos que sea necesario. Los nombres se relacionan, de la mejor manera posible, con su uso.

También siempre salgo con 0 si tiene éxito o con 1 para errores. Si el script tiene muchos tipos diferentes de errores (y realmente ayudaría a alguien, o podría usarse en algún código de alguna manera), elegiría una secuencia documentada sobre 1. En general, los códigos de salida no se aplican tan estrictamente en el * nix mundo. Desafortunadamente, nunca he encontrado un buen esquema de números generales.

Me gusta procesar los argumentos de la manera estándar. Siempre prefiero getopts, getopt. Nunca hago algún truco con comandos 'leer' y declaraciones if. También me gusta usar el enunciado de caso para evitar los if anidados. Utilizo un script de traducción para opciones largas, por lo que --help significa -h para getopts. Escribo todos los scripts en bash (si es aceptable) o genérico sh.

NUNCA utilizo símbolos interpretados bash (o cualquier símbolo interpretado) en nombres de archivo, o cualquier nombre para ese asunto. específicamente ... " '' $ & * #() {} [] -, utilizo _ para espacios

Recuerde, estas son sólo convenciones mejores prácticas, de ordinario, pero a veces se ven forzados fuera de la.. líneas. Lo más importante es ser coherente dentro y dentro de sus proyectos.

9

Utilizo el primer conjunto de líneas ## para la documentación de uso. No recuerdo ahora dónde lo vi por primera vez.

#!/bin/sh 
## Usage: myscript [options] ARG1 
## 
## Options: 
## -h, --help Display this message. 
## -n   Dry-run; only show what would be done. 
## 

usage() { 
    [ "$*" ] && echo "$0: $*" 
    sed -n '/^##/,/^$/s/^## \{0,1\}//p' "$0" 
    exit 2 
} 2>/dev/null 

main() { 
    while [ $# -gt 0 ]; do 
    case $1 in 
    (-n) DRY_RUN=1;; 
    (-h|--help) usage 2>&1;; 
    (--) shift; break;; 
    (-*) usage "$1: unknown option";; 
    (*) break;; 
    esac 
    done 
    : do stuff. 
} 
+3

La función usage() que reduce la secuencia de comandos se ve genial al principio, pero falla si la secuencia de comandos ejecuta un 'cd' en cualquier lugar y' $ 0' no es un nombre de archivo absoluto. Optaría por la función de uso() para hacer eco/printf/cat del mensaje de uso. – Jens

+0

Puede determinar la ruta de acceso absoluta de la secuencia de comandos y almacenarla en una variable. Puede ser agradable tener la información de uso en la parte superior de los comentarios. Si haces eso, entonces, al guardar la secuencia de comandos para imprimir la información de los comentarios, el código se SECA. – toxalot

2

Lo que puede hacer es hacer un script que crea una cabecera para un guión & y lo han automóvil abierto en su editor favorito. Vi a un tipo hacer que en este sitio:

http://code.activestate.com/recipes/577862-bash-script-to-create-a-header-for-bash-scripts/?in=lang-bash

#!/bin/bash -  
#title   :mkscript.sh 
#description  :This script will make a header for a bash script. 
#author   :your_name_here 
#date   :20110831 
#version   :0.3  
#usage   :bash mkscript.sh 
#notes   :Vim and Emacs are needed to use this script. 
#bash_version :4.1.5(1)-release 
#=============================================================================== 
3

Activación de la detección de error hace que sea mucho más fácil de detectar problemas en el guión principios: la escritura

set -o errexit 

salida con el primer error. De esta forma, evitarás continuar haciendo algo que dependía de algo anterior en el guión, quizás terminando con un estado de sistema extraño.

set -o nounset 

Tratar las referencias a variables unset como errores. Es muy importante evitar ejecutar cosas como rm -you_know_what "$var/" con unset $var. Si sabe que la variable puede estar desarmada, y esta es una situación segura, puede usar ${var-value} para usar un valor diferente si está desarmado o ${var:-value} para usar un valor diferente si está desarmado o vacío.

set -o noclobber 

Es fácil cometer el error de inserción de un > donde destinado a insertar <, y sobrescribir algún archivo que significan para leer. Si necesita interceptar un archivo en su secuencia de comandos, puede desactivar esto antes de la línea relevante y habilitarlo nuevamente después.

set -o pipefail 

utilizar el primer código de salida distinto de cero (si la hay) de un conjunto de comandos por tubería como el código de salida del conjunto completo de comandos. Esto facilita la depuración de los comandos canalizados.

shopt -s nullglob 

Evitar que su /foo/* pegote se interpreta literalmente si no hay archivos que coinciden con esa expresión.

Se pueden combinar todos estos en dos líneas:

set -o errexit -o nounset -o noclobber -o pipefail 
shopt -s nullglob 
4

Mi plantilla de Bash es la siguiente (juego en mi vim configuration):

#!/bin/bash 

## DESCRIPTION: 

## AUTHOR: $USER_FULLNAME 

declare -r SCRIPT_NAME=$(basename "$BASH_SOURCE" .sh) 

## exit the shell(default status code: 1) after printing the message to stderr 
bail() { 
    echo -ne "$1" >&2 
    exit ${2-1} 
} 

## help message 
declare -r HELP_MSG="Usage: $SCRIPT_NAME [OPTION]... [ARG]... 
    -h display this help and exit 
" 

## print the usage and exit the shell(default status code: 2) 
usage() { 
    declare status=2 
    if [[ "$1" =~ ^[0-9]+$ ]]; then 
     status=$1 
     shift 
    fi 
    bail "${1}$HELP_MSG" $status 
} 

while getopts ":h" opt; do 
    case $opt in 
     h) 
      usage 0 
      ;; 
     \?) 
      usage "Invalid option: -$OPTARG \n" 
      ;; 
    esac 
done 

shift $(($OPTIND - 1)) 
[[ "$#" -lt 1 ]] && usage "Too few arguments\n" 

#==========MAIN CODE BELOW========== 
5

Este es el encabezado que utilizo para mi guión shell (bash o ksh). Se parece a man y se usa para mostrar el uso() también.

#!/bin/ksh 
#================================================================ 
# HEADER 
#================================================================ 
#% SYNOPSIS 
#+ ${SCRIPT_NAME} [-hv] [-o[file]] args ... 
#% 
#% DESCRIPTION 
#% This is a script template 
#% to start any good shell script. 
#% 
#% OPTIONS 
#% -o [file], --output=[file] Set log file (default=/dev/null) 
#%         use DEFAULT keyword to autoname file 
#%         The default value is /dev/null. 
#% -t, --timelog     Add timestamp to log ("+%y/%m/%[email protected]%H:%M:%S") 
#% -x, --ignorelock    Ignore if lock file exists 
#% -h, --help     Print this help 
#% -v, --version     Print script information 
#% 
#% EXAMPLES 
#% ${SCRIPT_NAME} -o DEFAULT arg1 arg2 
#% 
#================================================================ 
#- IMPLEMENTATION 
#- version   ${SCRIPT_NAME} (www.uxora.com) 0.0.4 
#- author   Michel VONGVILAY 
#- copyright  Copyright (c) http://www.uxora.com 
#- license   GNU General Public License 
#- script_id  12345 
#- 
#================================================================ 
# HISTORY 
#  2015/03/01 : mvongvilay : Script creation 
#  2015/04/01 : mvongvilay : Add long options and improvements 
# 
#================================================================ 
# DEBUG OPTION 
# set -n # Uncomment to check your syntax, without execution. 
# set -x # Uncomment to debug this shell script 
# 
#================================================================ 
# END_OF_HEADER 
#================================================================ 

Y aquí es las funciones de uso para ir con:

#== needed variables ==# 
SCRIPT_HEADSIZE=$(head -200 ${0} |grep -n "^# END_OF_HEADER" | cut -f1 -d:) 
SCRIPT_NAME="$(basename ${0})" 

    #== usage functions ==# 
usage() { printf "Usage: "; head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "^#+" | sed -e "s/^#+[ ]*//g" -e "s/\${SCRIPT_NAME}/${SCRIPT_NAME}/g" ; } 
usagefull() { head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "^#[%+-]" | sed -e "s/^#[%+-]//g" -e "s/\${SCRIPT_NAME}/${SCRIPT_NAME}/g" ; } 
scriptinfo() { head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "^#-" | sed -e "s/^#-//g" -e "s/\${SCRIPT_NAME}/${SCRIPT_NAME}/g"; } 

Esto es lo que debe obtener:

# Display help 
$ ./template.sh --help 

    SYNOPSIS 
    template.sh [-hv] [-o[file]] args ... 

    DESCRIPTION 
    This is a script template 
    to start any good shell script. 

    OPTIONS 
    -o [file], --output=[file] Set log file (default=/dev/null) 
    use DEFAULT keyword to autoname file 
    The default value is /dev/null. 
    -t, --timelog     Add timestamp to log ("+%y/%m/%[email protected]%H:%M:%S") 
    -x, --ignorelock    Ignore if lock file exists 
    -h, --help     Print this help 
    -v, --version     Print script information 

    EXAMPLES 
    template.sh -o DEFAULT arg1 arg2 

    IMPLEMENTATION 
    version   template.sh (www.uxora.com) 0.0.4 
    author   Michel VONGVILAY 
    copyright  Copyright (c) http://www.uxora.com 
    license   GNU General Public License 
    script_id  12345 

# Display version info 
$ ./template.sh -v 

    IMPLEMENTATION 
    version   template.sh (www.uxora.com) 0.0.4 
    author   Michel VONGVILAY 
    copyright  Copyright (c) http://www.uxora.com 
    license   GNU General Public License 
    script_id  12345 

Puede hacer que la plantilla guión completo aquí: http://www.uxora.com/unix/shell-script/18-shell-script-template

+0

Su publicación no incluye su plantilla completa y su sitio coloca el código detrás de un "muro social", esto se debe considerar un mal comportamiento. – Hultner