Bash et scripts shell

Le shebang

L'entête d'un script shell devrait toujours commencer par un sha-bang qui assurera le chargement du bon shell.

Shebang du shell bash:

#!/bin/bash

Quel est mon shell

$ echo $0
bash

$ echo $SHELL
/bin/bash

$ cat /proc/$$/cmdline
bashuser@computer:~$

$ `cat /proc/$$/cmdline` --version
GNU bash, version 4.2.10(1)-release (i686-pc-linux-gnu)
Copyright (C) 2011 Free Software Foundation, Inc.
Licence GPLv3+ : GNU GPL version 3 ou ultérieure <http://gnu.org/licenses/gpl.html>

Les commentaires

Un simple commentaire

# ceci est un commentaire

Débogage

Lancer bash en mode débogage avec le nom du script en paramètre

$ bash -x deboge_moi.sh

Exécuter le script

Avant de pouvoir exécuter le script il faut que le fichier soit rendu exécutable

# chmod +x script.sh

Et pour l'exécuter il faut être dans le même répertoire

$ ./script.sh

Ou taper le chemin complet

$ /home/user/script.sh

Pour exécuter le script depuis n'importe où, exactement comme une commande, il faut le copier dans un des répertoires $PATH du système.
On peut connaitre les répertoires $PATH grâce à la commande echo $PATH ou echo $PATH | tr : \\n.

$ echo $PATH | tr : \\n
/usr/local/bin
/usr/bin
/bin
/usr/local/games
/usr/games

Les répertoires listés ci-dessus sont communs à tous les utilisateur du système, y copier un script le rendra accessible à tous. C'est peut-être ce que vous souhaitez mais dans la pratique il est plus commode de disposer d'un répertoire dédié à ce genre de travail dans son espace personnel, un répertoire "/home/user/bin" par exemple.

Pour ajouter ce répertoire "/home/user/bin" à la variable $PATH on procédera ainsi:

$ export PATH="$HOME/bin:$PATH"

Toute-fois cette commande est provisoire et devra être répétée chaque fois que vous fermez le terminal. Pour y remédier il faudra enregistrer cette commande dans le fichier caché .bashrc qui est sensé se trouver dans votre répertoire personnel. Ceci fait vous devez recharger ce fichier de configuration avec la commande $ source ~.bashrc ou fermer et rouvrir le terminal.

Voila il ne reste plus qu'à copier votre script dans ce répertoire pour y avoir accès d'où vous voulez.

$ cp script.sh /home/user/bin/script.sh

Il suffira ensuite de l'appeler le plus simplement du monde

$ script.sh

l'extension .sh n'est pas obligatoire, il est donc possible de copier le script
$ cp script.sh /home/utilisateur/bin/script et de l'appeler encore plus simplement
$ script comme n'importe quelle autre commande.

Les variables

La syntaxe est

 variable='valeur'

Et pour l'afficher

echo $variable

La valeur de la variable doit être entourée de "quotes", il en existe 3 sortes:

Selon les "quotes" utilisés le comportement de bash sera différent

Avec des simple quotes les variables ne sont pas interprétés

var='ceci est du texte'
echo 'la valeur est : $var'

Affichera en console

$ la valeur est : $var

Avec des doubles quotes le contenu est interprété

var='ceci est du texte'
echo "la valeur est : $var"

Affichera en console

$ la valeur est : ceci est du texte

Avec les back quotes la valeur de la variable est exécutée

var=`pwd`
echo "vous êtes ici : $var"

Affichera en console

$ vous êtes ici : /home/user/

Dans ce cas de figure préférez plutôt cette méthode: echo "vous êtes ici $(pwd)"

Les saut de lignes se font grâce à \n placé dans une chaîne de caractère mais pour qu'il soit interprété comme tel l'option -e doit être activée

$ echo -e "ligne 1 \n ligne 2"

Les variables d'environnement ou variables globales

Les variables d'environnement sont des variables accessibles à tout moment.

Afficher les variables d'environnement

