10. Les manipulations de variables

10.1. Manipulation de chaînes de caractères

Bash supporte un nombre impressionnant d'opérations de manipulation des chaînes de caractères. Malheureusement, ces outils manquent un peu de cohérence. Certains sont un sous-ensemble de la substitution de paramètres et les autres font partie des fonctionnalités de la commande UNIX expr. Ce qui produit une syntaxe de commandes non unifiée et des fonctionnalités qui se recoupent, sans parler de la confusion engendrée.

Longueur d'une chaîne de caractères

${#chaine}
expr length $chaine

C'est l'équivalent de la fonction strlen() en C.

expr "$chaine" : '.*'
chaineZ=abcABC123ABCabc

echo ${#chaineZ}                 # 15
echo `expr length $chaineZ`      # 15
echo `expr "$chaineZ" : '.*'`    # 15

Exemple 10.1. Insérer un saut de ligne entre les paragraphes d'un fichier texte

#!/bin/bash
# paragraph-space.sh

# Insère une ligne blanche entre les paragraphes d'un fichier texte.
# Usage: $0 < NOMFICHIER

LONGUEUR_MINI=45        # Il peut être nécessaire de changer cette valeur.
#  On suppose que les lignes plus petites que $LONGUEUR_MINI caractères
#+ terminent un paragraphe.

while read ligne  # Pour toutes les lignes du fichier...
do
  echo "$ligne"   # Afficher la ligne.

  longueur=${#ligne}
  if [ "$longueur" -lt "$LONGUEUR_MINI" ]
    then echo    # Ajoute une ligne blanche après chaque ligne courte.
  fi  
done

exit 0

Longueur de sous-chaînes correspondant à un motif au début d'une chaîne

expr match "$chaine" '$souschaine'

$souschaine est une expression rationnelle.

expr "$chaine" : '$souschaine'

$souschaine est une expression rationnelle.

chaineZ=abcABC123ABCabc
#       |------|
#       12345678

echo `expr match "$chaineZ" 'abc[A-Z]*.2'`   # 8
echo `expr "$chaineZ" : 'abc[A-Z]*.2'`       # 8

Position

expr index $chaine $souschaine

Position numérique dans $chaine du premier caractère de $souschaine qui s'y trouve.

chaineZ=abcABC123ABCabc
#       123456 ...
echo `expr index "$chaineZ" C12`             # 6
                                             # C position.

echo `expr index "$chaineZ" 1c`              # 3
# 'c' (à la position 3) se trouve avant '1'.

C'est l'équivalent le plus proche de strchr() en C.

Extraction d'une sous-chaîne

${chaine:position}

Extrait une sous-chaîne de $chaine à partir de la position $position.

Si le paramètre $chaine est « * » ou « @ », on extrait de cette manière les paramètres de position, [48] à partir de $position.

${chaine:position:longueur}

Extrait $longueur caractères d'une sous-chaîne de $chaine à la position $position.

chaineZ=abcABC123ABCabc
#       0123456789.....
#       comptage à partir de 0.

echo ${chaineZ:0}                            # abcABC123ABCabc
echo ${chaineZ:1}                            # bcABC123ABCabc
echo ${chaineZ:7}                            # 23ABCabc

echo ${chaineZ:7:3}                          # 23A
                                             # Trois caractères de la sous-chaîne.


# Est-il possible de compter depuis la fin de la chaîne ?

echo ${chaineZ:-4}                           # abcABC123ABCabc
# Par défaut la chaîne complète, comme dans ${parametre:-default}.
# Néanmoins...

echo ${chaineZ:(-4)}                         # Cabc
echo ${chaineZ: -4}                          # Cabc
# Maintenant, cela fonctionne.
#  Des parenthèses ou des espaces ajoutés permettent un échappement du paramètre
#+ de position.

# Merci à Dan Jacobson pour cette
#+ indication.

Les arguments position et longueur peuvent devenir des « paramètres », c'est-à-dire contenir une variable plutôt qu'une constante numérique.

Exemple 10.2. Générer « de manière aléatoire » une chaîne de huit caractères

#!/bin/bash
# rand-string.sh
# Générer de manière pseudo-aléatoire 
# une chaîne de 8 caractères.

if [ -n "$1" ]  #  S'il y a bien un argument sur la ligne de commande,
then            #+ l'utiliser comme chaîne initiale.
  ch0="$1"
else            #  Sinon, utiliser le PID du script.
  ch0="$$"
fi

POS=2  # À partir de la position 2 du script.
LEN=8  # Extraire huit caractères.

ch1=$( echo "$ch0" | md5sum | md5sum )
# Brouiller 2 fois:  ^^^^^^   ^^^^^^

chainealeatoire="${ch1:$POS:$LEN}"
# On peut paramétriser ^^^^ ^^^^ 

echo "$chainealeatoire"

exit $?

# bozo$ ./rand-string.sh mon-mot-de-passe
# 1bdd88c4

#  Mais ce n'est pas une méthode recommandée
#+ pour fabriquer des mots de passe solides.

Si le paramètre $chaine est « * » ou « @ », alors cette méthode extrait un maximum de $longueur paramètres de position, à partir de $position.

echo ${*:2}          # Affiche le deuxième paramètre de position et les suivants.
echo ${@:2}          # Identique à ci-dessus.

echo ${*:2:3}        # Affiche trois paramètres de position à partir du second.
expr substr $chaine $position $longueur

Extrait $longueur caractères de $chaine, à partir de $position.

chaineZ=abcABC123ABCabc
#       123456789......
#       comptage à partir de 1.

echo `expr substr $chaineZ 1 2`              # ab
echo `expr substr $chaineZ 4 3`              # ABC

expr match "$chaine" '\($souschaine\)'

Extrait $souschaine à partir du début de $chaine, $souschaine étant une expression rationnelle.

expr "$chaine" : '\($souschaine\)'

Extrait $souschaine à partir du début de $chaine, $souschaine étant une expression rationnelle.

chaineZ=abcABC123ABCabc
#       =======

echo `expr match "$chaineZ" '\(.[b-c]*[A-Z]..[0-9]\)'`   # abcABC1
echo `expr "$chaineZ" : '\(.[b-c]*[A-Z]..[0-9]\)'`       # abcABC1
echo `expr "$chaineZ" : '\(.......\)'`                   # abcABC1
# Toutes les écritures ci-dessus donnent le même résultat.
expr match "$chaine" '.*\($souschaine\)'

Extrait $souschaine à la fin de $chaine, où $souschaine est une expression rationnelle.

expr "$chaine" : '.*\($souschaine\)'

Extrait $souschaine à la fin de $chaine, où $souschaine est une expression rationnelle.

chaineZ=abcABC123ABCabc
#                ======

echo `expr match "$chaineZ" '.*\([A-C][A-C][A-C][a-c]*\)'`    # ABCabc
echo `expr "$chaineZ" : '.*\(......\)'`                       # ABCabc

Suppression de sous-chaînes

${chaine#souschaine}

Efface l'occurrence la plus courte de $souschaine à partir du début de $chaine.

${chaine##souschaine}

Efface l'occurrence la plus longue de $souschaine à partir du début de $chaine.

chaineZ=abcABC123ABCabc
#       |----|        la plus courte
#       |----------|  la plus longue

echo ${chaineZ#a*C}      # 123ABCabc
# Efface la plus courte occurrence entre 'a' et 'C'.

echo ${chaineZ##a*C}     # abc
# Efface la plus longue occurrence entre 'a' et 'C'.


# On peut paramétriser les sous-chaînes.

X='a*C'

echo ${chaineZ#$X}      # 123ABCabc
echo ${chaineZ##$X}     # abc
                        # Comme ci-dessus.
${chaine%souschaine}

Efface l'occurrence la plus courte de $souschaine à partir de la fin de $chaine.

Par exemple :

# Renomme tous les fichiers de $PWD
#+ en remplaçant le suffixe "TXT" par "txt".
# Par exemple, "fichier1.TXT" devient "fichier1.txt" . . .

SUFF=TXT
suff=txt

for i in $(ls *.$SUFF)
do
  mv -f $i ${i%.$SUFF}.$suff
  #  Modifie *uniquement* l'occurrence la plus courte de $SUFF
  #+ en partant du côté droit de $i . . .
done ### Ce petit script peut même être condensé en une seule ligne.

# Merci à Rory Winston.
${chaine%%souschaine}

Efface l'occurrence la plus longue de $souschaine, en partant de la fin de $chaine.

chaineZ=abcABC123ABCabc
#                    ||   la plus courte
#        |------------|   la plus longue

echo ${chaineZ%b*c}      # abcABC123ABCa
#  Coupe l'occurrence la plus courte entre 'b' et 'c', 
#+ en partant de la fin de $chaineZ.

echo ${chaineZ%%b*c}     # a
#  Coupe l'occurrence la plus longue entre 'b' et 'c', 
#+ en partant de la fin de $chaineZ.

Cet opérateur est utilisé pour générer des noms de fichier.

Exemple 10.3. Conversion de formats de fichiers graphiques avec modification du nom de fichier

#!/bin/bash
#  cvt.sh:
#  Convertit les fichiers d'image MacPaint contenus dans un répertoire 
#+ vers le format "pbm".

#  Utilise le binaire "macptopbm" provenant du paquetage "netpbm",
#+ qui est maintenu par Brian Henderson (bryanh@giraffe-data.com).
#  Netpbm est présent sur la plupart des distributions Linux.

OPERATION=macptopbm
SUFFIXE=pbm         # Suffixe des nouveaux noms de fichiers.

if [ -n "$1" ]
then
  repertoire=$1      # Si nom du répertoire donné en argument au script...
else
  repertoire=$PWD    # Sinon, utilise le répertoire courant.
fi  
  
#  Supposons que tous les fichiers du répertoire cible sont des fichiers d'image
# + MacPaint avec un suffixe de nom de fichier ".mac".

for fichier in $repertoire/*  # Remplacement de noms de fichiers
do
  nomfichier=${fichier%.*c} #  Enlève le suffixe ".mac" au nom du fichier
                            #+ ('.*c' correspond à tout ce qui se trouve
                                        #+ entre '.' et 'c', inclus).
  $OPERATION $fichier > $nomfichier.$SUFFIXE
    # Redirige la conversion vers le nouveau nom de fichier.
    rm -f $fichier          # Supprime le fichier d'origine après sa conversion.
  echo "$nomfichier.$SUFFIXE"  # Affiche sur stdout le déroulement des opérations.
done

exit 0

# Exercice
# --------
#  À ce stade, ce script convertit *tous* les fichiers du répertoire courant.
#  Modifiez-le pour qu'il renomme *seulement* les fichiers dont l'extension est
#+ ".mac".

Exemple 10.4. Conversion de fichiers audio en ogg

#!/bin/bash
# ra2ogg.sh: Convert streaming audio files (*.ra) to ogg.

# Uses the "mplayer" media player program:
#      http://www.mplayerhq.hu/homepage
# Uses the "ogg" library and "oggenc":
#      http://www.xiph.org/
#
# This script may need appropriate codecs installed, such as sipr.so ...
# Possibly also the compat-libstdc++ package.


OFILEPREF=${1%%ra}      # Strip off the "ra" suffix.
OFILESUFF=wav           # Suffix for wav file.
OUTFILE="$OFILEPREF""$OFILESUFF"
E_NOARGS=85

if [ -z "$1" ]          # Must specify a filename to convert.
then
  echo "Usage: `basename $0` [filename]"
  exit $E_NOARGS
fi


##########################################################################
mplayer "$1" -ao pcm:file=$OUTFILE
oggenc "$OUTFILE"  # Correct file extension automatically added by oggenc.
##########################################################################

rm "$OUTFILE"      # Delete intermediate *.wav file.
                   # If you want to keep it, comment out above line.

exit $?

#  Note:
#  ----
#  On a Website, simply clicking on a *.ram streaming audio file
#+ usually only downloads the URL of the actual *.ra audio file.
#  You can then use "wget" or something similar
#+ to download the *.ra file itself.


#  Exercises:
#  ---------
#  As is, this script converts only *.ra filenames.
#  Add flexibility by permitting use of *.ram and other filenames.
#
#  If you're really ambitious, expand the script
#+ to do automatic downloads and conversions of streaming audio files.
#  Given a URL, batch download streaming audio files (using "wget")
#+ and convert them on the fly.

Une simple émulation de getopt en utilisant des expressions avec extraction de sous-chaînes.

Exemple 10.5. Émuler getopt

#!/bin/bash
# getopt-simple.sh
# Auteur : Chris Morgan
# Utilisé dans le guide ABS avec la permission de l'auteur.
# Traducteur : Éric Deschamps

getopt_simple()
{
    echo "getopt_simple()"
    echo "Liste des paramètres '$*'"
    until [ -z "$1" ]
    do
      echo "Traitement du paramètre : '$1'"
      if [ ${1:0:1} = '/' ]
      then
          tmp=${1:1}                # Retrait du '/' au début de la chaîne
          parametre=${tmp%%=*}      # Extraction du nom
          valeur=${tmp##*=}         # Extraction de la valeur.
          echo "Paramètre : '$parametre', valeur : '$valeur'"
          eval $parametre=$valeur
      fi
      shift
    done
}

# Passage de l'ensemble des options a getopt_simple().
getopt_simple $*

echo "la valeur de test est '$test'"
echo "la valeur de test2 est '$test2'"

exit 0  # Voir aussi UseGetOpt.sh, version modifiée de ce script.

# ---
# bash getopt-simple.sh /test=valeur1 /test2=valeur2
# getopt_simple()
# Les paramètres sont '/test=valeur1 /test2=valeur2'
# Traitement du paramètre : '/test=valeur1'
# Paramètre : 'test', valeur : 'valeur1'
# Traitement du paramètre : '/test2=valeur2'
# Paramètre : 'test2', valeur: 'valeur2'
# la valeur de test est 'valeur1'
# la valeur de test2 est 'valeur2'
#

Remplacement d'une sous-chaîne

${chaine/souschaine/remplacement}

Remplace la première occurrence de $souschaine par $remplacement. [49]

${chaine//souschaine/remplacement}

Remplace toutes les occurrences de $souschaine par $remplacement.

chaineZ=abcABC123ABCabc

echo ${chaineZ/abc/xyz}           # xyzABC123ABCabc
                                  #  Remplace la première occurrence de
                                  #+ 'abc' par 'xyz'.

echo ${chaineZ//abc/xyz}          # xyzABC123ABCxyz
                                  #  Remplace toutes les occurrences de
                                  #+ 'abc' par 'xyz'.

echo  ---------------
echo "$chaineZ"                   # abcABC123ABCabc
echo  ---------------
# La chaîne elle-même n'est pas modifiée !

# Le motif de recherche et le remplacement peuvent-ils être
#+ paramétrisés ?

motif=abc
rempl=000
echo ${chaineZ/$motif/$rempl}  # 000ABC123ABCabc
#              ^      ^          ^^^
echo ${chaineZ//$motif/$rempl} # 000ABC123ABC000
# Oui !         ^      ^         ^^^         ^^^

echo

# Que se passe-t-il si aucune chaîne de remplacement n'est fournie ?
echo ${chaineZ/abc}           # ABC123ABCabc
echo ${chaineZ//abc}          # ABC123ABC
# On obtient une suppression pure et simple.
${chaine/#souschaine/remplacement}

Si le début de $chaine est conforme à $souschaine, substitue $remplacement à $souschaine.

${chaine/%souchaine/remplacement}

Si la fin de $chaine est conforme à $souschaine, substitue $remplacement à $souschaine.

chaineZ=abcABC123ABCabc

echo ${chaineZ/#abc/XYZ}          # XYZABC123ABCabc
                                  #  Remplace l'occurrence de début de
                                  #+ 'abc' par 'XYZ'.

echo ${chaineZ/%abc/XYZ}          # abcABC123ABCXYZ
                                  #  Remplace l'occurrence finale de
                                  #+ 'abc' par  'XYZ'.

10.1.1. Manipuler des chaînes de caractères avec awk

Un script Bash peut utiliser des fonctionnalités de manipulation de chaînes de caractères de awk comme alternative à ses propres fonctions intégrées.

Exemple 10.6. Autres moyens pour extraire ou situer des sous-chaînes

#!/bin/bash
# substring-extraction.sh

Chaine=23skidoo1
#      012345678    Bash
#      123456789    awk
# Observez les différents modes de comptage des caractères :
# En Bash, le premier caractère de la chaîne est compté '0'.
# En Awk, le premier caractère de la chaîne est compté '1'.

echo ${Chaine:2:4} # 3ème position (0-1-2), quatre caractères de long
                                         # skid

# L'équivalent awk de ${string:position:longueur} est substr(string,position,longueur).
echo | awk '
{ print substr("'"${Chaine}"'",3,4)      # skid
}
'
#  Envoyer un "echo" vide à awk évite
#+ d'avoir à fournir un nom de fichier en entrée.

exit 0



[48] S'applique soit aux arguments en ligne de commande, soit aux paramètres passés à une fonction.

[49] Remarque : suivant le contexte, $souschaine et $remplacement peuvent être soit des chaînes littérales, soit des variables. Voyez le premier exemple d'utilisation.