2009-01-09 10 views
20

Quelles seraient vos suggestions pour un bon modèle de script bash/ksh à utiliser comme standard pour tous les scripts nouvellement créés?modèles de script shell

Je commence généralement (après #! Line) avec un en-tête commenté avec un nom de fichier, synopsis, utilisation, valeurs de retour, auteur (s), changelog et entrerait dans des lignes de 80 caractères.

toutes les lignes de la documentation Je commence par les symboles de double-hachage: "##", donc je peux facilement les faire grimper et les noms var locaux sont précédés de "__".

d'autres meilleures pratiques? conseils? conventions de nommage? Qu'en est-il des codes de retour?

grâce

edit: commentaires sur le contrôle de version: nous avons obtenu svn bien, mais un autre département dans l'entreprise a une prise en pension séparée et c'est leur script - comment puis-je savoir qui contacter avec Q de s'il y a pas d'info @author? javadocs-entrées similaires ont un certain mérite, même dans le contexte shell, à mon humble avis, mais je me trompe peut-être. Merci pour les réponses

+4

Auteur (s)? Changelog? Obtenez un système de contrôle de version! – derobert

Répondre

18

Je transmets la réponse de Norman à 6 lignes, et le dernier de ceux-ci est vide:

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

La troisième ligne est une chaîne d'identification de contrôle de version - il est en fait un hybride avec un CSSC marqueur '@(#)' qui peut être identifié par le programme (SCCS) what et une chaîne de version RCS qui est développée lorsque le fichier est placé sous RCS, le VCS par défaut que j'utilise pour mon usage privé. Le programme RCS ident récupère la forme développée de $Id$, ce qui peut ressembler à $Id: mkscript.sh,v 2.3 2005/05/20 21:06:35 jleffler Exp $. La cinquième ligne me rappelle que le script devrait avoir une description de son but en haut; Je remplace le mot par une description réelle du script (ce qui explique pourquoi il n'y a pas de deux-points après celui-ci, par exemple). Par la suite, il n'y a pratiquement rien de standard pour un script shell. Des fragments standard apparaissent, mais aucun fragment standard n'apparaît dans chaque script. (Ma discussion suppose que les scripts sont écrits dans des notations shell Bourne, Korn, ou POSIX (Bash) Il y a une discussion séparée sur la raison pour laquelle quelqu'un mettant un dérivé C Shell après le sigil #! vit dans le péché.)

Par exemple, ce code apparaît sous une forme ou sous forme chaque fois qu'un script crée des fichiers intermédiaires (temporaires):

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 première ligne choisit un répertoire temporaire, par défaut/tmp si l'utilisateur n'a pas spécifiez une alternative ($ TMPDIR est très largement reconnu et est standardisé par POSIX). Il crée ensuite un préfixe de nom de fichier incluant l'ID de processus. Ce n'est pas une mesure de sécurité; il s'agit d'une mesure de simultanéité simple, empêchant plusieurs instances du script de piétiner les données de l'autre. (Pour des raisons de sécurité, utilisez des noms de fichiers non prévisibles dans un répertoire non public.) La deuxième ligne garantit que les commandes 'rm' et 'exit' sont exécutées si le shell reçoit l'un des signaux SIGHUP (1), SIGINT (2), SIGQUIT (3), SIGPIPE (13) ou SIGTERM (15). La commande 'rm' supprime tous les fichiers intermédiaires correspondant au modèle; la commande exit garantit que l'état est différent de zéro, indiquant une sorte d'erreur. Le 'trap' de 0 signifie que le code est également exécuté si le shell se termine pour une raison quelconque - il couvre la négligence dans la section «travail réel». Le code à la fin supprime alors tous les fichiers temporaires survivants, avant en levant le piège à la sortie, et enfin quitte avec un statut zéro (succès). De toute évidence, si vous voulez quitter avec un autre statut, vous pouvez - assurez-vous de le définir dans une variable avant d'exécuter les lignes rm et trap, puis d'utiliser exit $exitval.

J'utilise habituellement ce qui suit pour supprimer le chemin et le suffixe du script, donc je peux utiliser $arg0 quand signaler les erreurs:

arg0=$(basename $0 .sh) 

J'utilise souvent une fonction shell pour signaler les erreurs:

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

S'il n'y a qu'une ou peut-être deux sorties d'erreur, je ne dérange pas avec la fonction; s'il y en a d'autres, je le fais car cela simplifie le codage. Je crée également des fonctions plus ou moins élaborées appelées usage pour donner le résumé de l'utilisation de la commande - encore une fois, seulement s'il y a plus d'un endroit où il serait utilisé.

Un autre fragment assez standard est une boucle d'analyse syntaxique de l'option, en utilisant le haut-shell getopts:

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) 

ou:

shift $(($OPTIND - 1)) 

Les guillemets autour des espaces de poignée "OPTARG $" dans les arguments. Le Dflag est cumulatif, mais la notation utilisée ici perd la trace des espaces dans les arguments. Il existe des moyens (non standard) de contourner ce problème. La première notation de décalage fonctionne avec n'importe quel shell (ou ferait si j'utilisais back-ticks au lieu de '$(...)' .Le second fonctionne dans les coquilles modernes, il pourrait même y avoir une alternative entre crochets au lieu de parenthèses, mais cette Un dernier truc pour l'instant est que j'ai souvent à la fois la version GNU et une version non-GNU des programmes, et je veux être en mesure de choisir J'utilise.Beaucoup de mes scripts, donc utiliser des variables telles que:

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

Et puis, quand je dois appeler Perl ou sed, le script utilise $PERL ou $SED. Cela m'aide quand quelque chose se comporte différemment - je peux choisir la version opérationnelle - ou pendant le développement du script (je peux ajouter des options de débogage uniquement à la commande sans modifier le script).

+0

Salut @Jonathan quelle est la notation ": $ {VAR: = file}"? Merci d'avance – tmow

+1

@tmow: La notation '$ {VAR: = fichier}' signifie que si $ VAR est défini sur une valeur non vide, alors utilisez cette valeur, mais si $ VAR n'est pas défini ou est mettre à la chaîne vide, puis utiliser la valeur 'file', et mettre' $ VAR' à cette valeur. Donc, c'est un peu comme (mais beaucoup plus court que): '[-z" $ VAR "] && VAR = fichier; echo $ VAR'. –

+0

thx beaucoup, c'est vraiment utile !!! – tmow

3

Je suggère

#!/bin/ksh 

et c'est tout. Commentaires sur les blocs lourds pour les scripts shell Je reçois les willies.

Suggestions:

  1. La documentation doit être données ou de code, pas les commentaires. Au moins une fonction usage(). Regardez comment ksh et les autres outils AST se documentent avec les options --man sur chaque commande. (Impossible de lier car le site Web est arrêté.)

  2. Déclarez les variables locales avec typeset. C'est pour ça. Pas besoin de underscore méchants.

7

Tout code qui va être libéré dans la nature devrait avoir l'en-tête courte suivante:

# 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 

Garder un journal des modifications allant dans les en-têtes de code est un retour à partir de quand les systèmes de contrôle de version étaient terriblement pratique. Une date de dernière modification montre à quelqu'un l'âge du script. Si vous comptez sur les bashismes, utilisez #!/Bin/bash, et non/bin/sh, car sh est l'appel POSIX de n'importe quel shell. Même si/bin/sh pointe sur bash, de nombreuses fonctionnalités seront désactivées si vous l'exécutez via/bin/sh. La plupart des distributions Linux ne prendront pas de scripts basés sur des bashismes, essayez d'être portables.

Pour moi, les commentaires dans les scripts shell sont une sorte de bête à moins qu'ils lisent quelque chose comme:

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

script Shell est si simple que (à moins que votre écriture d'une démonstration pour apprendre à quelqu'un comment faire) le code presque toujours s'évite.

Certaines coquilles n'aiment pas être alimentées avec des variables 'locales' typées. Je crois à ce jour Busybox (un shell de sauvetage commun) est l'un d'entre eux. Faites GLOBALS_OBVIOUS à la place, c'est beaucoup plus facile à lire, en particulier lors du débogage via/bin/sh -x ./script.sh.

Ma préférence personnelle est de laisser la logique parler d'elle-même et de minimiser le travail pour l'analyseur. Par exemple, beaucoup de gens pourraient écrire:

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

Où je voudrais juste:

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

De même, quelqu'un peut écrire:

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

... où je:

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

La seule fois que j'utilise conventionnel if/th en/else est s'il y a un autre-si jeter dans le mélange.

Un exemple horriblement fou de très bon code shell portable peut être étudié en visualisant simplement le script 'configure' dans la plupart des logiciels libres qui utilisent autoconf. Je dis fou parce que ses 6300 lignes de code qui répondent à tous les systèmes connus de l'homme qui a un shell UNIX. Vous ne voulez pas ce genre de ballonnement, mais il est intéressant d'étudier certains des hacks de portabilité à l'intérieur .. comme étant gentil avec ceux qui pourraient pointer/bin/sh à zsh :)

Le seul autre conseil Je peux donner est de regarder votre expansion ici-docs, à savoir

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

... va $ expand nom, quand vous voulez probablement quitter la variable en place.Résolvez ceci via:

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

qui laissera $ name en tant que variable, au lieu de l'étendre.

Je recommande également fortement d'apprendre à utiliser le piège pour capter les signaux ... et d'utiliser ces gestionnaires comme code standard. Raconter un script en cours pour ralentir avec un simple SIGUSR1 est assez pratique :)

