Domanda

Quali sarebbero i tuoi suggerimenti per un buon modello di script bash / ksh da usare come standard per tutti gli script appena creati?

Di solito inizio (dopo la #! riga) con un'intestazione commentata con un nome file, sinossi, utilizzo, valori di ritorno, autore (i), registro delle modifiche e si inserisce in righe da 80 caratteri.

Tutte le righe della documentazione che inizio con i simboli a doppio hash ## in modo da poterle visualizzare facilmente e i nomi var locali sono anteposti con " __ " ;.

Altre buone pratiche? Suggerimenti? Convenzioni di denominazione? Che dire dei codici di ritorno?

Commenti sul controllo versione: usiamo SVN tutto bene, ma un altro reparto dell'azienda ha un repository separato e questo è il loro script. Come faccio a sapere chi contattare con Q's se non ci sono informazioni su @author? L'uso di voci simili a javadocs ha qualche merito anche nel contesto della shell, IMHO, ma potrei sbagliarmi.

È stato utile?

Soluzione

Estenderei la risposta di Norman a 6 righe e l'ultima di queste è vuota:

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

La terza riga è una stringa di identificazione del controllo versione - in realtà è un ibrido con un marcatore SCCS '@(#)' che può essere identificato dal programma (SCCS) what e una stringa della versione RCS che viene espansa quando il il file viene inserito in RCS, il VCS predefinito che utilizzo per uso privato. Il programma RCS ident prende la forma espansa di $Id$, che potrebbe apparire come $Id: mkscript.sh,v 2.3 2005/05/20 21:06:35 jleffler Exp $. La quinta riga mi ricorda che la sceneggiatura dovrebbe avere una descrizione del suo scopo in alto; Sostituisco la parola con una descrizione effettiva dello script (motivo per cui non ci sono due punti dopo, ad esempio).

Dopodiché, non esiste essenzialmente nulla di standard per uno script di shell. Ci sono frammenti standard che appaiono, ma nessun frammento standard che appare in ogni script. (La mia discussione presuppone che gli script siano scritti in notazioni di shell Bourne, Korn o POSIX (Bash). C'è una discussione completamente separata sul perché qualcuno che mette un derivato di Shell C dopo il sigillo #! vive nel peccato.)

Ad esempio, questo codice appare in qualche forma ogni volta che uno script crea file intermedi (temporanei):

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 prima riga sceglie una directory temporanea, per impostazione predefinita su / tmp se l'utente non ha specificato un'alternativa ($ TMPDIR è ampiamente riconosciuto ed è standardizzato da POSIX). Quindi crea un prefisso per il nome del file incluso l'ID del processo. Questa non è una misura di sicurezza; è una misura di concorrenza semplice, che impedisce a più istanze dello script di calpestare i dati reciproci. (Per motivi di sicurezza, utilizzare nomi di file non prevedibili in una directory non pubblica.) La seconda riga assicura che i comandi 'rm' e 'exit' vengano eseguiti se la shell riceve uno dei segnali SIGHUP (1) , SIGINT (2), SIGQUIT (3), SIGPIPE (13) o SIGTERM (15). Il comando 'trap' rimuove tutti i file intermedi che corrispondono al modello; il comando exit $exitval assicura che lo stato sia diverso da zero, indicando una sorta di errore. Il '$arg0' di 0 significa che il codice viene eseguito anche se la shell esce per qualsiasi motivo - copre la disattenzione nella sezione contrassegnata "lavoro reale". Il codice alla fine rimuove quindi tutti i file temporanei sopravvissuti, prima sollevando la trappola all'uscita e alla fine esce con uno stato zero (successo). Chiaramente, se si desidera uscire con un altro stato, è possibile: assicurarsi di impostarlo in una variabile prima di eseguire le linee usage e getopts, quindi utilizzare $(...).

Di solito utilizzo quanto segue per rimuovere il percorso e il suffisso dallo script, quindi posso usare sed quando riferisco errori:

arg0=$(basename $0 .sh)

Uso spesso una funzione shell per segnalare errori:

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

Se esiste solo una o forse due uscite di errore, non mi preoccupo della funzione; se ce ne sono altri, lo faccio perché semplifica la codifica. Creo anche funzioni più o meno elaborate chiamate $PERL per fornire un riepilogo su come usare il comando - di nuovo, solo se c'è più di un posto dove sarebbe usato.

Un altro frammento abbastanza standard è un ciclo di analisi delle opzioni, usando $SED shell integrata:

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))

