erplibre/script/database/lib_db_select.sh
Mathieu Benoit 292a03be8d [ADD] run: choisir la base au démarrage, sans jamais bloquer
« ./run.sh » sans argument partait sans base et l'on choisissait dans le
gestionnaire web. Il choisit maintenant : une seule base, il démarre
dessus ; plusieurs, il affiche un menu numéroté.

Ce qui décide n'est pas le menu mais l'endroit où il ne doit PAS s'ouvrir.
systemd écrit « ExecStart=/bin/bash …/run.sh » sans argument, avec
Restart=always : une question posée là serait une boucle de redémarrage.
Le défaut implicite exige donc un terminal sur stdin ET sur stderr — pas
stdout, qui est toujours un tube dans la substitution qui nous lit.
Mesuré : sans terminal, la ligne d'arguments est identique à l'octet et
la sonde, qui coûte 0,8 s, n'est jamais appelée.

--- EN ---

« ./run.sh » with no argument started without a database and one picked
in the web manager. It now picks: a single database, it starts on it;
several, it shows a numbered menu.

What decides is not the menu but where it must NOT open. systemd writes
« ExecStart=/bin/bash …/run.sh » with no argument and Restart=always: a
question asked there would be a restart loop. The implicit default
therefore requires a terminal on stdin AND on stderr — not stdout, which
is always a pipe inside the substitution that reads us. Measured: with no
terminal the argument line is byte-for-byte identical and the 0.8 s probe
is never called.

Assisted-by: Claude Opus 5
2026-08-23 17:26:41 -04:00

205 lines
7.2 KiB
Bash

