Skip to content

Aide mémoire pour les scripts bash

Auteur : Philippe Le Van - @plv

Date : 3 mars 2022

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}

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.

Pour aller plus loin

Quelques liens utiles :

Versions

  • 3 mars 2022 : construire une commande pas à pas
  • 8 fevrier 2022 : commande avec timeout
  • 12 octobre 2021 : création