Question

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

Je commence habituellement (après la #! ligne) par un en-tête commenté avec un nom de fichier, un synopsis, l'utilisation, les valeurs de retour, l'auteur (s), le journal des modifications et qui correspond à des lignes de 80 caractères.

Toutes les lignes de documentation, je commence par des symboles à double hachage ## afin que je puisse les grep facilement et les noms de variables locaux sont précédés de " __ & ";.

D'autres bonnes pratiques? Conseils? Conventions de nommage? Qu'en est-il des codes de retour?

Commentaires sur le contrôle de version: nous utilisons bien SVN, mais un autre service de l’entreprise a un référentiel séparé et c’est son script. Comment savoir à qui s'adresser avec les questions s'il n'y a pas d'informations sur @author? L'utilisation d'entrées similaires à javadocs présente certains avantages, même dans le contexte du shell, à mon humble avis, mais je me trompe peut-être.

Était-ce utile?

La solution

J'étendrais la réponse de Norman à 6 lignes, et la dernière de ces réponses est vide:

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

La troisième ligne est une chaîne d'identification de contrôle de version. Il s'agit en fait d'un hybride avec un marqueur SCCS '@(#)' qui peut être identifié par le programme (SCCS) what et une chaîne de version RCS qui est développée lorsque le Le fichier est placé sous RCS, le VCS par défaut que j'utilise pour mon usage privé. Le programme RCS ident reprend la forme développée de $Id$, qui pourrait 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 (c'est pourquoi, par exemple, il n'y a pas de signe deux-points).

Après cela, il n’existe essentiellement rien de standard pour un script shell. Des fragments standard apparaissent, mais aucun fragment standard ne figure dans chaque script. (Ma discussion suppose que les scripts sont écrits dans des notations de shell Bourne, Korn ou POSIX (Bash). Il existe une discussion distincte sur la raison pour laquelle toute personne qui utilise un dérivé du shell C après le #! sceau vit dans le péché.)

Par exemple, ce code apparaît sous une forme ou une autre à 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. La valeur par défaut est / tmp si l'utilisateur n'a pas spécifié d'alternative ($ TMPDIR est très largement reconnu et normalisé 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 simple mesure de concurrence, 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 fichier 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 'trap' supprime tous les fichiers intermédiaires correspondant au modèle. la commande exit $exitval garantit que le statut est différent de zéro, ce qui indique une sorte d'erreur. Le '$arg0' de 0 signifie que le code est également exécuté si le shell se ferme pour une raison quelconque - il couvre la négligence dans la section intitulée 'Travail réel'. Le code à la fin supprime ensuite tous les fichiers temporaires survivants, avant de lever le piège à la sortie, et quitte finalement avec le statut zéro (succès). De toute évidence, si vous souhaitez quitter avec un autre statut, vous pouvez simplement vous assurer de la définir dans une variable avant d'exécuter les lignes usage et getopts, puis utilisez $(...).

J'utilise généralement les éléments suivants pour supprimer le chemin d'accès et le suffixe du script afin de pouvoir utiliser sed lorsque des erreurs sont signalées:

arg0=$(basename $0 .sh)

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

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

S'il n'y a qu'une ou deux sorties d'erreur, je ne me préoccupe pas de la fonction. s'il y en a plus, je le fais parce que cela simplifie le codage. Je crée également des fonctions plus ou moins élaborées, appelées $PERL pour récapituler l'utilisation de la commande - à nouveau, uniquement s'il existe plusieurs emplacements où elle serait utilisée.

Un autre fragment assez standard est une boucle d’analyse d’options, utilisant le $SED intégré au 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)

ou:

shift $(($OPTIND - 1))

Les guillemets autour de " $ OPTARG " gérer les espaces dans les arguments. Le Dflag est cumulatif, mais la notation utilisée ici perd la trace des espaces dans les arguments. Il existe également des moyens (non standard) de résoudre ce problème.

La notation du premier décalage fonctionne avec n’importe quel shell (ou le serait si j’utilisais des guillemets au lieu de '${VAR:=value}'. Le second fonctionne avec les shells modernes; il pourrait même y avoir une alternative avec des crochets au lieu de parenthèses, mais cela fonctionne, donc je n'ai pas pris la peine de trouver ce que c'est.

Une dernière astuce pour l’instant, c’est que j’ai souvent à la fois une version des programmes GNU et une version non GNU, et que je veux pouvoir choisir celui que j’utiliserai. Bon nombre de mes scripts utilisent donc varides éléments tels que:

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

Ensuite, lorsque j'ai besoin d'appeler Perl ou <=>, le script utilise <=> ou <=>. Cela m'aide lorsque quelque chose se comporte différemment - je peux choisir la version opérationnelle - ou lors du développement du script (je peux ajouter des options supplémentaires de débogage uniquement à la commande sans modifier le script). (Voir Expansion des paramètres du shell pour plus d'informations sur les <=> et notations associées.)

Autres conseils

J'utilise le premier ensemble de lignes ## pour la documentation d'utilisation. Je ne me souviens plus maintenant où j'ai vu cela 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.
}

Tout code qui sera publié dans la nature devrait avoir l'en-tête court suivant:

# 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

Le fait de garder un journal des modifications dans les en-têtes de code est un retour en arrière depuis que les systèmes de contrôle de version étaient terriblement gênants. Une date de dernière modification indique à quel âge le script a été créé.

Si vous comptez utiliser des bashismes, utilisez #! / bin / bash, pas / bin / sh, car sh est l’invocation POSIX de n’importe quel shell. Même si / bin / sh pointe vers 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 qui reposent sur des bashismes, essayez d’être portables.

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

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

Les scripts shell sont si simples que (sauf si vous écrivez une démonstration pour apprendre à le faire), le code s’évite presque toujours.

Certains réservoirs n'aiment pas être alimentés avec des variables 'locales' typées. Je crois que, à ce jour, Busybox (un shell de secours commun) en fait partie. Faites GLOBALS_OBVIOUS à la place, il 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 de l'analyseur. Par exemple, beaucoup de gens pourraient écrire:

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

Où je voulais juste:

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

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

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

... où je:

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

La seule fois où j'utilise conventionnel if / then / else, c'est s'il y a un autre-if à ajouter.

Un exemple horriblement fou de très bon code shell portable peut être étudié en visualisant simplement le script 'configure' de la plupart des logiciels libres qui utilisent autoconf. Je dis fou parce que ses 6300 lignes de code s’adressent à tous les systèmes connus de l’homme possédant un shell UNIX. Vous ne voulez pas ce genre de foutaises, mais il est intéressant d’étudier quelques-uns des divers problèmes de portabilité au sein de .. comme être gentil avec ceux qui pourraient pointer / bin / sh vers zsh:)

