Aspects avancés de la programmation shell

Comparatif des variables $* et $@

Utilisation de $* et de $@

Les variables $* et $@ contiennent la liste des arguments d'un script shell.
Lorsqu'elles ne sont pas entourées par des guillemets, elles sont équivalentes.

Exemple :

$ nl scr01.sh
     1  #!/bin/bash
     2  cpt=1
     3  echo "Utilisation de la variable \$*"
     4  for arg in $*
     5  do
     6          echo "Argument $cpt : $arg"
     7          ((cpt+=1))
     8  done
     9  cpt=1
    10  echo "Utilisation de la variable \$@"
    11  for arg in $@
    12  do
    13          echo "Argument $cpt : $arg"
    14          ((cpt+=1))
    15  done
    16  exit 0
$ ./scr01.sh a b "c d e" f
Utilisation de la variable $*
Argument 1 : a
Argument 2 : b
Argument 3 : c
Argument 4 : d
Argument 5 : e
Argument 6 : f
Utilisation de la variable $@
Argument 1 : a
Argument 2 : b
Argument 3 : c
Argument 4 : d
Argument 5 : e
Argument 6 : f
$

Interprétation :

  1. $* et $@ contiennent exactement la même liste d'arguments.
  2. Les guillemets protégeant les arguments ne sont pas pris en compte
  3. Ce sont les espaces qui délimitent les arguments

Utilisation de "$*"

Les guillemets autour de $* supprime la signification des espaces contenus dans $*.

Exemple :

$ nl scr02.sh
     1  #!/bin/bash
     2  cpt=1
     3  echo "Utilisation de la variable \"\$*\""
     4  for arg in "$*"
     5  do
     6          echo "Argument $cpt : $arg"
     7          ((cpt+=1))
     8  done
     9  exit 0
$ ./scr02.sh a b c "d e f" g
Utilisation de la variable "$*"
Argument 1 : a b c d e f g
$

Interprétation :

  1. Les guillemets entourant les arguments ne sont pas pris en compte.
  2. Tous les arguments sont considérés comme étant un seul argument.

Utilisation de "$@"

La variable $@ placée entre guillemets permet de conserver la protection des arguments par les guillemets.

Exemple :

$ nl scr03.sh
     1  #!/bin/bash
     2  cpt=1
     3  echo "Utilisation de la variable \"\$@\""
     4  for arg in "$@"
     5  do
     6          echo "Argument $cpt : $arg"
     7          ((cpt+=1))
     8  done
     9  exit 0
$ ./scr03.sh a b c "d e f" g
Utilisation de la variable "$@"
Argument 1 : a
Argument 2 : b
Argument 3 : c
Argument 4 : d e f
Argument 5 : g
$

Interprétation :

  1. Les espaces délimitent la liste des arguments.
  2. Les arguments placés entre guillemets sont considérés comme étant un seul argument

Substitution de variables

Les shells BASH et KSH offrent des fonctionnalités supplémentaires au niveau des substitution de variables.

Connaitre la longueur d'une chaine d'une variable :

Syntaxe :