#!/usr/bin/env bash
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
#
# Bibliothèque SOURÇABLE : quelle base de données démarrer.
#
# « ./run.sh » sans argument démarrait Odoo sans base, et l'on choisissait
# dans le gestionnaire web. Quand il n'y a qu'une base, la question ne se
# pose pas ; quand il y en a quinze, un menu au terminal vaut mieux qu'une
# page web.
#
# TOUT ce qui s'affiche part sur stderr. Sur stdout, UNIQUEMENT le nom
# retenu : l'appelant lit `el_db_select` par substitution de commande, et
# un menu écrit sur stdout deviendrait une partie du nom de la base.
#
# Aucun `set` ici : une bibliothèque sourcée impose ses options à son hôte,
# et run.sh lit `$ODOO_MODE_TEST` sans valeur par défaut — `set -u` le
# tuerait avant tout le reste.
# Les couleurs peuvent déjà venir de l'hôte ; ne pas les écraser.
: "${Red:=\033[0;31m}"
: "${Yellow:=\033[0;33m}"
: "${Color_Off:=\033[0m}"
_el_db_say() {
echo -e "$*" >&2
}
_el_db_tty() {
# Deux descripteurs, et le second est stderr — PAS stdout.
#
# `validate_to_continue.sh` teste stdout parce qu'il y écrit sa question.
# Ici la question part sur stderr, et l'appelant nous lit par
# substitution de commande : dans `$( … )`, stdout est TOUJOURS un tube,
# donc `-t 1` serait faux même devant un vrai terminal et le menu ne
# s'ouvrirait jamais. Ce qu'il faut savoir, c'est « la personne verra-
# t-elle l'invite », et l'invite sort par stderr.
#
# stdin compte tout autant : sans lui, `read` ne rendrait jamais rien.
# Le TUI de todo.py met stdout ET stderr en tube en laissant stdin
# intact — c'est le cas que ce garde doit refuser.
#
# Fonction isolée pour qu'un test puisse la remplacer sans pseudo-terminal.
[ -t 0 ] && [ -t 2 ]
}
el_db_already_chosen() {
# L'appelant a-t-il déjà nommé sa base ? Les quatre formes qu'optparse
# accepte, plus « -d » nu en fin de ligne — ce que produit
# « ./run.sh -d $(bd) » de conf/make.robotlibre.Makefile quand bd est
# vide. Dans tous ces cas on se retire : c'est son intention, pas la
# nôtre, et on ne complète pas un « -d » resté sans valeur.
while [ $# -gt 0 ]; do
case "$1" in
-d | -d?* | --database | --database=*) return 0 ;;
esac
shift
done
return 1
}
el_db_config_has_name() {
# Une configuration de production nomme sa base. L'option de ligne de
# commande l'emporte sur le fichier : injecter « -d » par-dessus
# écraserait ce choix en silence.
local fichier="$1"
[ -f "${fichier}" ] || return 1
grep -Eq '^[[:space:]]*db_name[[:space:]]*=[[:space:]]*[^[:space:]]' \
"${fichier}" || return 1
# « False » et « None » sont la façon d'Odoo d'écrire « aucune ».
if grep -Eq \
'^[[:space:]]*db_name[[:space:]]*=[[:space:]]*(False|false|None)[[:space:]]*$' \
"${fichier}"; then
return 1
fi
return 0
}
el_db_list() {
# Les bases, une par ligne. Sortie non nulle si la SONDE a échoué — et
# alors on ne rend rien plutôt que d'inventer.
#
# stderr jeté, jamais fusionné : le CLI « db » n'appelle pas
# parse_config(), donc aucun journal n'est configuré et tout part nu sur
# stderr. Fusionner ferait de chaque ligne d'une trace un nom de base —
# le défaut que script/todo/database_manager.py a déjà dû corriger.
#
# Le mode couverture est neutralisé pour ce seul appel : sinon la sonde
# se mesure elle-même et sème un .coverage.* dans le rapport.
local brut retenu
brut="$(ODOO_MODE_COVERAGE= ./odoo_bin.sh db --list 2>/dev/null)" || return 1
# « _cache_ » nomme les bases-modèles, pas des bases de travail. Sur une
# machine qui vient de lancer les tests, ce peut être la SEULE : sans ce
# filtre, la règle « une seule base » démarrerait Odoo sur un modèle.
retenu="$(printf '%s\n' "${brut}" \
| tr -d '\r' \
| grep -v '^[[:space:]]*$' \
| grep -v '^_cache_')"
[ -n "${retenu}" ] && printf '%s\n' "${retenu}"
return 0
}
el_db_choose() {
# Le menu, entièrement sur stderr ; le nom retenu sur stdout.
#
# Numérotation à partir de 1 et « [0] » pour annuler : le vocabulaire
# des menus Python du projet, pour qu'on reconnaisse le même produit.
local -a bases=("$@")
local rang nom choix
while true; do
_el_db_say ""
_el_db_say "🗄 ${#bases[@]} bases de données. Laquelle démarrer ?"
rang=1
for nom in "${bases[@]}"; do
_el_db_say " [${rang}] ${nom}"
rang=$((rang + 1))
done
_el_db_say " [0] Annuler"
printf 'Choix : ' >&2
# shellcheck disable=SC2162
if ! read -r choix; then
# Ctrl-D : finir la ligne avant de partir, sinon l'invite reste
# collée au shell qui reprend la main.
_el_db_say ""
return 130
fi
case "${choix}" in
0) return 130 ;;
"" | *[!0-9]*) ;;
*)
if [ "${choix}" -ge 1 ] && [ "${choix}" -le "${#bases[@]}" ]; then
printf '%s\n' "${bases[$((choix - 1))]}"
return 0
fi
;;
esac
_el_db_say "${Red}Choix invalide${Color_Off} :" \
"un nombre entre 0 et ${#bases[@]}."
done
}
el_db_select() {
# Le nom de la base à démarrer, sur stdout — ou rien. 130 si l'on renonce.
#
# Les gardes vont de la moins chère à la plus chère : la sonde coûte
# 0,8 s (l'import d'Odoo, pas la requête) et ne se paie que lorsqu'elle
# peut servir. Sur tous les chemins automatisés, zéro appel.
#
# `explicite` distingue « --auto-erplibre a été demandé » de « run.sh a
# simplement reçu zéro argument ». Le défaut implicite doit rester
# timide : systemd lance « /bin/bash …/run.sh » SANS argument, avec
# Restart=always — un comportement neuf s'y déclencherait à chaque
# démarrage de production, sans que personne l'ait demandé.
local config="$1"
local sans_cli="$2"
local explicite="$3"
shift 3
el_db_already_chosen "$@" && return 0
el_db_config_has_name "${config}" && return 0
# Sans demande explicite, il faut un terminal des deux côtés pour que la
# sélection s'arme du tout. Demandée, elle peut servir sans terminal —
# mais seulement là où aucune question n'est posée.
if [ "${explicite}" != "1" ] && ! _el_db_tty; then
return 0
fi
local liste
if ! liste="$(el_db_list)"; then
_el_db_say "${Yellow}⚠${Color_Off} Impossible de lister les bases ;" \
"Odoo démarre sans base présélectionnée."
return 0
fi
local -a bases=()
while IFS= read -r nom; do
[ -n "${nom}" ] && bases+=("${nom}")
done <<< "${liste}"
if [ "${#bases[@]}" -eq 0 ]; then
_el_db_say "${Yellow}⚠${Color_Off} Aucune base de données ;" \
"Odoo démarre sur son gestionnaire web."
return 0
fi
if [ "${#bases[@]}" -eq 1 ]; then
_el_db_say "🗄 Une seule base : ${bases[0]}"
printf '%s\n' "${bases[0]}"
return 0
fi
if [ "${sans_cli}" = "1" ]; then
_el_db_say "${Yellow}⚠${Color_Off} ${#bases[@]} bases et" \
"--no-cli-erplibre : aucune n'est présélectionnée."
return 0
fi
if ! _el_db_tty; then
_el_db_say "${Yellow}⚠${Color_Off} ${#bases[@]} bases, mais pas de" \
"terminal pour choisir : aucune n'est présélectionnée."
return 0
fi
el_db_choose "${bases[@]}"
}