$ env
ORBIT_SOCKETDIR=/tmp/orbit-manu
SSH_AGENT_PID=1545
GIO_LAUNCHED_DESKTOP_FILE_PID=5845
GPG_AGENT_INFO=/tmp/keyring-ImY6lo/gpg:0:1
TERM=xterm
SHELL=/bin/bash
XDG_SESSION_COOKIE=abc0a91a3425c3d8b2b42de90000000a-1323506409.211427-1853304978
WINDOWID=69206020
GNOME_KEYRING_CONTROL=/tmp/keyring-ImY6lo
GTK_MODULES=canberra-gtk-module:canberra-gtk-module
...

Quelques explications:

Pour s'en servir il suffit de les appeler par leur nom

echo "l'éditeur par défaut sur cet ordinateur est $EDITOR"

Les variables paramètre de script

Les scripts peuvent accepter des paramètres:

$ ./script.sh param1 param2 param3

Récupérer ces paramètres dans le script

#!/bin/bash
echo "Vous avez lancé $0, il y a $# paramètres"
echo "Le paramètre 1 est $1"

si on lance le script avec des paramètres cela affichera

$ ./script.sh param1 param2 param3
Vous avez lancé ./script.sh, il y a 3 paramètres
Le paramètre 1 est param1

Quelques explications:

Seule 9 paramètres peuvent êtres entrés mais si toutefois il y en avait plus il est possible de les décaler un à un.

Utiliser shift pour décaler les paramètres:

#!/bin/bash
echo "Le paramètre 1 est $1"
shift
echo "Le paramètre 2 est $1"
...

à l'affichage

$ ./script.sh param1 param2 param3
Le paramètre 1 est param1
Le paramètre 2 est param2
...

Passer un tableau en paramètre

Supposons que l'on dispose d'un script contenant plusieurs tableaux sous cette forme.

jp=('Jean-Perre' 'Dupont' 'jeanpierre@mail.com' '01.18.17.16.15')
mimil=('Emile' 'Laporte' 'milo@mail.com' '04.28.27.26.25')
...

Et que l'on souhaite appeler ce script avec comme paramètre le nom de variable d'un tableau comme ceci

./script.sh mimil

Afin de récupérer chacune des entrée du tableau il faudra obligatoirement passer par des références indirectes

nom=$1[0]
prenom=$1[1]
mail=$1[2]
tel=$1[3]
echo ${!nom}
echo ${!prenom}
echo ${!mail}
echo ${!tel}

affichera:

Emile
Laporte
milo@mail.com
04.28.27.26.25

demander une saisie

Syntaxe:

read nomvariable
echo "la valeur est : $nomvariable"

Lors de l'exécution du script le prompt attendra une saisie de l'utilisateur et une fois validé affichera:

