Chapitre 35. Bash, version 2

La version actuelle de Bash, celle que vous avez sur votre machine, est la version 2.XX.Y.
bash$ echo $BASH_VERSION
2.05.8(1)-release
	      
Cette mise à jour du langage de script Bash classique ajoute les variables de type tableau, [1] l'expansion de chaînes de caractères et de paramètres, et une meilleure méthode pour les références de variables indirectes, parmi toutes les fonctionnalités.

Exemple 35-1. Expansion de chaîne de caractères

#!/bin/bash

# Expansion de chaînes de caractères.
# Introduit avec la version 2 de Bash.

# Les chaînes de caractères de la forme $'xxx' ont les caractères d'échappement
# standard interprétés.

echo $'Trois cloches sonnant à la fois \a \a \a'
echo $'Trois retours chariot \f \f \f'
echo $'10 retours chariot \n\n\n\n\n\n\n\n\n\n'

exit 0

Exemple 35-2. Références de variables indirectes - la nouvelle façon

#!/bin/bash

# Référencement de variables indirectes.
# Ceci a quelques uns des attributs du C++.


a=lettre_de_l_alphabet
lettre_de_l_alphabet=z

echo "a = $a"           # Référence directe.

echo "Maintenant a = ${!a}"    # Référence indirecte.
# La notation ${!variable} est bien supérieure à l'ancien "eval var1=\$$var2"

echo

t=cellule_table_3
cellule_table_3=24
echo "t = ${!t}"        # t = 24
cellule_table_3=387
echo "La valeur de t a changé en ${!t}"    # 387

# Ceci est utile pour référencer les membres d'un tableau ou d'une table, ou
# pour simuler un tableau multi-dimensionnel.
# Une option d'indexage aurait été bien (sigh).

exit 0

Exemple 35-3. Simple application de base de données, utilisant les références de variables indirectes

#!/bin/bash
# resistor-inventory.sh
# Simple base de données utilisant le référencement indirecte de variables.

# ============================================================== #
# Données

B1723_value=470                                   # ohms
B1723_powerdissip=.25                             # watts
B1723_colorcode="yellow-violet-brown"             # color bands
B1723_loc=173                                     # where they are
B1723_inventory=78                                # how many

B1724_value=1000
B1724_powerdissip=.25
B1724_colorcode="brown-black-red"
B1724_loc=24N
B1724_inventory=243

B1725_value=10000
B1725_powerdissip=.25
B1725_colorcode="brown-black-orange"
B1725_loc=24N
B1725_inventory=89

# ============================================================== #


echo

PS3='Entrez le numéro du catalogue: '

echo

select numero_catalogue in "B1723" "B1724" "B1725"
do
  Inv=${numero_catalogue}_inventory
  Val=${numero_catalogue}_value
  Pdissip=${numero_catalogue}_powerdissip
  Loc=${numero_catalogue}_loc
  Ccode=${numero_catalogue}_colorcode

  echo
  echo "Catalogue numéro $numero_catalogue:"
  echo "Il existe ${!Inv} résistances de [${!Val} ohm / ${!Pdissip} watt] en stock."
  echo "Elles sont situées dans bin # ${!Loc}."
  echo "Leur code couleur est \"${!Ccode}\"."

  break
done

echo; echo

# Exercice:
# --------
#  Réécrire ce script en utilisant des tableaux, plutôt qu'en utilisant le
#+ référencement indirecte des variables.
# Quelle methode est plus logique et intuitive?


# Notes:
# -----
#  Les scripts shells sont inappropriés pour tout, sauf des applications simples
#+ de base de données, et même là, cela implique des astuces.
#  Il est bien mieux d'utiliser un langage supportant nativement les structures
#+ de données, tels que C++ ou Java (voire même Perl).

exit 0

Exemple 35-4. Utiliser des tableaux et autres astuces pour gérer quatre mains aléatoires dans un jeu de cartes

#!/bin/bash
# A besoin d'être appelé avec  #!/bin/bash2  sur les anciennes machines.

# Cartes:
# gère quatre mains d'un jeu de cartes.

UNPICKED=0
PICKED=1

DUPE_CARD=99

LOWER_LIMIT=0
UPPER_LIMIT=51
CARDS_IN_SUIT=13
CARDS=52

declare -a Deck
declare -a Suits
declare -a Cards
# It would have been easier and more intuitive
# with a single, 3-dimensional array.
# Perhaps a future version of Bash will support multidimensional arrays.


initialize_Deck ()
{
i=$LOWER_LIMIT
until [ "$i" -gt $UPPER_LIMIT ]
do
  Deck[i]=$UNPICKED   # Set each card of "Deck" as unpicked.
  let "i += 1"
done
echo
}

initialize_Suits ()
{
Suits[0]=C #Clubs
Suits[1]=D #Diamonds
Suits[2]=H #Hearts
Suits[3]=S #Spades
}

