B.2. Awk

Awk est un langage de manipulation de texte plein de fonctionnalités avec une syntaxe proche du C. Alors qu'il possède un ensemble impressionnant d'opérateurs et de fonctionnalités, nous n'en couvrirons que quelques-uns, les plus utiles pour l'écriture de scripts shell.

Awk casse chaque ligne d'entrée en champs. Par défaut, un champ est une chaîne de caractères consécutifs délimités par des espaces, bien qu'il existe des options pour changer le délimiteur. Awk analyse et opère sur chaque champ. Ceci rend awk idéal pour gérer des fichiers texte structurés, particulièrement des tableaux, des données organisées en ensembles cohérents, tels que des lignes et des colonnes.

Des guillemets forts (guillemets simples) et des accolades entourent les segments de code awk dans un script shell.

awk '{print $3}' $nomfichier
# Affiche le champ #3 du fichier $nomfichier sur stdout.

awk '{print $1 $5 $6}' $nomfichier
# Affiche les champs #1, #5 et #6 du fichier $nomfichier.

Nous venons juste de voir la commande awk print en action. Les seules autres fonctionnalités de awk que nous avons besoin de gérer ici sont des variables. Awk gère les variables de façon similaire aux scripts shell, quoiqu'avec un peu plus de flexibilité.

{ total += ${numero_colonne} }
Ceci ajoute la valeur de numero_colonne au total << total >>. Finalement, pour afficher << total >>, il existe un bloc de commandes END, exécuté après que le script ait opéré sur toute son entrée.
END { print total }

Correspondant au END, il existe BEGIN, pour un bloc de code à exécuter avant que awk ne commence son travail sur son entrée.

Pour des exemples de awk à l'intérieur de scripts shell, jetez un oeil sur:

  1. Exemple 11-10

  2. Exemple 16-7

  3. Exemple 12-24

  4. Exemple 34-3

  5. Exemple 9-22

  6. Exemple 11-16

  7. Exemple 28-1

  8. Exemple 28-2

  9. Exemple 10-3

  10. Exemple 12-42

  11. Exemple 9-26

  12. Exemple 12-3

  13. Exemple 9-12

  14. Exemple 34-11

  15. Exemple 10-8

C'est tout ce que nous allons voir sur awk, mais il existe bien plus à apprendre. Voyez les références appropriées dans la Bibliographie.

>
#!/bin/bash

SUCCESS=0
E_BADINPUT=65

test "$1" -ne 0 -o "$1" -eq 0 2>/dev/null
# Un entier est soit égal à 0 soit différent de 0.
# 2>/dev/null supprime les messages d'erreur.

if [ $? -ne "$SUCCESS" ]
then
  echo "Usage: `basename $0` integer-input"
  exit $E_BADINPUT
fi

let "sum = $1 + 25"             # Donnera une erreur si $1 n'est pas un entier.
echo "Sum = $sum"

#  Toute variable, pas simplement un paramètre de ligne de commande, peut être
#+ testé de cette façon.

exit 0

  • L'échelle 0 - 255 des valeurs de retour des fonctions est une limitation importante. Les variables globales et autres moyens de contourner ce problème sont souvent des problèmes en eux-même. Une autre méthode, pour que la fonction communique une valeur de retour au corps principal du script, est que la fonction écrive sur stdout la << valeur de sortie >>, et d'assigner cette sortie à une variable.

    Exemple 34-10. Astuce de valeur de retour

    #!/bin/bash
    # multiplication.sh
    
    multiplie ()                     # Multiplie les paramètres passés.
    {                               # Acceptera un nombre variable d'arguments.
    
      local produit=1
    
      until [ -z "$1" ]             # Jusqu'à la fin de tous les arguments...
      do
        let "produit *= $1"
        shift
      done
    
      echo $produit                 #  N'affichera pas sur stdout,
    }                               #+ car cela va être affecté à une variable.
    
    mult1=15383; mult2=25211
    val1=`multiplie $mult1 $mult2`
    echo "$mult1 X $mult2 = $val1"
                                    # 387820813
    
    mult1=25; mult2=5; mult3=20
    val2=`multiplie $mult1 $mult2 $mult3`
    echo "$mult1 X $mult2 X $mult3 = $val2"
                                    # 2500
    
    mult1=188; mult2=37; mult3=25; mult4=47
    val3=`multiplie $mult1 $mult2 $mult3 $mult4`
    echo "$mult1 X $mult2 X $mult3 X mult4 = $val3"
                                    # 8173300
    
    exit 0

    La même technique fonctionne aussi pour les chaînes de caractères alphanumériques. Ceci signifie qu'une fonction peut << renvoyer >> une valeur non-numérique.

    capitaliser_ichar ()          #  Capitaliser le premier caractère
    {                                   #+ de(s) chaîne(s) de caractères passées.
    
      chaine0="$@"                      # Accepte de multiples arguments.
    
      premiercaractere=${chaine0:0:1}   # Premier caractère.
      chaine1=${chaine0:1}              # Reste de(s) chaîne(s) de caractères.
    
      PremierCaractere=`echo "$premiercaractere" | tr a-z A-Z`
                                        # Capitalise le premier caractère.
    
      echo "$PremierCaractere$chaine1"  # Sortie vers stdout.
    
    }  
    
    nouvellechaine=`capitalize_ichar "chaque phrase doit commencer avec une lettre majuscule."`
    echo "$nouvellechaine"          # Chaque phrase doit commencer avec une lettre majuscule.

    Il est même possible pour une fonction de << renvoyer >> plusieurs valeurs avec cette méthode.

    Exemple 34-11. Une astuce permettant de renvoyer plus d'une valeur de retour

    #!/bin/bash
    # sum-product.sh
    # Une fonction peut "renvoyer" plus d'une valeur.
    
    somme_et_produit ()   # Calcule à la fois la somme et le produit des arguments.
    {
      echo $(( $1 + $2 )) $(( $1 * $2 ))
    # Envoie sur stdout chaque valeur calculée, séparée par un espace.
    }
    
    echo
    echo "Entrez le premier nombre "
    read premier
    
    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.