Scripts Shell

On trouvera dans ce chapitre une initiation pratique au scripting Bash.

Les sections qui suivent dans ce chapitre s’inspirent notamment du livre Scripts shell Linux et Unix de Christophe Blaess qu’il est conseillé d’acquérir. L’ouvrage est orienté embarqué mais convient parfaitement pour un apprentissage précis, rapide, intéressant et dynamique.

1. Scripts Bash : notions

Cette section expose des rudiments pour commencer à automatiser ses tâches d’administration en Bash.

1.1. Scripts Bash

Voici une liste de départ des concepts à maîtriser pour le scripting Bash :

  • shebang
  • variables positionnelles
  • variables internes
  • fonctions et programme principal
  • fin de script
  • test
  • conditions
  • boucles
  • débogage
  • ~/.bashrc
  • Références et exemples

1.2. Shebang

Le shebang, représenté par #!, est un en-tête d’un fichier texte qui indique au système d’exploitation que ce fichier n’est pas un fichier binaire mais un script (ensemble de commandes) ; sur la même ligne est précisé l’interpréteur permettant d’exécuter ce script. Pour indiquer au système qu’il s’agit d’un script qui sera interprété par bash on placera le shebang sur la première ligne :

#!/bin/bash

1.3. Hello World

#!/bin/bash
# script0.sh
echo "Hello World"
exit

Donner les droits d’exécution au script.

chmod +x script0.sh

1.4 Variables prépositionnées

Certaines variables ont une signification spéciale réservée. Ces variables sont très utilisées lors la création de scripts :

  • pour récupérer les paramètres transmis sur la ligne de commande,
  • pour savoir si une commande a échoué ou réussi,
  • pour automatiser le traitement de tous paramètres.

Liste de variables prépositionnées

  • $0 : nom du script. Plus précisément, il s’agit du paramètre 0 de la ligne de commande, équivalent de argv[0]
  • $1, $2, …, $9 : respectivement premier, deuxième, …, neuvième paramètre de la ligne de commande
  • $* : tous les paramètres vus comme un seul mot
  • $@ : tous les paramètres vus comme des mots séparés : “$@” équivaut à “$1” “$2” …
  • $# : nombre de paramètres sur la ligne de commande
  • $- : options du shell
  • $? : code de retour de la dernière commande
  • $$ : PID du shell
  • $! : PID du dernier processus lancé en arrière-plan
  • $_ : dernier argument de la commande précédente

Par exemple :

#!/bin/bash
# script1.sh
echo "Nom du script $0"
echo "premier paramètre $1"
echo "second paramètre $2"
echo "PID du shell " $$
echo "code de retour $?"
exit

Donner les droits d’exécution du script par l’utilisateur :

chmod +ux script1.sh

Exécuter le script avec deux paramètres :

./script1.sh 10 zozo

1.5. Variables internes

En début de script, on peut définir la valeur de départ des variables utilisées dans le script. Elles ne sont connues que par le processus associé au lancement du script.

VARIABLE="valeur"

Elles s’appellent comme ceci dans le script :

echo $VARIABLE

Il peut être utile de marquer les limites d’une variable avec les accolades.

echo ${VARIABLE}

Par exemple :

#!/bin/bash
# script2.sh
PRENOM="francois"
echo "dossier personnel /home/${PRENOM}"
exit

1.6. Interaction utilisateur

La commande echo pose une question à l’utilisateur.

La commande read lit les valeurs entrées au clavier et les stocke dans une variable à réutiliser.

echo "question"
read reponse
echo $response

On peut aller plus vite avec read -p qui sort du texte et attend une valeur en entrée :

read -p "question" reponse
echo $reponse

1.7. Fonctions

Une fonction est un bloc d’instructions que l’on peut appeller ailleurs dans le script. Pour déclarer une fonction, on utilise la syntaxe suivante :

maFonction()
{
  echo hello world
}

Ou de manière plus ancienne :

function ma_fonction {
  echo hello world
}

La déclaration d’une fonction doit toujours se situer avant son appel. On mettra donc les fonctions en début de script.

Par exemple :

#!/bin/bash
# script3.sh
read -p "quel votre prénom ?" prenom
reponse() {
    echo $0
    echo "merci $prenom"
    exit 1
}
reponse
exit

2. Structures conditionnelles

2.1. Structure conditionnelle if/then

if condition ; then
    commande1
else
    commande2
fi

2.2. Tests

La condition pourra contenir un test. Deux manières de réaliser un test (avec une préférence pour la première) :

[ expression ]

ou

test expression