${#variable}

Exemple :

$ variable="Une chaine de texte"
$ echo $variable
Une chaine de texte
$ echo "La chaine contient ${#variable} caractères"
La chaine contient 19 caractères
$

Manipuler des chaines de caractères :

Exemple avec la variable suivante :

$ variable="col1|col2|col3|col4|col5"
$ echo $variable
col1|col2|col3|col4|col5
$

Cette variable est constituée de 5 segments délimités par le caractère |

Le contenu de la variable n'est jamais modifié

Exclure le premier segment

Syntaxe :

${variable#modele}

modele est une chaine de caractère incluant les caractères spéciaux *, ?, [ ], ?(expression), +(expression), *(expression), @(expression), !(expression)

Rappel des expressions pour la substitution des noms de fichiers

Le caractère # signifie "Chaine la plus courte possible en partant de la gauche"

Exemple :

$ echo ${variable#*|}
col2|col3|col4|col5
$

Conserver le dernier segment

Syntaxe :

${variable##modele}

Les caractères ## signifient "Chaine la plus longue possible en partant de la gauche"

Exemple :

$ echo ${variable##*|}
col5
$

Exclure le dernier segment

Syntaxe :

${variable%modele}

Le caractère % signifie "Chaine la plus courte possible en partant de la droite"

Exemple :

$ echo ${variable%|*}
col1|col2|col3|col4
$

Conserver le premier segment

Syntaxe :

${variable%%modele}

Les caractères %% signifient "Chaine la plus longue possible en partant de la droite"

Exemple :

$ echo ${variable%%|*}
col1
$

En bash, ne pas oublier d'activer l'option extglob avec la commande shopt -s extglob afin de permettre au shell d'interpréter les modèles utilisant les expressions complexes.

Tableaux

Avec les shells récents, il est possible d'utiliser des tableaux à 1 dimension.
Les éléments du tableau commencent toujours à l'indice 0.

Ajouter un élément à un tableau :

Syntaxe :

tableau[indice]=valeur

Exemple :

$ tableau[0]=15
$ tableau[3]=20
$

Les indices non initialisés sont vides.

Afficher un élément d'un tableau :

Syntaxe :

${tableau[indice]}

Exemple :

$ echo ${tableau[3]}
20
$ echo ${tableau[0]}
15
$ echo ${tableau[2]}
 
$

L'indice 2 du tableau est vide.

Initialiser un tableau avec plusieurs valeurs :

Syntaxe :

tableau=(valeur1 valeur2 valeur3 ..... valeurn)

Exemple :

$ tableau=(02 40 35 68 98 45 52 03)
$ echo ${tableau[0]}
02
$ echo ${tableau[2]}
35
$

Toutes les précédentes valeurs contenues dans le tableaux sont effacées.

Afficher tous les éléments d'un tableau :

Syntaxe :

${tableau[*]}

Exemple :

$ echo ${tableau[*]}
02 40 35 68 98 45 52 03
$

Afficher le nombre d'éléments d'un tableau :

Syntaxe :

${#tableau[*]}

Exemple :

$ echo ${#tableau[*]}
8
$

Obtenir la longueur d'un élément d'un tableau :

Syntaxe :

${#tableau[indice]}

Exemple :

$ echo ${#tableau[0]}
2
$

Parcourir tous les éléments d'un tableau :

$ tab=("un" "deux" "trois")
$ for m in ${tab[@]}; do echo $m; done
un
deux
trois
$

Initialisation des paramètres positionnels avec set

La commande set utilisée sans option mais suivie d'arguments affecte ces derniers aux paramètres positionnels $1, $2, $3 ....., $*, $@ et $#.

Cela permet de manipuler plus facilement le résultat de diverses substitutions.

Exemple :

$ ls
fichier_0  fichier_2  fichier_4  fichier_6  fichier_8
fichier_1  fichier_3  fichier_5  fichier_7  fichier_9
$ set `ls`
$

La liste des fichiers obtenue avec la commande ls est maintenant affecté aux paramètres positionnels.

$ echo $#
10
$ echo $*
fichier_0 fichier_1 fichier_2 fichier_3 fichier_4 fichier_5 fichier_6 fichier_7 fichier_8 fichier_9
$ echo $1
fichier_0
$ echo $3
fichier_2
$ echo $9
fichier_8
$ echo ${10}
fichier_9
$

Les fonctions

Les fonctions permettent de regrouper et d'exécuter des commandes à différents endroits d'un script.

Cela permet de créer des fonctions personnalisées réutilisables.

Définition d'une fonction

Une fonction doit être définie au début d'un script, avant sa première utilisation.

Il existe 2 syntaxes permettant de définir une fonction.

Première syntaxe :

Ce sont les doubles parenthèses ( ) qui indique au shell la définition d'une fonction

Définition d'une fonction

maFonction () {
     commande1
     commande2
     .....
}

Appel d'une fonction

maFonction

Seconde syntaxe :

Le mot clé function remplace les doubles parenthèses ( )

Définition d'une fonction

function maFonction {
     commande1
     commande2
     .....
}

Appel d'une fonction

maFonction

Une fonction peut être appelée aussi bien à partir du programme principal qu'à partir d'une autre fonction.

Exemple :

$ nl fonction01.sh
     1  #!/bin/bash
 
     2  fctn01 () {
     3          echo "Fonction fctn01"
     4  }
 
     5  function fctn02 {
     6          echo "Fonction fctn02"
     7  }
 
     8  echo "Début du programme principal"
     9  echo "Appel de la fonction fctn01"
    10  fctn01
    11  echo "Appel de la fonction fctn02"
    12  fctn02
    13  echo "Fin du programme principal"
    14  exit 0
$ ./fonction01.sh
Début du programme principal
Appel de la fonction fctn01
Fonction fctn01
Appel de la fonction fctn02
Fonction fctn02
Fin du programme principal
$

Dès qu'une fonction est définie, celle-ci est considérée par le shell comme étant une commande interne.

Code de retour d'une fonction

Comme toutes commandes Linux, une fonction retourne également un code d'erreur.
Si le code erreur n'est pas spécifié, celui retourné par défaut correspond au code erreur de la dernière commande exécutée dans la fonction.
La commande return permet de retourner le code erreur de la fonction concernée. Ce code doit obligatoirement correspondre à un nombre compris entre 0 et 255.
Le code erreur retourné par la fonction est récupérable grâce à la variable $?.

Exemple :

Le script suivant test si l'utilisateur saisi existe sur le système.

$ nl fonction02.sh
     1  #!/bin/bash
 
     2  function pause {
     3          echo "Appuyer sur Entrée pour continuer"
     4          read x
     5  }
 
     6  function existUser {
     7          echo -e "Saisir le nom d'un utilisateur : \c"
     8          read user
     9          if grep -q "^$user:" /etc/passwd ; then
    10                  return 0
    11          fi
    12          return 1
    13  }
 
    14  while true
    15  do
    16          clear
    17          echo "- 1 - Savoir si un utilisateur existe"
    18          echo "- 2 - Connaitre l'UID d'un utilisateur"
    19          echo "- 3 - Fin"
    20          echo -e "Votre choix : \c"
    21          read choix
    22          case $choix in
    23                  1)      if existUser
    24                          then
    25                                  echo "L'utilisateur $user existe"
    26                          else
    27                                  echo "l'utilisateur $user n'existe pas"
    28                          fi
    29                          ;;
 
    30                  2)      echo "Option non disponible"
    31                          ;;
 
    32                  3)      exit 0
    33                          ;;
    34          esac
    35          pause
    36  done
$

Portée des variables

Dans un script shell, sans définition particulière, toutes les variables utilisées sont globales à tout le script.

Qu'une variable soit définie au niveau du programme principal ou d'une fonction, elle est accessible n'importe où dans le script.

Par exemple, dans le script fonction02.sh, la variable $user est initialisée dans la fonction existUser (ligne 8) et utilisée également au niveau du programme principal (ligne 25 et 27).

$ nl fonction02.sh
     1  #!/bin/bash
 
     2  function pause {
     3          echo "Appuyer sur Entrée pour continuer"
     4          read x
     5  }
 
     6  function existUser {
     7          echo -e "Saisir le nom d'un utilisateur : \c"
     8          read user
     9          if grep -q "^$user:" /etc/passwd ; then
    10                  return 0
    11          fi
    12          return 1
    13  }
 
    14  while true
    15  do
    16          clear
    17          echo "- 1 - Savoir si un utilisateur existe"
    18          echo "- 2 - Connaitre l'UID d'un utilisateur"
    19          echo "- 3 - Fin"
    20          echo -e "Votre choix : \c"
    21          read choix
    22          case $choix in
    23                  1)      if existUser
    24                          then
    25                                  echo "L'utilisateur $user existe"
    26                          else
    27                                  echo "l'utilisateur $user n'existe pas"
    28                          fi
    29                          ;;
 
    30                  2)      echo "Option non disponible"
    31                          ;;
 
    32                  3)      exit 0
    33                          ;;
    34          esac
    35          pause
    36  done
$

Définition de variables locales

La commande typeset permet de définir des variables locales à une fonction.

Syntaxe :

typeset variable
typeset variable=valeur

Exemple :

$ nl fonction03.sh
     1  #!/bin/bash
     2  function f1 {
     3          # var1 est une variable locale
     4          typeset var1
     5          echo "Dans la fonction f1 => var1 avant : $var1"
     6          var1=100
     7          echo "Dans la fonction f1 => var1 après : $var1"
     8          echo "Dans la fonction f1 => var2 avant : $var2"
     9          var2=200
    10          echo "Dans la fonction f1 => var2 après : $var2"
    11  }
    12  # var1 et var2 sont des variables globales
    13  var1=1
    14  var2=2
    15  echo "Dans le programme principal => var1 avant appel f1 : $var1"
    16  echo "Dans le programme principal => var2 avant appel f1 : $var2"
    17  f1
    18  echo "Dans le programme principal => var1 après appel f1 : $var1"
    19  echo "Dans le programme principal => var2 après appel f1 : $var2"
    20  exit 0
$ ./fonction03.sh
Dans le programme principal => var1 avant appel f1 : 1
Dans le programme principal => var2 avant appel f1 : 2
Dans la fonction f1 => var1 avant :
Dans la fonction f1 => var1 après : 100
Dans la fonction f1 => var2 avant : 2
Dans la fonction f1 => var2 après : 200
Dans le programme principal => var1 après appel f1 : 1
Dans le programme principal => var2 après appel f1 : 200
$

2 variables globales var1 et var2 sont définies et initialisées ligne 13 et 14.
1 variable locale var1 est définie dans la fonction ligne 4 et initialisée à 100 ligne 6.
Après exécution de la fonction f1, la variable globale var1 a conservée sa valeur (1) alors que la variable globale var2 a été modifiée.

Passage d'arguments

Dans un script shell, il est tout à fait possible de passer des arguments à une fonction étant donné qu'une fonction est reconnue par le shell comme étant une commande à part entière.

Ces arguments sont récupérables dans les fonctions grâce aux variables spéciales $1, $2, $3, ....., ${10} ......$*, $@ et $#. Ces variables sont aussi locales aux fonctions.

Par contre, la variable $0 contient toujours le nom du script.

Exemple :

$ nl fonction04.sh
     1  #!/bin/bash
 
     2  function f1 {
     3          echo "Arguments de la fonction f1 :"
     4          echo "\$0 => $0"
     5          echo "\$1 => $1"
     6          echo "\$2 => $2"
     7          echo "\$3 => $3"
     8          echo "\$* => $*"
     9          echo "\$# => $#"
    10  }
 
    11  function f2 {
    12          echo "Arguments de la fonction f2 :"
    13          echo "\$0 => $0"
    14          echo "\$1 => $1"
    15          echo "\$2 => $2"
    16          echo "\$3 => $3"
    17          echo "\$* => $*"
    18          echo "\$# => $#"
    19  }
 
    20  function f3 {
    21          echo "Arguments de la fonction f3 :"
    22          echo "\$0 => $0"
    23          echo "\$1 => $1"
    24          echo "\$2 => $2"
    25          echo "\$3 => $3"
    26          echo "\$* => $*"
    27          echo "\$# => $#"
    28  }
 
    29  echo "Arguments du programme principal :"
    30  echo "\$0 => $0"
    31  echo "\$1 => $1"
    32  echo "\$2 => $2"
    33  echo "\$3 => $3"
    34  echo "\$* => $*"
    35  echo "\$# => $#"
 
    36  # Appel de la fonction f1 avec 3 arguments
    37  f1 a b c
 
    38  # Appel de la fonction f2 avec 3 arguments
    39  f2 file.c 2000 500
 
    40  # Appel de la fonction f3 avec 2 arguments provenant du programme principal
    41  f3 $2 $3
 
    42  exit 0
$

Appel du script fonction04.sh avec 3 arguments :

$ ./fonction04.sh arg1 arg2 arg3
Arguments du programme principal :
$0 => ./fonction04.sh
$1 => arg1
$2 => arg2
$3 => arg3
$* => arg1 arg2 arg3
$# => 3
Arguments de la fonction f1 :
$0 => ./fonction04.sh
$1 => a
$2 => b
$3 => c
$* => a b c
$# => 3
Arguments de la fonction f2 :
$0 => ./fonction04.sh
$1 => file.c
$2 => 2000
$3 => 500
$* => file.c 2000 500
$# => 3
Arguments de la fonction f3 :
$0 => ./fonction04.sh
$1 => arg2
$2 => arg3
$3 =>
$* => arg2 arg3
$# => 2
$

Exploiter l'affichage d'une fonction

Comme n'importe quelle commande renvoyant un résultat, une fonction peut également être placée à l'intérieur de caractères de substitution de commande `` ou $( ).

Exemple :

$ nl fonction05.sh
     1  #!/bin/bash
 
     2  function getUid {
     3          grep "^$1:" /etc/passwd | cut -d':' -f3
     4  }
 
     5  # Initialisation de la variable globale uid
     6  uid=""
 
     7  # Appel de la fonction getUid avec l'argument du programme principal
     8  # Juste pour l'affichage
     9  getUid $1
 
    10  # Affectation du résultat de la fonction getUid à la variable uid
    11  uid=$(getUid $1)
 
    12  if [[ $uid != "" ]]
    13  then
    14          echo "L'utilisateur $1 a pour UID : $uid"
    15  else
    16          echo "L'utilisateur $1 n'existe pas"
    17  fi
 
    18  exit 0
$ ./fonction05.sh root
0
L'utilisateur root a pour UID : 0
$

Exemple complet

Exemple d'un script reprenant toutes les commandes et fonctions vues précédement

$ nl fonction06.sh
     1  #!/bin/bash
 
     2  # Pour faire une pause
     3  function pause {
     4          echo "Appuyer sur Entrée pour continuer"
     5          read x
     6  }
 
     7  # Pour savoir si un utilisateur existe
     8  function existUser {
     9          grep -qi "^$1:" /etc/passwd && return 0
    10          return 1
    11  }
 
    12  # Pour connaitre l'uid de l'utilisateur
    13  function getUid {
    14          grep -i "^$1:" /etc/passwd | cut -d':' -f3
    15  }
 
    16  # Initialisation des variables globales
    17  uid=""
    18  user=""
    19  choix=""
 
    20  while true
    21  do
    22          clear
    23          echo "- 1 - Savoir si un utilisateur existe"
    24          echo "- 2 - Connaitre l'UID d'un utilisateur"
    25          echo "- 3 - Fin"
    26          echo -e "Votre choix : \c"
    27          read choix
 
    28          if [[ $choix = @(1|2) ]] ; then
    29                  echo -e "Saisir le nom d'un utilisateur : \c"
    30                  read user
    31          fi
 
    32          case $choix in
 
    33                  1)      if existUser $user ; then
    34                                  echo "L'utilisateur $user existe"
    35                          else
    36                                  echo "l'utilisateur $user n'existe pas"
    37                          fi
    38                          ;;
 
    39                  2)      if existUser $user ; then
    40                                  uid=$(getUid $user)
    41                                  echo "l'UID de l'utilisateur $user est : $uid"
    42                          else
    43                                  echo "L'utilisateur $user n'existe pas"
    44                          fi
    45                          ;;
 
    46                  3)      exit 0
    47                          ;;
    48          esac
    49          pause
    50  done