Le seul conseil que je puisse vous donner est de surveiller votre développement dans here-docs, c.-à-d.

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

... va développer $ name, quand vous voudrez probablement laisser la variable en place. Résoudre ceci via:

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

qui laissera $ name en tant que variable, au lieu de le développer.

Je recommande également vivement d'apprendre à utiliser trap pour capturer les signaux .. et utiliser ces gestionnaires comme code passe-partout. Dire à un script en cours de ralentir avec un simple SIGUSR1 est très pratique:)

La plupart des nouveaux programmes que j'écris (qui sont orientés outils / ligne de commande) débutent sous la forme de scripts shell, c'est un excellent moyen de prototyper les outils UNIX.

Vous pouvez également aimer le compilateur de scripts SHC, à consulter ici . .

C’est l’en-tête que j’utilise pour mon shell de script (bash ou ksh). C'est un man aspect identique 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/%d@%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 qui vont 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/%d@%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

L'activation de la détection d'erreur facilite la détection précoce des problèmes dans le script:

set -o errexit

Quittez le script à la première erreur. De cette façon, vous évitez de continuer à faire quelque chose qui dépendait de quelque chose de plus tôt dans le script, ce qui pourrait vous amener à un état système étrange.

set -o nounset

Traitez les références aux variables non définies comme des erreurs. Il est très important d'éviter d'exécuter des opérations telles que rm -you_know_what "$var/" avec un $var non défini. Si vous savez que la variable peut être non définie et que la situation est sûre, vous pouvez utiliser ${var-value} pour utiliser une valeur différente si non définie ou ${var:-value} pour utiliser une valeur différente si non définie ou vide.

set -o noclobber

Il est facile de commettre l'erreur d'insérer un > emplacement où vous vouliez insérer < et d'écraser un fichier que vous vouliez lire. Si vous devez insérer un fichier dans votre script, vous pouvez le désactiver avant la ligne concernée et le réactiver par la suite.

set -o pipefail

Utilisez le premier code de sortie non nul (le cas échéant) d'un ensemble de commandes piped comme code de sortie de l'ensemble complet de commandes. Cela facilite le débogage des commandes canalisées.

shopt -s nullglob

Évitez que votre /foo/* glob soit interprété littéralement s'il n'y a aucun fichier 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

Mon modèle bash est comme ci-dessous (défini dans ma configuration de vim . ):

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

Je suggérerais

#!/bin/ksh

et c'est tout. Des commentaires de bloc lourds pour les scripts shell? Je reçois les willies.

Suggestions:

  1. La documentation devrait être des données ou du code, pas des commentaires. Au moins une usage() fonction. Examinez comment ksh et les autres outils AST se documentent avec les options --man sur chaque commande. (Lien impossible, le site Web est en panne.)

  2. Déclarez les variables locales avec typeset. C'est pour ça. Pas besoin de gros caractères de soulignement.

Ce que vous pouvez faire est de créer un script qui crée un en-tête pour un script & amp; et l’ouvrez automatiquement dans votre éditeur favori. J'ai vu un gars le faire 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
#===============================================================================

En règle générale, je tiens à respecter quelques conventions pour chaque script que j'écris. J'écris tous les scripts en supposant 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 la recherche dans grep. J'utilise les accolades '[' pour indiquer que les gens doivent saisir eux-mêmes le texte. s'ils se produisent en dehors d'un commentaire, j'essaie de les démarrer avec '# ['. Ainsi, si quelqu'un les colle tels quels, ils ne seront pas pris pour une entrée ou une commande de test. Consultez la section utilisation sur une page de manuel pour voir ce style à titre d'exemple.

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

Toutes mes variables shell, je préfère le faire en CAPS. J'essaie de garder entre 4 et 8 caractères, sauf indication contraire. Les noms portent le mieux possible sur leur utilisation.

Je quitte aussi toujours avec 0 en cas de succès ou 1 pour les erreurs. Si le script contient de nombreux types d'erreurs différents (et aiderait réellement quelqu'un, ou pourrait être utilisé dans un code quelconque), je choisirais une séquence documentée au lieu de 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 numérotation général.

J'aime traiter les arguments de manière standard. Je préfère toujours getopts, getopt. Je ne fais jamais de piratage avec les commandes 'read' et les instructions if. J'aime aussi utiliser l'instruction case pour éviter les if imbriqués. J'utilise un script de traduction pour les options longues, donc --help signifie -h to getopts. J'écris tous les scripts en bash (si acceptable) ou générique sh.

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

N'oubliez pas que ce ne sont que des conventions. Meilleure pratique, de grossière, mais parfois vous êtes forcé en dehors des lignes. Le plus important est d’être cohérent entre vos projets.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top