Introduction au shell basée sur le cours
An Introduction to Shell Programing par
Reg Quinton <REGGERS@JULIAN.UWO.CA>
Un shell est un interpreteur de lignes de commande. Il recoit les commandes et les execute. Cela peut s'apparenter a un langage de programmation. Le Bourne shell (ndt:bsh) est utilise pour creer des scripts shell -- en d'autres termes, des programmes interpretes/executes par le shell. Vous pouvez ecrire des scripts shell avec du C-shell; pour des raisons de longueur, nous ne le traiterons pas ici.
Supposez que vous utilisez souvent la commande
find . -name file -print
et que vous prefereriez taper une commande simple telle que
sfind file
Creez un script shell
% cd ~/bin % emacs sfind % page sfind find . -name $1 -print % chmod a+x sfind % rehash % cd /usr/local/bin % sfind tcsh ./shells/tcsh
Ce rapide exemple est loin d'etre tres adequate mais permet les observations suivantes:
%chmod a+x sfind
Tous les scripts Bourne Shell doivent commencer par la sequence
#!/bin/sh
Exemple tire de la page du manuel de la commande exec (2):
"Sur la premiere ligne de l'interpreteur de script suivant le "#!" devra figurer le nom du programme qui doit etre utilise pour interprete le contenu du fichier. Par exemple, si la premiere ligne contient "#! /bin/sh", le contenu du fichier sera alors execute comme un script shell."
Vous pouvez passer outre cet formalite, mais vous ne devriez pas. Tous les bon scripts etablissent explicitement quel interpreteur doit etre utilise. Il y a longtemps, il y avait uniquement un seul interpreteur (Le Bourne Shell) mais aujourd'hui, il y a beaucoup plus d'interpreteurs -- Csh, Ksh, Bash, et bien d'autres.
Les commentaires sont precedes du caractere diese (#). Un commentaire peut commencer n'importe ou sur une ligne et continue jusqu'a le fin de la ligne.
Tous les scripts shell devraient inclure un chemin de recherche specifique:
PATH=/usr/ucb:/usr/bin:/bin; export PATH
PATH=/usr/ucb:/usr/bin:/bin; export PATH
Une specification du PATH est recommandee -- Dans bien des cas le script ne fonctionnera pas chez differents utilisateurs car le chemin de recherche est manquant ou incomplet.
Le Bourne Shell n'exporte pas les variables d'environnement a son heritage a moins que vous l'ayez explicitement declare en utilisant la commande export.
Un bon script shell doit verifier que les arguments passes (s'il y en a) sont corrects
if [ $# -ne 3 ]; then
echo 1>&2 Usage: $0 19 Oct 91
exit 127
fi
Ce script necessite trois arguments et averti en cas de probleme.
Toutes les applications Unix doivent retourner un status de sortie (ndt: Pas forcement, si vous proggez a la porc, y'en a pas besoin..Enjoy :)
# is the year out of range for me?
if [ $year -lt 1901 -o $year -gt 2099 ]; then
echo 1>&2 Year \"$year\" out of range
exit 127
fi
etc...
# All done, exit ok
exit 0
Un status de sortie different de zero indique une condition d'erreur alors qu'un status a zero indique le script s'est execute correctement.
Sur les systemes BSD, il y a un classement en categorie des codes de sorties les plus souvent usites. Voir /usr/include/sysexits.h
les codes de sorties sont important pour la plupart des gens qui utilisent votre code. Beaucoup construisent des tests sur le status de sortie d'une commande.
La condition de construction est:
if command; then
command
fi
Par exemple,
if tty -s; then
echo Enter text end with \^D
fi
Votre code devrait etre ecrit dans la vision que d'autres pourront l'utiliser. Assurez vous que vous retournez un code de sortie significatif. Cela les aidera beaucoup.
Entree standard, sortie standard et la sortie d'erreur sont les descripteurs de fichier 0, 1 et 2. Chacun a un role particulier et doit etre utilise a propos.
# is the year out of range for me?
if [ $year -lt 1901 -o $year -gt 2099 ]; then
echo 1>&2 Year \"$year\" out of my range
exit 127
fi
etc...
# ok, you have the number of days since Jan 1, ...
case `expr $days % 7` in
0)
echo Mon;;
1)
echo Tue;;
etc...
Le message d'erreur doit apparaitre sur stderr et pas sur stdout! Les sorties doivent apparaitre sur stdout. Comme pour le dialogue input/outpout:
# give the fellow a chance to quit
if tty -s ; then
echo This will remove all files in $* since ...
echo $n Ok to procede? $c; read ans
case "$ans" in
n*|N*)
echo File purge abandoned;
exit 0 ;;
esac
RM="rm -rfi"
else
RM="rm -rf"
fi
Note: Ce code se comporte differement s'il doit communiquer avec un utilisateur. (Si l'entree standard est un tty plutot qu'un pipe, ou un fichier, ou etc. Voir tty(1)).
La boucle POUR
Elle substitue les variables et effectues les taches comprises dans la boucle:
for variable in word ...
do
command
done
Par exemple:
for i in `cat $LOGS`
do
mv $i $i.$TODAY
cp /dev/null $i
chmod 664 $i
done
Vous pourrez aussi voir le code ecrit comme cela:
for variable in word ...; do command; done
Selectionne les actions a effectuer en fonction des correspondances de cas
case word in
[ pattern [ | pattern ... ] )
command ;; ] ...
esac
Par exemple: case "$year" in [0-9][0-9]) year=19${year} years=`expr $year - 1901` ;; [0-9][0-9][0-9][0-9]) years=`expr $year - 1901` ;; *) echo 1>&2 Year \"$year\" out of range ... exit 127 ;; esac
if commandPar exemple:
then
command
[ else
command ]
fi
if [ $# -ne 3 ]; thenVous pourrez aussi voir:
echo 1>&2 Usage: $0 19 Oct 91
exit 127
fi
if command; then command; [ else command; ] fi
Repete la tache tant que la condition retourne un code de retour valide
{while | until} commandPar exemple:
do
command
done
# Pour chaque argument mentionne, on purge le repertoireVous pouvez aussi voir
while [ $# -ge 1 ]; do
_purge $1
shift
done
while command; do command; done
Les variables sont des sequences de lettres, de chiffres ou de traits commencant avec une lettre ou un underscore. Pour obtenir le contenu d'une variable, vous devez preceder le nom de la variable par le symbole $.
Les variables numeriques (comme $1, etc.) sont des variables de position pour le passage des arguments.
Assigner directement une valeur a une variable
(variable=valeur). Par exemple:
PATH=/usr/ucb:/usr/bin:/bin; export PATHOu
TODAY=`(set \`date\`; echo $1)`
Les variables ne sont pas exportees vers les variables filles a moins que cela soit explicitement marque.
# Nous devons avoir une variable d'environnement DISPLAYPour les variables comme PRINTER, vous pouvez les mettre dans le .profile de l'utilisateur, ce qui evitera de le specifier dans chaque programme
if [ "$DISPLAY" = "" ]; then
if tty -s ; then
echo "DISPLAY (`hostname`:0.0)? \c";
read DISPLAY
fi
if [ "$DISPLAY" = "" ]; then
DISPLAY=`hostname`:0.0
fi
export DISPLAY
fi
PRINTER=PostScript; export PRINTERNote: Le Cshell (CSH) exportes toutes les variables d'environnement.
Utiliser $variable (ou si necessaire ${variable}) pour referencer une valeur.
#La plupart des utilisatejurs ont un /bin propreLes accolades sont necessaires pour la concatenation.
if [ "$USER" != "root" ]; then
PATH=$HOME/bin:$PATH
else
PATH=/etc:/usr/etc:$PATH
fi
$p_01La valeur de la variable "p_01".
${p}_01La valeur de la variable "p" avec "_01" accole a la fin
${variable-word}Si la variable a ete declaree, elle utilise sa valeur, sinon, elle utilise le mot.
POSTSCRIPT=${POSTSCRIPT-PostScript};Si la variable a ete declaree et qu'elle est non nulle, alors elle utilise sa valeur, sinon, elle utilise le mot.
export POSTSCRIPT
${variable:-word}
Ce sont des constructions tres utiles pour le partage de l'environement utilisateur. L'utilisateur du script peut outrepasser les assignements de variable. Conferez vous aux programmes comme lpr(1) definissant la variable d'environnement PRINTER, vous pouvez utiliser la meme astuce avec vos scripts.
${variable:?word}Si la variable est declaree, alors on utilise sa valeur, sinon, on ecrit le mot et on sort.
Les arguments des lignes de commande sont des arguments passe aux scripts shell comme des variables positionnees:
$0, $1, ...La commande et les arguments. Avec $0, on obtient la commande passee; le reste correspond aux arguments
$#Permet d'obtenir le nombre d'arguments
$*, $@Tous les arguments passes dans une chaine separes par des espaces. Voir le man pour connaitre la difference entre "$*" et "$@".
shiftDecale la position des variables d'une place et decremente le nombre d'arguments.
set arg arg ...Ajoute les variables positionnees a la liste d'argument.
Parcours de la ligne d'arguments en utilisant shift:
#parcourir la liste d'argumentsUne utilisation de la commande SET:
while [ $# -ge 1 ]; do
case $1 in
process arguments...
esac
shift
done
#Ressors le jour
TODAY=`(set \`date\`; echo $1)`
cd $SPOOL
for i in `cat $LOGS`
do
mv $i $i.$TODAY
cp /dev/null $i
chmod 664 $i
done
$$L'ID du process en cours. Ceci est tres utile pour contruire des fichiers temporaires.
tmp=/tmp/cal0$$Le status de sortie de la derniere commande.
trap "rm -f $tmp /tmp/cal1$$ /tmp/cal2$$"
trap exit 1 2 13 15
/usr/lib/calprog >$tmp
$?
$command
# Run target file if no errors and ...
if [ $? -eq 0 ]
then
etc...
fi
Caracteres speciaux pour terminer les mots:
; & ( ) | ^ < > new-line space tabCes caracteres sont utilises pour les sequences de commande, les travaux en tache de fond, etc. Pour apostropher (ndt: pour ceux qui ne comprendraient pas "mettre out") un de ces caracteres, il faut utiliser un anti-slash ou une parenthese avec les guillemets ("" ou '')
Les apostrophes simples
A l'interieur des apostrophes, tous les caracteres sont apostrophes (NDT:sans fonction) - ce qui comprends aussi l'anti-slash. Le resultat est appele mot.
grep :${gid}: /etc/group | awk -F: '{print $1}'Les guillemets
A l'interieur des guillemets, la substitution des variables s'effectue (par exemple, le signe dollar est interprete), mais pas la generation de nom de fichiers. Le resultat est appele mot.
if [ ! "${parent}" ]; thenL'accent grave
parent=${people}/${group}/${user}
fi
L'accent grave permet l'emulation de la commande passee et substitue la sortie.
if [ "`echo -n`" = "-n" ]; then
n=""
c="\c"
else
n="-n"
c=""
fi
et
TODAY=`(set \`date\`; echo $1)`
Les fonctions sont des composantes tres importants dans l'univers du script shell. Cependant, elle sont loin d'etre suffisament employees. La syntaxe est:
name ()Par exemple:
{
commands
}
#Purger un repertoireA l'interieur d'une fonction, les parametres positionnes $0, $1, etc. sont les arguments passes a la fonction et non pas ceux passes a la ligne de commande.
_purge()
{
#On verifie que c'est bien un repertoire.
if [ ! -d $1 ]; then
echo $1: No such directory 1>&2
return
fi
etc...
}
A l'interieur d'une fonction, utilisez return au lieu d'exit.
Les fonctions sont pratique pour l'encalpsulation. vous pouvez piper, rediriger a l'interieur, etc. vers d'autres fonctions. Par exemple:
#Traite un fichier, ajoute les personnes une par une.
do_file()
{
while parse_one
etc...
}
etc...
#Prends l'entree standard (ou un fichier specifie) et effectue le traitement.
if [ "$1" != "" ]; then
cat $1 | do_file
else
do_file
fi
Vous pouvez executer des scripts shell a l'interieur de script shell. Il y a deux possibilites:
sh command
Lance le script shell dans un shell separe. Par exemple, sur des machines Sun dans /etc/rc:
sh /etc/rc.local. command
Lance le script shell a l'interieur du script en cours
#Lis les infos de configQuel est l'interet de chacune de ces commandes? quelles sont les differences? La seconde forme est tres pratique pour les fichiers de configuration où les variables d'environnement sont declarees pour le script. Par exemple:
. /etc/hostconfig
for HOST in $HOSTS; doL'utilisation des fichiers de configuration dans cette optique rend possible l'ecriture de scripts qui sont automatiquement configures selon l'environnement.
#Y a t'il un fichier de config pour cet hote?
if [ -r ${BACKUPHOME}/${HOST} ]; then
. ${BACKUPHOME}/${HOST}
fi
etc...
La commande la plus puissante est la commande test(1).
if test expression; thenet (l'argument doit etre entre les crochets)
etc...
if [ expression ]; thenSur les machines dotees du systemes V, cela est implicite (regardez le man de /bin/test)
etc...
Sur les machines dotees de BSD (comme les machines Suns) comparez la commande /usr/bin/test avec /usr/bin/[.
Les expressions les plus employees sont:
test { -w, -r, -x, -s, ... } filenameLe fichier est il ouvert en ecriture, lisible, executable, vide, etc.?
test n1 { -eq, -ne, -gt, ... } n2sont egaux, differents, superieurs, etc?
test s1 { =, != } s2Les chaines sont elles identiques ou differentes?
test cond1 { -o, -a } cond2OU binaire; ET binaire; l'utilisation de ! signifie la negation.
Par exemple
if [ $year -lt 1901 -o $year -gt 2099 ]; thenApprenez cette commande par coeur, elle vous sera tres utile.
echo 1>&2 Year \"$year\" out of range
exit 127
fi
La commande test fournit une comparaison sur une correspondance limite en caractere. L'astuce reside dans l'utilisation d'un cas.
#Parcours la liste d'argumentsBien sur, l'emploi de la commande getopt sera recommandee.
while [ $# -ge 1 ]; do
case $1 in
-c*) rate=`echo $1 | cut -c3-`;;
-c) shift; rate=$1 ;;
-p*) prefix=`echo $1 | cut -c3-`;;
-p) shift; prefix=$1 ;;
-*) echo $Usage; exit 1 ;;
*) disks=$*; break ;;
esac
shift
done
Sur un systeme BSD, pour obtenir le prompt, nous utiliserions:
echo -n Ok to procede?; read ansSur un systeme SysV, il faut dire:
echo Ok to procede? \c; read ansDans un effort de portabilite du code, nous ecrirons:
#Precise quel echo il faut utiliser
if [ "`echo -n`" = "-n" ]; then
n=""; c="\c"
else
n="-n"; c=""
fi
etc...
echo $n Ok to procede? $c; read ans
La traditoin Unix veut qu'un programme soit execute le plus rapidement possible. Specialement pour les pipelines, les cron jobs, etc.
L'utilisation des prompts n'est pas necessaire s'il n'y a pas d'utilisateur.
#Si jamais il y a une personne, laissez lui une chance...La tradition (ndt: encore elle) veut aussi que l'on determine vers ou est redirigee la sortie
if tty -s; then
echo Enter text end with \^D
fi
# Si la sortie est redirigee vers un terminal, soyez prolixe a souhait :)ATTENTION: Ce n'est pas parce que stdin est un tty que stdout le sera forcement. Le prompt peut etre redirige vers un terminal.
if tty -s <&1; then
verbose=true
else
verbose=false
fi
# S'il y a qqn dans les parages, donnez lui une chance de surpasser l'envie de RAZ ;pAvez vous deja vu un programme arreter d'attendre l'appui d'une touche parce que la sortie est redirigee ailleurs?
if tty -s; then
echo Enter text end with \^D >&0
fi
Nous avons l'habitude de rediriger les entrees. Par exemple
#Prennez l'entree standard (ou un fichier specifique) et faites cela.Alternativement, redirection d'un fichier
if [ "$1" != "" ]; then
cat $1 | do_file
else
do_file
fi
#Prennez l'entree standard (ou un fichier specifique) et faites cela.Vous pouvez ainsi construire des fichiers a la volee.
if [ "$1" != "" ]; then
do_file < $1
else
do_file
fi
rmail bsmtp <<EOF helo news mail from:><$1@newshost.uwo.ca>Note : Les variables sont definies dans l'entrees.
rcpt to:<LISTSERV@$3>
data
from: <$1@newshost.uwo.ca>
to:<LISTSERV@$3>
Subject: Signon $2
subscribe $2 Usenet Feeder at UWO
.
quit
EOF
Une des choses les plus courantes que vous aurez a faire est de parcourir une chaine. Quelques conseils:
TIME=`date | cut -c12-19`Avec beaucoup d'attention, redefinir les separateurs des champs d'entrees peut etre tres utile.
TIME=`date | sed 's/.* .* .* \(.*\) .* .*/\1/'`
TIME=`date | awk '{print $4}'`
TIME=`set \`date\`; echo $4`
TIME=`date | (read u v w x y z; echo $x)`
#!/bin/sh
# convert IP number to in-addr.arpa name
name()
{ set `IFS=".";echo $1`
echo $4.$3.$2.$1.in-addr.arpa
}
if [ $# -ne 1 ]; then
echo 1>&2 Usage: bynum IP-address
exit 127
fi
add=`name $1`
nslookup < < EOF | grep "$add" | sed 's/.*= //'
set type=any
$add
EOF
Le shell a beaucoup d'indicateurs qui permet un debugage tres aise:
sh -n command
Lit le script mais ne l'execute pas.
sh -x command
Affiche les commandes et les arguments comme ils seront executes. Dans beaucoup de mes scripts, vous verrez afficher:
# Uncomment the next line for testing
# set -x