$

Commandes d'affichage

La commande print

En KSH, la commande print apporte des fonctionnalités supplémentaires à la commande echo.

Exemple :

Utilisation simple

$ print coucou
coucou
$

Supprimer le saut de ligne

$ print -n coucou
coucou$

Afficher des arguments commançant par le caractère "-"

$ print - "-i : Option invalide"
-i : Option invalide
$

Ecrire sur un descripteur particulier

$ print -u2 "Message d'erreur"
Message d'erreur
$

Comparaison avec la commande echo

$ echo "Message d'erreur" 1>&2
Message d'erreur
$

 

La commande printf

En BASH, cette commande est identique à celle du langage C.
Elle permet de formater les affichages.

Syntaxe :

printf chaine expr1 expr2 ..... exprn

chaine représente la chaîne qui sera affichée à l'écran.
Elle peut contenir des formats qui seront substitués par la valeur des expressions citées à sa suite.
Il doit y avoir autant de formats que d'expressions.

Exemple de formats utilisés.

%20s Affichage d'une chaine (string) sur 20 positions avec cadrage à droite
%-20s Affichage d'une chaine (string) sur 20 positions avec cadrage à gauche
%3d Affichage d'un entier (décimal) sur 3 positions avec cadrage à droite
%03d Affichage d'un entier (décimal) sur 3 positions avec cadrage à droite et complété avec des 0 à gauche
%-3d Affichage d'un entier (décimal) sur 3 positions avec cadrage à gauche
%+3d Affichage d'un entier (décimal) sur 3 positions avec cadrage à droite et affichage systématique du signe (un nombre négatif est toujours affiché avec son signe)
%10.2f Affichage d'un nombre flottant sur 10 positions dont 2 décimales
%+010.2f Affichage d'un nombre flottant sur 10 positions dont 2 décimales, complété par des 0 à gauche, avec cadrage à droite et affichage systématique du signe

Exemple :

$ article="Livres"
$ quantite=3
$ prix=3,5
$ printf "%-20s***%03d***%+10.2f\n" $article $quantite $prix
Livres              ***003***     +3,50
$

En utilisant un tableau

$ liste=(livre 10 3,5 cd 5 10,65 dvd 7 19,70 bd 80 5,25)
$ printf "%-20s***%03d***%+10.2f\n" ${liste[*]}
livre               ***010***     +3,50
cd                  ***005***    +10,65
dvd                 ***007***    +19,70
bd                  ***080***     +5,25
$

Gestion des entrées / sorties d'un script

Redirection des entrées/sorties standard

La commande interne exec permet de manipuler les descripteurs de fichier du shell courant.
Utilisée à l'intérieur d'un script, elle permet de rediriger de manière globale les entrées/sorties de celui-ci.

Rediriger l'entrée standard d'un script :

exec 0< fichier1

Toutes les commandes du script placées après cette directive et qui lisent leur entrée standard vont extraire leurs données à partir de fichier1.
Il n'y aura donc pas d'interaction avec le clavier.

Rediriger la sortie standard et la sortie d'erreur standard d'un script :

exec 1> fichier1 2> fichier2

Toutes les commandes du script placées après cette directive et qui écrivent sur leur sortie standard enverront leurs résultat dans fichier1.
Celles qui écrivent sur leur sortie d'erreur standard enverront leurs erreurs dans fichier2.

Rediriger la sortie standard et la sortie d'erreur standard d'un script dans un même fichier :

exec 1> fichier1 2>&1

Toutes les commandes du script placées après cette directive enverront leurs résultats et leurs erreurs dans fichier1

Premier exemple :

Redirection de la sortie standard vers /tmp/fichier1.log et redirection de la sortie d'erreur standard vers /tmp/fichier2.log.

$ nl test.sh
     1  #!/bin/bash
     2  exec 1> /tmp/fichier1.log 2> /tmp/fichier2.log
     3  echo "Début du traitement : $(date)"
     4  ls
     5  cp *.zzz /tmp
     6  rm *.zzz
     7  sleep 5
     8  echo "Fin du traitement : $(date)"
     9  exit 0
$

Exécution du script.

$ ./test.sh
$

Affichage du fichier /tmp/fichier1.log

$ nl /tmp/fichier1.log
     1  Début du traitement : lundi 12 décembre 2011, 08:23:33 (UTC+0100)
     2  1coucou
     3  24902
     4  25013
     5  25031
     6  25043
     7  boucleFor01.sh
     8  boucleFor02.sh
     9  boucleUntil01.sh
     .....
    87  Fin du traitement : lundi 12 décembre 2011, 08:23:38 (UTC+0100)
$