Note : /user/bin/[ est renseigné comme un programme sur le système.

On peut aussi utiliser la version étendue de la commande test :

[[ expression ]]

Il y a beaucoup d’opérateurs disponibles pour réaliser des tests sur les fichiers, sur du texte ou sur des valeurs arithmétiques. La commande man test donnera une documentation à lire avec attention : tout s’y trouve.

Par exemple :

#!/bin/bash
# script4.sh test si $passwdir existe
passwdir=/etc/passwdd
checkdir() {
    if [ -e $passwdir ]; then
        echo "le fichier $passwdir existe"
    else
        echo "le fichier $passwdir n'existe pas"
    fi
}
checkdir
exit

Variante : script4a.sh

On reprend la fonction checkdir qui lit la valeur de la variable donnée par l’utilisateur :

#!/bin/bash
# script4a.sh test si $passwdir existe
read -p "quel est le dossier à vérifier ?" passwdir
checkdir() {
    if [ -e $passwdir ]; then
        echo "le fichier $passwdir existe"
    else
        echo "le fichier $passwdir n'existe pas"
    fi
}
checkdir
exit

2.3. Structure de base d’un script

Quel serait la structure de base d’un script Bash ?

  1. Shebang
  2. Commentaires
  3. Fonction gestion de la syntaxe
  4. Fonction(s) utile(s)
  5. Corps principal
  6. Fin
#!/bin/bash
# script5.sh structure de base d’un script
target=$1
usage() {
    echo "Usage: $0 <fichier>"
    echo "Compte les lignes d'un fichier"
    exit
}
main() {
    ls -l $target
    echo "nombre de lignes : $(wc -l $target)"
    stat $target
}
if [ $# -lt 1 ]; then
    usage
elif [ $# -eq 1 ]; then
    main
else
    usage
fi
exit

2.4. Autres exemples de test

La page man de test pourrait nous inspirer, man test.

execverif() {
    if [ -x $target ] ; then
        #('x' comme "e_x_ecutable")
        echo $target " est exécutable."
    else
        echo $target " n'est pas exécutable."
    fi
}
#! /bin/sh
# 01_tmp.sh
dir="${HOME}/tmp/"
if [ -d ${dir} ] ; then
    rm -rf ${dir}
    echo "Le dossier de travail ${dir} existe et il est effacé"
fi
mkdir ${dir}
echo "Le dossier de travail ${dir} est créé"

3. Boucles

3.1. Boucle for-do

Faire la même chose pour tous les éléments d’une liste. En programmation, on est souvent amené à faire la même chose pour tous les éléments d’une liste. Dans un shell script, il est bien évidemment possible de ne pas réécrire dix fois la même chose. On dira que l’on fait une boucle. Dans la boucle “for-do-done”, la variable prendra successivement les valeurs dans la liste et les commandes à l’intérieur du “do-done” seront répétées pour chacune de ces valeurs.

for variable in liste_de_valeur ; do
    commande
    commande
done

Par défaut, for utilise la liste in "$@" si on omet ce mot-clé.

Supposons que nous souhaitions créer 10 fichiers .tar.gz factices, en une seule ligne :

for num in 0 1 2 3 4 5 6 7 8 9 ; do touch fichier$num.tar.gz ; done

Mieux :

for num in {0..9} ; do touch fichier$num.tar.gz ; done

Supposons que nous souhaitions renommer tous nos fichiers *.tar.gz en *.tar.gz.old :

#!/bin/bash
# script6.sh boucle
#x prend chacune des valeurs possibles correspondant au motif : *.tar.gz
for x in ./*.tar.gz ; do
    # tous les fichiers $x sont renommés $x.old
    echo "$x -> $x.old"
    mv "$x" "$x.old"
#on finit notre boucle
done
exit

Script inverse

Voici le script inverse, c’est sans compter sur de meilleurs outils dédiés à la maniplation des noms de fichier :

#!/bin/sh
# script6r.sh inverse
#x prend chacune des valeurs possibles correspondant au motif : *.tar.gz.old
for x in ./*.tar.gz.old ; do
    # tous les fichiers $x sont renommés $x sans le .old
    echo "$x -> ${x%.old}"
    mv $x ${x%.old}
# on finit notre boucle
done
exit

3.2. Boucle while

Faire une même chose tant qu’une certaine condition est remplie. Pour faire une certaine chose tant qu’une condition est remplie, on utilise une boucle de type “while-do-done” et “until-do-done”.

while condition ; do
    commandes
done

while;do répète les commandes tant que la condition est vérifiée.

until condition ; do
    commandes
done

until ; do ; done répète les commandes jusqu’à ce que la condition soit vraie, ou alors tant qu’elle est fausse.

Comment rompre ou reprendre une boucle ?

  • Rupture avec break,
  • Reprise avec continue.

Exercice.

Supposons, par exemple que vous souhaitiez afficher les 100 premiers nombres (pour une obscure raison) ou que vous vouliez créer 100 machines virtuelles.

#!/bin/bash
# script7.sh boucle while
i=0
while [ $i -lt 100 ] ; do
    echo $i
    i=$[$i+1]
done
exit

De manière plus élégante avec l’instruction for :

#!/bin/bash
# for ((initial;condition;action))
for ((i=0;i<100;i=i+1)); do
    echo $i
done
exit

3.3. Boucle case-esac

L’instruction “case-esac” permet de modifier le déroulement du script selon la valeur d’un paramètre ou d’une variable. On l’utilise le plus souvent quand les valeurs possibles sont en nombre restreint et peuvent être prévues. Les imprévus peuvent alors être représentés par le signe *.

Demandons par exemple à l’utilisateur s’il souhaite afficher ou non les fichiers cachés du répertoire en cours.

#!/bin/sh
# script8.sh case-esac
#pose la question et récupère la réponse
echo "Le contenu du répertoire courant va être affiché."
read -p "Souhaitez-vous afficher aussi les fichiers cachés (oui/non) : " reponse
#agit selon la réponse
case $reponse in
    oui)
        clear
        ls -a ;;
    non)
        ls;;
    *) echo "Veuillez répondre par oui ou par non." ;;
esac
exit

3.4. Divers

Boîtes de dialogue

On pourrait aussi s’intéresser à Whiptail : https://en.wikibooks.org/wiki/Bash_Shell_Scripting/Whiptail qui permet de créer des boîtes de dialogue.

Déboggage de script

On peut débogguer l’exécution du script en le lançant avec bash -x. Par exemple :

$ bash -x script7.sh

Etude de ~/.bashrc

Le fichier ~/.bashrc est lu à chaque connexion de l’utilisateur.

$ head ~/.bashrc
# .bashrc
# Source global definitions
if [ -f /etc/bashrc ]; then
    . /etc/bashrc
fi
# Uncomment the following line if you don't like systemctl's auto-paging feature:
# export SYSTEMD_PAGER=

4. Variables : concepts avancés

4.1. Affection des variables

  • On affecte une valeur à une variable en la déclarant variable=valeur
  • La valeur d’une variable est traitée par défaut comme une chaîne de caractère.
  • Le nom d’une variable ne peut pas commencer par un chiffre.

4.2. Protection des variables

On peut annuler la signification des caractères spéciaux comme *, ?, #, |, [], {} en utilisant des caractères d’échappement, qui sont également des caractères génériques.

\ Antislash

L’antislash \, qu’on appelle le caractère d’échappement, annule le sens de tous les caractères génériques, en forçant le shell à les interpréter littéralement.

echo \$var
$var
echo "\$var"
$var

" " Guillemets

Les guillemets (doubles) " " sont les guillemets faibles mais annulent la plupart des méta-caractères entourés à l’exception du tube (|), de l’antislash (\) et des variables ($var).

var=5
echo la valeur de la variable est $var
la valeur de la variable est 5
echo "la valeur de la variable est $var"
la valeur de la variable est 5

' ' Apostrophes

Les guillemets simples, ou apostrophes (' ') annulent le sens de tous les caractères génériques sauf l’antislash.

echo '\$var'
\$var

4.3. Variables d’environnement

Variable shell $PS1

Le shell utilise toute une série de variables par exemple $PS1 (Prompt String 1) :

echo $PS1
\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\u@\h:\w\$

Cette variable liée à la session connectée est une variable d’environnement fixée dans le fichier ~/.bashrc.

Variables d’environnement

Des variables d’environnement sont disponibles dans toutes les sessions. Par exemple PATH indique les chemins des exécutables :

echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games

Pour afficher les variables d’environnement :

printenv

4.4. Variables spéciales

4.5. Portées des variables

Il y a deux types de variables : les variables locales et les variables globales (exportées).

Variables locales

Les variables locales ne sont accessibles que sur le shell actif. Les variables exportées ou globales sont accessibles à la fois par le shell actif et par tous les processus fils lancés à partir de ce shell.

  • commande set / unset
  • commande env
  • commande export une variable, export -f pour une fonction
  • préceder de local la valorisation d’une variable dans une fonction afin d’en limiter sa portée.

Variables globales

Commande export. La commande export rend la variable disponible dans tous les processus enfant de celui qui l’a lancée. Ainsi placée dans un fichier lu au démarrage d’une session “exportera” la valeur dans tous les shells “enfants”.

4.6. Valeurs par défaut des variables

bash assign default value

Valeur par défaut -

${parameter-default}, ${parameter:-default}

Si le paramètre n’est pas défini, on utilise la valeur par défaut. Après l’appel, le paramètre n’est toujours pas défini. Le deux-points : ne fait une différence que lorsque le paramètre a été déclaré et est nul.

unset EGGS
echo 1 ${EGGS-spam}   # 1 spam
echo 2 ${EGGS:-spam}  # 2 spam

EGGS=
echo 3 ${EGGS-spam}   # 3
echo 4 ${EGGS:-spam}  # 4 spam

EGGS=cheese
echo 5 ${EGGS-spam}   # 5 cheese
echo 6 ${EGGS:-spam}  # 6 cheese

Valeur par défaut =

${parameter=default}, ${parameter:=default}

Si le paramètre n’est pas défini, on utilise la valeur par défaut. Les deux formes sont presque équivalentes. Le deux-points : ne fait une différence que lorsque le paramètre a été déclaré et est nul.

# sets variable without needing to reassign
# colons suppress attempting to run the string
unset EGGS
: ${EGGS=spam}
echo 1 $EGGS     # 1 spam
unset EGGS
: ${EGGS:=spam}
echo 2 $EGGS     # 2 spam

EGGS=
: ${EGGS=spam}
echo 3 $EGGS     # 3        (set, but blank -> leaves alone)
EGGS=
: ${EGGS:=spam}
echo 4 $EGGS     # 4 spam

EGGS=cheese
: ${EGGS:=spam}
echo 5 $EGGS     # 5 cheese
EGGS=cheese
: ${EGGS=spam}
echo 6 $EGGS     # 6 cheese

Valeur par défaut +

${parameter+alt_value}, ${parameter:+alt_value}

Si le paramètre est défini, on utilise alt_value, sinon on utilise une chaîne de caractères nulle. Après l’appel, la valeur du paramètre n’est pas modifiée. Le deux-points : ne fait une différence que lorsque le paramètre a été déclaré et est nul.

unset EGGS
echo 1 ${EGGS+spam}  # 1
echo 2 ${EGGS:+spam} # 2

EGGS=
echo 3 ${EGGS+spam}  # 3 spam
echo 4 ${EGGS:+spam} # 4

EGGS=cheese
echo 5 ${EGGS+spam}  # 5 spam
echo 6 ${EGGS:+spam} # 6 spam

4.7. Expansions de paramètres avec extraction

CHEMIN="/home/francois/archives/francois_2021-05-24-120409.zip"

Extraction de sous-chaînes

On peut extraire des sous-chaînes de caractères :

À partir du début de la valeur de la variable selon la méthode suivante ${variable:debut:longueur}

echo ${CHEMIN}
/home/francois/archives/francois_2021-05-24-120409.zip
echo ${CHEMIN:16:7}
rchives

Recherche de motifs

Les caractères génériques englobent d’autres caractères :

  • * signifie tout caractère
  • ? signifie un seul caractère
  • [Aa-Zz] correspond à une plage
  • {home,zip} corresond à une liste

Extraction du début et de la fin

Extraction du début retirant un motif selon ${variable#motif} :

echo ${CHEMIN}
/home/francois/archives/francois_2021-05-24-120409.zip
echo ${CHEMIN#/home/francois}
/archives/francois_2021-05-24-120409.zip
echo ${CHEMIN#*francois}
/archives/francois_2021-05-24-120409.zip
echo ${CHEMIN##*francois}
_2021-05-24-120409.zip

Extraction de la fin

Extraction de la fin retirant un motif selon ${variable%motif}

echo ${CHEMIN}
/home/francois/archives/francois_2021-05-24-120409.zip
echo ${CHEMIN%francois_2021-05-24-120409.zip}
/home/francois/archives/
echo ${CHEMIN%/francois*}
/home/francois/archives
echo ${CHEMIN%%/francois*}
/home

Remplacement sur motif

${variable/motif/remplacement}

echo ${CHEMIN/francois/amina}
/home/amina/archives/francois_2021-05-24-120409.zip
echo ${CHEMIN//francois/amina}
/home/amina/archives/amina_2021-05-24-120409.zip
echo ${CHEMIN//.zip/}
/home/amina/archives/amina_2021-05-24-120409

Compter les lettres

var=anticonstitutionnellement
echo Il y a ${#var} caractères dans cette variable
Il y a 25 caractères dans cette variable

Exercice de manipulation de variable

Considérons une liste de chemin de fichiers séparés par le signe , :

FICHIERS="/home/amina/francois/francois_2021-05-24-120409.zip,/home/amina/archives/amina_2021-05-22-220411.zip"

Exercice 1

Créer une liste de noms de fichier séparée par un espace avec cette variable.

francois_2021-05-24-120409.zip
amina_2021-05-22-220411.zip

Solution 1

for file_path in ${FICHIERS//,/ } ; do echo ${file_path##*/} ; done

Exercice 2

Extraire uniquement l’horodatage.

2021-05-24-120409
2021-05-22-220411

Solution 2

for file_path in ${FICHIERS//,/ } ; do
  name_extension=${file_path##*/} ; filename=${name_extension/.zip/}
  echo ${filename##*_}
done

4.8. Paramètres positionnels

Les paramètres positionnels représentent les éléments d’une commande en variables

On peut utiliser le script suivant pour illustrer les paramètres positionnels :

#! /bin/sh
# 06_affiche_arguments.sh
echo 0 : $0
if [ -n "$1"  ] ; then echo 1  : $1  ; fi
if [ -n "$2"  ] ; then echo 2  : $2  ; fi
if [ -n "$3"  ] ; then echo 3  : $3  ; fi
if [ -n "$4"  ] ; then echo 4  : $4  ; fi
if [ -n "$5"  ] ; then echo 5  : $5  ; fi
if [ -n "$6"  ] ; then echo 6  : $6  ; fi
if [ -n "$7"  ] ; then echo 7  : $7  ; fi
if [ -n "$8"  ] ; then echo 8  : $8  ; fi
if [ -n "$9"  ] ; then echo 9  : $9  ; fi
if [ -n "${10}" ] ; then echo 10 : ${10} ; fi

On obtient ceci :

./06_affiche_arguments.sh un deux trois quatre zozo petzouille sept huit neuf 10
0 : ./06_affiche_arguments.sh
1 : un
2 : deux
3 : trois
4 : quatre
5 : zozo
6 : petzouille
7 : sept
8 : huit
9 : neuf
10 : 10

4.9. Commande shift

On peut optimiser les opérations avec la commande shift qui décale les paramètres vers la gauche (supprime le premier paramètre) :

#! /bin/sh
# 07_affiche_arguments_3.sh
while [ -n "$1" ] ; do
  echo $1
  shift
done

$#représente le nombre total de paramètres. On peut voir ceci :

#! /bin/sh
# 08_affiche_arguments_4.sh
while [ $# -ne 0 ]; do
  echo $1
  shift
done

On peut encore illustrer d’autres paramètres positionnels :

#!/bin/bash
# 09_affiche_arguments_spéciaux.sh
echo "Nom du script $0"
echo "Premier paramètre $1"
echo "Second paramètre $2"
echo "Tous les paramètres $*"
echo "Tous les paramètres (préservant des espaces) $@"
echo "Nombre de paramètres $#"
echo "PID du shell $$"
echo "code de retour $?"
exit

4.10. Substitution de commandes

Le résultat d’une commande peut valoriser une variable :

kernel=$(uname -r)
echo $kernel

Ou encore selon cette méthode :

kernel=`uname -r`
echo $kernel

4.11. Expansions arithmétiques

$(( expression ))
declare -i variable

4.12. Tableaux

Exemple de création de tableau var :

var=('valeur1' 'valeur2' 'valeur3')
# ou
declare -a var=('valeur1' 'valeur2' 'valeur3')
# ou
var('valeur1' 'valeur2' 'valeur3')
# ou
var=([0]'valeur1' [1]'valeur2' [2]'valeur3')

Manipulations de base d’un tableau :

# Affiche toutes les entrées du tableau
echo ${var[@]}
valeur1 valeur2 valeur3
# Affiche toutes les entrées du tableau aussi
echo ${var[*]}
valeur1 valeur2 valeur3
# Affiche la valeur de l'indice 0 (premier)
echo ${var[0]}
valeur1
# Affiche le nombre d'indices
echo ${#var[@]}
3
Affiche tous les indices
echo ${!var[@]}
0 1 2

Bouclage dans un tableau :

for i in "${!var[@]}"; do
  printf "%s\t%s\n" "$i" "${var[$i]}"
done

Autres exemples, exercices et références

4.13. Gestion des processus

# Create a process
cat /dev/urandom > /dev/null &
# Get the pid and write it somewhere
custompid=$! ; echo $custompid > /tmp/custompid.pid
# Check the process running
ps aux | grep random
# Kill the process with his pid
kill -9 `cat /tmp/custompid.pid`
# Clean the pid file
echo /dev/null > /tmp/custompid.pid
# Check if the process is running
ps aux | grep random

5. Modèles et figures Bash

5.1. Sélection d’instructions

Structure if-then-else

if condition_1
then
    commande_1
elif condition_2
then
    commande_2
else
    commande_n
fi
if condition ; then
    commande
fi

Conditions et tests

  • La condition peut-être n’importe quelle commande,
  • souvent la commande test représentée aussi par [ ] ou [[ ]].
  • Le code de retour est alors vérifié :
    • 0 : condition vraie
    • 1 : condition fausse

man test donne les arguments de la commande test.

Structure case-esac

case expression in
    motif_1 ) commande_1 ;;
    motif_2 ) commande_2 ;;
esac

L’expression indiquée à la suite du case est évaluée et son résultat est comparé aux différents motifs. En cas de correspondance avec le motif, une commande suivante est réalisée. Elle se termine par ;;

Le motif peut comprendre des caractères génériques :

case
    *) ;;
    ?) ;;
    O* | o* | Y* | y*) ;;
    3.*) ;;
esac

Exercices

  1. Écrivez un script qui vérifie l’existence d’au moins un paramètre dans la commande.
  2. Écrivez un script qui vérifie que deux paramètres sont compris endéans un intervalle compris entre 0 et 100.
  3. Écrivez un script qui demande O/o/Oui/oui et N/n/Non/non dans toutes ses formes et qui rend la valeur.
  4. Écrivez un script qui ajoute un utilisateur existant dans un groupe.
  5. Écrivez un script qui crée un rapport sommaire sur les connexions erronées sur votre machine.
  6. Écrivez un script qui utilise les options de la commande test (ou [ ]) pour décrire les fichiers qui lui sont passés en argument.

5.2. Figures de boucles

for i in 0 1 2 3 ; do echo "ligne ${i}" ; done

for i in {0..3} ; do echo "ligne ${i}" ; done

i=0 ; while [ i < 4 ] ; do echo "ligne ${i}" ; i=$[$i+1] ; done
for ((i=0;i<4;i=i+1)); do echo "ligne ${i}" ; done

5.3. Figures de substitution

I="ubuntu2004.qcow2"
echo ${I#ubuntu2004.}
qcow2
I="ubuntu2004.qcow2"
echo ${I#*.}
qcow2
I="ubuntu2004.qcow2"
echo ${I%.qcow2}
ubuntu2004
I="ubuntu2004.qcow2"
echo ${I%.qcow2}
ubuntu2004
I="ubuntu2004.qcow2"
echo ${I:11}
qcow2
I="ubuntu2004.qcow2"
echo ${I/qcow2/img}
ubuntu2004.img

echo ${I/u/\-}
-buntu2004.qcow2
echo ${I//u/\-}
-b-nt-2004.qcow2

5.4. Figures de vérification

1. Fonction are_you_sure

are_you_sure () {
read -r -p "Are you sure? [y/N] " response
case "$response" in
    [yY][eE][sS]|[yY])
       sleep 1
        ;;
    *)
        exit
        ;;
esac
}

2. Fonction check_distribution

check_distribution () {
if [ -f /etc/debian_version ]; then
echo "Debian/Ubuntu OS Type"
elif [ -f /etc/redhat-release ]; then
echo "RHEL/Centos OS Type"
fi
}

3. Fonctions check_variable

check_variable () {
case ${variable} in
    isolated) echo "isolated" ;;
    nat) echo "nat" ;;
    full) echo "full"  ;;
    *) echo "isolated, nat or full ? exit" ; exit 1 ;;