La plupart des nouveaux programmes que j'écris (qui sont orientés outil/ligne de commande) commencent comme des scripts shell, c'est un excellent moyen de prototyper des outils UNIX.

Vous pourriez également aimer le compilateur de script shell SHC, check it out here.

+3

Si vous ne voulez pas que les documents soient développés, utilisez << 'EOF' pour supprimer l'expansion. N'utilisez les barres obliques inverses que lorsque vous voulez étendre certaines choses et que certaines ne sont pas développées. –

1

Généralement, j'ai quelques conventions auxquelles je tiens pour chaque script que j'écris. J'écris tous les scripts avec l'hypothèse que d'autres personnes pourraient les lire.

Je commence chaque script avec mon en-tête,

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

J'utilise ce format de date, pour faciliter grep/recherche. J'utilise les ['accolades pour indiquer le texte dont les gens ont besoin pour entrer eux-mêmes. s'ils se produisent en dehors d'un commentaire, j'essaie de les démarrer avec '# ['. De cette façon, si quelqu'un les colle tel quel, il ne sera pas confondu avec une entrée ou une commande de test. Vérifiez la section d'utilisation sur une page de manuel, pour voir ce style comme un exemple.

Lorsque je veux commenter une ligne de code, j'utilise un simple '#'. Quand je fais un commentaire en note, j'utilise un double '##'. Le /etc/nanorc utilise également cette convention. Je trouve utile de différencier un commentaire qui a été choisi de ne pas être exécuté; vers un commentaire qui a été créé comme une note.

Toutes mes variables shell, je préfère faire en CAPS. J'essaie de garder entre 4 et 8 caractères, sauf si nécessaire. Les noms se rapportent, autant que possible, à leur utilisation.

Je quitte également toujours avec 0 en cas de succès, ou un 1 pour les erreurs. Si le script a de nombreux types d'erreurs (et aiderait quelqu'un, ou pourrait être utilisé dans un code quelconque), je choisirais une séquence documentée sur 1. En général, les codes de sortie ne sont pas aussi strictement appliqués dans le * monde nix. Malheureusement, je n'ai jamais trouvé un bon schéma de nombre général. J'aime traiter les arguments de la manière standard. Je préfère toujours getopts, getopt. Je ne fais jamais de piratage avec les commandes 'read' et if statements. J'aime aussi utiliser l'instruction case, pour éviter les ifs imbriquées. J'utilise un script de traduction pour les options longues, donc --help signifie -h à getopts. J'écris tous les scripts dans bash (si acceptable) ou générique sh.