Le virgolette intorno a " $ OPTARG " gestire gli spazi negli argomenti. Il Dflag è cumulativo, ma la notazione usata qui perde traccia di spazi negli argomenti. Esistono anche modi (non standard) per aggirare quel problema.

La prima notazione di turno funziona con qualsiasi shell (o lo farebbe se usassi back-tick invece di '${VAR:=value}'. Il secondo funziona con shell moderne; potrebbe esserci anche un'alternativa con parentesi quadre anziché parentesi, ma funziona così non mi sono preoccupato di capire di cosa si tratta.

Un trucco finale per ora è che ho spesso sia la versione GNU che una versione non GNU dei programmi, e voglio essere in grado di scegliere quale uso. Molti dei miei script, quindi, usano varicapacità come:

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

E poi, quando devo invocare Perl o <=>, lo script utilizza <=> o <=>. Questo mi aiuta quando qualcosa si comporta in modo diverso - posso scegliere la versione operativa - o durante lo sviluppo dello script (posso aggiungere ulteriori opzioni di solo debug al comando senza modificare lo script). (Vedi Espansione dei parametri della shell per informazioni su il <=> e le relative notazioni.)

Altri suggerimenti

Uso la prima serie di righe ## per la documentazione di utilizzo. Non ricordo ora dove l'ho visto per la prima volta.

#!/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.
}

Qualsiasi codice che verrà rilasciato in natura dovrebbe avere la seguente intestazione breve:

# 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

Mantenere un registro delle modifiche attivo nelle intestazioni di codice è un ritorno da quando i sistemi di controllo della versione erano terribilmente scomodi. Un'ultima data modificata mostra a qualcuno quanti anni ha lo script.

Se stai per fare affidamento su bashismi, usa #! / bin / bash, non / bin / sh, poiché sh è l'invocazione POSIX di qualsiasi shell. Anche se / bin / sh punta a bash, molte funzioni verranno disattivate se lo esegui tramite / bin / sh. La maggior parte delle distribuzioni Linux non prenderà script che si basano su basismi, cercano di essere portatili.

Per me, i commenti negli script della shell sono una specie di sciocco a meno che non leggano qualcosa del tipo:

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

Lo scripting della shell è così semplice che (a meno che tu non stia scrivendo una dimostrazione per insegnare a qualcuno come farlo) il codice quasi sempre si annulla da solo.

Ad alcune shell non piace essere alimentate con variabili 'locali'. Credo che oggi Busybox (un comune guscio di salvataggio) sia uno di questi. Rendi GLOBALS_OBVIOUS invece, è molto più facile da leggere, specialmente durante il debug tramite / bin / sh -x ./script.sh.

La mia preferenza personale è lasciare che la logica parli da sola e minimizzi il lavoro per il parser. Ad esempio, molte persone potrebbero scrivere:

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

Dove avrei appena:

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

Allo stesso modo, qualcuno potrebbe scrivere:

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

... dove avrei:

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

L'unica volta che uso convenzionale if / then / else è se c'è un altro-if da inserire nel mix.

Un esempio orribilmente folle di ottimo codice shell portatile può essere studiato semplicemente visualizzando lo script 'configura' nella maggior parte dei pacchetti software gratuiti che usano autoconf. Dico pazzo perché le sue 6300 righe di codice si rivolgono a ogni sistema conosciuto dall'uomo che ha una shell come UNIX. Non vuoi quel tipo di gonfiore, ma è interessante studiare alcuni dei vari hack portabilità all'interno .. come essere gentile con coloro che potrebbero puntare / bin / sh a zsh :)

L'unico altro consiglio che posso dare è guardare la tua espansione in qui-documenti, cioè

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

... espanderà $ name, quando probabilmente vorrai lasciare la variabile al suo posto. Risolvi tramite:

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

che lascerà $ name come variabile, invece di espanderlo.

Consiglio vivamente anche di imparare come usare la trappola per catturare i segnali .. e usare quegli handler come codice del boilerplate. Raccontare uno script in esecuzione per rallentare con un semplice SIGUSR1 è abbastanza utile :)

La maggior parte dei nuovi programmi che scrivo (che sono orientati a strumenti / riga di comando) iniziano come script di shell, è un ottimo modo per prototipare gli strumenti UNIX.

Potrebbe piacerti anche il compilatore di script della shell SHC, provalo qui .

Questa è l'intestazione che uso per la mia shell di script (bash o ksh). È un man simile e viene utilizzato anche per visualizzare use ().

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

Ed ecco le funzioni d'uso da utilizzare 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"; }

Ecco cosa dovresti ottenere:

# 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

Puoi ottenere il modello di script completo qui: http: / /www.uxora.com/unix/shell-script/18-shell-script-template