esac
}

4. Fonction check_parameters

parameters=$#
check_parameters () {
# Check the number of parameters given and display help
if [ "$parameters" -ne 2  ] ; then
echo "Description : This script do this"
echo "Usage       : $0 <type : isolated or nat or full>"
echo "Example     : '$0 isolated' or '$0 nat'"
exit
fi
}

5. Fonction check_root_id

check_root_id () {
if [ "$EUID" -ne 0 ]
  then echo "Please run as root"
  exit
fi
}

6. Vérification de la disponibilité d’un binaire

curl -V >/dev/null 2>&1 || { echo >&2 "Please install curl"; exit 2; }

7. Tests avec grep et exécutions conditionnelles

if ! grep -q "vmx" /proc/cpuinfo ; then echo "Please enable virtualization instructions" ; exit 1 ; fi
{ grep -q "vmx" /proc/cpuinfo ; [ $? == 0 ]; } || { echo "Please enable virtualization instructions" ; exit 1 ;  }
[ `grep -c "vmx" /proc/cpuinfo` == 0 ] && { echo "Please enable virtualization instructions" ; exit 1 ;  }

8. Fonction check_interface

check_interface () {
if grep -qw "${interface:=lo}" <<< $(ls /sys/class/net) ; then
echo "This interface ${interface} exists"
else
echo "This interface ${interface} does not exist"
fi
}