Affichage du fichier /tmp/fichier2.log (les fichiers *.zzz n'exsistant pas, 2 erreurs sont générées)

$ nl /tmp/fichier2.log
     1  cp: impossible d'évaluer «*.zzz»: Aucun fichier ou dossier de ce type
     2  rm: impossible de supprimer «*.zzz»: Aucun fichier ou dossier de ce type
$

Deuxième exemple :

Redirection de la sortie standard et de la sortie d'erreur standard vers le fichier /tmp/fichier3.log

$ nl ./test2.sh
     1  #!/bin/bash
     2  exec 1> /tmp/fichier3.log 2>&1
     3  echo "Début du traitement : $(date)"
     4  ls
     5  cp *.zzz /tmp
     6  rm *.zzz
     7  sleep 5
     8  echo "Fin du traitement : $(date)"
     9  exit 0
$

Exécution du script

$ ./test2.sh
$

Affichage du fichier /tmp/fichier3.log

$ nl /tmp/fichier3.log
     1  Début du traitement : lundi 12 décembre 2011, 12:56:35 (UTC+0100)
     2  1coucou
     3  24902
     4  25013
     5  25031
     6  25043
     .....
    88  cp: impossible d'évaluer «*.zzz»: Aucun fichier ou dossier de ce type
    89  rm: impossible de supprimer «*.zzz»: Aucun fichier ou dossier de ce type
    90  Fin du traitement : lundi 12 décembre 2011, 12:56:40 (UTC+0100)
$

Troisième exemple :

Redirection de l'entrée standard

$ nl test3.sh
     1  #!/bin/bash
     2  exec 0< /etc/passwd
     3  cpt=1
     4  while read ligne
     5  do
     6          echo "Lecture de la ligne $cpt"
     7          echo $ligne
     8          ((cpt+=1))
     9  done
    10  exit 0
$

Exécution du script

$ ./test3.sh
Lecture de la ligne 1
root:x:0:0:root:/root:/bin/bash
Lecture de la ligne 2
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
Lecture de la ligne 3
bin:x:2:2:bin:/bin:/bin/sh
Lecture de la ligne 4
sys:x:3:3:sys:/dev:/bin/sh
Lecture de la ligne 5
sync:x:4:65534:sync:/bin:/bin/sync
Lecture de la ligne 6
games:x:5:60:games:/usr/games:/bin/sh
Lecture de la ligne 7
man:x:6:12:man:/var/cache/man:/bin/sh
.....
$

Gestion de fichiers

Les shells récents apportent une fonctionnalité supplémentaire.
La possibilité d'ouvrir et de manipuler des fichiers en utilisant des descripteurs compris entre 3 et 9 (en supplément de 0, 1 et 2).
L'avantage est de pouvoir manipuler des fichiers tout en conservant les descripteurs 0, 1 et 2 connectés sur le terminal.

Ouverture de fichier :

En lecture

exec  desc <fichier

En écriture

exec  desc >fichier

Lecture à partir d'un fichier :

read variable1 variable2 ..... variablen <&desc

ou

read -udesc variable1 variable2 ..... variablen

Ecriture dans un fichier :

echo variable1 variable2 ..... variablen >&desc

ou

print -udesc variable1 variable2 ..... variablen

Fermeture d'un fichier :

Syntaxe :

Ouvert en lecture

exec  desc <&-

Ouvert en écriture

exec  desc >&-

Exemple :

$ nl test4.sh
     1  #!/bin/bash
     2  # Ouverture du fichier /etc/passwd en lecture sous le descripteur 3
     3  # et du fichier /tmp/resultat.log en écriture sous le descripteur 4
     4  exec 3</etc/passwd 4>/tmp/resultat.log
     5  cpt=1
     6  # Lecture ligne par ligne du fichier /etc/passwd
     7  # correspondant au descripteur 3
     8  while read -u3 ligne
     9  do
    10          # Ecriture des données dans le fichier /tmp/resultat.log
    11          # correspondant au descripteur 4
    12          echo "Ecriture de la ligne $cpt" >&4
    13          echo $ligne >&4
    14          ((cpt+=1))
    15  done
    16  # Fermeture du fichier /etc/passwd correspondant au descripteur 3
    17  exec 3<&-
    18  # Fermeture du fichier /tmp/resultat.log correspondant au descripteur 4
    19  exec 4>&-
    20  exit 0
$

Résultat :

$ ./test4.sh
$ nl /tmp/resultat.log
     1  Ecriture de la ligne 1
     2  root:x:0:0:root:/root:/bin/bash
     3  Ecriture de la ligne 2
     4  daemon:x:1:1:daemon:/usr/sbin:/bin/sh
     5  Ecriture de la ligne 3
     6  bin:x:2:2:bin:/bin:/bin/sh
     7  Ecriture de la ligne 4
     8  sys:x:3:3:sys:/dev:/bin/sh
     9  Ecriture de la ligne 5
    10  sync:x:4:65534:sync:/bin:/bin/sync
    11  Ecriture de la ligne 6
    12  games:x:5:60:games:/usr/games:/bin/sh
    13  Ecriture de la ligne 7
    14  man:x:6:12:man:/var/cache/man:/bin/sh
    .....
$

Traitement d'un fichier

Les diffétrentes façons d'exploiter un fichier

Rediriger l'exécution du script

Les redirections peuvent également être faites au moment de l'exécution du script.

Exemple :

Avec le script suivant

$ nl test5.sh
     1  #!/bin/bash
     2  cpt=1
     3  # Lecture ligne par ligne du fichier passé en paramètre
     4  # ou lecture de la saisie clavier si pas de fichier en paramètre
     5  while read ligne
     6  do
     7          # Ecriture des données dans le fichier passé en paramètre
     8          # ou affichage à l'écran si pas de fichier en paramètre
     9          echo "Ecriture de la ligne $cpt"
    10         echo $ligne
    11         ((cpt+=1))
    12  done
    13  exit 0
$

Exécution du script sans paramètre

$ ./test5.sh
saisie 1     # saisie
Ecriture de la ligne 1
saisie 1
saisie 2     # saisie
Ecriture de la ligne 2
saisie 2
saisie 3     # saisie
Ecriture de la ligne 3
saisie 3
^d
$

Exécution du script avec paramètre en entrée

$ ./test5.sh < /etc/passwd
Ecriture de la ligne 1
root:x:0:0:root:/root:/bin/bash
Ecriture de la ligne 2
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
Ecriture de la ligne 3
bin:x:2:2:bin:/bin:/bin/sh
Ecriture de la ligne 4
sys:x:3:3:sys:/dev:/bin/sh
Ecriture de la ligne 5
sync:x:4:65534:sync:/bin:/bin/sync
Ecriture de la ligne 6
games:x:5:60:games:/usr/games:/bin/sh
Ecriture de la ligne 7
man:x:6:12:man:/var/cache/man:/bin/sh
.....
$

Exécution du script avec paramètres en entrée et en sortie

$ ./test5.sh < /etc/passwd > /tmp/resultat.log
$ nl /tmp/resultat.log
     1  Ecriture de la ligne 1
     2  root:x:0:0:root:/root:/bin/bash
     3  Ecriture de la ligne 2
     4  daemon:x:1:1:daemon:/usr/sbin:/bin/sh
     5  Ecriture de la ligne 3
     6  bin:x:2:2:bin:/bin:/bin/sh
     7  Ecriture de la ligne 4
     8  sys:x:3:3:sys:/dev:/bin/sh
     9  Ecriture de la ligne 5
    10  sync:x:4:65534:sync:/bin:/bin/sync
    11  Ecriture de la ligne 6
    12  games:x:5:60:games:/usr/games:/bin/sh
    13  Ecriture de la ligne 7
    14  man:x:6:12:man:/var/cache/man:/bin/sh
     .....
$

Redirections internes au script

Les redirections d'entrée (0) et de sortie (1) standards peuvent également être faite à l'intérieur du script.

Exemple :

Avec le script suivant

$ nl test6.sh
     1  #!/bin/bash
     2  exec </etc/passwd >/tmp/resultat.log
     3  cpt=1
     4  while read ligne
     5  do
     6          echo "Ecriture de la ligne $cpt"
     7          echo $ligne
     8          ((cpt+=1))
     9  done
    10  exit 0
$

Exécution du script et affichage du résultat

$ ./test6.sh
$ nl /tmp/resultat.log
     1  Ecriture de la ligne 1
     2  root:x:0:0:root:/root:/bin/bash
     3  Ecriture de la ligne 2
     4  daemon:x:1:1:daemon:/usr/sbin:/bin/sh
     5  Ecriture de la ligne 3
     6  bin:x:2:2:bin:/bin:/bin/sh
     7  Ecriture de la ligne 4
     8  sys:x:3:3:sys:/dev:/bin/sh
     9  Ecriture de la ligne 5
    10  sync:x:4:65534:sync:/bin:/bin/sync
    11  Ecriture de la ligne 6
    12  games:x:5:60:games:/usr/games:/bin/sh
    13  Ecriture de la ligne 7
    14  man:x:6:12:man:/var/cache/man:/bin/sh
     .....
$

Redirection d'un bloc

Il est également possible de rediriger uniquement les commandes situées à l'intérieur d'une structure de contrôle.
Les redirections doivent être écrites derrière le mot clé qui ferme la structure de contrôle.
A l'exécution, elles sont mises en place avant le traitement de la structure de contrôle.*

Exemple :

Dans le script suivant, seules les commandes situées à l'intérieur de la boucle while seront redirigées.

$ nl test7.sh
     1  #!/bin/bash
     2  echo "Lancement du script"
     3  cpt=1
     4  while read ligne
     5  do
     6          echo "Ecriture de la ligne $cpt"
     7          echo $ligne
     8          ((cpt+=1))
     9  done </etc/passwd >/tmp/resultat.log
    10  exit 0
$ ./test7.sh
Lancement du script
$ nl /tmp/resultat.log
     1  Ecriture de la ligne 1
     2  root:x:0:0:root:/root:/bin/bash
     3  Ecriture de la ligne 2
     4  daemon:x:1:1:daemon:/usr/sbin:/bin/sh
     5  Ecriture de la ligne 3
     6  bin:x:2:2:bin:/bin:/bin/sh
$

Rediriger un bloc avec des fichiers ouverts en amont

$ nl test8.sh
     1  #!/bin/bash
     2  exec 3</etc/passwd 4>/tmp/resultat.log
     3  echo "Lancement du script"
     4  cpt=1
     5  while read ligne
     6  do
     7          # Ecriture des données dans le fichier /tmp/resultat.log
     8          # correspondant au descripteur 4
     9          echo "Ecriture de la ligne $cpt"
    10          echo $ligne
    11          ((cpt+=1))
    12  done <&3 >&4
    13  echo "Fin du script"
    14  exec 3<&-
    15  exec 4>&-
    16  exit 0
$ ./test8.sh
Lancement du script
Fin du script
$ nl /tmp/resultat.log
     1  Ecriture de la ligne 1
     2  root:x:0:0:root:/root:/bin/bash
     3  Ecriture de la ligne 2
     4  daemon:x:1:1:daemon:/usr/sbin:/bin/sh
     5  Ecriture de la ligne 3
     6  bin:x:2:2:bin:/bin:/bin/sh
$

Découper une ligne en champs

Si les lignes du fichier à traiter sont structurées en champs, il est très facile de récupérer chacun de ceux ci dans une variable.
Pour cela, il faut modifier la valeur de la variable IFS.

Exemple :

Le script suivant génère, à partir du fichier /etc/passwd, un affichage à l'écran du username suivi de son uid.
La variable IFS est sauvegardée (ligne 13) afin d'être restaurée (ligne 19) et son contenu est remplacé par ":" (ligne 14).
":" étant le caractère séparant les champs du fichier /etc/passwd.
La commande read reçoit 7 variables en argument (ligne 15). Autant de variables que de champs dans le fichier /etc/passwd.
Elle découpe donc la ligne lue (ligne 18) en champs en utilisant le caractère ":" comme séparateur.
La ligne est donc automatiquement découpée et les valeurs récupérables via les variables indiquées.

$ nl test9.sh
     1  #!/bin/bash
     2  if (( $# != 1 ))
     3  then
     4          echo "Mauvais nombre d'arguments"
     5          echo "Usage : $0 fichier"
     6          exit 1
     7  fi
     8  if [[ ( ! -f "$1" ) || ( ! -r "$1" ) ]]
     9  then
    10          echo "$1 n'est pas un fichier ordinaire ou n'est pas accessible en lecture"
    11          exit 2
    12  fi
    13  oldIFS="$IFS"
    14  IFS=":"
    15  while read username password uid gid nomComplet home shell
    16  do
    17          echo "$username ==> $uid"
    18  done < $1
    19  IFS="$oldIFS"
    20  exit 0
$ ./test9.sh /etc/passwd
root ==> 0
daemon ==> 1
bin ==> 2
sys ==> 3
$

Ce script peut très bien être dirigé dans un tube et exploité avec la commande grep

$ ./test9.sh /etc/passwd | grep "root"
root ==> 0
$

La commande eval

Syntaxe :

eval expr1 expr2 ..... exprn

La commande eval permet de faire subir à une ligne de commande une double évaluation.

Exemple :

Initialisation de la variable nom

$ nom=toto

Initialisation de la variable var avec le nom de la variable définie ci dessus

$ var=nom

Affichage des valeurs des variables

$ echo $nom
toto
$ echo $var
nom
$ echo \$$var
$nom
$

Pour obtenir $var=toto, il faut obligatoirement utiliser la commande eval de cette manière

$ eval echo \$$var
toto
$

La commande eval évalue en premier la variable $var, le premier $ étant protégé par un anti-slash

==> eval echo \$nom

puis évalue le résultat de la première évaluation, c'est à dire $nom (suppression de l'anti-slash)

==> eval echo $nom

Initialisation de la variable var2

$ var2=\$$var
$ echo $var2
$nom
$ eval var2=\$$var
$ echo $var2
toto
$

Gestion des signaux

Il est possible de modifier le comportement des signaux envoyés au shell en utilisant la commande trap.

Principaux signaux

Liste des signaux :

$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX
$

Détail des signaux :

  1. HUP    hangup : envoie un signal de réinitialisation
  2. INT    Interruption
  3. QUIT    Core dump
  4. ILL    Le programme tente d'exécuter du code malformé, inconnu ou avec de mauvais privilèges
  5. TRAP    Le programme envoie un signal au débugger (message capturé)
  6. IOT    idem SIGABRT : interruption du programme
  7. EMT    emulator trap : un programme émulé ou virtualisé a posé problème
  8. FPE    floating-point exception : le programme a réalisé une opération arithmétique erronée
  9. KILL    arrête le programme immédiatement
  10. BUS    Le programme a causé une erreur de bus
  11. SEGV    Segmentation fault : le programme fait référence à un mauvais emplacement de mémoire
  12. SYS    Un mauvais argument est passé en paramètre
  13. PIPE    Un programme tente d'écrire dans un pipe sans processus connecté à l'autre bout
  14. ALRM    La limite de temps est dépassée
  15. TERM    Envoie un signal au programme pour le terminer
  16. USR1/USR2    Envoie un signal dans des conditions définies par un utilisateur
  17. CHLD/CLD    child : signal envoyé par un programme lorsqu'un processus fils est achevé
  18. PWR    power : le système subit un problème d'alimentation
  19. VTALRM    virtual alarm : signal envoyé lorsque le temps limite a été dépassé
  20. PROF    profiler : signal envoyé lorsqu'un timer a expiré
  21. POLL    polling : un problème est survenu lors d'un événement I/O asynchrone
  22. WINCH    window [size] change : signal envoyé au programme lorsque la fenêtre de contrôle change de taille
  23. STOP    signal demandant au programme de se suspendre
  24. TSTP    tty stop : signal envoyé au programme lorsqu'un terminal suspend ses requêtes
  25. CONT    Redémarre un programme suspendu par STOP
  26. TTIN    Le programme tente de lire tty alors qu'il est en arrière-plan
  27. TTOU    Le programme tente d'écrire sur tty alors qu'il est en arrière-plan
  28. URG    Un socket a une donnée urgente à lire
  29. LOST    Le verrou d'un fichier a été perdu
  30. XCPU    Le programme a utilisé le CPU prend trop longtemps
  31. XFSZ    Le fichier a dépassé la taille maximale autorisée
  32. RTMIN/RTMIN+n    real-time minimum : signaux définis par l'application
  33. RTMAX/RTMAX-n    real-time maximum : signaux définis par l'application

Dans les commandes, les signaux peuvent être exprimés sous forme numérique ou symbolique.
Les signaux HUP, INT, TERM et KILL possèdent la même valeur numérique sur toutes les plates-formes Unix, ce qui n'est pas le cas de tous les signaux.
Il est donc préférable d'utiliser la forme symbolique.

Le signal INT est généré à partir du clavier.
Il est utilisé pour tuer le processus qui tourne en avant plan.
Pour connaitre la saisie clavier correspondant au signal INT, voir le paramètre intr de la commande stty -a.

$ stty -a
speed 38400 baud; rows 36; columns 134; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S;
susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany -imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke
$

Ignorer un signal

Syntaxe :

trap ' ' sig1 sig2

Exemple :

Le shell courant correspond au PID 30819

$ echo $$
30819

Modification des signaux HUP et TERM

$ trap '' HUP TERM

Envoi des signaux HUP et TERM

$ kill -HUP 30819
$ kill -TERM 30819

Les signaux sont ignorés et le processus est toujours actif

$ echo $$
30819
$

Modifier le traitement associé à un signal

Syntaxe :

trap 'cmd1 ; cmd2 ; cmd3 ; ..... ; cmdn' sig1 sig2

Exemple :

Le shell courant correspond au PID 23217

$ ps
  PID TTY          TIME CMD
22109 pts/0    00:00:00 bash
23217 pts/0    00:00:00 bash
23465 pts/0    00:00:00 ps
$ echo $$
23217

Modification du traitement associé au signal ^C (ctrl+c)
On demande au shell d'afficher le message "Signal INT reçu" après avoir appuyer sur ^C (ctrl+c)

$ trap 'echo "Signal INT reçu" ; exit 1' INT
$ ^C     # Saisie
$ Signal INT reçu

Le shell courant correspondant au PID 23217 n'existe plus

$ ps
  PID TTY          TIME CMD
22109 pts/0    00:00:00 bash
24229 pts/0    00:00:00 ps
$ echo $$
22109
$

Repositionner le traitement par défaut du shell vis-à-vis d'un signal

Syntaxe :

trap - sig1 sig2 ..... sign

Exemple :

Création du fichier /tmp/fichier

$ > /tmp/fichier
$ ls /tmp/fichier
/tmp/fichier

Modification du traitement associé au signal INT et TERM
On demande au shell de supprimer le fichier "/tmp/fichier" après avoir appuyer sur ^C (ctrl+c)

$ trap 'rm -f /tmp/fichier' INT TERM

Le shell courant correspond au PID 22109

$ echo $$
22109

Envoi du signal INT "^C" (ctrl+c)

$ ^C

Le fichier a bien été suprimé

$ ls /tmp/fichier
ls: impossible d'accéder à /tmp/fichier: Aucun fichier ou dossier de ce type

Création d'un nouveau fichier /tmp/fichier

$ > /tmp/fichier
$ ls /tmp/fichier
/tmp/fichier

Traitement associé au signal INT et TERM remis par défaut

$ trap - INT TERM

Le shell courant correspond au PID 22109

$ echo $$
22109

Envoi du signal INT "^C" (ctrl+c)

$ ^C

Le fichier n'a pas été supprimé

$ ls /tmp/fichier
/tmp/fichier
$

Utiliser trap à partir d'un script shell

L'utilisation de trap dans un script shell va permettre de gérer des actions en fonctions de différents signaux reçus.

Exemple :

Dans le script suivant, à la réception d'un signal HUP INT ou TERM, la fonction "fin" est appelée et le fichier $fileTmp est supprimé.

$ nl signaux.sh
     1  #!/bin/bash
     2
     3  # Nom du fichier temporaire
     4  fileTmp=/tmp/fileTemp
     5
     6  # Fonction appelée lors de la réception d'un signal HUP INT TERM
     7  function fin {
     8          echo -e "\nSuppression du fichier $fileTmp"
     9          echo "Fin du script"
    10          rm -f $fileTmp
    11          ls $fileTmp
    12          exit 1
    13  }
    14
    15  # Paramétrage de la fonction "fin" à la réception d'un signal HUP INT TERM
    16  trap fin HUP INT TERM
    17
    18  # Création du fichier temporaire
    19  > $fileTmp
    20
    21  echo "Lancement du script"
    22  # Vérification de la création du fichier temporaire
    23  ls $fileTmp
    24  sleep 100
    25  echo "Arrêt du script"
    26  exit 0
$

Exécution du script :

$ ./signaux.sh
Lancement du script
/tmp/fileTemp
^C     # Envoi du signal
Suppression du fichier /tmp/fileTemp
Fin du script
ls: impossible d'accéder à /tmp/fileTemp: Aucun fichier ou dossier de ce type
$

Gestion de menu avec select

Syntaxe :

select var in item1 item2 item3 ..... itemn
do
     commandes
done

La commande interne select est une structure de contrôle de type boucle qui permet d'afficher de manière cyclique un menu.
La liste des items sera affichée à l'écran à chaque tour de boucle.
Les items sont indicés automatiquement.
La variable var sera initialisée avec l'item correspondant au choix de l'utilisateur.

Cette commande utilise également deux variables réservées :

  • La variable PS3 représente le prompt utilisé pour la saisiedu choix de l'utilisateur.
    Sa valeur par défaut est #?. Elle peut être modifiée si on le souhaite.
  • La variable REPLY qui contient l'indice de l'item sélectionné.

La variable var contient le libellé du choix et REPLY l'indice de ce dernier.

Exemple :

$ nl select.sh
     1  #!/bin/bash
     2
     3  function sauve {
     4          echo "Lancement de la sauvegarde"
     5  }
     6
     7  function restaure {
     8          echo "Lancement de la restauration"
     9  }
    10
    11  PS3="Votre choix : "
    12
    13  select item in "- Sauvegarde -" "- Restauration -" "- Fin -"
    14  do
    15          echo "Vous avez choisi l'item $REPLY : $item"
    16          case $REPLY in
    17                  1)
    18                          # Appel de la fonction sauve
    19                          sauve
    20                          ;;
    21                  2)
    22                          # Appel de la fonction restaure
    23                          restaure
    24                          ;;
    25                  3)
    26                          echo "Fin du script"
    27                          exit 0
    28                          ;;
    29                  *)
    30                          echo "Choix incorrect"
    31                          ;;
    32          esac
    33  done