L'abilitazione del rilevamento degli errori rende molto più semplice il rilevamento precoce dei problemi nello script:

set -o errexit

Esci dallo script al primo errore. In questo modo eviti di continuare a fare qualcosa che dipendeva da qualcosa in precedenza nello script, forse finendo con uno strano stato del sistema.

set -o nounset

Tratta i riferimenti a variabili non impostate come errori. Molto importante per evitare di eseguire cose come rm -you_know_what " $ var / " con un $ var non impostato. Se sai che la variabile può essere disinserita e questa è una situazione sicura, puoi usare $ {var-value} per usare un valore diverso se è disinserito o $ {var: - valore} per utilizzare un valore diverso se non è impostato o vuoto.

set -o noclobber

È facile commettere l'errore di inserire un > dove intendevi inserire < e sovrascrivere alcuni file che intendevi leggere. Se devi bloccare un file nello script, puoi disabilitarlo prima della riga pertinente e abilitarlo nuovamente in seguito.

set -o pipefail

Utilizza il primo codice di uscita diverso da zero (se presente) di un set di comandi inoltrato come codice di uscita del set completo di comandi. Questo semplifica il debug dei comandi in pipe.

shopt -s nullglob

Evita che il tuo / foo / * glob sia interpretato letteralmente se non ci sono file corrispondenti a quell'espressione.

Puoi combinare tutti questi in due righe:

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

Il mio modello bash è il seguente (impostato nella mia 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==========

Vorrei suggerire

#!/bin/ksh

e basta. Commenti a blocchi pesanti per gli script di shell? Prendo i willies.

Suggerimenti:

  1. La documentazione dovrebbe essere dati o codice, non commenti. Almeno una funzione use () . Dai un'occhiata a come ksh e gli altri strumenti AST si documentano con le opzioni --man su ogni comando. (Impossibile collegarsi perché il sito Web non è attivo.)

  2. Dichiara le variabili locali con typeset . Ecco a cosa serve. Non c'è bisogno di cattivi caratteri di sottolineatura.

Quello che puoi fare è creare uno script che crei un'intestazione per uno script & amp; e aprilo automaticamente nel tuo editor preferito. Ho visto un ragazzo farlo in questo sito:

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

In generale, ho alcune convenzioni a cui mi piace attenermi per ogni sceneggiatura che scrivo. Scrivo tutti gli script supponendo che altre persone li possano leggere.

Inizio ogni script con la mia intestazione,

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

Uso quel formato data, per grep / ricerche più semplici. Uso le parentesi "[" per indicare il testo che le persone devono inserire. se si verificano all'esterno di un commento, provo a avviarli con '# ['. In questo modo, se qualcuno li incolla così com'è, non verrà scambiato per input o per un comando di prova. Controlla la sezione di utilizzo in una pagina man, per vedere questo stile come esempio.

Quando voglio commentare una riga di codice, uso un singolo '#'. Quando faccio un commento come nota, utilizzo un doppio "##". Il / etc / nanorc usa anche quella convenzione. Trovo utile differenziare un commento che è stato scelto per non eseguire; versi un commento che è stato creato come una nota.

Tutte le mie variabili shell, preferisco fare in MAIUSCOLO. Cerco di mantenere tra 4 e 8 caratteri, se non diversamente necessario. I nomi si riferiscono, nel miglior modo possibile, al loro utilizzo.

Inoltre, esco sempre con 0 in caso di successo o con 1 per errori. Se lo script presenta molti tipi diversi di errori (e aiuterebbe davvero qualcuno, o potrebbe essere usato in qualche modo in qualche codice), sceglierei una sequenza documentata su 1. In generale, i codici di uscita non sono così rigorosamente applicati nel mondo * nix. Purtroppo non ho mai trovato un buon schema di numeri generali.

Mi piace elaborare gli argomenti in modo standard. Preferisco sempre getopts, getopt. Non faccio mai qualche hack con i comandi 'read' e if statement. Mi piace anche usare l'istruzione case, per evitare if nidificati. Uso uno script di traduzione per lunghe opzioni, quindi --help significa -h per getopts. Scrivo tutti gli script in bash (se accettabile) o sh generico

Non uso MAI simboli interpretati bash (o qualsiasi simbolo interpretato) nei nomi di file o in qualsiasi altro nome. in particolare ... " '' $ & amp; * # () {} [] -, utilizzo _ per gli spazi.

Ricorda, queste sono solo convenzioni. Best practice, di massima, ma a volte sei costretto fuori dalle righe. Il più importante è essere coerenti tra e all'interno dei tuoi progetti.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top