revision.devops

Bash

En entretien, on peut te demander d'écrire un petit script ou une commande d'une ligne au tableau. Ce qu'on regarde : est-ce que ton script s'arrête proprement en cas d'erreur, est-ce que tes variables sont protégées, est-ce qu'il est lisible.

L'en-tête d'un script robuste

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
  • -e : arrêt à la première commande qui échoue.
  • -u : erreur si on utilise une variable non définie (une faute de frappe dans $REPERTOIRE ne donne pas silencieusement une chaîne vide).
  • -o pipefail : un pipe échoue si n'importe quelle commande échoue, pas seulement la dernière.

Limites de set -e à connaître : il ne s'applique pas aux commandes testées dans un if, un while, ou avant && / ||. Et une commande dans une substitution $(...) assignée avec local (local x=$(cmd)) ne fait pas échouer le script, car local réussit. Écrire local x; x=$(cmd).

Le piège n°1 : les guillemets

fichier="mon rapport.txt"
rm $fichier       # supprime "mon" et "rapport.txt"
rm "$fichier"     # supprime le bon fichier

Toujours "$var", toujours "$@" pour transmettre les arguments. Le jour où un nom contient un espace, le script sans guillemets fait des dégâts. Le pire cas connu : rm -rf "$DIR/"* avec DIR vide. Protection : rm -rf "${DIR:?DIR non défini}/"*.

Variables

nom="valeur"                   # pas d'espace autour du =
echo "${nom}"
echo "${nom:-defaut}"          # valeur par défaut si vide ou non définie
echo "${nom:?message}"         # erreur et sortie si vide
echo "${#nom}"                 # longueur
echo "${fichier%.log}"         # retire le suffixe .log
echo "${chemin##*/}"           # garde ce qui suit le dernier /, comme basename
readonly SEUIL=90

Codes de retour

Chaque commande renvoie un code : 0 = succès, autre chose = échec. $? contient le code de la dernière commande.

if grep -q "ERROR" app.log; then
  echo "Erreurs trouvées"
fi

commande || { echo "échec" >&2; exit 1; }

Un script de supervision doit renvoyer un code non nul quand il détecte un problème. C'est ce que lisent cron, systemd, la CI ou Nagios.

Tests

[[ -f "$f" ]]      # fichier existe
[[ -d "$d" ]]      # dossier existe
[[ -z "$v" ]]      # chaîne vide
[[ -n "$v" ]]      # chaîne non vide
[[ "$a" == "$b" ]]
(( n > 90 ))       # comparaison numérique
[[ "$s" =~ ^[0-9]+$ ]]   # regex

Préférer [[ ]] à [ ] : moins de pièges avec les guillemets et les chaînes vides.

Boucles

for srv in nginx sshd chronyd; do
  systemctl is-active --quiet "$srv" || echo "$srv KO"
done

while IFS= read -r ligne; do
  echo "$ligne"
done < fichier.txt

for f in /var/log/*.log; do
  [[ -e "$f" ]] || continue   # si aucun fichier ne correspond
  gzip "$f"
done

Ne jamais faire for f in $(ls) : ça casse sur les espaces.

Fonctions

log() {
  printf '%s [%s] %s\n' "$(date -Is)" "$1" "$2" >&2
}

check_disk() {
  local mount="$1" seuil="$2" usage
  usage=$(df --output=pcent "$mount" | tail -1 | tr -dc '0-9')
  if (( usage >= seuil )); then
    log WARN "$mount à ${usage}%"
    return 1
  fi
}

Nettoyage avec trap

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

Le dossier temporaire est supprimé quoi qu'il arrive : fin normale, erreur, Ctrl+C.

Arguments

usage() { echo "Usage: $0 [-v] -e <env>" >&2; exit 2; }

verbose=false
env=""
while getopts ":ve:" opt; do
  case "$opt" in
    v) verbose=true ;;
    e) env="$OPTARG" ;;
    *) usage ;;
  esac
done
[[ -n "$env" ]] || usage

$0 nom du script, $1... arguments, $# nombre d'arguments, $@ tous les arguments.

Traitement de texte

grep -E "ERROR|WARN" app.log
grep -c "500" access.log                 # nombre de lignes
grep -rn "password" /etc/ 2>/dev/null
awk '{print $1}' access.log              # 1re colonne
awk -F: '$3 >= 1000 {print $1}' /etc/passwd   # utilisateurs "humains"
awk '{s+=$10} END {print s}' access.log  # somme d'une colonne
sed -i 's/ancien/nouveau/g' fichier
sed -n '10,20p' fichier                  # lignes 10 à 20
cut -d, -f2 fichier.csv
sort | uniq -c | sort -rn | head         # top des occurrences
xargs -n1 -P4                            # parallélisme

Le top 10 des IP d'un access log, question classique :

awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10

Les codes HTTP 5xx de la dernière heure, par URL :

awk '$9 ~ /^5/ {print $7}' access.log | sort | uniq -c | sort -rn | head

Redirections

cmd > out.log 2>&1      # stdout et stderr dans le fichier
cmd &> out.log          # pareil, syntaxe bash
cmd 2>/dev/null         # jeter les erreurs
cmd | tee -a out.log    # afficher et écrire
cat <<EOF > config.ini
host=${HOST}
EOF

Planification

crontab -e
# min heure jour-du-mois mois jour-semaine
*/5 * * * * /opt/scripts/check.sh >> /var/log/check.log 2>&1
0 2 * * 0   /opt/scripts/backup.sh

Pièges de cron : le PATH y est minimal (utiliser des chemins absolus), pas de variables d'environnement de ta session. Alternative moderne : les timers systemd, avec logs dans journald et gestion des dépendances.

Qualité

  • shellcheck sur tous les scripts, en CI.
  • Au-delà de ~100 lignes, ou dès qu'il faut manipuler du JSON ou des structures, passer en Python.
  • bash -x script.sh pour tracer l'exécution.

Questions d'entretien à préparer

  • Que fait set -euo pipefail ?
  • Pourquoi mettre les variables entre guillemets ?
  • Écris une commande qui donne le top 10 des IP d'un access log.
  • Comment faire un nettoyage garanti à la fin d'un script ? (trap ... EXIT)
  • Différence entre $@ et $* ? ("$@" garde chaque argument séparé)
  • Ton script marche à la main mais pas en cron, pourquoi ?
Faire le QCMExercicesPython →