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 deargv[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 ?
- Shebang
- Commentaires
- Fonction gestion de la syntaxe
- Fonction(s) utile(s)
- Corps principal
- 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 listein "$@"
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
-
$RANDOM
renvoie des valeurs aléatoires. - Voir aussi https://michel.mauny.net/sii/variables-shell.html
- Les variables suivantes sont relatives à la gestion des processus.
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
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
for i in "${!var[@]}"; do
printf "%s\t%s\n" "$i" "${var[$i]}"
done
Autres exemples, exercices et références
- The Bash Manual
- Introduction aux tableaux en bash
- Manipulation de tableaux indicés en bash
- Manipulation des chaînes de caractères et des tableaux en bash
- Chaînes et tableaux en BASH TP3
- script bash : Tableaux
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
- Écrivez un script qui vérifie l’existence d’au moins un paramètre dans la commande.
- Écrivez un script qui vérifie que deux paramètres sont compris endéans un intervalle compris entre 0 et 100.
- Écrivez un script qui demande O/o/Oui/oui et N/n/Non/non dans toutes ses formes et qui rend la valeur.
- Écrivez un script qui ajoute un utilisateur existant dans un groupe.
- Écrivez un script qui crée un rapport sommaire sur les connexions erronées sur votre machine.
- É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 :
- Déclarations de variables dont locales
- 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
- Wiki Bash Hackers
- Scripts shell Linux et Unix de Christophe Blaess
- https://mywiki.wooledge.org/BashGuide
- Synatxe Bash obsolète et dépréciée
- Erreur du débutant en Bash
- Advanced Bash-Scripting Guide
- https://bash.cyberciti.biz/guide/Main_Page
- Shell Style Guide
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