$

Exécution :

$ ./select.sh
1) - Sauvegarde -
2) - Restauration -
3) - Fin -
Votre choix : 2
Vous avez choisi l'item 2 : - Restauration -
Lancement de la restauration

La saisie de la touche [Entrée] permet de réafficher le menu :

$ .
Votre choix :     # Appui sur la touche [Entrée]
1) - Sauvegarde -
2) - Restauration -
3) - Fin -
Votre choix : 3
Vous avez choisi l'item 3 : - Fin -
Fin du script
$

Analyse des options d'un script avec getopts

$ type getopts
getopts est une primitive du shell
$

Syntaxe :

getopts listeOptionsAttendues option

La commande interne getopts permet à un script d'anayser les options passées en argument.
Chaque appel à la commande getopts analyse l'option suivante de la ligne de commande.
Pour vérifier la validité de chacune des options, il faut appeler getopts à partir d'une boucle.

Définition d'une option

Pour getopts, une option est composée d'un caractère précédé du signe "+" ou "-".

Premier exemple :

$ ls -l *.sh

Par exemple, pour la commande ls, "-l" est une option et "*.sh" est un argument.
Une option peut fonctionner seule ou être associée à un argument.

Second exemple :

Le script suivant détail la manière d'utiliser la commande getopts.

