Notes d’apprentissage§

Un script bash automatise une suite de commandes. L’enjeu à l’examen n’est pas d’écrire du code élégant, mais du code robuste : qui échoue proprement, vérifie ses entrées, et journalise.

Modèle mental : un script est un programme avec entrées, traitement, sorties.

arguments ($1, $2...)  ──►  vérifications  ──►  traitement  ──►  code de sortie + logs
                            (garde-fous)                          (exit 0/1)

Le squelette défensif à connaître par cœur :

#!/usr/bin/env bash
set -euo pipefail    # la ligne qui sauve

Sans ces options, un script continue malgré les erreurs et peut faire des dégâts silencieux. C’est la première chose qu’un correcteur regarde.

Vérifier les arguments (garde-fous + code de sortie) :

[[ $# -lt 1 ]] && { echo "Usage: $0 <dir>" >&2; exit 1; }   # argument manquant
[[ ! -d "$1" ]] && exit 1                                    # pas un dossier

Le code de sortie est le contrat du script : 0 = succès, ≠0 = échec. C’est ce que cron, systemd ou un autre script testeront.

Le quoting, source n°1 de bugs. Toujours "$var" entre guillemets : un chemin avec espace non quoté est découpé en plusieurs arguments. Réflexe systématique.

Variables spéciales utiles : $# (nb d’arguments), $@ (tous, quotés), $? (dernier code de sortie), $$ (PID courant), $0 (nom du script).

Manipulations de chaînes (sans appeler de commande externe) :

"${file##*/}"   # basename
"${file%/*}"    # dirname
"${file%.txt}"  # retirer un suffixe
"${var:-defaut}" # valeur par défaut si vide

Pièges :

Énoncé§

Solve this question on: terminal

Write a bash script /opt/course/27/backup.sh that:

  1. Takes one argument: the directory to back up.
  2. Creates a tar.gz archive named <basename>-YYYYMMDD.tar.gz under /var/backups/.
  3. Exits with code 1 if the argument is missing or the directory does not exist.
  4. Logs the result (success / failure) to /var/log/backup.log with a timestamp.

Make the script executable. Schedule it to run daily at 02:30 via cron for /etc/.

Solution§

sudo tee /opt/course/27/backup.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

LOG=/var/log/backup.log
DEST=/var/backups

log() {
  echo "$(date '+%F %T') $*" >> "$LOG"
}

if [[ $# -lt 1 ]]; then
  log "ERROR missing argument"
  echo "Usage: $0 <directory>" >&2
  exit 1
fi

SRC=$1

if [[ ! -d "$SRC" ]]; then
  log "ERROR $SRC is not a directory"
  exit 1
fi

NAME=$(basename "$SRC")
DATE=$(date +%Y%m%d)
ARCHIVE="$DEST/${NAME}-${DATE}.tar.gz"

mkdir -p "$DEST"

if tar -czf "$ARCHIVE" -C "$(dirname "$SRC")" "$NAME"; then
  log "OK $ARCHIVE"
else
  log "FAIL $ARCHIVE"
  exit 1
fi
EOF

sudo chmod +x /opt/course/27/backup.sh

Add the cron entry:

echo '30 2 * * * root /opt/course/27/backup.sh /etc' | sudo tee /etc/cron.d/backup-etc

Bash essentials worth recognising§

Shebang + safe flags

#!/usr/bin/env bash
set -e          # exit on first error
set -u          # error on unset variable
set -o pipefail # fail if any pipe stage fails
set -x          # trace (debug)

Variables and quoting

name="alice"
echo "$name"            # expand
echo '$name'            # literal
echo "${name^^}"        # uppercase
echo "${file%.txt}"     # strip suffix
echo "${file##*/}"      # basename
echo "${file%/*}"       # dirname
echo "${var:-default}"  # default if unset
echo "${#name}"         # length

Conditionals

if [[ -f $file ]]; then ... fi          # file exists
if [[ -d $dir ]]; then ... fi           # directory
if [[ -z $var ]]; then ... fi           # empty
if [[ $a == $b ]]; then ... fi
if [[ $n -gt 10 ]]; then ... fi
[[ -r $f && -w $f ]] && echo "rw"

Loops

for f in *.log; do echo "$f"; done
for i in {1..10}; do echo $i; done
while read -r line; do echo "$line"; done < file.txt
until [[ -e /tmp/ready ]]; do sleep 1; done

Functions and exit codes

greet() {
  local name=$1
  echo "Hello $name"
  return 0
}
greet "Bob"
echo "last exit: $?"

Arguments

TokenMeaning
$0script name
$1$9positional args
$#number of args
$@all args (each quoted)
$*all args (one string)
$$current PID
$?last exit code
$!PID of last backgrounded job

Argument parsing with getopts

while getopts "vf:" opt; do
  case $opt in
    v) verbose=1 ;;
    f) file=$OPTARG ;;
    *) echo "usage..." >&2; exit 1 ;;
  esac
done
shift $((OPTIND-1))

Trap (cleanup on exit)

TMP=$(mktemp)
trap 'rm -f "$TMP"' EXIT