$ la valeur est : [ce que l'utilisateur aura saisi]
read var1 var2
echo "la saisie 1 est $var1 et la saisie 2 est $var2"

read assigne chaque mot saisi à une variable, si l'utilisateur entre plus de mots qu'il y a de variables c'est la dernière variable qui prendra tous les mots en trop.

Le paramètre -p permet d'afficher un message de prompt pour que l'utilisateur sache quoi entrer

read -p 'entrez un mot: ' mot
echo "vous dites $mot"

Le paramètre -n limite le nombre de caractères

read -p 'Entrez un mot de 5 caractères maximum : ' -n 5 mot
echo -e "\n vous dites $mot"

Le paramètre -t limite le temps autorisé pour la saisie

read -p 'vous avez 5 secondes pour dire un mot : ' -t 5 mot

Le paramètre -s masque ce que l'utilisateur écrit

read -p 'Entrez votre mot de passe : ' -s motdepasse

Opérations mathématiques

Bash reconnait les variables comme des chaînes de caractères, il est donc nécessaire d'utiliser la commande let pour faire des opérations.

L'addition:

let "a = 5 + 4"

La soustraction:

let "a = 5 - 4"

La multiplication:

let "a = 4 * 2"

La division:

let "a = 12 / 4"

Le reste de la division (modulo):

let "a = 12 % 4"

La puissance ou exposant:

let "a = 10 ** 2"

Il est également possible de concaténer les opérations

let "a = a * 4"

est équivalent à

let "a *= 4"

ici les retours sont des nombres entiers. Pour utiliser des décimaux il faut utiliser la commande bc

Nombres aléatoires

La variable globale $RANDOM génère un nombre aléatoire compris entre 0 et 32767
Ainsi si on tape dans un terminal:

$ echo $RANDOM

Affichera aléatoirement un nombre entre 0 et 32767.

Mais il est des situations ou l'on n'a pas 32768 possibilités et on aimerait bien obtenir un nombre aléatoire entre 0 et 20 par exemple.
Don't panic, imaginons que $RANDOM nous retourne 23752
si on divise 23752 par 20 on obtient 1187,6 et si à l'inverse on multiplie 1187 * 20 on obtient 23740.
En d'autre terme il y à 1187 fois le nombre 20 dans 23752 et il reste 12.
Vous l'aurez compris c'est le reste de la division que l'on va garder qui ne sera jamais supérieur à 19 et vaudra au minimum 0.

Donc si on souhaite un nombre compris entre 0 et 20 on écrira dans un shell:

$ echo $[$RANDOM % 21]

et pour un nombre entre 1 et 20 on fera simplement

$ echo $[$RANDOM % 20 + 1]

En mathématique la multiplication et la division sont prioritaire sur l'addition et la soustraction donc "$RANDOM % 20 + 1" équivaut à écrire "($RANDOM % 20) + 1".

Si on souhaite un nombre compris entre 30 et 100 on fera alors:

$ echo $[$RANDOM % (101 - 30) + 30]

Pour automatiser un peu les choses on pourrait dans un script bash écrire ceci:

min=30
max=100
n=$[($RANDOM % ($[$max - $min] + 1)) + $min]
echo $n

et l'adapter aux besoins simplement en changeant min et max

min=0
max=9
n=$[($RANDOM % ($[$max - $min] + 1)) + $min]
echo $n

Les tableaux

Déclaration d'un tableau

tableau=('var1' 'var2' 'var3')

Accéder à une valeur du tableau

${tableau[2]}

/!\ La clé des tableau commence à 0 donc dans l'exemple ${tableau[2]} vaut var3 /!\

Incrémenter le tableau d'une valeur supplémentaire

tableau[3]='var4'

Afficher les valeurs de tout un tableau

echo ${tableau[*]}

Affichera

var1 var2 var3 var4

Compter le nombre d'éléments d'un tableau

tab_tld=('.com' '.net' '.org' '.fr' '.ch' '.be')
n=${#tab_tld[*]}

Les conditions

La syntaxes if then

If then syntaxe 1

if [ condition ]
then
    echo "ok"
fi

If then syntaxe 2

if [ condition ]; then
    echo "ok"
fi

/!\ les espaces avant et après la condition [ condition ] sont indispensables /!\

Un exemple concret

#!/bin/bash
var='bob'
if [ $var = "bob" ]; then
    echo "je suis d'accord"
fi

Si la condition est fausse le script n'exécute pas le echo

If then else

#!/bin/bash
var='bob'
if [ $var = "bob" ]; then
    echo "je suis d'accord"
else
    echo "je ne suis pas d'accord"
fi

If then elif else

#!/bin/bash
var='bob'
if [ $var = "bob" ]; then
    echo "je suis d'accord"
elif [ $var = "boby" ]; then
    echo "je suis moyennement d'accord"
else
    echo "je ne suis pas d'accord"
fi

La syntaxe case

Autre méthode pour tester si des conditions sont justes mais avec une syntaxe plus lisible

#!/bin/bash
case $varAtester in
    "Bruno")
         echo "Salut Bruno"
    ;;
    "Michel")
         echo "Salut Michel"
    ;;
    "Jean")
         echo "Salut Jean?"
    ;;
    *)
         echo "Je ne sais pas qui vous êtes!"
    ;;
esac

/!\ Il est possible de faire des tests de condition partiels ainsi: case $varAtester in "B*") testera si $varAtester commence par un B majuscule, peu importe le reste du mot

Tout comme if then, Case peut tester plusieurs conditions:

#!/bin/bash
case $varAtester in
    "Chien" | "Chat" | "Souris")
         echo "C'est un mammifère"
    ;;
    "Moineau" | "Pigeon")
         echo "C'est un oiseau"
    ;;
    *)
         echo "Je ne sais pas ce que c'est"
    ;;
esac

Type de test

Il est possible de faire des tests sur:

Test sur les chaînes

Test d'égalité

if [ $chaine1 = $chaine2 ]
if [ $chaine1 == $chaine2 ]

Test la différence