5.5. Figures de génération aléatoire

1. Fonctions create_ip_range


net_id1="$(shuf -i 0-255 -n 1)"
net_id2="$(shuf -i 0-255 -n 1)"
# random /24 in 10.0.0.0/8 range
ip4="10.${net_id1}.${net_id2}."
ip6="fd00:${net_id1}:${net_id2}::"
# Fix your own range
#ip4="192.168.1."
#ip6="fd00:1::"
create_ip_range () {
# Reporting Function about IPv4 and IPv6 configuration
cat << EOF > ~/report.txt
Bridge IPv4 address : ${ip4}1/24
IPv4 range          : ${ip4}0 255.255.255.0
DHCP range          : ${ip4}128 - ${ip4}150
Bridge IPv6 address : ${ip6}1/64
IPv6 range          : ${ip6}/64
DHCPv6 range        : ${ip6}128/64 - ${ip6}150/64
DNS Servers         : ${ip4}1 and ${ip6}1
EOF
echo "~/report.txt writed : "
cat ~/report.txt
}

2.Fonction create_mac_address

create_mac_address () {
mac=$(tr -dc a-f0-9 < /dev/urandom | head -c 10 | sed -r 's/(..)/\1:/g;s/:$//;s/^/02:/')
echo $mac
}

3. Fonction de génération d’aléas / UUID