initialize_Cards ()
{
Cards=(2 3 4 5 6 7 8 9 10 J Q K A)
# Alternate method of initializing an array.
}

pick_a_card ()
{
card_number=$RANDOM
let "card_number %= $CARDS"
if [ "${Deck[card_number]}" -eq $UNPICKED ]
then
  Deck[card_number]=$PICKED
  return $card_number
else  
  return $DUPE_CARD
fi
}

parse_card ()
{
number=$1
let "suit_number = number / CARDS_IN_SUIT"
suit=${Suits[suit_number]}
echo -n "$suit-"
let "card_no = number % CARDS_IN_SUIT"
Card=${Cards[card_no]}
printf %-4s $Card
# Print cards in neat columns.
}

seed_random ()  # Seed random number generator.
{
seed=`eval date +%s`
let "seed %= 32766"
RANDOM=$seed
}

deal_cards ()
{
echo

cards_picked=0
while [ "$cards_picked" -le $UPPER_LIMIT ]
do
  pick_a_card
  t=$?

  if [ "$t" -ne $DUPE_CARD ]
  then
    parse_card $t

    u=$cards_picked+1
    # Change back to 1-based indexing (temporarily).
    let "u %= $CARDS_IN_SUIT"
    if [ "$u" -eq 0 ]   # Nested if/then condition test.
    then
     echo
     echo
    fi
    # Separate hands.

    let "cards_picked += 1"
  fi  
done  

echo

return 0
}


# Structured programming:
# entire program logic modularized in functions.

#================
seed_random
initialize_Deck
initialize_Suits
initialize_Cards
deal_cards

exit 0
#================



# Exercise 1:
# Add comments to thoroughly document this script.

# Exercise 2:
# Revise the script to print out each hand sorted in suits.
# You may add other bells and whistles if you like.

# Exercise 3:
# Simplify and streamline the logic of the script.

Notes

[1]

Chet Ramey a promit des tableaux associatifs (une fonctionnalité Perl) dans une future version de Bash.