Je n'utilise JAMAIS les symboles interprétés bash (ou tout symbole interprété) dans les noms de fichiers, ou n'importe quel nom d'ailleurs. spécifiquement ... " '` & $ * #() {} [] -, j'utilise _ pour les espaces

Rappelez-vous, ce ne sont que des conventions Les meilleures pratiques, de gros, mais parfois vous sont forcés en dehors de la.. Le plus important est d'être cohérent à travers et au sein de vos projets

9

J'utilise le premier ensemble de lignes ## pour la documentation d'utilisation.Je ne me souviens pas où je l'ai vu pour la première fois.

#!/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 fonction usage() greping le script a l'air cool au début, mais elle échoue si le script exécute un 'cd' n'importe où et' $ 0' n'est pas un nom de fichier absolu. J'opterais pour la fonction usage() pour echo/printf/cat le message d'utilisation. – Jens

+0

Vous pouvez déterminer le chemin absolu du script et le stocker dans une variable. Cela peut être sympa d'avoir les informations d'utilisation en haut dans les commentaires. Si vous faites cela, puis grepping le script pour imprimer les informations à partir des commentaires rend le code DRY. – toxalot

2

Ce que vous pouvez faire est de faire un script qui crée un en-tête pour un script & et avoir ouvert automatiquement dans votre éditeur préféré. J'ai vu un gars faire ça sur ce site:

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

Activation de la détection d'erreur, il est beaucoup plus facile de détecter les problèmes dans le script début:

set -o errexit 

script sortie sur la première erreur. De cette façon, vous évitez de continuer à faire quelque chose qui dépendait de quelque chose plus tôt dans le script, peut-être finir avec un état de système étrange.

set -o nounset 

Traiter les références aux variables non définies comme des erreurs. Très important pour éviter de courir des choses comme rm -you_know_what "$var/" avec un $var non réglé. Si vous savez que la variable peut être désactivée et qu'il s'agit d'une situation sûre, vous pouvez utiliser ${var-value} pour utiliser une valeur différente si elle n'est pas définie ou ${var:-value} pour utiliser une valeur différente si elle est vide ou vide.

set -o noclobber 

Il est facile de faire l'erreur d'insérer un > où vous vouliez dire insérer <, et remplacer certains fichiers que vous vouliez lire. Si vous avez besoin d'écraser un fichier dans votre script, vous pouvez le désactiver avant la ligne correspondante et l'activer à nouveau par la suite.

set -o pipefail 

utiliser le premier code de sortie non nul (le cas échéant) d'un ensemble de commande courante en tant que le code de sortie de l'ensemble des commandes. Cela facilite le débogage des commandes redirigées.

shopt -s nullglob 

Évitez que votre /foo/* glob est interprété littéralement s'il n'y a pas de fichiers correspondant à cette expression.

Vous pouvez combiner tous ces éléments en deux lignes:

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

Mon modèle bash est comme ci-dessous (défini dans mon 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

Ceci est l'en-tête que j'utilise pour mon script shell (bash ou ksh). Il ressemble à man et il est également utilisé pour afficher l'utilisation().

#!/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 
#================================================================ 

Et voici les fonctions d'utilisation pour aller avec:

#== 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"; } 

Voici ce que vous devriez obtenir:

# 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 

Vous pouvez obtenir le modèle de script complet ici: http://www.uxora.com/unix/shell-script/18-shell-script-template

+0

Votre message n'inclut pas votre modèle complet et votre site met le code derrière un "mur de type social", cela devrait être considéré comme un mauvais comportement. – Hultner

Questions connexes