alea () {
apt-get -y install uuid-runtime openssl
alea1=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32};echo;)
echo "1. urandom alea : $alea1"
alea2=$(date +%s | sha256sum | base64 | head -c 32 ; echo)
echo "2. date alea $alea2"
alea3=$(openssl rand -base64 32)
echo "3. openssl alea : $alea3"
alea4=$(uuidgen -t)
echo "4. time based uuid : $alea4"
alea5=$(uuidgen -r)
echo "5. random based uuid : $alea5"
echo "6. random based uuid résumé : ${alea5:25}"
echo "7. random based uuid résumé : ${alea5//\-/}"
}

5.8. Getopts : arguments de la ligne de commande

getopts est outil puissant analyse les arguments de la ligne de commande transmis au script. C’est l’équivalent en Bash de la commande externe getopt et de la fonction de bibliothèque getopt familière aux programmeurs C. Il permet de passer et de concaténer plusieurs options et les arguments associés à un script (par exemple scriptname -abc -e /usr/local).

La construction getopts utilise deux variables implicites. $OPTIND est le pointeur d’argument (“OPTion INDex”) et $OPTARG (“OPTion ARGument”) l’argument (facultatif) attaché à une option. Un deux-points suivant le nom de l’option dans la déclaration marque cette option comme ayant un argument associé.

