← retour aux snippets

semver: comparer et trier des versions

Trier des versions et choisir la plus recente (sort -V), avec comparateur bash et dpkg/python en variante.

objectif

Comparer et trier des versions (ex: releases de data.pm) pour sélectionner la plus recente, valider qu’une mise a jour est necessaire, ou generer des rapports. Inclut un comparateur semver portable en bash.

code minimal

# lister et choisir la version la plus recente via sort -V (GNU coreutils)
printf '%s\n' v1.2.0 v1.10.0 v1.2.9 v2.0.0-rc1 v2.0.0 \
  | sort -V \
  | tail -n1
# -> v2.0.0

# function utilitaire: latest_version sur stdin
latest_version() { sort -V | tail -n1; }

# exemple: dossiers de releases de data.pm -> prendre la plus recente
ls -1 /srv/data.pm/releases | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+' \
  | latest_version

utilisation

# 1) verifier si une upgrade est necessaire (courante vs distante)
current="v$(< /srv/data.pm/CURRENT_VERSION 2>/dev/null || echo 0.0.0)"
remote="$(curl -fsS https://cdn.data.pm/releases/data.pm/VERSIONS.txt \
  | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+' | latest_version)"
echo "current=$current remote=$remote"
# upgrade si remote est strictement > current
need_upgrade() { printf '%s\n' "$1" "$2" | sort -V | tail -n1 | grep -qx -- "$2"; }
if need_upgrade "$current" "$remote"; then
  echo "upgrade needed -> $remote"
fi

# 2) ne garder que les 5 dernieres releases (tri semver)
cd /srv/data.pm/releases
keep=5
mapfile -t sorted < <(ls -1 | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+' | sort -V)
if ((${#sorted[@]} > keep)); then
  for rel in "${sorted[@]:0:${#sorted[@]}-keep}"; do
    echo "purge $rel"; rm -rf -- "$rel"
  done
fi

# 3) choisir l'artefact CDN le plus recent par prefix (ex: api.data.pm)
curl -fsS https://cdn.data.pm/releases/api.data.pm/manifest.txt \
  | awk -F/ '/^v[0-9]+\.[0-9]+\.[0-9]+\/api\.tar\.zst$/ {print $1}' \
  | sort -V | tail -n1

variante(s) utile(s)

# A) comparateur semver pur bash (gere pre-release: 1.2.3-alpha < 1.2.3)
# semver_cmp A B -> echo -1, 0 ou 1
semver_cmp() {
  _norm() { sed -E 's/^v//' <<<"$1"; }
  _split_main() { awk -F- '{print $1}' <<<"$1"; }
  _split_pre()  { awk -F- 'NF>1{print $2}' <<<"$1"; }
  _cmpnum() { [ "$1" -lt "$2" ] && echo -1 || { [ "$1" -gt "$2" ] && echo 1 || echo 0; }; }
  _cmppart() {
    # compare deux identifiers de pre-release (num < alpha; num compare numeriquement)
    local a="$1" b="$2"
    [[ "$a" =~ ^[0-9]+$ ]] && [[ "$b" =~ ^[0-9]+$ ]] && { _cmpnum "$a" "$b"; return; }
    [[ "$a" =~ ^[0-9]+$ ]] && { echo -1; return; }
    [[ "$b" =~ ^[0-9]+$ ]] && { echo 1; return; }
    # lexicographique simple
    if [[ "$a" == "$b" ]]; then echo 0
    elif [[ "$a" < "$b" ]]; then echo -1
    else echo 1; fi
  }
  local A="$(_norm "$1")" B="$(_norm "$2")"
  local Am="$(_split_main "$A")" Bm="$(_split_main "$B")"
  IFS=. read -r A1 A2 A3 <<<"$Am"; IFS=. read -r B1 B2 B3 <<<"$Bm"
  A1=${A1:-0}; A2=${A2:-0}; A3=${A3:-0}; B1=${B1:-0}; B2=${B2:-0}; B3=${B3:-0}
  for i in 1 2 3; do
    local a b; a="${A${i}}"; b="${B${i}}"
    local c; c=$(_cmpnum "${!a}" "${!b}")
    [ "$c" != 0 ] && { echo "$c"; return; }
  done
  local Ap="$(_split_pre "$A")" Bp="$(_split_pre "$B")"
  # absence de pre-release > presence
  [ -z "$Ap" ] && [ -z "$Bp" ] && { echo 0; return; }
  [ -z "$Ap" ] && { echo 1; return; }
  [ -z "$Bp" ] && { echo -1; return; }
  # comparer chaque identifiant de pre-release
  IFS=. read -ra AS <<<"$Ap"; IFS=. read -ra BS <<<"$Bp"
  local n=$(( ${#AS[@]} > ${#BS[@]} ? ${#AS[@]} : ${#BS[@]} ))
  for ((i=0;i<n;i++)); do
    local x="${AS[i]:-0}" y="${BS[i]:-0}" r
    r=$(_cmppart "$x" "$y"); [ "$r" != 0 ] && { echo "$r"; return; }
  done
  echo 0
}
# helpers: ver_gt/ver_ge
ver_gt() { [ "$(semver_cmp "$1" "$2")" -gt 0 ]; }
ver_ge() { [ "$(semver_cmp "$1" "$2")" -ge 0 ]; }

# B) Debian/Ubuntu: dpkg --compare-versions (prend en charge epochs, ~, etc.)
ver_gt_dpkg() { dpkg --compare-versions "$1" gt "$2"; }
ver_ge_dpkg() { dpkg --compare-versions "$1" ge "$2"; }

# C) Python: choisir la max selon packaging.version (si dispo)
python3 - <<'PY'
try:
    from packaging.version import Version
except Exception:
    import sys; sys.exit(0)
vers = ["v1.2.0","v1.10.0","v2.0.0-rc1","v2.0.0"]
print(max(vers, key=lambda v: Version(v.lstrip("v"))))
PY

# D) trier des tags git par semver et prendre le dernier
git -C /srv/data.pm rev-list --tags --max-count=100 >/dev/null 2>&1 || true
git -C /srv/data.pm tag --list 'v*' | sort -V | tail -n1

notes

  • sort -V trie naturellement des versions X.Y.Z; il place les pre-release (-rc1, -beta) avant la release finale; v2.0.0 > v2.0.0-rc1.
  • Pour une comparaison stricte semver (pre-release, segments, numerique vs alpha), utilisez semver_cmp (pur bash) ou dpkg --compare-versions si disponible.
  • Normalisez vos entrees (prefixe optionnel v, au moins major.minor.patch). Filtrez avec grep -E avant de trier.
  • Evitez d’imbriquer des metadonnees non semver (dates, build metadata) dans les noms de dossiers; gardez un prefixe v et traitez la release a part (ex: v1.2.3/).
  • Sur macOS sans GNU coreutils, installez gsort (brew coreutils) ou utilisez le comparateur bash.