$ nl test_getopts_1.sh
     1  #!/bin/bash
     2
     3  while getopts "abcd:e:" option
     4  do
     5          echo "getopts a trouvé l'option $option"
     6          case $option in
     7                  a)
     8                          echo "Exécution des commandes de l'option a"
     9                          echo "Indice de la prochaine option à traiter : $OPTIND"
    10                          ;;
    11                  b)
    12                          echo "Exécution des commandes de l'option b"
    13                          echo "Indice de la prochaine option à traiter : $OPTIND"
    14                          ;;
    15                  c)
    16                          echo "Exécution des commandes de l'option c"
    17                          echo "Indice de la prochaine option à traiter : $OPTIND"
    18                          ;;
    19                  d)
    20                          echo "Exécution des commandes de l'option d"
    21                          echo "Liste des arguments à traiter : $OPTARG"
    22                          echo "Indice de la prochaine option à traiter : $OPTIND"
    23                          ;;
    24                  e)
    25                          echo "Exécution des commandes de l'option e"
    26                          echo "Liste des arguments à traiter : $OPTARG"
    27                          echo "Indice de la prochaine option à traiter : $OPTIND"
    28                          ;;
    29          esac
    30  done
    31  echo "Analyse des options terminée"
    32  exit 0
$

L'appel à la commande getopts récupère l'option suivante et retourne un code vrai tant qu'il reste des options à analyser.
La liste des options utilisables avec ce script sont définies à la ligne 3 (getopts "abcd:e:" option). Il s'agit des options -a, -b, -c, -d et -e.
Le caractère ":" inscrit après les options "d" et "e" (getopts "abcd:e:" option) indique que ces options doivent être suivies obligatoirement d'un argument.
La variable "option" (getopts "abcd:e:" option) permet de récupérer la valeur de l'option en cours de traitement par la boucle while.
La variable réservée "$OPTIND" contient l'indice de la prochaine option à traiter.
La variable réservée "$OPTARG" contient l'argument associé à l'option.

Exécution du script avec des options valides :

$ ./test_getopts_1.sh -a -b -c -d toto -e tata,tutu
getopts a trouvé l'option a
Exécution des commandes de l'option a
Indice de la prochaine option à traiter : 2
getopts a trouvé l'option b
Exécution des commandes de l'option b
Indice de la prochaine option à traiter : 3
getopts a trouvé l'option c
Exécution des commandes de l'option c
Indice de la prochaine option à traiter : 4
getopts a trouvé l'option d
Exécution des commandes de l'option d
Liste des arguments à traiter : toto
Indice de la prochaine option à traiter : 6
getopts a trouvé l'option e
Exécution des commandes de l'option e
Liste des arguments à traiter : tata,tutu
Indice de la prochaine option à traiter : 8
Analyse des options terminée
$

Option invalide

Lorsque la commande getopts détecte une option invalide, la variable option est initialisée avec la caractère "?" et un message d'erreur est affiché à l'écran.
Les options suivantes sont analysées.

Exemple :

L'option -z ne fait pas partie de la liste des options attendues.

$ ./test_getopts_1.sh -a -z -b -c -d toto -e tata,tutu
getopts a trouvé l'option a
Exécution des commandes de l'option a
Indice de la prochaine option à traiter : 2
./test_getopts_1.sh : option non permise -- z
getopts a trouvé l'option ?

getopts a trouvé l'option b
Exécution des commandes de l'option b
Indice de la prochaine option à traiter : 4
getopts a trouvé l'option c
Exécution des commandes de l'option c
Indice de la prochaine option à traiter : 5
getopts a trouvé l'option d
Exécution des commandes de l'option d
Liste des arguments à traiter : toto
Indice de la prochaine option à traiter : 7
getopts a trouvé l'option e
Exécution des commandes de l'option e
Liste des arguments à traiter : tata,tutu
Indice de la prochaine option à traiter : 9
Analyse des options terminée
$