Une construction getopts est généralement empaquetée dans une boucle “while”, qui traite les options et les arguments un par un, puis incrémente la variable implicite $OPTIND pour pointer vers la suivante.

Les arguments passés de la ligne de commande au script doivent être précédés d’un tiret (-). C’est le préfixe - qui permet à getopts de reconnaître les arguments de la ligne de commande comme des options. En fait, getopts ne traitera pas les arguments sans le préfixe -, et terminera le traitement de l’option au premier argument rencontré sans eux.

Le modèle getopts diffère légèrement du modèle standard en boucle, en ce sens qu’il ne contient pas de crochets de condition.

Exemple 1

#!/bin/bash
NO_ARGS=0
if [ $# -eq "$NO_ARGS" ]
then
  echo "Usage: `basename $0` options (-abc)"
  exit 1
fi

while getopts ":abc:" Option
do
  case $Option in
    a ) echo "option a [OPTIND=${OPTIND}]";;
    b ) echo "option b [OPTIND=${OPTIND}]";;
    c ) echo "option c [OPTIND=${OPTIND}] ${OPTARG}";;
  esac
done

shift $(($OPTIND - 1))

Exemple 2

An example of how to use getopts in bash

#!/bin/bash

usage() { echo "Usage: $0 [-s <45|90>] [-p <string>]" 1>&2; exit 1; }

while getopts ":s:p:" o; do
    case "${o}" in
        s)
            s=${OPTARG}
            ((s == 45 || s == 90)) || usage
            ;;
        p)
            p=${OPTARG}
            ;;
        *)
            usage
            ;;
    esac
done
shift $((OPTIND-1))

if [ -z "${s}" ] || [ -z "${p}" ]; then
    usage
fi

echo "s = ${s}"
echo "p = ${p}"

Démonstation :

$ ./myscript.sh
Usage: ./myscript.sh [-s <45|90>] [-p <string>]

$ ./myscript.sh -h
Usage: ./myscript.sh [-s <45|90>] [-p <string>]

$ ./myscript.sh -s "" -p ""
Usage: ./myscript.sh [-s <45|90>] [-p <string>]

