Skip to content

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 :

1
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>

1
2
3
# 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.

1
(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 :

1
2
3
4
5
# 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.

1
2
3
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

1
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 :

1
2
3
4
5
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 :

1
2
3
4
5
# 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() :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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 :

1
2
3
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