Gestion des erreurs

Si le caractère ":" est placé en première position dans la liste des options à traiter (ligne 3), les erreurs sont gérées différemment.

En cas d'option invalide :
- getopts n'affichera pas de message d'erreur.
- la variable OPTARG sera initialisée avec la valeur de l'option incorrecte (ligne 29).

Exemple :

$ nl test_getopts_1.sh
     1  #!/bin/bash
     2
     3  while getopts ":abcd:e:" option
     4  do
     5          echo "getopts a trouvé l'option $option"
     6          case $option in
     7                  a)
     8                          echo "Exécution des commandes de l'option a"
     9                          echo "Indice de la prochaine option à traiter : $OPTIND"
    10                          ;;
    11                  b)
    12                          echo "Exécution des commandes de l'option b"
    13                          echo "Indice de la prochaine option à traiter : $OPTIND"
    14                          ;;
    15                  c)
    16                          echo "Exécution des commandes de l'option c"
    17                          echo "Indice de la prochaine option à traiter : $OPTIND"
    18                          ;;
    19                  d)
    20                          echo "Exécution des commandes de l'option d"
    21                          echo "Liste des arguments à traiter : $OPTARG"
    22                          echo "Indice de la prochaine option à traiter : $OPTIND"
    23                          ;;
    24                  e)
    25                          echo "Exécution des commandes de l'option e"
    26                          echo "Liste des arguments à traiter : $OPTARG"
    27                          echo "Indice de la prochaine option à traiter : $OPTIND"
    28                          ;;
    29                  \?)
    30                          echo "$OPTARG : option invalide"
    31                          exit 1
    32                          ;;
    33          esac
    34  done
    35  echo "Analyse des options terminée"
    36  exit 0
$

Le message d'erreur généré automatiquement par getopts n'apparait plus et la variable $OPTARG a été substituée par la valeur de l'option incorrecte.

Ligne 29, le "?" doit être protégé par un anti-slash pour ne pas être interprété par le shell.

$ ./test_getopts_1.sh -a -z -b -c -d toto -e tata,tutu
getopts a trouvé l'option a
Exécution des commandes de l'option a
Indice de la prochaine option à traiter : 2
getopts a trouvé l'option ?
z : option invalide
$

Option valide avec argument manquant

Lorsque l'argument d'une option est absent, la variable option est initialisée avec le caractère ":" et OPTARG contient la valeur de l'option concernée (ligne 29).

Exemple :

$ nl test_getopts_1.sh
     1  #!/bin/bash
     2
     3  while getopts ":abcd:e:" option
     4  do
     5          echo "getopts a trouvé l'option $option"
     6          case $option in
     7                  a)
     8                          echo "Exécution des commandes de l'option a"
     9                          echo "Indice de la prochaine option à traiter : $OPTIND"
    10                          ;;
    11                  b)
    12                          echo "Exécution des commandes de l'option b"
    13                          echo "Indice de la prochaine option à traiter : $OPTIND"
    14                          ;;
    15                  c)
    16                          echo "Exécution des commandes de l'option c"
    17                          echo "Indice de la prochaine option à traiter : $OPTIND"
    18                          ;;
    19                  d)
    20                          echo "Exécution des commandes de l'option d"
    21                          echo "Liste des arguments à traiter : $OPTARG"
    22                          echo "Indice de la prochaine option à traiter : $OPTIND"
    23                          ;;
    24                  e)
    25                          echo "Exécution des commandes de l'option e"
    26                          echo "Liste des arguments à traiter : $OPTARG"
    27                          echo "Indice de la prochaine option à traiter : $OPTIND"
    28                          ;;
    29                  :)
    30                          echo "L'option $OPTARG requiert un argument"
    31                          exit 1
    32                          ;;
    33                  \?)
    34                          echo "$OPTARG : option invalide"
    35                          exit 1
    36                          ;;
    37          esac
    38  done
    39  echo "Analyse des options terminée"
    40  exit 0
$

Exécution du script en oubliant l'argument de l'option -e

$ ./test_getopts_1.sh -a -b -c -d toto -e
getopts a trouvé l'option a
Exécution des commandes de l'option a
Indice de la prochaine option à traiter : 2
getopts a trouvé l'option b
Exécution des commandes de l'option b
Indice de la prochaine option à traiter : 3
getopts a trouvé l'option c
Exécution des commandes de l'option c
Indice de la prochaine option à traiter : 4
getopts a trouvé l'option d
Exécution des commandes de l'option d
Liste des arguments à traiter : toto
Indice de la prochaine option à traiter : 6
getopts a trouvé l'option :
L'option e requiert un argument
$

Gestion d'arguments supplémentaires

Les options sont stockées dans les paramètres positionnels $1, $2 ..... $n.
Une fois que celles ci sont analysées, il est possible de s'en débarasser avec la commande shift.
Ceci est intéressant s'il reste des arguments à traiter derrière les options.

Exemple :

$ nl test_getopts_1.sh | tail
    37          esac
    38  done
    39  echo "Analyse des options terminée"
    40  echo "Avant shift : "
    41  echo "Liste des arguments : $*"
    42  echo "Indice de la prochaine option à traiter : $OPTIND"
    43  shift $((OPTIND-1))
    44  echo "Après shift : "
    45  echo "Liste des arguments : $*"
    46  exit 0
$

L'instruction à la ligne 43 permet de retirer les options de la liste des arguments.
L'expression OPTIND-1 représente le nombre d'options analysées, donc la valeur du décalage à réaliser.

Exécution du script avec des arguments supplémentaires :

$ ./test_getopts_1.sh -a -b -c -d toto -e tata,tutu arg1 arg2 arg3 arg4
getopts a trouvé l'option a
Exécution des commandes de l'option a
Indice de la prochaine option à traiter : 2
getopts a trouvé l'option b
Exécution des commandes de l'option b
Indice de la prochaine option à traiter : 3
getopts a trouvé l'option c
Exécution des commandes de l'option c
Indice de la prochaine option à traiter : 4
getopts a trouvé l'option d
Exécution des commandes de l'option d
Liste des arguments à traiter : toto
Indice de la prochaine option à traiter : 6
getopts a trouvé l'option e
Exécution des commandes de l'option e
Liste des arguments à traiter : tata,tutu
Indice de la prochaine option à traiter : 8
Analyse des options terminée
Avant shift :
Liste des arguments : -a -b -c -d toto -e tata,tutu arg1 arg2 arg3 arg4
Indice de la prochaine option à traiter : 8
Après shift :
Liste des arguments : arg1 arg2 arg3 arg4

$

Gestion d'un processus en arrière plan

La commande wait permet au shell d'attendre la terminaison d'un processus lancé en arrière-plan.

Syntaxes :

Attendre la terminaison du processus dont le PID est donné en argument :

wait pid1

Attendre la terminaison de tous les processus lancés en arrière-plan à partir du shell courant :

wait

Attendre la terminaison du processus dont le numéro de job est donné en argument :

wait %job

Exemples :

La commande find est lançée en arrière-plan et a pour PID 2878 :

$ find / -name /etc/passwd 1>/tmp/resu 2>/dev/null &
[1] 2878
$ jobs
[1]+  Running                 find / -name /etc/passwd > /tmp/resu 2> /dev/null &
$

Le shell s'endort en attendant la terminaison du processus 2878 :

$ wait 2878     # Ou wait %1

Le shell est réveillé lorsque le processus 2878 est terminé :

$ .
[1]+  Exit 1                  find / -name /etc/passwd > /tmp/resu 2> /dev/null
$

Le PID de la dernière commande lançée en arrière-plan est contenu dans la variable spéciale $!

Le script suivant lance en arrière-plan une recherche du fichier /etc/passwd (ligne 3).
D'autres actions peuvent être exécutées en attendant (ligne 5 à 8) puis le shell attend la fin de la recherche (ligne 9) avant d'afficher à l'écran le contenu du fichier /tmp/resu (ligne 12).

$ nl test_wait_1.sh
     1  #!/bin/bash
     2
     3  find / -name /etc/passwd 1>/tmp/resu 2>&1 &
     4  echo "Le PID du script lancé en arrière-plan est le : $!"
     5  echo "Début des autres commandes"
     6
     7  echo "Fin des autres commandes"
     8  echo "Recherche en cours - Attente de la fin de la recherche"
     9  wait $!
    10  echo "La recherche est terminée"
    11  echo "Affichage du résultat"
    12  most /tmp/resu
    13  exit 0
$
$ ./test_wait_1.sh
Le PID du script lancé en arrière-plan est le : 11697
Début des autres commandes
Fin des autres commandes
Recherche en cours - Attente de la fin de la recherche
.....      # Le shell s'endort
.....
La recherche est terminée
Affichage du résultat
$