if [ $chaine1 != $chaine2 ]

Test si la chaîne est vide

if [ -z $chaine ]

Test si la chaîne est non vide

if [ -n $chaine ]

Test sur les nombres

Test d'égalité (equal)

if [ $nbr1 -eq $nbr2 ]

Test la différence (non equal)

if [ $nbr1 -ne $nbr2 ]

Test l'infériorité (lower than)

if [ $nbr1 -lt $nbr2 ]

Test l'infériorité ou l'égalité (lower or equal)

if [ $nbr1 -le $nbr2 ]

Test la supériorité (greater than)

if [ $nbr1 -gt $nbr2 ]

Test la supériorité ou l'égalité (greater or equal)

if [ $nbr1 -ge $nbr2 ]

Test sur les fichiers

Test si le fichier existe

if [ -e $fichier ]

Test si le fichier est un répertoire

if [ -d $fichier ]

Test si le fichier est un fichier

if [ -f $fichier ]

Test si le fichier est un lien symbolique

if [ -L $fichier ]

Test si le fichier est en lecture

if [ -r $fichier ]

Test si le fichier est en écriture

if [ -w $fichier ]

Test si le fichier est exécutable

if [ -x $fichier ]

Teste si fichier1 est plus récent que fichier2 (newer than)

if [ $fichier1 -nt $fichier2 ]

Teste si fichier1 est plus vieux que fichier2 (older than)

if [ $fichier1 -ot $fichier2 ]

Il est à noter que bash test si le fichier existe

Combiner les conditions

condition ET condition

