Aide mémoire pour les scripts bash
Auteur : Philippe Le Van - @plv@framapiaf.org
Date : 3 mars 2022
Dernière mise à jour : 11 juillet 2024
Introduction
Cette page recense quelques syntaxes utilisables dans des scripts bash.
Tableau associatifs (ou dictionnaires, hashtable)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | #!/bin/bash
# déclarer la variable comme un tableau associatif
declare -A CONFIG_LIST
# on définit le tableau globalement
CONFIG_LIST=(\
["foo"]="bar" \
["mr blue"]="mr red" \
)
# on ajoute une valeur
CONFIG_LIST['gatekeeper']='zul'
# on parcourt les valeurs et on les affiche
for val in "${CONFIG_LIST[@]}"; do
echo "${val}"
done
# on parcourt les clés et on affiche les clés et les valeurs
# (notez le "!" pour parcourir les clés)
# pensez au " autour du "${!CONFIG_LIST[@]}" sinon ça va pas marcher avec des espaces dans les clés
for key in "${!CONFIG_LIST[@]}"; do
echo "${key}"
echo "${CONFIG_LIST[${key}]}"
done
|
Note
- Uniquement en bash 4, vous devez avoir #!/bin/bash en haut de votre fichier.
- notez les "" autour des noms de variables, sinon il y aura des problèmes avec les clés
qui ont des espaces
Inspiré de l'article stack overflow : How to define hash tables in Bash
Poser une question à l'internaute (pour une confirmation par exemple)
Parfois on veut poser une question du genre "êtes-vous certains de xxx (yes|no)".
Pour ça on utilise dans nos scripts un function question :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | #!/bin/bash
question() {
local question=$1
local choices=$2
local default=$3
echo -e "== $question (${choices}) [${default}] "
read RESPONSE
if [ "x$RESPONSE" = "x" ]; then
RESPONSE=$default
fi
}
question "Etes vous certain de vouloir faire cette action ?" "yes|no" "no"
if [ "$RESPONSE" = "yes" ]; then
echo "vous avez bien confirmé l'opération"
else
echo "Vous avez abandonné l'opération"
fi
|
Parser une ligne de commande bash
Là je donne juste un exemple de script (un script de deploy)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107 | #!/bin/bash
# liste des stacks qu'on peut déployer avec cette commande
STACK_NAME_LIST=(taiga penpot)
# liste des hosts où on peut déployer
HOST_LIST=("srv1.example.com" "srv2.example.com")
# fichier d'environnement
ENV_FILE=".env"
# valeurs par défaut des paramètres
DEPLOY_STACK="./docker-compose.yml"
DEPLOY_HOST="undefined"
STACK_NAME="undefined"
DELETE_STACK="NO"
# help message
function display_help() {
SCRIPT_NAME=$(basename "$0")
echo "${0} [--user xxx] [--help] <stack_name>"
echo " <stack_name> name of the stack to deploy : ($(IFS=\| ; echo "${STACK_NAME_LIST[*]}")) "
echo " --deploy_host xx host where we want to deploy : ($(IFS=\| ; echo "${HOST_LIST[*]}")) "
echo " --stack xx fichier de stack à déployer (relatif au répertoire de ce script)"
echo " --help display this help message"
echo " --user xx deploy with specific user"
echo " --delete-stack delete the entire stack"
echo
echo " Ex:"
echo " ${0} --user philippe --stack ./docker-compose.yml --deploy_host srv1.example.com taiga"
}
# on parse les arguments
while test $# -gt 0; do
_key="$1"
case "$_key" in
--help)
display_help
exit 0
;;
--user)
DEPLOY_USER=$2
shift
;;
--stack)
DEPLOY_STACK=$2
shift
;;
--deploy_host)
DEPLOY_HOST=$2
shift
;;
--delete-stack)
DELETE_STACK=YES
;;
*)
STACK_NAME=$1
;;
esac
shift
done
# on va dans le répertoire du script que veut lancer
ROOT_DIR=$(dirname "${0}")
cd ${ROOT_DIR}
DEPLOY_USER=${DEPLOY_USER:-$USER}
# check if env file exist
if [ ! -f ${ENV_FILE} ]; then
echo "I can't find the .env file. You should copy the env_sample to .env and adapt vars"
exit 1
fi
# import env vars
set -o allexport; source ${ENV_FILE}; set +o allexport
# validate param STACK_NAME
if ! grep -w ${STACK_NAME} <<< "${STACK_NAME_LIST[@]}" > /dev/null; then
echo "ERROR : the stack_name to deploy can be only ($(IFS=\| ; echo "${STACK_NAME_LIST[*]}"))"
display_help;
exit 1
fi
# validate param DEPLOY_HOST
if ! grep -w "${DEPLOY_HOST}" <<< "${HOST_LIST[@]}" > /dev/null; then
echo "ERROR : the host to deploy can be only ($(IFS=\| ; echo "${HOST_LIST[*]}"))"
display_help;
exit 1
fi
export DOCKER_HOST="ssh://${DEPLOY_USER}@${DEPLOY_HOST}"
echo "set DOCKER_HOST to ${DOCKER_HOST}"
if [ ${DELETE_STACK} = "YES" ]; then
question "delete stack ${STACK_NAME}?" "delete ${STACK_NAME}|no" "no"
if [ "$RESPONSE" = "delete ${STACK_NAME}" ]; then
DEPLOY_CMD="docker stack rm ${STACK_NAME}"
echo "DEPLOY_CMD=${DEPLOY_CMD}"
# commande à lancer : ${DEPLOY_CMD}
exit 0
else
echo "delete aborted"
exit 0
fi
fi
DEPLOY_CMD="docker stack deploy --with-registry-auth --prune -c $DEPLOY_STACK ${STACK_NAME}"
echo "DEPLOY_CMD=${DEPLOY_CMD}"
# commande à lancer ${DEPLOY_CMD}
|
Fonctions info() et error() pour gérer proprement les traces d'un script
Voilà un exemple de définitions de fonctions pour envoyer des messages d'info ou d'erreur.
Les messages d'info ne s'affiche que quand on a mis la variable VERBOSE à YES.
Les messages d'erreur vont dans stderr.
Voilà un exemple d'output :
| Wed Jun 28 10:44:24 CEST 2023 [INFO] BEGIN of transfer of toto.pdf
|
Le script :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | function info() {
local message=$1
if [ ${VERBOSE} == "YES" ]; then
echo "$(date) [INFO] ${message}"
fi
}
function error() {
local message=$1
echo "$(date) [ERROR] ${message}" > /dev/stderr
}
if [ ! -f ${FILE_IN_ARG} ]; then
error "the file ${FILE_IN_ARG} does not exist";
exit 1
fi
info "BEGIN of transfer of ${FILE_IN_ARG}"
|
Construire une commande à lancer
Parfois on a besoin de construire une commande morceau par morceau, par exemple en fonction de paramètres fournis en ligne de commande.
ici par exemple, on cherche à créer une commande curl qui envoie en POST un fichier binaire sur une URL donnée.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | # on initialiser un tableau qu'on va faire grandir ensuite.
CMD=(curl)
# on ajoute un flag -v à curl si l'utilisateur a demandé le
# mode verbose.
if [ ${VERBOSE} = "yes" ]; then
CMD+=("-v")
fi
CMD+=(--header "X-token: ${TOKEN}" --header "Content-Type: application/octet-stream" -k -X POST)
CMD+=(--data-binary @${PUB_FILE} https://${URL_PARAM}/data)
# pour que la commande soit copiable, j'ajoute des " autour de
# chaque élément du tableau dans la sortie du script.
echo "=> run command : " $(printf "\"%s\" " "${CMD[@]}")
# on lance la commande réellement ici
"${CMD[@]}"
|
Lancer une commande avec un timeout
le principe : timeout <temps en s> <ma commande bash>
| # lis en direct ce qui arrive dans /var/log/syslog pendant
# 5 secondes puis rend la main
timeout 5 tail -f /var/log/syslog
|
Lire une commande temps réel qui ne rend pas la main et la tuer sur une condition
Exemple : docker events
ne rend pas la main. On veut attendre un event en particulier
et récupérer la main après.
| (docker events &) | while read event; do echo ${event}; pkill -f "docker events"; done
|
Points à améliorer :
- je ne suis pas fan du pkill, il faudrait récupérer le PID du docker events
- idée 1 : utiliser $! mais à relier au while : (docker events &) && DOCKER_EVENTS_PID=$!
- il faudrait un timeout : cf paragraphe sur les timeout dans cette page.
Créer un fichier d'une taille choisie
Pour créer un fichier binaire de 100M, on peut utiliser la commande dd
:
| # créer un fichier rempli de 0x00
dd if=/dev/zero of=100M.bin bs=1024 count=0 seek=$[1024*100]
# pour créer un fichier binaire avec des valeurs aléatoires
dd if=/dev/urandom of=100M.bin bs=100M count=1
|
Note : j'ai eu besoin de créer un fichier binaire aléatoire pour augmenter artificiellement
la taille d'un fichier zip. En étant aléatoire, il ne se compressait pas.
Trouver le dernier fichier créé dans un répertoire
On peut imaginer utiliser cette commande pour vérifier un script de backup par exemple.
| find . -type f -printf "%T@\t %t %p\n" | sort -n | tail -1
1682272980.1953262370 Sun Apr 23 20:03:00.1953262370 2023 ./subdir/titi.txt
|
Chercher tous les fichiers entre 2 dates avec find
| find . -type f -newermt "2024-06-13 08:00:00" -not -newermt "2024-06-24 13:34:00"
|
explode / implode avec bash et cut
On a une liste renvoyée par une commande :
| 2024-06-20/149094/4/9734_FACTURE-00431.pdf
2024-06-20/149094/4/9753_FACTURE-00435.pdf
2024-06-20/149094/4/9752_FACTURE-00434.pdf
2024-06-20/149094/4/9750_FACTURE-00432.pdf
2024-06-20/145970/4/9775_FACTURE-00231.pdf
|
On veut enlever la date au début de chaque ligne :
| # on enlève la date au début de chaque ligne
echo "${LISTE}" | cut -d'/' -f2-
# le -d donne le délimiteur, ici le /
# le -f2- tous les champs à partir du 2ème
|
Ajouter des logs dans un script bash
Définition des fonctions info() et error() :
| function info() {
local message=$1
if [ ${VERBOSE} == "YES" ]; then
echo "$(date) [INFO] ${message}"
fi
}
function error() {
local message=$1
echo "$(date) [ERROR] ${message}" > /dev/stderr
}
|
Utilisation des fonctions dans un script :
| info "file copied, basename=${BASENAME}"
error "FTP transfer failed, invoice-file=$INVOICE_FILE to target-dir=${TARGET_DIR}"
|
Pour aller plus loin
Quelques liens utiles :
Versions
- 11 juillet 2024 : ajout de fonction de logs dans un script bash
- 25 juin 2024 : ajout de la commande find pour chercher des fichiers entre 2 dates et cut pour retravailler une chaines de caractères
- 11 février 2024 : ajout de la génération d'un fichier binaire aléatoire
- 28 juin 2023 : fonctions info() et error()
- 21 mai 2023 : trouver le dernier fichié modifié d'un répertoire
- 24 avril 2023 : créer un fichier d'une taille donnée
- 3 mars 2022 : construire une commande pas à pas
- 8 fevrier 2022 : commande avec timeout
- 12 octobre 2021 : création