$ ./myscript.sh -s 10 -p foo
Usage: ./myscript.sh [-s <45|90>] [-p <string>]

$ ./myscript.sh -s 45 -p foo
s = 45
p = foo

$ ./myscript.sh -s 90 -p bar
s = 90
p = bar

Exemple 3

How do I parse command line arguments in Bash?

cat >/tmp/demo-getopts.sh <<'EOF'
#!/bin/sh

# A POSIX variable
OPTIND=1         # Reset in case getopts has been used previously in the shell.

# Initialize our own variables:
output_file=""
verbose=0

while getopts "h?vf:" opt; do
    case "$opt" in
    h|\?)
        show_help
        exit 0
        ;;
    v)  verbose=1
        ;;
    f)  output_file=$OPTARG
        ;;
    esac
done

shift $((OPTIND-1))

[ "${1:-}" = "--" ] && shift

echo "verbose=$verbose, output_file='$output_file', Leftovers: $@"
EOF

chmod +x /tmp/demo-getopts.sh

/tmp/demo-getopts.sh -vf /etc/hosts foo bar

Résultat :

verbose=1, output_file='/etc/hosts', Leftovers: foo bar

5.7. Modèle de script bash

Source : https://github.com/leonteale/pentestpackage/blob/master/BashScriptTemplate.sh

#!/bin/bash
##########################################################################
# Copyright: Leon Teale @leonteale https://leonteale.co.uk
##########################################################################
##########################################################################
# Program: <APPLICATION DESCRIPTION HERE>
##########################################################################
VERSION="0.0.1"; # <release>.<major change>.<minor change>
PROGNAME="<APPLICATION NAME>";
AUTHOR="You, you lucky so and so";

##########################################################################
## Pipeline:
## TODO:
##########################################################################

##########################################################################
# XXX: Coloured variables
##########################################################################
red=`echo -e "\033[31m"`
lcyan=`echo -e "\033[36m"`
yellow=`echo -e "\033[33m"`
green=`echo -e "\033[32m"`
blue=`echo -e "\033[34m"`
purple=`echo -e "\033[35m"`
normal=`echo -e "\033[m"`

##########################################################################
# XXX: Configuration
##########################################################################

declare -A EXIT_CODES

EXIT_CODES['unknown']=-1
EXIT_CODES['ok']=0
EXIT_CODES['generic']=1
EXIT_CODES['limit']=3
EXIT_CODES['missing']=5
EXIT_CODES['failure']=10

DEBUG=0
param=""

##########################################################################
# XXX: Help Functions
##########################################################################
show_usage() {
        echo -e """Web Application scanner using an array of different pre-made tools\n
        Usage: $0 <target>
        \t-h\t\tshows this help menu
        \t-v\t\tshows the version number and other misc info
        \t-D\t\tdisplays more verbose output for debugging purposes"""

        exit 1
        exit ${EXIT_CODES['ok']};
}

show_version() {
        echo "$PROGNAME version: $VERSION ($AUTHOR)";
        exit ${EXIT_CODES['ok']};
}

debug() {
        # Only print when in DEBUG mode
        if [[ $DEBUG == 1 ]]; then
                echo $1;
        fi
}

err() {
        echo "$@" 1>&2;
        exit ${EXIT_CODES['generic']};
}

##########################################################################
# XXX: Initialisation and menu
##########################################################################
if [ $# == 0 ] ; then
        show_usage;
fi

while getopts :vhx opt
do
  case $opt in
  v) show_version;;
  h) show_usage;;
  *)  echo "Unknown Option: -$OPTARG" >&2; exit 1;;
  esac
done



# Make sure we have all the parameters we need (if you need to force any parameters)
#if [[ -z "$param" ]]; then
#        err "This is a required parameter";
#fi

##########################################################################
# XXX: Kick off
##########################################################################
header() {
        clear
        echo -e """
----------------------------------
 $PROGNAME v$VERSION $AUTHOR
----------------------------------\n"""
}

main() {

#start coding here
  echo "start coding here"

}

header
main "$@"

debug $param;

6. Script rm amélioré

Cette section est une reprise de l’exercice de script rm_secure.sh de Christophe Blaess.

On trouvera bon nombre d’exemples de scripts à télécharger sur la page https://www.blaess.fr/christophe/livres/scripts-shell-linux-et-unix/. Le script rm_secure.sh est situé dans le dossier exemples/ch02-Programmation_Shell/.

6.1. Commande rm

rm --help
Usage: rm [OPTION]... FILE...
Remove (unlink) the FILE(s).

  -f, --force           ignore nonexistent files and arguments, never prompt
  -i                    prompt before every removal
  -I                    prompt once before removing more than three files, or
                          when removing recursively; less intrusive than -i,
                          while still giving protection against most mistakes
      --interactive[=WHEN]  prompt according to WHEN: never, once (-I), or
                          always (-i); without WHEN, prompt always
      --one-file-system  when removing a hierarchy recursively, skip any
                          directory that is on a file system different from
                          that of the corresponding command line argument
      --no-preserve-root  do not treat '/' specially
      --preserve-root   do not remove '/' (default)
  -r, -R, --recursive   remove directories and their contents recursively
  -d, --dir             remove empty directories
  -v, --verbose         explain what is being done
      --help     display this help and exit
      --version  output version information and exit

By default, rm does not remove directories.  Use the --recursive (-r or -R)
option to remove each listed directory, too, along with all of its contents.