if [ $# -ge 1 ] && [ $1 = 1 ]

condition OU condition

if [ $# -ge 1 ] || [ $1 = 1 ]

Inverser une condition !

if [ $1 != 1 ]

Les boucles

While/until

While : tant que la condition est vérifié, la boucle répète l'instruction do

while [ condition ]
do
    echo 'on est dans la boucle'
done

Tout comme if, on peut écrire le début de la condition sur une seule ligne grâce au ";"

while [ condition ]; do
    echo 'on est dans la boucle'
done

Until : jusqu'à ce que la condition soit vérifiée, la boucle répète l'instruction do

until [ condition ]; do
    echo 'on a pas fini la boucle'
done

For

La boucle "for" permet de parcourir une liste de valeurs et de boucler autant de fois qu'il y en a.

#!/bin/bash
for variable in 'valeur1' 'valeur2' 'valeur3'
do
     echo "La variable vaut $variable"
done

Elle devient même très intéressante quand on la combine avec des commandes.

Ici la boucle liste le contenu du répertoire courant et affiche les fichiers

for fichier in `ls`
do
     echo "Fichier trouvé : $fichier"
done

Ou mieux, on renomme chaque fichier grâce à la concaténation

for fichier in `ls`
do
    mv $fichier $fichier-old
done

Simuler une boucle "for" classique:

Dans cet exemple, "seq" génère tous les nombres allant du premier paramètre au dernier paramètre, donc 1 2 3 4 5 6 7 8 9 10

for i in `seq 1 10`;
do
     echo $i
done

La même chose mais en avançant de 2 en 2

for i in `seq 1 2 10`;
do
     echo $i
done

Couleurs et mise en forme du texte

Syntaxe

echo -e "\<delimiteur>[<codes_couleurs_et_mise_en_forme>m <texte à mettre en forme> \<délimiteur_de_fin>

exemple: mettre une partie du texte en rouge

echo -e "\033[31m texte en rouge \033[0m ce texte n'est plus en rouge"

Quelques explications s'imposent:

Tableau des codes couleurs

couleur du texte couleur du fond couleur
30 40 noir
31 41 rouge
32 42 vert
33 43 jaune
34 44 bleu
35 45 magenta
36 46 cyan
37 47 blanc

Options de mise en forme

Un exemple pour la forme

Mettre du texte en jaune gras souligné sur fond rouge

echo -e "\033[1;4;41;33mCeci est le texte mis en forme\033[0m"

les chaînes de caractères

string="ceci est Une.chaine.Pour L'exemple"

Isoler un ou plusieurs caractères

echo ${string:0:1}
c
echo ${string:0:3}
cec
echo ${string:3:8}
i est Un

La première valeur numérique indique le caractère à partir du quel on commence à récupérer, la seconde indique le nombre de caractère à récupérer.

Compter le nombre de caractère

echo ${#string}
34

Le premier caractère en majuscule

echo ${string^}
Ceci est Une.chaine.Pour L'exemple

La même chose avec sed

echo $string | sed 's/\(.\)/\U\1/'

Tout en majuscule

echo ${string^^}
CECI EST UNE.CHAINE.POUR L'EXEMPLE

La même chose avec tr

echo $string | tr [:lower:] [:upper:]

Le premier caractère en minuscule

${string,}
ceci est Une.chaine.Pour L'exemple

string à déjà son premier caractère en minuscule mais faites moi confiance, ça marche!

La même chose avec sed

echo $string | sed 's/\(.\)/\L\1/'

Tout en minuscule

echo ${string,,}
ceci est une.chaine.pour l'exemple

La même chose avec tr

echo $string | tr [:upper:] [:lower:]

Supprimer un ou plusieurs caractère

echo $string | tr -d ${string:0:1}
eci est Une.chaine.Pour L'exemple
echo $string | tr -d ${string:20:5}
ceci est Une.chaine. L'exemple

Supprimer les espaces blanc

echo $string | tr -d [:blank:]
ceciestUne.chaine.PourL'exemple

On peut remplacer [:blank:] par un caractère quelconque. Par exemple:

echo $string | tr -d .
ceci est UnechainePour L'exemple

echo $string | tr -d c
ei est Une.haine.Pour L'exemple

Remplacer un caractère par un autre

echo $string | tr . ' '
ceci est Une chaine Pour L'exemple

Récupérer le retour (d'une commande en générale) dans une variable

var=`echo ${string^^}`
echo $var
CECI EST UNE.CHAINE.POUR L'EXEMPLE

Récuperer une sous chaine d'après un délimiteur

$ myvar=abc_def_ghi
$ echo ${myvar%%_*}
abc


$ thefile=DSC0123-987.jpg
$ echo ${thefile%%-*}
DSC0123

Partie finissante (suffix)

$ echo ${thefile##*.}
jpg

Retirer le suffix

$ thefile=DSC0123.abc.jpg
$ echo ${thefile%.*}
DSC0123.abc

Les fonctions

Quand on à dans notre code une répétition d'instructions il peut être intéressant de les placer dans une fonction.

La syntaxe de la déclaration d'une fonction se fait ainsi:

maFonction() {
     # code ...
}

et on l'appèle ainsi:

maFonction param1 param2 ...

Prenons un exemple concret avec les nombres aléatoires cité plus haut.
On avait la suite d'instructions suivante qui permet de générer un nombre aléatoire entre 0 et 9

min=0
max=9
n=$[($RANDOM % ($[$max - $min] + 1)) + $min]
echo $n

On pourrait transformer tout cela comme ça:

random() {
   n=$[($RANDOM % ($[$2 - $1] + 1)) + $1]
}

random 0 9
echo $n
random 50 100
echo $n

Il faut savoir que dans les fonction, les variables sont globales à l'ensemble du script, d'ailleurs comme on peut le voir dans l'exemple ci-dessus, la variable $n n'est déclarée que dans la fonction mais appelée depuis l'extérieur.

Il est possible de déclarer une variable local en faisant précéder son nom du mot clé local. Il est donc possible d'écrire la même fonction ainsi:

random() {
    local plus1=1
    n=$[($RANDOM % ($[$2 - $1] + $plus1)) + $1]
}

La fonction date

La fonction date retourne comme on peut s'y attendre la date du jour, mais le format dépendra de votre distribution et de la façon dont elle est paramétrée.

date
mar. 18 mai 2021 08:45:56 CEST

Ici on peut s'apercevoir que l'impression de retour est en français et qu'il s'agit de l'heure qu'il est en Europe centrale (CEST – Central European Summer Time). Toutefois si vous avez besoin d'utiliser une date dans un script bash vous aurez besoin d'un format plus strict et stable sur lequel vous pourrez vous appuyer.

Si vous pratiquez déjà un langage de programmation et que vous avez eu à manipuler les dates vous savez ce que représentent les symboles d m y h m s ..., pour ceux qui l'ignorent il s'agit de signes interprétables par la fonction date dans le but d'afficher une valeur du temps actuel dans un format précis. Par exemple date +"%Y" affichera l'année en cours à 4 chiffres. Les pages de man en dressent la liste complète.

Avant de vous livrer quelques exemples en vrac on notera simplement que pour afficher la date en console un simple date "+%d-%m-%Y" suffira à afficher 18-05-2021 mais dans un script il convient d'utiliser echo ou de stocker le retour de la commande dans une variable tel que ci-dessous:

DATE=$(date +"%d-%m-%Y")

Libre à vous par la suite d'utiliser la variable $DATE comme vous l'entendez.

Quelques exemples en vrac

date "+%d-%m-%Y %H:%M"
18-05-2021 08:45

echo `date "+%d-%m-%Y %H:%M"`
18-05-2021 08:45

echo `date -d tomorrow "+%d-%m-%Y %H:%M"`
19-05-2021 08:45

echo `date -d "yesterday 12:15" "+%d-%m-%Y %H:%M"`
17-05-2021 12:15

echo `date -d "yesterday -1 months" +"%d-%m-%Y %H:%M"`
17-04-2021 08:45

echo `date -d "+1 months -3 days" "+%d-%m-%Y %H:%M"`
15-06-2021 08:45

echo `date +"%d-%m-%Y" -d '1 day ago'`
17-05-2021

echo `date -d "this monday" "+%d-%m-%Y"`
24-05-2021

echo `date -d "next mon" "+%d-%m-%Y"`
24-05-2021

echo `date -d "last mon" "+%d-%m-%Y"`
17-05-2021

echo `date --date="this tuesday" "+%d-%m-%Y"`
18-05-2021

echo `date --date="next tue" "+%d-%m-%Y"`
25-05-2021

echo `date --date="last tue" "+%d-%m-%Y"`
11-05-2021

echo `date +"%s"`
1621320300

echo `date -d '@1621320300' +"%d-%m-%Y %H:%M"`
18-05-2021 08:45

J'attire votre attention sur le fait que le symbole + peut être placé indifféremment à l'intérieur ou à l'extérieur des guillemets.

Astuces

Lister les éléments du répertoire courant

La particularité quand on boucle sur le retour d'une commande c'est que ce sont les espaces qui délimitent les éléments.

for element in `ls`; do echo $element; done
un
repertoire
un
fichier

On préférera la boucle while et commande read qui marquera chaque élément trouvé par ls.

ls | while read element; do echo $element; done
un repertoire
un fichier

On préférera également entourer les variables du shell de guillemets ce qui évitera des messages d'erreur du genre "la cible ' . . . ' n'est pas un répertoire". Dans lexemple ci-dessous on cherche à renommer les éléments avec un préfixe.

ls | while read element; do mv "$element" "prefixe - $element"; done

Récuperer des noms de fichiers ayant des espaces blanc dans leur nom

OLDIFS=$IFS;
IFS=$(echo -en "\n\b" );
for i in `ls`;
do
    # code ...
done;
IFS=$OLDIFS;

Pour en savoir plus à propos de $IFS : Le séparateur standard du shell

if $var == regex

string="chaîne ou doit se faire le test"
regex=`expr match "$string" '\(^[a-zA-Z].*\)'` 
if [ "$string" != "$regex" ] 
then 
   echo "syntaxe error" 
else 
   echo "syntaxe ok" 
fi;

dans l'exemple (^[a-zA-Z].*) test si la chaîne commence par une lettre

Bash | md5sum

Quand dans php on demande à afficher la chaîne toto en md5

<?php echo md5('toto'); ?>

On obtient ça: f71dbe52628a3f83a77ab494817525c6

Mais quand on demande la même chose à bash dans une console

$ echo "toto" | md5sum

On obtient ça: 11a3e229084349bc25d97e29393ced1d
Alors, bash ne saurait-il pas "hasher" en md5?
En fait bash hash la chaîne plus un saut de ligne ("toto\n")
On contourne facilement le problème en demandant à bash de hasher la chaîne avec php

$ echo '<? echo md5("toto");?>' | php

Ou en supprimant le saut de ligne grâce à l'option -n

$ echo -n "toto" | md5sum

On obtient donc dans ces deux cas: f71dbe52628a3f83a77ab494817525c6


Faire des recherches sur

22-Dec-2023
^