Script d'archivage incrémental et transfert sftp automatique

Le script suivant utilise toutes les techniques vues précédemment

CPIO

Pré-requis pour que le script suivant fonctionne :

1 - Paramétrer sur le poste à sauvegarder un accès ssh au serveur de sauvegarde via un système d'authentification par clé privée / publique.

Tout est expliqué ici : http://quennec.fr/linux/utilisation/connexion-a-une-machine-distante-sans-saisir-le-mot-de-passe

2 - Le programme bzip2 doit être installé sur le poste à sauvegarder.

$ apt-get install bzip2

3 - Rendre les scripts exécutables

$ chmod +x uploadBackup.sh && chmod +x fonctions.inc.sh

Script d'archivage incrémental et transfert sftp automatique

Ce script permet d'effectuer une sauvegarde incrémentale d'un répertoire vers un serveur de sauvegarde.
Il utilise un système de niveau correspondant au jour du mois - 1.
Tous les 1er de chaque mois, une sauvegarde totale (niveau 0) est effectuée.
Une sauvegarde incrémentale est effectuée les jours suivants.

Le script est composé de 2 fichiers.
Un fichier comportant le script principal (uploadBackup.sh) et un fichier comportant les fonctions personnalisées utiles au script principal (fonctions.inc.sh).

Les fichiers à sauvegarder (variable DATADIR) sont enregistrés dans une archive CPIO compressée avec BZIP2 et stockée dans un répertoire local sur le poste à sauvegarder (variable ARCHIVEDIR).
Avant chaque sauvegarde, un fichier de niveau est créé dans le répertoire local.
Toutes les logs sont enregistrées dans un fichier stocké également dans le répertoire local.
La sauvegarde incrémentale utilise le fichier niveau précédent afin de sauvegarder tous les fichiers modifiés depuis la sauvegarde précédente grâce à la commande find et l'option -newer.
Enfin, l'archive CPIO est envoyée sur le serveur de sauvegarde (variable serveur_sauvegarde) via SFTP et stockée dans des sous-répertoires correspondant à l'année et au mois de la sauvegarde dans le dossier de sauvegarde (variable dossier_distant) .

Détail du fichier uploadBackup.sh

$ nl uploadBackup.sh
     1  #!/bin/bash
     2  # set -x
     3  # Répertoire des scripts shell
     4  SCRIPTDIR="/root/script_archivage_incremental"
     5  # Répertoire des fichiers à sauvegarder
     6  DATADIR="/root/dossier_a_sauvegarder"
     7  # Répertoire local des archives
     8  ARCHIVEDIR="/root/local_backup"
     9  # Inclure les fonctions
    10  . $SCRIPTDIR/fonctions.inc.sh
    11  # Fichier de log
    12  LOG=$ARCHIVEDIR/`getDate`.log
    13  # Redirection de toutes les sorties du script vers le fichier de log
    14  exec 1>$LOG 2>&1
    15  # Déterminer le niveau de sauvegarde
    16  # Le 1er du mois => niveau 0
    17  jourDuMois=`getDayForCalcul`
    18  ((niveau=$jourDuMois-1))
    19  case $niveau in
    20          0) # Sauvegarde totale
    21                  # Nettoyage du répertoire d'archive
    22                  rm -i $ARCHIVEDIR/*.bz2 $ARCHIVEDIR/niveau*
    23                  # Création du fichier de niveau (niveau 0)
    24                  touch $ARCHIVEDIR/niveau0
    25                  archive="$ARCHIVEDIR/`getDate`_niveau0.cpio"
    26                  find $DATADIR | cpio -ocv | bzip2 -c > $archive.bz2
    27                  ;;
    28          *)
    29                  # Creation du fichier de niveau
    30                  touch $ARCHIVEDIR/niveau$niveau
    31                  archive="$ARCHIVEDIR/`getDate`_niveau${niveau}.cpio"
    32                  # Determination du niveau precedent
    33                  ((niveauPrec=$niveau-1))
    34                  # Test si le fichier de niveau precedent existe
    35                  if [[ ! -f $ARCHIVEDIR/niveau$niveauPrec ]]
    36                  then
    37                          # Si il n'existe pas, sauvegarde integrale du repertoire
    38                          echo "Fichier de niveau $niveauPrec inexistant"
    39                          echo "Execution d'une sauvegarde integrale en cours de mois"
    40                          find $DATADIR | cpio -ocv | bzip2 -c > $archive.bz2
    41                  else
    42                          # Sauvegarde incrementale
    43                          find $DATADIR -newer $ARCHIVEDIR/niveau$niveauPrec | cpio -ocv | bzip2 -c > $archive.bz2
    44                  fi
    45                  ;;
    46  esac
    47  # Vérification de la validité de l'archive
    48  if isArchiveInvalide $archive.bz2 ; then
    49          echo "Archive $archive.bz2 INVALIDE - Fichier non transfere"
    50          exit 1
    51  fi
    52  # Transfert du fichier vers le serveur de sauvegarde
    53  if transfert ${archive}.bz2 ; then
    54          echo "Transfert realise avec succes"
    55          exit 0
    56  fi
    57  # Si le transfert a echoue
    58  echo "Echec de transfert"
    59  exit 1
$

Détail du fichier fonctions.inc.sh

$ nl fonctions.inc.sh
     1  #!/bin/bash
     2  function transfert {
     3          typeset mois
     4          typeset annee
     5          # Recuperation de la valeur du premier argument passe a la fonction
     6          # Recuperation du nom de l'archive a envoyer au serveur de sauvegarde
     7          typeset ficATransferer=$1
     8          # Adresse du serveur de sauvegarde
     9          typeset serveur_sauvegarde="192.168.1.113"
    10          # Compte utilise pour se connecter au serveur de sauvegarde
    11          typeset user="root"
    12          # Chemin absolu du dossier ou sera stocke la sauvegarde
    13          typeset dossier_distant="/root/sauvegarde_dossier"
    14          mois=`getMonth`
    15          annee=`getYear`
    16          # Test si le dossier de sauvegarde existe sur le serveur de sauvegarde
    17          # Connexion au serveur avec le user root
    18          ssh $user@$serveur_sauvegarde test -d $dossier_distant/$annee/$mois
    19          # Test sur le code retour de la commande precedente
    20          case $? in
    21                  0)
    22                          ;;
    23                  255)
    24                          echo "Echec de la commande SSH"
    25                          return 1
    26                          ;;
    27                  *)
    28                          # Si code retour different de 0 et 255
    29                          # Creation du repertoire de la sauvegarde
    30                          ssh $user@$serveur_sauvegarde mkdir -p $dossier_distant/$annee/$mois
    31                          ;;
    32          esac
    33          # Connexion au serveur de sauvegarde en FTP sécurisé
    34          # Ne pas oublier les doubles chevrons << avant le mot cle FIN
    35          # Ne pas mettre d'espace entre << et FIN
    36          sftp -b - $user@$serveur_sauvegarde <<FIN
    37          # Positionnement dans le repertoire de la sauvegarde
    38          cd $dossier_distant/$annee/$mois
    39          pwd
    40          # Envoi de la sauvegarde
    41          put $ficATransferer
    42  FIN
    43  # Ne pas mettre de tabulation ou d'espace devant le mot clé FIN
    44  # Sinon celui-ci n'est pas reconnu
    45          # Test sur le code retour de la commande SFTP
    46          # Si le code retour est different de 0
    47          # Une anomalie a ete rencontree
    48          (( $? != 0 )) && return 1
    49          # Tester si archive valide sur serveur de sauvegarde
    50          ficSurMachineCible=$(basename $ficATransferer)
    51          ssh $user@$serveur_sauvegarde bzip2 -t $dossier_distant/$annee/$mois/$ficSurMachineCible
    52          case $? in
    53                  0)
    54                          ;;
    55                  255)
    56                          echo "Echec de la commande SSH"
    57                          return 1
    58                          ;;
    59                  *)
    60                          # Si code retour different de 0 et 255
    61                          # Alors l'archive est invalide
    62                          echo "Archive $dossier_distant/$annee/$mois/$ficSurMachineCible INVALIDE"
    63                          return 1
    64                          ;;
    65          esac
    66          return 0
    67  }
    68  function isArchiveInvalide {
    69          typeset archive=$1
    70          bzip2 -t $archive 2>/dev/null && return 1
    71          return 0
    72  }
    73  function getDate {
    74          date '+%Y_%m_%d'
    75  }
    76  function getYear {
    77          date '+%Y'
    78  }
    79  function getMonth {
    80          date '+%m'
    81  }
    82  function getDayForCalcul {
    83          date '+%e' | sed 's/ //'
    84  }
$