To remove a file whose name starts with a '-', for example '-foo',
use one of these commands:
  rm -- -foo

  rm ./-foo

6.2. Description

  • Il s’agit d’une fonction à “sourcer” qui ajoute des fonctionnalités à la commande /bin/rm : une sorte de corbeille temporaire
  • Trois options supplémentaires et sept standards sont à interpréter
  • Des fichiers/dossiers sont à interpréter comme arguments possibles
  • Les fichiers/dossiers effacés sont placés dans une corbeille temporaire avant suppression.
  • Ces fichiers peuvent être listés et restaurés à l’endroit de l’exécution de la commande.

Quelles options peut-on ajouter ?

  • une vérification des droits sur le dossier temporaire
  • une option qui précise le point de restauration (approche par défaut, récupération emplacement original absolu)
  • une gestion des écrasements lors de la restauration (versionning, diff)
  • une gestion des écrasements de fichiers mis en corbeille

6.3. Concepts

Le script met en oeuvre les notions suivantes :

  • définition de variables
  • imbrications de boucles
  • boucle while; do command; done
  • Traitement d’options getopts
  • boucle case/esac ) ;;
  • condition if/then
  • tests [ ]
  • trap commande signal

6.4. Structure

Le Script exécute un traitement séquentiel :

  1. Déclarations de variables dont locales
  2. Traitement des options

6.5. Sourcer le script

En Bash :

source rm_secure.sh

6.6. Script automatique

Pour que le script démarre automatiquement au démarrage de la session de l’utilisateur :

  • ~/.bashrc
  • ~/.bash_profile

7. Références

Archive d’exemples

Archive : Exercices de scripts sur les noms de fichiers

On vous présente un cas où nous sommes invités à renommer des fichiers ayant l’extension tar.gz en tar.gz.old et inversément.

Pour réaliser cet exercice nous avons besoin d’un certain nombre de fichiers. Une idée serait d’utiliser la commande touch. Supposons qu’il faille créer 100 fichiers numérotés dans un dossier temporaire.

Cas : vider et créer un dossier temporaire de travail

Pour vider et créer un dossier temporaire de travail, on pourrait proposer ceci d’illustrer la fonction conditionnelle if condition ; then commandes; else commandes; fi :

#! /bin/sh
# 01_tmp.sh
dir="${HOME}/tmp/"
if [ -d ${dir} ] ; then
    rm -rf ${dir}
    echo "Le dossier de travail ${dir} existe et il est effacé"
fi
mkdir ${dir}
echo "Le dossier de travail ${dir} est créé"

Cas : créer des fichiers à la volée

Pour créer des fichiers, on peut utilser la commande touch :

TOUCH(1)                  BSD General Commands Manual                 TOUCH(1)

NAME
     touch -- change file access and modification times

SYNOPSIS
     touch [-A [-][[hh]mm]SS] [-acfhm] [-r file] [-t [[CC]YY]MMDDhhmm[.SS]] file ...

DESCRIPTION
     The touch utility sets the modification and access times of files.  If any file does not
     exist, it is created with default permissions.

     By default, touch changes both modification and access times.  The -a and -m flags may be used
     to select the access time or the modification time individually.  Selecting both is equivalent
     to the default.  By default, the timestamps are set to the current time.  The -t flag explic-
     itly specifies a different time, and the -r flag specifies to set the times those of the spec-
     ified file.  The -A flag adjusts the values by a specified amount.

Pour faire une certaine chose tant qu’une condition est remplie on utilise une boucle while condition ; do commandes ; done

#! /bin/sh
# 02_creation_fichiers0.sh
dir="${HOME}/tmp/"
i=0
while [ $i -lt 100 ] ; do
	touch ${dir}fic$i.tar.gz
	echo "Création de ${dir}fic$i.tar.gz"
	i=$[$i+1]
done

De manière peut-être plus élégante avec l’instruction for ((initial;condition;action)); do commandes; done :

#!/bin/sh
# 03_creation_fichiers.sh
dir="${HOME}/tmp/"
i=0
#for ((initial;condition;action))
for ((i=0;i<100;i=i+1)); do
    touch ${dir}fic$i.tar.gz
	echo "Création de ${dir}fic$i.tar.gz"
done

Cas : renommage

Cas : renommage de *.tar.gz en *.tar.gz.old

Supposons maintenant que nous souhaitions renommer tous nos fichiers *.tar.gz en *.tar.gz.old, nous taperons le script suivant :

#!/bin/sh
# 04_renommage.sh
#x prend chacune des valeurs possibles correspondant au motif : *.tar.gz
dir="${HOME}/tmp/"
for x in ${dir}*.tar.gz ; do
  # tous les fichiers $x sont renommés $x.old
  echo "$x -> $x.old"
  mv "$x" "$x.old"
  # on finit notre boucle
done

Cas : renommage inverse

Cas : renommage inverse *.tar.gz.old *.gz.old

Voici le script inverse, c’est sans compter sur d’autres outils pour d’autres situations :

#!/bin/sh
# 05_denommage.sh
#x prend chacune des valeurs possibles correspondant au motif : *.tar.gz.old
dir="${HOME}/tmp/"
for x in ${dir}*.tar.gz.old ; do
  # tous les fichiers $x sont renommés $x sans le .old
  echo "$x -> ${x%.old}"
  mv $x ${x%.old}
  # on finit notre boucle
done

Cas : script extraction_serveurs.sh

On peut réaliser l’exercice extraction_serveurs.sh