echo echo "Entrez le deuxième nombre " read second echo valretour=`somme_et_produit $premier $second` # Affecte à la variable la sortie #+ de la fonction. somme=`echo "$valretour" | awk '{print $1}'` # Affecte le premier champ. produit=`echo "$valretour" | awk '{print $2}'`# Affecte le deuxième champ. echo "$premier + $second = $somme" echo "$premier * $second = $produit" echo exit 0
  • Ensuite dans notre liste d'astuces se trouvent les techniques permettant de passer un tableau à une fonction, << renvoyant  >> alors un tableau en retour à la fonction principale du script.

    Le passage d'un tableau nécessite de charger des éléments séparés par un espace d'un tableau dans une variable avec la substitution de commandes. Récupérer un tableau comme << valeur de retour >> à partir d'une fonction utilise le stratagème mentionné précédemment de la sortie (echo) du tableau dans la fonction, puis d'invoquer la substitution de commande et l'opérateur ( ... ) pour l'assigner dans un tableau.

    Exemple 34-12. Passer et renvoyer un tableau

    #!/bin/bash
    # array-function.sh: Passer un tableau à une fonction et...
    #                   "renvoyer" un tableau à partir d'une fonction
    
    
    Passe_Tableau ()
    {
      local tableau_passe   # Variable locale.
      tableau_passe=( `echo "$1"` )
      echo "${tableau_passe[@]}"
      #  Liste tous les éléments du nouveau tableau déclaré
      #+ et initialisé dans la fonction.
    }
    
    
    tableau_original=( element1 element2 element3 element4 element5 )
    
    echo
    echo "tableau_original = ${tableau_original[@]}"
    #                      Liste tous les éléments du tableau original.
    
    
    # Voici une astuce qui permet de passer un tableau à une fonction.
    # **********************************
    argument=`echo ${tableau_original[@]}`
    # **********************************
    #  Emballer une variable
    #+ avec tous les éléments du tableau original séparés avec un espace.
    #
    # Notez que d'essayer de passer un tableau en lui-même ne fonctionnera pas.
    
    
    # Voici une astuce qui permet de récupérer un tableau comme "valeur de retour".
    # *****************************************
    tableau_renvoye=( `Passe_Tableau "$argument"` )
    # *****************************************
    # Affecte une sortie de la fonction à une variable de type tableau.
    
    echo "tableau_renvoye = ${tableau_renvoye[@]}"
    
    echo "============================================================="
    
    #  Maintenant, essayez encore d'accèder au tableau en dehors de la
    #+ fonction.
    Passe_Tableau "$argument"
    
    # La fonction liste elle-même le tableau, mais...
    #+ accèder au tableau de l'extérieur de la fonction est interdit.
    echo "Tableau passé (de l'intérieur de la fonction) = ${tableau_passe[@]}"
    # Valeur NULL comme il s'agit d'une variable locale.
    
    echo
    
    exit 0

    Pour un exemple plus élaboré du passage d'un tableau dans les fonctions, voir Exemple A-11.

  • En utilisant la construction en double parenthèses, il est possible d'utiliser la syntaxe style C pour initialiser et incrémenter des variables ainsi que dans des boucles for et while. Voir Exemple 10-12 et Exemple 10-17.

  • Une technique de scripts utile est d'envoyer de manière répétée la sortie d'un filtre (par un tuyau) vers le même filtre, mais avec un ensemble différent d'arguments et/ou options. Ceci est spécialement intéressant pour tr et grep.

    # De l'exemple "wstrings.sh".
    
    wlist=`strings "$1" | tr A-Z a-z | tr '[:space:]' Z | \
    tr -cs '[:alpha:]' Z | tr -s '\173-\377' Z | tr Z ' '`

    Exemple 34-13. Un peu de fun avec des anagrammes

    #!/bin/bash
    # agram.sh: Jouer avec des anagrammes.
    
    # Trouver les anagrammes de...
    LETTRES=etaoinshrdlu
    
    anagram "$LETTRES" |   # Trouver tous les anagrammes de cet ensemble de lettres...
    grep '.......' |       # Avec au moins 7 lettres,
    grep '^is' |           # commençant par 'is'
    grep -v 's$' |         # sans les puriels
    grep -v 'ed$'          # Sans verbe au passé ("ed" en anglais)
    
    #  Utilise l'utilitaire "anagram"
    #+ qui fait partie du paquetage de liste de mots "yawl" de l'auteur.
    #  http://ibiblio.org/pub/Linux/libs/yawl-0.2.tar.gz
    
    exit 0                 # Fin du code.
    
    bash$ sh agram.sh
    islander
    isolate
    isolead
    isotheral

    Voir aussi Exemple 28-2, Exemple 12-18, et Exemple A-10.

  • Utiliser des << documents anonymes >> pour mettre en commentaire des blocs de code, pour ne pas avoir à mettre en commentaire chaque ligne avec un #. Voir Exemple 17-10.

  • Lancer sur une machine un script dépendant de la présence d'une commande qui peut être absente est dangereux. Utilisez whatis pour éviter des problèmes potentiels avec ceci.

    CMD=commande1                 # Premier choix.
    PlanB=commande2               # Option en cas de problème.
    
    command_test=$(whatis "$CMD" | grep 'nothing appropriate')
    #  Si 'commande1' n'est pas trouvé sur ce système, 'whatis' renverra
    #+ "commande1: nothing appropriate."
    
    
    if [[ -z "$command_test" ]]  # Vérifie si la commande est présente.
    then
      $CMD option1 option2       #  Lancez commande1 avec ses options.
    else                         #  Sinon,
      $PlanB                     #+ lancez commande2. 
    fi

  • La commande run-parts est utile pour lancer un ensemble de scripts dans l'ordre, particulièrement en combinaison avec cron ou at.

  • Il serait bien d'être capable d'invoquer les objets X-Windows à partir d'un script shell. Il existe plusieurs paquets qui disent le faire, à savoir Xscript, Xmenu, et widtools. Les deux premiers ne semblent plus maintenus. Heureusement, il est toujours possible d'obtenir widtools ici.

    Attention

    Le paquet widtools (widget tools, outils pour objets) nécessite que la bibliothèque XForms soit installée. De plus, le Makefile a besoin d'être édité de façon judicieuse avant que le paquet ne soit construit sur un système Linux typique. Finalement, trois des six objets offerts ne fonctionnent pas (en fait, ils génèrent un défaut de segmentation).

    Pour plus d'efficacité des scripts utilisant des widgets, essayez Tk ou wish (des dérivés de Tcl), PerlTk (Perl avec des extensions Tk), tksh (ksh avec des extensions Tk), XForms4Perl (Perl avec des extensions XForms), Gtk-Perl (Perl avec des extensions Gtk), ou PyQt (Python avec des extensions Qt).

  • do # Q[n] = Q[n - Q[n-1]] + Q[n - Q[n-2]] for n>2 # Nécessaire de casser l'expression en des termes intermédiaires. # car Bash ne gère pas très bien l'arithmétique des tableaux complexes. let "n1 = $n - 1" # n-1 let "n2 = $n - 2" # n-2 t0=`expr $n - ${Q[n1]}` # n - Q[n-1] t1=`expr $n - ${Q[n2]}` # n - Q[n-2] T0=${Q[t0]} # Q[n - Q[n-1]] T1=${Q[t1]} # Q[n - Q[n-2]] Q[n]=`expr $T0 + $T1` # Q[n - Q[n-1]] + Q[n - ![n-2]] echo -n "${Q[n]} " if [ `expr $n % $LONGUEURLIGNE` -eq 0 ] # Formatte la sortie. then # mod echo # Retour chariot pour des ensembles plus jolis. fi done echo exit 0 # C'est une implémentation itérative la Q-series. # L'implémentation récursive plus intuitive est laissée comme exercice. # Attention: calculer cette série récursivement prend *beaucoup* de temps.

    --

    Bash supporte uniquement les tableaux à une dimension, néanmoins une petite astuce permet de simuler des tableaux à plusieurs dimensions.

    Exemple 26-9. Simuler un tableau à deux dimensions, puis son test

    #!/bin/bash
    # Simuler un tableau à deux dimensions.
    
    # Un tableau à deux dimensions stocke les lignes séquentiellement.
    
    Lignes=5
    Colonnes=5
    
    declare -a alpha     # char alpha [Lignes] [Colonnes];
                         # Déclaration inutile.
    
    charge_alpha ()
    {
    local rc=0
    local index
    
    
    for i in A B C D E F G H I J K L M N O P Q R S T U V W X Y
    do
      local ligne=`expr $rc / $Colonnes`
      local colonne=`expr $rc % $Lignes`
      let "index = $ligne * $Lignes + $colonne"
      alpha[$index]=$i   # alpha[$ligne][$colonne]
      let "rc += 1"
    done  
    
    # Un peu plus simple
    #   declare -a alpha=( A B C D E F G H I J K L M N O P Q R S T U V W X Y )
    # mais il manque néanmoins le "bon goût" d'un tableau à deux dimensions.
    }
    
    affiche_alpha ()
    {
    local ligne=0
    local index
    
    echo
    
    while [ "$ligne" -lt "$Lignes" ]  # Affiche dans l'ordre des lignes -
    do                                # les colonnes varient
                                      # tant que ligne (la boucle externe) reste
    				  # identique
      local colonne=0
      
      while [ "$colonne" -lt "$Colonnes" ]
      do
        let "index = $ligne * $Lignes + $colonne"
        echo -n "${alpha[index]} "  # alpha[$ligne][$colonne]
        let "colonne += 1"
      done
    
      let "ligne += 1"
      echo
    
    done  
    
    # Un équivalent plus simple serait
    #   echo ${alpha[*]} | xargs -n $Colonnes
    
    echo
    }
    
    filtrer ()     # Filtrer les index négatifs du tableau.
    {
    
    echo -n "  "
    
    if [[ "$1" -ge 0 &&  "$1" -lt "$Lignes" && "$2" -ge 0 && "$2" -lt "$Colonnes" ]]
    then
        let "index = $1 * $Lignes + $2"
        # Maintenan, l'affiche après rotation.
        echo -n " ${alpha[index]}"  # alpha[$ligne][$colonne]
    fi    
    
    }
      
    
    
    
    rotate ()  # Bascule le tableau de 45 degrés
    {          # (le "balance" sur le côté gauche en bas).
    local ligne
    local colonne
    
    for (( ligne = Lignes; ligne > -Lignes; ligne-- ))  # Traverse le tableau en
    #	sens inverse.
    do
    
      for (( colonne = 0; colonne < Colonnes; colonne++ ))
      do
    
        if [ "$ligne" -ge 0 ]
        then
          let "t1 = $colonne - $ligne"
          let "t2 = $colonne"
        else
          let "t1 = $colonne"
          let "t2 = $colonne + $ligne"
        fi  
    
        filtrer $t1 $t2   # Filtre les index négatifs du tableau.
      done
    
      echo; echo
    
    done 
    
    # Rotation du tableau inspirée par les exemples (pp. 143-146) de
    # "Advanced C Programming on the IBM PC", par Herbert Mayer
    # (voir bibliographie).
    
    }
    
    
    #-----------------------------------------------------#
    charge_alpha     # Charge le tableau.
    affiche_alpha    # L'affiche.
    rotate           # Le fait basculer sur 45 degrés dans le sens contraire des
                     # aiguilles d'une montre.
    #-----------------------------------------------------#
    
    
    # C'est une simulation assez peu satisfaisante.
    #
    # Exercices:
    # ---------
    # 1)  Réécrire le chargement du tableau et les fonctions d'affichage
    #   + d'une façon plus intuitive et élégante.
    #
    # 2)  Comprendre comment les fonctions de rotation fonctionnent.
    #     Astuce: pensez aux implications de l'indexage arrière du tableau.
    
    exit 0

    Un tableau à deux dimensions est essentiellement équivalent à un tableau à une seule dimension mais avec des modes d'adressage supplémentaires pour les références et les manipulations d'éléments individuels par la position de la << ligne >> et de la << colonne >>.

    Pour un exemple encore plus élaboré de simulation d'un tableau à deux dimensions, voir Exemple A-11.