<pman>Intégration de la branche dev74

This commit is contained in:
Jephté Clain 2025-10-22 07:37:11 +04:00
commit 7420704880
247 changed files with 13864 additions and 2076 deletions

2
.idea/nulib-base.iml generated
View File

@ -4,7 +4,7 @@
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/php/src" isTestSource="false" packagePrefix="nulib\" />
<sourceFolder url="file://$MODULE_DIR$/php/tests" isTestSource="true" packagePrefix="nulib\" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/php/cli" isTestSource="false" packagePrefix="cli\" />
<excludeFolder url="file://$MODULE_DIR$/php/vendor" />
</content>
<orderEntry type="inheritedJdk" />

View File

@ -17,6 +17,36 @@
</DockerContainerSettings>
</value>
</entry>
<entry key="385aa179-8c50-45e2-9ad7-4d9bfba298a3">
<value>
<DockerContainerSettings>
<option name="version" value="1" />
<option name="volumeBindings">
<list>
<DockerVolumeBindingImpl>
<option name="containerPath" value="/opt/project" />
<option name="hostPath" value="$PROJECT_DIR$" />
</DockerVolumeBindingImpl>
</list>
</option>
</DockerContainerSettings>
</value>
</entry>
<entry key="38915385-b3ff-4f4b-8a9a-d5f3ecae559e">
<value>
<DockerContainerSettings>
<option name="version" value="1" />
<option name="volumeBindings">
<list>
<DockerVolumeBindingImpl>
<option name="containerPath" value="/opt/project" />
<option name="hostPath" value="$PROJECT_DIR$" />
</DockerVolumeBindingImpl>
</list>
</option>
</DockerContainerSettings>
</value>
</entry>
</map>
</list>
</component>

77
.idea/php.xml generated
View File

@ -2,7 +2,7 @@
<project version="4">
<component name="MessDetector">
<phpmd_settings>
<phpmd_by_interpreter asDefaultInterpreter="true" interpreter_id="846389f7-9fb5-4173-a868-1dc6b8fbb3fa" timeout="30000" />
<phpmd_by_interpreter asDefaultInterpreter="true" interpreter_id="38915385-b3ff-4f4b-8a9a-d5f3ecae559e" timeout="30000" />
</phpmd_settings>
</component>
<component name="MessDetectorOptionsConfiguration">
@ -17,44 +17,61 @@
</component>
<component name="PhpCodeSniffer">
<phpcs_settings>
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="846389f7-9fb5-4173-a868-1dc6b8fbb3fa" timeout="30000" />
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="38915385-b3ff-4f4b-8a9a-d5f3ecae559e" timeout="30000" />
</phpcs_settings>
</component>
<component name="PhpIncludePathManager">
<include_path>
<path value="$PROJECT_DIR$/php/vendor/symfony/polyfill-ctype" />
<path value="$PROJECT_DIR$/php/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/php/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/php/vendor/symfony/yaml" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/php/vendor/composer" />
<path value="$PROJECT_DIR$/php/vendor/dflydev/dot-access-data" />
<path value="$PROJECT_DIR$/php/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/php/vendor/league/commonmark" />
<path value="$PROJECT_DIR$/php/vendor/league/config" />
<path value="$PROJECT_DIR$/php/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/php/vendor/nette/schema" />
<path value="$PROJECT_DIR$/php/vendor/nette/utils" />
<path value="$PROJECT_DIR$/php/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/php/vendor/nulib/tests" />
<path value="$PROJECT_DIR$/php/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/php/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/php/vendor/phpmailer/phpmailer" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/php/vendor/psr/cache" />
<path value="$PROJECT_DIR$/php/vendor/psr/container" />
<path value="$PROJECT_DIR$/php/vendor/psr/event-dispatcher" />
<path value="$PROJECT_DIR$/php/vendor/psr/log" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/code-unit-reverse-lookup" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/resource-operations" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/php/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/code-unit-reverse-lookup" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/php/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/php/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/php/vendor/nulib/tests" />
<path value="$PROJECT_DIR$/php/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/php/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/php/vendor/composer" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/php/vendor/symfony/cache" />
<path value="$PROJECT_DIR$/php/vendor/symfony/cache-contracts" />
<path value="$PROJECT_DIR$/php/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/php/vendor/symfony/expression-language" />
<path value="$PROJECT_DIR$/php/vendor/symfony/polyfill-ctype" />
<path value="$PROJECT_DIR$/php/vendor/symfony/polyfill-php73" />
<path value="$PROJECT_DIR$/php/vendor/symfony/polyfill-php80" />
<path value="$PROJECT_DIR$/php/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/php/vendor/symfony/var-exporter" />
<path value="$PROJECT_DIR$/php/vendor/symfony/yaml" />
<path value="$PROJECT_DIR$/php/vendor/theseer/tokenizer" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.4">

View File

@ -4,7 +4,7 @@ UPSTREAM=dev74
DEVELOP=dev82
FEATURE=wip82/
RELEASE=rel82-
MAIN=dist82
MAIN=main82
TAG_SUFFIX=p82
HOTFIX=hotf82-
DIST=

View File

@ -1,3 +1,7 @@
# nulib
* [wip](wip/TODO.md)
# nulib/bash
* [nulib/bash](bash/TODO.md)

View File

@ -6,7 +6,8 @@ function __esection() {
local length="${COLUMNS:-80}"
setx lsep=__complete "$prefix" "$length" -
recho "$COULEUR_BLEUE$lsep$COULEUR_NORMALE"
recho "
$COULEUR_BLEUE$lsep$COULEUR_NORMALE"
[ -n "$*" ] || return 0
length=$((length - 1))
setx -a lines=echo "$1"

View File

@ -6,7 +6,8 @@ function __esection() {
local length="${COLUMNS:-80}"
setx lsep=__complete "$prefix" "$length" -
recho "$lsep"
recho "
$lsep"
[ -n "$*" ] || return 0
length=$((length - 1))
setx -a lines=echo "$1"

View File

@ -184,7 +184,7 @@ function __nulib_args_parse_args() {
*) die "Invalid arg definition: expected option, got '$1'" || return;;
esac
# est-ce que l'option prend un argument?
local __def __longdef __witharg __valdesc
local __def __longdef __witharg __valdesc __defvaldesc
__witharg=
for __def in "${__defs[@]}"; do
if [ "${__def%::*}" != "$__def" ]; then
@ -346,16 +346,19 @@ $prefix$usage"
fi
# est-ce que l'option prend un argument?
__witharg=
__valdesc=value
__valdesc=
__defvaldesc=value
for __def in "${__defs[@]}"; do
if [ "${__def%::*}" != "$__def" ]; then
[ "$__witharg" != : ] && __witharg=::
[ -n "${__def#*::}" ] && __valdesc="[${__def#*::}]"
__defvaldesc="[value]"
elif [ "${__def%:*}" != "$__def" ]; then
__witharg=:
[ -n "${__def#*:}" ] && __valdesc="${__def#*:}"
fi
done
[ -n "$__valdesc" ] || __valdesc="$__defvaldesc"
# description de l'option
local first=1 thelp tdesc
for __def in "${__defs[@]}"; do

View File

@ -21,6 +21,13 @@ if [ -z "$NULIB_NO_INIT_ENV" ]; then
fi
[ -n "$NULIBDIR" ] || NULIBDIR="$MYDIR"
# Si le script courant est un lien, calculer le répertoire destination
if [ -n "$MYREALSELF" -a -n "$MYSELF" ]; then
MYREALSELF="$(readlink -f "$MYSELF")"
MYREALDIR="$(dirname -- "$MYREALSELF")"
MYREALNAME="$(basename -- "$MYREALSELF")"
fi
# Repertoire temporaire
[ -z "$TMPDIR" -a -d "$HOME/tmp" ] && TMPDIR="$HOME/tmp"
[ -z "$TMPDIR" ] && TMPDIR="${TMP:-${TEMP:-/tmp}}"

147
bash/src/install.sh Normal file
View File

@ -0,0 +1,147 @@
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
##@cooked nocomments
module: install "Outils de haut niveau pour installer des fichiers de configuration"
require: base
if [ -z "$NULIB_INSTALL_CONFIGURED" ]; then
# Faut-il afficher le nom des fichiers copié/créés?
export NULIB_INSTALL_VERBOSE=1
# Faut-il afficher les destinations avec ppath?
export NULIB_INSTALL_USES_PPATH=
fi
export NULIB_INSTALL_CONFIGURED=1
function ensure_exists() {
# Créer le fichier vide "$1" s'il n'existe pas déjà, avec les permissions
# $2(=644). retourner vrai si le fichier a été créé sans erreur
[ -f "$1" ] || {
if [ -n "$NULIB_INSTALL_VERBOSE" ]; then
if [ -n "$NULIB_INSTALL_USES_PPATH" ]; then
estep "$(ppath "$1")"
else
estep "$1"
fi
fi
mkdirof "$1"
local r=0
touch "$1" && chmod "${2:-644}" "$1" || r=$?
return $r
}
return 1
}
function __nulib_install_show_args() {
if [ -z "$NULIB_INSTALL_VERBOSE" ]; then
:
elif [ -n "$NULIB_INSTALL_USES_PPATH" ]; then
estep "$1 --> $(ppath "$2")${3:+/}"
else
estep "$1 --> $2${3:+/}"
fi
}
function copy_replace() {
# Copier de façon inconditionnelle le fichier $1 vers le fichier $2, en
# réinitialisation les permissions à la valeur $3
local src="$1" dest="$2"
local srcname="$(basename -- "$src")"
[ -d "$dest" ] && dest="$dest/$srcname"
mkdirof "$dest" || return 1
if [ -n "$NULIB_INSTALL_VERBOSE" ]; then
local destarg destname slash
destarg="$(dirname -- "$dest")"
destname="$(basename -- "$dest")"
if [ "$srcname" == "$destname" ]; then
slash=1
else
destarg="$destarg/$destname"
fi
__nulib_install_show_args "$srcname" "$destarg" "$slash"
fi
local r=0
if cp "$src" "$dest"; then
if [ -n "$3" ]; then
chmod "$3" "$dest" || r=$?
fi
fi
return $r
}
function copy_new() {
# Copier le fichier "$1" vers le fichier "$2", avec les permissions $3(=644)
# Ne pas écraser le fichier destination s'il existe déjà
# Retourner vrai si le fichier a été copié sans erreur
local src="$1" dest="$2"
[ -d "$dest" ] && dest="$dest/$(basename -- "$src")"
mkdirof "$dest" || return 1
if [ ! -e "$dest" ]; then
copy_replace "$src" "$dest" "$3"
else
return 1
fi
}
function copy_update() {
# Copier le fichier "$1" vers le fichier "$2", si $2 n'existe pas, ou si $1
# a été modifié par rapport à $2. Réinitialiser le cas échéant les
# permissions à la valeur $3
# Retourner vrai si le fichier a été copié sans erreur.
local src="$1" dest="$2"
[ -d "$dest" ] && dest="$dest/$(basename -- "$src")"
mkdirof "$dest" || return 1
if [ ! -e "$dest" ]; then
copy_replace "$src" "$dest" "$3"
elif testdiff "$src" "$dest"; then
copy_replace "$src" "$dest" "$3"
else
return 1
fi
}
COPY_UPDATE_ASK_DEFAULT=
function copy_update_ask() {
# Copier ou mettre à jour le fichier $1 vers le fichier $2.
# Si le fichier existe déjà, la différence est affichée, et une confirmation
# est demandée pour l'écrasement du fichier.
# si $1 commence par '-' alors on s'en sert comme option pour configurer le
# niveau d'interaction pour demander la confirmation. les paramètres sont
# alors décalés
# Retourner vrai si le fichier a été copié sans erreur.
local interopt=-c
if [[ "$1" == -* ]]; then
interopt="$1"
shift
fi
local src="$1" dest="$2"
[ -d "$dest" ] && dest="$dest/$(basename -- "$src")"
mkdirof "$dest" || return 1
[ -f "$dest" ] || copy_replace "$src" "$dest"
if testdiff "$src" "$dest"; then
check_interaction "$interopt" && diff -u "$dest" "$src"
if ask_yesno "$interopt" "Voulez-vous remplacer $(ppath "$dest") par la nouvelle version?" "${COPY_UPDATE_ASK_DEFAULT:-C}"; then
copy_replace "$src" "$dest" "$3"
return $?
elif ! check_interaction "$interopt"; then
ewarn "Les mises à jours suivantes sont disponibles:"
diff -u "$dest" "$src"
ewarn "Le fichier $(ppath "$dest") n'a pas été mis à jour"
fi
fi
return 1
}
function link_new() {
# Si $2 n'existe pas, créer le lien symbolique $2 pointant vers $1
[ -e "$2" ] && return 0
__nulib_install_show_args "$(basename -- "$2")" "$(dirname -- "$1")"
ln -s "$1" "$2"
}

View File

@ -1,7 +1,5 @@
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
## configuration par défaut
UPSTREAM=
DEVELOP=develop
FEATURE=wip/

View File

@ -25,9 +25,146 @@ DIST=
NOAUTO=
CONFIG_VARS=(
UPSTREAM DEVELOP FEATURE RELEASE MAIN TAG_PREFIX TAG_SUFFIX HOTFIX DIST NOAUTO
UPSTREAM DEVELOP FEATURE RELEASE MAIN HOTFIX DIST
TAG_PREFIX TAG_SUFFIX NOAUTO
)
################################################################################
PMAN_BRANCHES=(UPSTREAM DEVELOP FEATURE MAIN DIST)
PMAN_TOOL_PDEV=DEVELOP
PMAN_TOOL_PWIP=FEATURE
PMAN_TOOL_PMAIN=MAIN
PMAN_TOOL_PDIST=DIST
UPSTREAM_BASE=DEVELOP ; UPSTREAM_MERGE_FROM= ; UPSTREAM_MERGE_TO=DEVELOP ; UPSTREAM_PREL= ; UPSTREAM_DELETE=
DEVELOP_BASE=MAIN ; DEVELOP_MERGE_FROM=FEATURE ; DEVELOP_MERGE_TO=MAIN ; DEVELOP_PREL=from ; DEVELOP_DELETE=to
MAIN_BASE=DEVELOP ; MAIN_MERGE_FROM=DEVELOP ; MAIN_MERGE_TO=DIST ; MAIN_PREL=to ; MAIN_DELETE=
DIST_BASE=MAIN ; DIST_MERGE_FROM=MAIN ; DIST_MERGE_TO= ; DIST_PREL= ; DIST_DELETE=
FEATURE_BASE=DEVELOP ; FEATURE_MERGE_FROM= ; FEATURE_MERGE_TO=DEVELOP ; FEATURE_PREL= ; FEATURE_DELETE=from
UPSTREAM_CREATE_FUNCTION=_create_upstream_action
function get_base_branch() {
# afficher la branche depuis laquelle créer la branche $1
# retourner 1 en cas d'erreur (pas de branche source)
local branch="$1" infos
[ -n "$branch" ] || return 1
infos="${branch^^}_BASE"; branch="${!infos}"
[ -n "$branch" ] && echo "$branch" || return 1
}
function get_merge_from_branch() {
# afficher la branche depuis laquelle la branche $1 doit merger
# retourner 1 en cas d'erreur (pas de branche source)
local branch="$1" infos
[ -n "$branch" ] || return 1
infos="${branch^^}_MERGE_FROM"; branch="${!infos}"
[ -n "$branch" ] && echo "$branch" || return 1
}
function get_merge_to_branch() {
# afficher la branche dans laquelle la branche $1 doit merger
# retourner 1 en cas d'erreur (pas de branche destination)
local branch="$1" infos
[ -n "$branch" ] || return 1
infos="${branch^^}_MERGE_TO"; branch="${!infos}"
[ -n "$branch" ] && echo "$branch" || return 1
}
function should_prel_merge() {
# tester si la branche $1 doit être mergée avec prel dans la direction
# $2(=to)
local branch="$1" merge_dir="${2:-to}" infos
[ -n "$branch" ] || return 1
infos="${branch^^}_PREL"
[ "${!infos}" == "$merge_dir" ]
}
function should_delete_merged() {
# tester si la branche $1 doit être supprimée après avoir été mergée dans la
# direction $2(=to)
local branch="$1" merge_dir="${2:-to}" infos
[ -n "$branch" ] || return 1
infos="${branch^^}_DELETE"
[ "${!infos}" == "$merge_dir" ]
}
: "
# description des variables #
* REF_BRANCH -- code de la branche de référence basé sur le nom de l'outil
* RefBranch -- nom effectif de la branche si elle est définie dans
.pman.conf, vide sinon
* IfRefBranch -- nom effectif de la branche *si elle existe*, vide sinon
* REF_UNIQUE -- si la branche de référence est unique. est vide pour les
codes de branches multiples, telle que FEATURE
* BASE_BRANCH -- branche de base à partir de laquelle créer la branche
de référence
* BaseBranch -- nom effectif de la branche de base si elle est définie
dans .pman.conf, vide sinon
* IfBaseBranch -- nom effectif de la branche de base *si elle existe*, vide
sinon
* MERGE_FROM -- code de la branche source à partir de laquelle la fusion
est faite dans REF_BRANCH. vide si la branche n'a pas de source
* MERGE_TO -- code de la branche destination dans laquelle la fusion est
faite depuis REF_BRANCH. vide si la branche n'a pas de destination
* MERGE_DIR -- direction de la fusion:
'from' si on fait REF_BRANCH --> MERGE_TO
'to' si on fait MERGE_FROM --> REF_BRANCH
* PREL_MERGE -- si la fusion devrait se faire avec prel
* DELETE_MERGED -- s'il faut supprimer la branche source après la fusion
* MERGE_SRC -- code de la branche source pour la fusion, ou vide si la
fusion n'est pas possible
* MergeSrc -- nom effectif de la branche source si elle est définie
dans .pman.conf
* IfMergeSrc -- nom effectif de la branche source *si elle existe*, vide
sinon
* MERGE_DEST -- code de la branche destination pour la fusion, ou vide si
la fusion n'est pas possible
* MergeDest -- nom effectif de la branche destination si elle est
définie dans .pman.conf
* IfMergeDest -- nom effectif de la branche source *si elle existe*, vide
sinon"
function set_pman_vars() {
RefBranch="${!REF_BRANCH}"
case "$REF_BRANCH" in
FEATURE|RELEASE|HOTFIX) REF_UNIQUE=;;
*) REF_UNIQUE=1;;
esac
BASE_BRANCH=$(get_base_branch "$REF_BRANCH")
[ -n "$BASE_BRANCH" ] && BaseBranch="${!BASE_BRANCH}" || BaseBranch=
MERGE_FROM=$(get_merge_from_branch "$REF_BRANCH")
MERGE_TO=$(get_merge_to_branch "$REF_BRANCH")
if [ -n "$1" ]; then MERGE_DIR="$1"
else MERGE_DIR=from
fi
PREL_MERGE=$(should_prel_merge "$REF_BRANCH" "$MERGE_DIR" && echo 1)
DELETE_MERGED=$(should_delete_merged "$REF_BRANCH" "$MERGE_DIR" && echo 1)
case "$MERGE_DIR" in
from)
MERGE_SRC="$REF_BRANCH"
MERGE_DEST="$MERGE_TO"
;;
to)
MERGE_SRC="$MERGE_FROM"
MERGE_DEST="$REF_BRANCH"
;;
esac
[ -n "$MERGE_SRC" ] && MergeSrc="${!MERGE_SRC}" || MergeSrc=
[ -n "$MERGE_DEST" ] && MergeDest="${!MERGE_DEST}" || MergeDest=
}
################################################################################
function _init_changelog() {
setx date=date +%d/%m/%Y-%H:%M
ac_set_tmpfile changelog
@ -77,7 +214,7 @@ $1 == "|" {
}
function _list_commits() {
local source="${1:-$SrcBranch}" dest="${2:-$DestBranch}" mergebase
local source="${1:-$MergeSrc}" dest="${2:-$MergeDest}" mergebase
setx mergebase=git merge-base "$dest" "$source"
git log --oneline --graph --no-decorate "$mergebase..$source" |
grep -vF '|\' | grep -vF '|/' | sed -r 's/^(\| )+\* +/| /; s/^\* +/+ /' |
@ -85,7 +222,7 @@ function _list_commits() {
}
function _show_diff() {
local source="${1:-$SrcBranch}" dest="${2:-$DestBranch}" mergebase
local source="${1:-$MergeSrc}" dest="${2:-$MergeDest}" mergebase
setx mergebase=git merge-base "$dest" "$source"
git diff ${_sd_COLOR:+--color=$_sd_COLOR} "$mergebase..$source"
}
@ -147,22 +284,27 @@ EOF
################################################################################
# Config
function ensure_gitdir() {
function check_gitdir() {
# commencer dans le répertoire indiqué
local chdir="$1"
if [ -n "$chdir" ]; then
cd "$chdir" || die || return
cd "$chdir" || return 1
fi
# se mettre à la racine du dépôt git
local gitdir
git_ensure_gitvcs
git_check_gitvcs || return 1
setx gitdir=git_get_toplevel
cd "$gitdir" || die || return
cd "$gitdir" || return 1
}
function ensure_gitdir() {
# commencer dans le répertoire indiqué
check_gitdir "$@" || die || return 1
}
function load_branches() {
local what="${1:-all}"; shift
local branch what="${1:-all}"; shift
case "$what" in
all)
[ -n "$Origin" ] || Origin=origin
@ -172,30 +314,6 @@ function load_branches() {
setx -a AllBranches=git_list_pbranches "$Origin"
;;
current)
SrcBranch="$1"
[ -n "$SrcBranch" ] || SrcBranch="$CurrentBranch"
case "$SrcBranch" in
"$UPSTREAM") SrcType=upstream; DestBranch="$DEVELOP";;
"$FEATURE"*) SrcType=feature; DestBranch="$DEVELOP";;
"$DEVELOP") SrcType=develop; DestBranch="$MAIN";;
"$RELEASE"*) SrcType=release; DestBranch="$MAIN";;
"$HOTFIX"*) SrcType=hotfix; DestBranch="$MAIN";;
"$MAIN") SrcType=main; DestBranch="$DIST";;
"$DIST") SrcType=dist; DestBranch=;;
*) SrcType=; DestBranch=;;
esac
case "$DestBranch" in
"$UPSTREAM") DestType=upstream;;
"$FEATURE"*) DestType=feature;;
"$DEVELOP") DestType=develop;;
"$RELEASE"*) DestType=release;;
"$HOTFIX"*) DestType=hotfix;;
"$MAIN") DestType=main;;
"$DIST") DestType=dist;;
*) DestType=;;
esac
local branch
UpstreamBranch=
FeatureBranches=()
DevelopBranch=
@ -203,23 +321,32 @@ function load_branches() {
HotfixBranch=
MainBranch=
DistBranch=
IfRefBranch=
IfBaseBranch=
IfMergeSrc=
IfMergeDest=
for branch in "${LocalBranches[@]}"; do
if [ "$branch" == "$UPSTREAM" ]; then
UpstreamBranch="$branch"
elif [[ "$branch" == "$FEATURE"* ]]; then
elif [ -n "$FEATURE" ] && [[ "$branch" == "$FEATURE"* ]]; then
FeatureBranches+=("$branch")
elif [ "$branch" == "$DEVELOP" ]; then
elif [ -n "$DEVELOP" -a "$branch" == "$DEVELOP" ]; then
DevelopBranch="$branch"
elif [[ "$branch" == "$RELEASE"* ]]; then
elif [ -n "$RELEASE" ] && [[ "$branch" == "$RELEASE"* ]]; then
ReleaseBranch="$branch"
elif [[ "$branch" == "$HOTFIX"* ]]; then
elif [ -n "$HOTFIX" ] && [[ "$branch" == "$HOTFIX"* ]]; then
HotfixBranch="$branch"
elif [ "$branch" == "$MAIN" ]; then
elif [ -n "$MAIN" -a "$branch" == "$MAIN" ]; then
MainBranch="$branch"
elif [ "$branch" == "$DIST" ]; then
elif [ -n "$DIST" -a "$branch" == "$DIST" ]; then
DistBranch="$branch"
fi
[ -n "$RefBranch" -a "$branch" == "$RefBranch" ] && IfRefBranch="$branch"
[ -n "$BaseBranch" -a "$branch" == "$BaseBranch" ] && IfBaseBranch="$branch"
[ -n "$MergeSrc" -a "$branch" == "$MergeSrc" ] && IfMergeSrc="$branch"
[ -n "$MergeDest" -a "$branch" == "$MergeDest" ] && IfMergeDest="$branch"
done
[ -n "$IfMergeSrc" -a "$IfMergeDest" ] && IfCanMerge=1 || IfCanMerge=
;;
esac
}
@ -244,9 +371,6 @@ function load_config() {
elif [ -f .pman.conf ]; then
ConfigFile="$(pwd)/.pman.conf"
source "$ConfigFile"
elif [ -n "$1" -a -n "${MYNAME#$1}" ]; then
ConfigFile="$NULIBDIR/bash/src/pman${MYNAME#$1}.conf.sh"
source "$ConfigFile"
else
ConfigFile="$NULIBDIR/bash/src/pman.conf.sh"
fi
@ -319,10 +443,8 @@ function _mscript_start() {
#!/bin/bash
$(qvals source "$NULIBDIR/load.sh") || exit 1
$(echo_setv SrcBranch="$SrcBranch")
$(echo_setv SrcType="$SrcType")
$(echo_setv DestBranch="$DestBranch")
$(echo_setv DestType="$DestType")
$(echo_setv MergeSrc="$MergeSrc")
$(echo_setv MergeDest="$MergeDest")
merge=
delete=
@ -342,32 +464,32 @@ function _mscript_merge_branch() {
local msg
# basculer sur la branche
_scripta "switch to branch $DestBranch" <<EOF
$comment$(qvals git checkout "$DestBranch")$or_die
_scripta "switch to branch $MergeDest" <<EOF
$comment$(qvals git checkout "$MergeDest")$or_die
EOF
if [ -n "$SquashMsg" ]; then
msg="$SquashMsg"
[ -n "$TechMerge" ] && msg="<pman>$msg"
_scripta "squash merge $SrcBranch" <<EOF
$comment$(qvals git merge "$SrcBranch" --squash)$or_die
_scripta "squash merge $MergeSrc" <<EOF
$comment$(qvals git merge "$MergeSrc" --squash)$or_die
$comment$(qvals git commit -m "$msg")$or_die
EOF
else
msg="Intégration de la branche $SrcBranch"
msg="Intégration de la branche $MergeSrc"
[ -n "$TechMerge" ] && msg="<pman>$msg"
_scripta "merge $SrcBranch" <<EOF
$comment$(qvals git merge "$SrcBranch" --no-ff -m "$msg")$or_die
_scripta "merge $MergeSrc" <<EOF
$comment$(qvals git merge "$MergeSrc" --no-ff -m "$msg")$or_die
EOF
fi
array_addu push_branches "$DestBranch"
array_addu push_branches "$MergeDest"
}
function _mscript_delete_branch() {
_scripta "delete branch $SrcBranch" <<EOF
$comment$(qvals git branch -D "$SrcBranch")$or_die
_scripta "delete branch $MergeSrc" <<EOF
$comment$(qvals git branch -D "$MergeSrc")$or_die
EOF
array_addu delete_branches ":$SrcBranch"
array_addu delete_branches ":$MergeSrc"
}
################################################################################
@ -379,13 +501,11 @@ function _rscript_start() {
#!/bin/bash
$(qvals source "$NULIBDIR/load.sh") || exit 1
$(echo_setv SrcBranch="$SrcBranch")
$(echo_setv SrcType="$SrcType")
$(echo_setv MergeSrc="$MergeSrc")
$(echo_setv Version="$Version")
$(echo_setv Tag="$Tag")
$(echo_setv ReleaseBranch="$ReleaseBranch")
$(echo_setv DestBranch="$DestBranch")
$(echo_setv DestType="$DestType")
$(echo_setv MergeDest="$MergeDest")
create=
merge=
@ -409,25 +529,28 @@ function _rscript_create_release_branch() {
## Release $Tag du $date
"
_list_commits | _filter_changes | _format_md >>"$changelog"
if [ -s CHANGES.md ]; then
echo >>"$changelog"
cat CHANGES.md >>"$changelog"
fi
"${EDITOR:-nano}" +7 "$changelog"
[ -s "$changelog" ] || exit_with ewarn "Création de la release annulée"
# créer la branche de release et basculer dessus
_scripta "create branch $ReleaseBranch" <<EOF
$(qvals git checkout -b "$ReleaseBranch" "$SrcBranch")$or_die
$(qvals git checkout -b "$ReleaseBranch" "$MergeSrc")$or_die
EOF
# créer le changelog
_scripta "update CHANGES.md" <<EOF
tmpchanges=/tmp/pman_CHANGES.$$.md
$(qvals echo "$(awk <"$changelog" '
BEGIN { p = 0 }
p == 0 && $0 == "" { p = 1; next }
p == 1 { print }
')") >CHANGES.md
')") >"\$tmpchanges"
if [ -s CHANGES.md ]; then
echo >>"\$tmpchanges"
cat CHANGES.md >>"\$tmpchanges"
fi
cat "\$tmpchanges" >CHANGES.md
rm -f "\$tmpchanges"
git add CHANGES.md
EOF
@ -471,3 +594,176 @@ function _rscript_delete_release_branch() {
$comment$(qvals git branch -D "$ReleaseBranch")$or_die
EOF
}
################################################################################
# Outils
function dump_action() {
enote "Valeurs des variables:
REF_BRANCH=$REF_BRANCH${RefBranch:+ RefBranch=$RefBranch IfRefBranch=$IfRefBranch}
BASE_BRANCH=$BASE_BRANCH${BaseBranch:+ BaseBranch=$BaseBranch IfBaseBranch=$IfBaseBranch}
MERGE_FROM=$MERGE_FROM
MERGE_TO=$MERGE_TO
MERGE_DIR=$MERGE_DIR
PREL_MERGE=$PREL_MERGE
DELETE_MERGED=$DELETE_MERGED
MERGE_SRC=$MERGE_SRC${MergeSrc:+ MergeSrc=$MergeSrc IfMergeSrc=$IfMergeSrc}
MERGE_DEST=$MERGE_DEST${MergeDest:+ MergeDest=$MergeDest IfMergeDest=$IfMergeDest}
CurrentBranch=$CurrentBranch
LocalBranches=${LocalBranches[*]}
RemoteBranches=${RemoteBranches[*]}
AllBranches=${AllBranches[*]}
UpstreamBranch=$UpstreamBranch
FeatureBranches=${FeatureBranches[*]}
DevelopBranch=$DevelopBranch
ReleaseBranch=$ReleaseBranch
HotfixBranch=$HotfixBranch
MainBranch=$MainBranch
DistBranch=$DistBranch
"
}
function resolve_unique_branch() {
if [ "$REF_BRANCH" == FEATURE ]; then
if [ $# -gt 0 ]; then
RefBranch="$FEATURE${1#$FEATURE}"
elif [[ "$CurrentBranch" == "$FEATURE"* ]]; then
RefBranch="$CurrentBranch"
elif [ ${#FeatureBranches[*]} -eq 0 ]; then
die "Vous devez spécifier la branche de feature"
elif [ ${#FeatureBranches[*]} -eq 1 ]; then
RefBranch="${FeatureBranches[0]}"
else
simple_menu \
RefBranch FeatureBranches \
-t "Branches de feature" \
-m "Veuillez choisir la branche de feature" \
-d "${FeatureBranches[0]}"
fi
else
die "resolve_unique_branch: $REF_BRANCH: non implémenté"
fi
if [ "$MERGE_DIR" == from ]; then
MergeSrc="$RefBranch"
elif [ "$MERGE_DIR" == to ]; then
MergeDest="$RefBranch"
fi
}
function _ensure_ref_branch() {
[ -n "$RefBranch" ] || die "\
La branche $REF_BRANCH n'a pas été définie.
Veuillez éditer le fichier .pman.conf"
[ "$1" == init -o -n "$IfRefBranch" ] || die "$RefBranch: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
}
function _ensure_base_branch() {
[ -n "$BaseBranch" ] || die "\
La branche $BASE_BRANCH n'a pas été définie.
Veuillez éditer le fichier .pman.conf"
[ "$1" == init -o -n "$IfBaseBranch" ] || die "$BaseBranch: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
}
function _create_default_action() {
enote "Vous allez créer la branche ${COULEUR_BLEUE}$RefBranch${COULEUR_NORMALE} <-- ${COULEUR_ROUGE}$BaseBranch${COULEUR_NORMALE}"
ask_yesno "Voulez-vous continuer?" O || die
einfo "Création de la branche $RefBranch"
git checkout -b "$RefBranch" "$BaseBranch" || die
push_branches+=("$RefBranch")
}
function _create_upstream_action() {
enote "Vous allez créer la branche ${COULEUR_BLEUE}$RefBranch${COULEUR_NORMALE}"
ask_yesno "Voulez-vous continuer?" O || die
# faire une copie de la configuration actuelle
local config; ac_set_tmpfile config
set -x; ls -l "$ConfigFile" #XXX
cp "$ConfigFile" "$config"
set +x #XXX
einfo "Création de la branche $RefBranch"
git checkout --orphan "$RefBranch" || die
git rm -rf .
cp "$config" .pman.conf
git add .pman.conf
git commit -m "commit initial"
push_branches+=("$RefBranch")
einfo "Fusion dans $DevelopBranch"
git checkout "$DevelopBranch"
git merge \
--no-ff -m "<pman>Intégration initiale de la branche $RefBranch" \
-srecursive -Xours --allow-unrelated-histories \
"$RefBranch"
push_branches+=("$DevelopBranch")
}
function checkout_action() {
local -a push_branches
[ -n "$REF_UNIQUE" ] || resolve_unique_branch "$@"
_ensure_ref_branch init
if array_contains LocalBranches "$RefBranch"; then
git checkout "$RefBranch"
elif array_contains AllBranches "$RefBranch"; then
enote "$RefBranch: une branche du même nom existe dans l'origine"
ask_yesno "Voulez-vous basculer sur cette branche?" O || die
git checkout "$RefBranch"
else
_ensure_base_branch
resolve_should_push
local create_function
create_function="${REF_BRANCH}_CREATE_FUNCTION"; create_function="${!create_function}"
[ -n "$create_function" ] || create_function=_create_default_action
"$create_function"
_push_branches
fi
}
function ensure_merge_branches() {
[ -n "$MergeSrc" ] || die "\
$RefBranch: configuration de fusion non trouvée: la branche $MERGE_SRC n'a pas été définie.
Veuillez éditer le fichier .pman.conf"
[ -n "$MergeDest" ] || die "\
$RefBranch: configuration de fusion non trouvée: la branche $MERGE_DEST n'a pas été définie.
Veuillez éditer le fichier .pman.conf"
local branches
[ "$1" == -a ] && branches=AllBranches || branches=LocalBranches
array_contains "$branches" "$MergeSrc" || die "$MergeSrc: branche source introuvable"
array_contains "$branches" "$MergeDest" || die "$MergeDest: branche destination introuvable"
}
function _show_action() {
local commits
setx commits=_list_commits "$MergeSrc" "$MergeDest"
if [ -n "$commits" ]; then
if [ $ShowLevel -ge 2 ]; then
{
echo "\
# Commits à fusionner $MergeSrc --> $MergeDest
$commits
"
_sd_COLOR=always _show_diff
} | less -eRF
else
einfo "Commits à fusionner $MergeSrc --> $MergeDest"
eecho "$commits"
fi
fi
}
function show_action() {
git_check_cleancheckout || ewarn "$git_cleancheckout_DIRTY"
[ -n "$REF_UNIQUE" ] || resolve_unique_branch "$@"
ensure_merge_branches
_show_action "$@"
}

View File

@ -1,14 +1,10 @@
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
## configuration de la branche 7.4 d'un projet PHP multiversion
# il s'agit d'un projet avec deux branches parallèles: 7.4 et 8.2, les
# modifications de la 7.4 étant incluses dans la branche 8.2
UPSTREAM=
DEVELOP=dev74
FEATURE=wip74/
RELEASE=rel74-
MAIN=dist74
MAIN=main74
TAG_PREFIX=
TAG_SUFFIX=p74
HOTFIX=hotf74-

View File

@ -1,14 +1,10 @@
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
## configuration de la branche 8.2 d'un projet PHP multiversion
# il s'agit d'un projet avec deux branches parallèles: 7.4 et 8.2, les
# modifications de la 7.4 étant incluses dans la branche 8.2
UPSTREAM=dev74
DEVELOP=dev82
FEATURE=wip82/
RELEASE=rel82-
MAIN=dist82
MAIN=main82
TAG_PREFIX=
TAG_SUFFIX=p82
HOTFIX=hotf82-

12
bash/src/pman84.conf.sh Normal file
View File

@ -0,0 +1,12 @@
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
UPSTREAM=dev74
DEVELOP=dev84
FEATURE=wip84/
RELEASE=rel84-
MAIN=main84
TAG_PREFIX=
TAG_SUFFIX=p84
HOTFIX=hotf84-
DIST=
NOAUTO=

View File

@ -22,7 +22,12 @@ et \$2 vaudra alors 'file'
si un fichier \${2#.}.local existe (e.g 'file.ext.local'), prendre ce fichier à
la place comme source
Ajouter file au tableau userfiles"
Ajouter file au tableau userfiles
retourner:
- 0 en cas de copie avec succès
- 2 si la source n'existe pas
- 3 si une erreur I/O s'est produite lors de la copie"
function template_copy_replace() {
local src="$1" dest="$2"
local srcdir srcname lsrcname
@ -37,8 +42,28 @@ function template_copy_replace() {
lsrcname="${srcname#.}.local"
[ -e "$srcdir/$lsrcname" ] && src="$srcdir/$lsrcname"
[ -e "$src" ] || return 2
userfiles+=("$dest")
cp -P "$src" "$dest"
local have_backup
if [ -e "$dest" ]; then
# copie de sauvegarde avant
if ! cp -P --preserve=all "$dest" "$dest.bck.$$"; then
rm "$dest.bck.$$"
return 3
fi
have_backup=1
fi
if ! cp -P "$src" "$dest"; then
rm "$dest"
if [ -n "$have_backup" ]; then
# restaurer la sauvegarde en cas d'erreur
cp -P --preserve=all "$dest.bck.$$" "$dest" &&
rm "$dest.bck.$$"
fi
return 3
fi
[ -n "$have_backup" ] && rm "$dest.bck.$$"
return 0
}
@ -51,7 +76,13 @@ et \$2 vaudra alors 'file'
si un fichier \${1#.}.local existe (e.g 'file.ext.local'), prendre ce fichier à
la place comme source
Ajouter file au tableau userfiles"
Ajouter file au tableau userfiles
retourner:
- 0 en cas de copie avec succès
- 1 si le fichier existait déjà
- 2 si la source n'existe pas
- 3 si une erreur I/O s'est produite lors de la copie"
function template_copy_missing() {
local src="$1" dest="$2"
local srcdir srcname lsrcname
@ -63,15 +94,33 @@ function template_copy_missing() {
dest="$srcdir/$dest"
fi
userfiles+=("$dest")
if [ ! -e "$dest" ]; then
lsrcname="${srcname#.}.local"
[ -e "$srcdir/$lsrcname" ] && src="$srcdir/$lsrcname"
lsrcname="${srcname#.}.local"
[ -e "$srcdir/$lsrcname" ] && src="$srcdir/$lsrcname"
cp -P "$src" "$dest"
return 0
[ -e "$src" ] || return 2
userfiles+=("$dest")
[ -e "$dest" ] && return 1
if ! cp -P "$src" "$dest"; then
# ne pas garder le fichier en cas d'erreur de copie
rm "$dest"
return 3
fi
return 1
return 0
}
function: template_ioerror "\
tester si une erreur de copie s'est produite lors de l'appel à
template_copy_missing() ou template_copy_replace(), par exemple en cas de
dépassement de capacité du disque ou si le fichier source n'existe pas
il faut appeler cette fonction avec la valeur de retour de ces fonctions, e.g
template_copy_missing file
template_ioerror $? && die"
function template_ioerror() {
local r="${1:-$?}"
[ $r -ge 2 ]
}
function: template_dump_vars "\
@ -219,8 +268,13 @@ function _template_can_process() {
esac
}
function: template_process_userfiles "\
retourner:
- 0 en cas de traitement avec succès des fichiers
- 3 si une erreur I/O s'est produite lors du traitement d'un des fichiers"
function template_process_userfiles() {
local awkscript sedscript workfile userfile
local have_backup
ac_set_tmpfile awkscript
ac_set_tmpfile sedscript
template_generate_scripts "$awkscript" "$sedscript" "$@"
@ -231,10 +285,28 @@ function template_process_userfiles() {
if cat "$userfile" | awk -f "$awkscript" | sed -rf "$sedscript" >"$workfile"; then
if testdiff "$workfile" "$userfile"; then
# n'écrire le fichier que s'il a changé
cat "$workfile" >"$userfile"
if [ -e "$userfile" ]; then
# copie de sauvegarde avant
if ! cp -P --preserve=all "$userfile" "$userfile.bck.$$"; then
rm "$userfile.bck.$$"
return 3
fi
have_backup=1
fi
if ! cat "$workfile" >"$userfile"; then
rm "$userfile"
if [ -n "$have_backup" ]; then
# restaurer la sauvegarde en cas d'erreur
cp -P --preserve=all "$userfile.bck.$$" "$userfile" &&
rm "$userfile.bck.$$"
fi
return 3
fi
[ -n "$have_backup" ] && rm "$userfile.bck.$$"
fi
fi
done
ac_clean "$awkscript" "$sedscript" "$workfile"
return 0
}

1
bin/.cachectl.php Symbolic link
View File

@ -0,0 +1 @@
../php/bin/cachectl.php

1
bin/.dumpser.php Symbolic link
View File

@ -0,0 +1 @@
../php/bin/dumpser.php

1
bin/.json2yml.php Symbolic link
View File

@ -0,0 +1 @@
../php/bin/json2yml.php

1
bin/.mysql.capacitor.php Symbolic link
View File

@ -0,0 +1 @@
../php/bin/mysql.capacitor.php

View File

@ -2,9 +2,7 @@
<?php
require __DIR__ . "/../php/vendor/autoload.php";
use nulib\tools\pman\ComposerFile;
use nulib\tools\pman\ComposerPmanFile;
use nulib\ValueException;
use cli\pman\ComposerFile;
$composer = new ComposerFile();

View File

@ -2,8 +2,8 @@
<?php
require __DIR__ . "/../php/vendor/autoload.php";
use nulib\tools\pman\ComposerFile;
use nulib\tools\pman\ComposerPmanFile;
use cli\pman\ComposerFile;
use cli\pman\ComposerPmanFile;
use nulib\ValueException;
$composer = new ComposerFile();

1
bin/.pgsql.capacitor.php Symbolic link
View File

@ -0,0 +1 @@
../php/bin/pgsql.capacitor.php

1
bin/.sqlite.capacitor.php Symbolic link
View File

@ -0,0 +1 @@
../php/bin/sqlite.capacitor.php

1
bin/.yml2json.php Symbolic link
View File

@ -0,0 +1 @@
../php/bin/yml2json.php

View File

@ -9,4 +9,4 @@ args=(
)
parse_args "$@"; set -- "${args[@]}"
exec "$MYDIR/pmer" --tech-merge -Bdev82 dev74 ${dev74:+-a "git checkout dev74"} "$@"
exec "$MYDIR/ptool" -fupstream -Bdev82 -m --tech-merge ${dev74:+-a "git checkout dev74"} "$@"

View File

@ -19,7 +19,7 @@ export RUNPHP_BUILD_FLAVOUR=
runphp=("$MYDIR/../runphp/runphp" --bs)
[ -z "$force" ] && runphp+=(--ue)
for RUNPHP_DIST in d12 d11; do
for RUNPHP_DIST in d13 d12 d11; do
for RUNPHP_BUILD_FLAVOUR in +ic none; do
flavour="$RUNPHP_BUILD_FLAVOUR"
[ "$flavour" == none ] && flavour=

85
bin/ff Executable file
View File

@ -0,0 +1,85 @@
#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
source "$(dirname -- "$0")/../load.sh" || exit 1
function filter_arg() {
local inverse="$1"; shift
while read arg; do
if [ "$inverse" == 0 ]; then
[ $1 "$arg" ] && echo "$arg"
else
[ $1 "$arg" ] || echo "$arg"
fi
done
}
function filter_broken() {
local inverse="$1"; shift
while read arg; do
[ -L "$arg" ] || continue
if [ "$inverse" == 0 ]; then
[ -e "$arg" ] || echo "$arg"
else
[ -e "$arg" ] && echo "$arg"
fi
done
}
chdir=
lsdirs=
lsfiles=
lsnames=
dir=
file=
link=
broken=
exists=
nonzero=
inverse=0
args=(
"filtrer une liste de fichiers"
"[filter]"
-C:,--chdir chdir= "changer le répertoire courant avant de lister les fichiers"
--lsdirs . "n'afficher que les répertoires du répertoire courant"
--lsfiles . "n'afficher que les fichiers du répertoire courant"
--lsnames . "n'afficher que les noms de fichier"
-d,--dir . "garder uniquement les répertoires"
-f,--file . "garder uniquement les fichiers"
-L,--link . "garder uniquement les liens symboliques"
-b,--broken . "garder uniquement les liens symboliques cassés"
-e,--exists . "garder uniquement les fichiers/répertoires qui existent"
-s,--nonzero . "garder uniquement les fichiers ayant une taille non nulle"
-n,--inverse inverse=1 "inverser le sens du filtre"
)
parse_args "$@"; set -- "${args[@]}"
if [ -n "$chdir" ]; then
cd "$chdir" || die
fi
if in_isatty; then
# lister les fichiers
setx cwd=pwd
[ -n "$lsnames" ] && withpath= || withpath=1
if [ -n "$lsdirs" -a -n "$lsfiles" ]; then
cmd="{
$(qvals ls_dirs ${withpath:+-p} "$cwd" "$@")
$(qvals ls_files ${withpath:+-p} "$cwd" "$@")
}"
elif [ -n "$lsdirs" ]; then
cmd="$(qvals ls_dirs ${withpath:+-p} "$cwd" "$@")"
elif [ -n "$lsfiles" ]; then
cmd="$(qvals ls_files ${withpath:+-p} "$cwd" "$@")"
else
cmd="$(qvals ls_all ${withpath:+-p} "$cwd" "$@")"
fi
else
cmd="cat"
fi
[ -n "$dir" ] && cmd="$cmd | filter_arg $inverse -d"
[ -n "$file" ] && cmd="$cmd | filter_arg $inverse -f"
[ -n "$link" ] && cmd="$cmd | filter_arg $inverse -L"
[ -n "$broken" ] && cmd="$cmd | filter_broken $inverse"
[ -n "$exists" ] && cmd="$cmd | filter_arg $inverse -e"
[ -n "$nonzero" ] && cmd="$cmd | filter_arg $inverse -s"
eval "$cmd"

1
bin/json2yml.php Symbolic link
View File

@ -0,0 +1 @@
runphp

1
bin/mysql.capacitor.php Symbolic link
View File

@ -0,0 +1 @@
runphp

View File

@ -20,8 +20,8 @@ fi
[ -f /etc/profile ] && source /etc/profile
[ -f ~/.bash_profile ] && source ~/.bash_profile
# Modifier le PATH. Ajouter aussi le chemin vers les uapps python
PATH=$(qval "$NULIBDIR/bin:$PATH")
# Modifier le PATH
PATH=$(qval "$NULIBDIR/wip:$NULIBDIR/bin:$PATH")
if [ -n '$DEFAULT_PS1' ]; then
DEFAULT_PS1=$(qval "[nlshell] $DEFAULT_PS1")

1
bin/pcomp-local_deps.php Symbolic link
View File

@ -0,0 +1 @@
runphp

View File

@ -0,0 +1 @@
runphp

1
bin/pdev Symbolic link
View File

@ -0,0 +1 @@
ptool

1
bin/pdist Symbolic link
View File

@ -0,0 +1 @@
ptool

1
bin/pgsql.capacitor.php Symbolic link
View File

@ -0,0 +1 @@
runphp

192
bin/pinit Executable file
View File

@ -0,0 +1,192 @@
#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
source "$(dirname -- "$0")/../load.sh" || exit 1
require: git pman pman.conf
################################################################################
# Informations
################################################################################
SHOW_VARS=(
--Configuration
"${CONFIG_VARS[@]}"
--Paramètres
CurrentBranch
CurrentType=SrcType
)
function show_action() {
local var src
echo_setv ConfigBranch="$ConfigBranch"
echo_setv ConfigFile="$(ppath "$ConfigFile")"
for var in "${SHOW_VARS[@]}"; do
if [ "${var#--}" != "$var" ]; then
estep "${var#--}"
else
splitfsep "$var" = var src
[ -n "$src" ] || src="$var"
echo_setv "$var=${!src}"
fi
done
}
################################################################################
# Initialisation
################################################################################
function _init_config() {
if [ ! -f .pman.conf -o -n "$ForceCreate" ]; then
ac_set_tmpfile config
cp "$ConfigFile" "$config"
"${EDITOR:-nano}" "$config"
[ -s "$config" ] || return 1
cp "$config" .pman.conf
if testdiff .pman.conf "$ConfigFile"; then
ConfigFile="$(pwd)/.pman.conf"
load_config
load_branches current "$SrcBranch"
fi
git add .pman.conf
fi
if [ ! -f ".gitignore" ]; then
echo >.gitignore "\
.~lock*#
.*.swp"
git add .gitignore
fi
return 0
}
function init_repo_action() {
local -a push_branches; local config
[ ${#LocalBranches[*]} -eq 0 ] || die "Ce dépôt a déjà été initialisé"
_init_config || exit_with ewarn "Initialisation du dépôt annulée"
einfo "Création de la branche $MAIN"
git symbolic-ref HEAD "refs/heads/$MAIN"
git commit -m "commit initial"
push_branches+=("$MAIN")
einfo "Création de la branche $DEVELOP"
git checkout -b "$DEVELOP"
push_branches+=("$DEVELOP")
_push_branches
}
function init_config_action() {
local -a push_branches; local config
[ -f .pman.conf -a -z "$ForceCreate" ] && die "La configuration pman a déjà été initialisée"
resolve_should_push
_init_config || exit_with ewarn "Initialisation de la configuration annulée"
git commit -m "configuration pman"
push_branches+=("$CurrentBranch")
_push_branches
}
function _init_composer() {
if [ ! -f .composer.pman.yml -o -n "$ForceCreate" ]; then
ac_set_tmpfile config
cat >"$config" <<EOF
# -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8
composer:
match_prefix:
match_prefix-dev:
profiles: [ dev, dist ]
dev:
link: true
require:
reqire-dev:
dist:
link: false
require:
reqire-dev:
EOF
"${EDITOR:-nano}" "$config"
[ -s "$config" ] || return 1
cp "$config" .composer.pman.yml
git add .composer.pman.yml
fi
return 0
}
function init_composer_action() {
local -a push_branches; local config
[ -f .composer.pman.yml -a -z "$ForceCreate" ] && die "La configuration pman composer a déjà été initialisée"
resolve_should_push
_init_composer || exit_with ewarn "Initialisation de la configuration annulée"
git commit -m "configuration pman composer"
push_branches+=("$CurrentBranch")
_push_branches
}
function init_action() {
local what="${1:-repo}"; shift
case "$what" in
repo|r) init_repo_action "$@";;
config|c) init_config_action "$@";;
composer|o) init_composer_action "$@";;
*) die "$what: destination non implémentée"
esac
}
################################################################################
# Programme principal
################################################################################
chdir=
ConfigFile=
action=init
Origin=
Push=1
ForceCreate=
args=(
"initialiser un dépôt git"
"repo|config|composer|all"
-d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
-c:,--config-file:CONFIG ConfigFile= "++\
fichier de configuration des branches. cette option est prioritaire sur --config-branch
par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe"
-w,--show-config action=show "++\
afficher la configuration chargée"
-O:,--origin Origin= "++\
origine vers laquelle pousser les branches"
-n,--no-push Push= "\
ne pas pousser les branches vers leur origine après leur création"
--push Push=1 "++\
pousser les branches vers leur origine après leur création.
c'est l'option par défaut"
-f,--force-create ForceCreate=1 "\
Forcer la (re)création des fichiers de configuration (notamment .pman.conf,
.composer.pman.yml, etc.)"
)
parse_args "$@"; set -- "${args[@]}"
# charger la configuration
ensure_gitdir "$chdir"
load_branches all
load_config
load_branches current
# puis faire l'action que l'on nous demande
case "$action" in
show) show_action "$@";;
init)
git_ensure_cleancheckout
init_action "$@"
;;
*) die "$action: action non implémentée";;
esac

1
bin/pmain Symbolic link
View File

@ -0,0 +1 @@
ptool

View File

@ -7,33 +7,7 @@ git_cleancheckout_DIRTY="\
Vous avez des modifications locales.
Enregistrez ces modifications avant de créer une release"
function show_action() {
local commits
setx commits=_list_commits
if [ -n "$commits" ]; then
if [ $ShowLevel -ge 2 ]; then
{
echo "\
# Commits à fusionner $SrcBranch --> $DestBranch
$commits
"
_sd_COLOR=always _show_diff
} | less -eRF
else
einfo "Commits à fusionner $SrcBranch --> $DestBranch"
eecho "$commits"
fi
fi
}
function ensure_branches() {
[ -n "$SrcBranch" -a -n "$DestBranch" ] ||
die "$SrcBranch: Aucune configuration de fusion trouvée pour cette branche"
array_contains LocalBranches "$SrcBranch" || die "$SrcBranch: branche source introuvable"
array_contains LocalBranches "$DestBranch" || die "$DestBranch: branche destination introuvable"
function ensure_rel_infos() {
Tag="$TAG_PREFIX$Version$TAG_SUFFIX"
local -a tags
setx -a tags=git tag -l "${TAG_PREFIX}*${TAG_SUFFIX}"
@ -71,14 +45,14 @@ L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine"
if [ -n "$Merge" ]; then
enote "\
Ce script va:
- créer la branche de release ${COULEUR_VERTE}$ReleaseBranch${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$SrcBranch${COULEUR_NORMALE}
- créer la branche de release ${COULEUR_VERTE}$ReleaseBranch${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$MergeSrc${COULEUR_NORMALE}
- la provisionner avec une description des changements
- la fusionner dans la branche destination ${COULEUR_ROUGE}$DestBranch${COULEUR_NORMALE}${Push:+
- la fusionner dans la branche destination ${COULEUR_ROUGE}$MergeDest${COULEUR_NORMALE}${Push:+
- pousser les branches modifiées}"
else
enote "\
Ce script va:
- créer la branche de release ${COULEUR_VERTE}$ReleaseBranch${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$SrcBranch${COULEUR_NORMALE}
- créer la branche de release ${COULEUR_VERTE}$ReleaseBranch${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$MergeSrc${COULEUR_NORMALE}
- la provisionner avec une description des changements
Vous devrez:
- mettre à jour les informations de release puis relancer ce script"
@ -123,8 +97,8 @@ EOF
$BEFORE_MERGE_RELEASE
)$or_die
EOF
_rscript_merge_release_branch "$DestBranch" "$Tag"
_rscript_merge_release_branch "$SrcBranch"
_rscript_merge_release_branch "$MergeDest" "$Tag"
_rscript_merge_release_branch "$MergeSrc"
_rscript_delete_release_branch
[ -n "$AFTER_MERGE_RELEASE" ] && _scripta <<EOF
(
@ -183,14 +157,14 @@ function merge_release_action() {
enote "\
Vous allez:
- fusionner la branche de release ${COULEUR_VERTE}$ReleaseBranch${COULEUR_NORMALE}
dans la branche destination ${COULEUR_ROUGE}$DestBranch${COULEUR_NORMALE}"
dans la branche destination ${COULEUR_ROUGE}$MergeDest${COULEUR_NORMALE}"
ask_yesno "Voulez-vous continuer?" O || die
}
function merge_hotfix_action() {
enote "\
Vous allez intégrer la branche de hotfix ${COULEUR_JAUNE}$HotfixBranch${COULEUR_NORMALE}
dans la branche destination ${COULEUR_ROUGE}$DestBranch${COULEUR_NORMALE}"
dans la branche destination ${COULEUR_ROUGE}$MergeDest${COULEUR_NORMALE}"
ask_yesno "Voulez-vous continuer?" O || die
}
@ -206,14 +180,14 @@ _Fake=
_KeepScript=
action=release
ShowLevel=0
[ -z "$PMAN_NO_MERGE" ] && Merge=1 || Merge=
[ -z "$PMAN_NO_PUSH" ] && Push=1 || Push=
Merge=1
Push=1
Version=
CurrentVersion=
ForceCreate=
args=(
"faire une nouvelle release à partir de la branche source"
" -v VERSION [source]
"faire une nouvelle release"
" -v VERSION
CONFIGURATION
Le fichier .pman.conf contient la configuration des branches. Les variables
@ -261,8 +235,10 @@ parse_args "$@"; set -- "${args[@]}"
# charger la configuration
ensure_gitdir "$chdir"
load_branches all
load_config "$MYNAME"
load_branches current "$1"; shift
load_config
REF_BRANCH=DEVELOP
set_pman_vars
load_branches current
[ -n "$Merge" -a -n "$NOAUTO" ] && ManualRelease=1 || ManualRelease=
[ -n "$ManualRelease" ] && Merge=
@ -272,19 +248,12 @@ resolve_should_push quiet
# puis faire l'action que l'on nous demande
case "$action" in
show)
git_check_cleancheckout || ewarn "$git_cleancheckout_DIRTY"
ensure_branches
show_action "$@"
;;
show) show_action "$@";;
release)
[ -z "$_Fake" ] && git_ensure_cleancheckout
ensure_branches
case "$SrcType" in
release) merge_release_action "$@";;
hotfix) merge_hotfix_action "$@";;
*) create_release_action "$@";;
esac
ensure_merge_branches
ensure_rel_infos
create_release_action "$@"
;;
*)
die "$action: action non implémentée"

383
bin/ptool Executable file
View File

@ -0,0 +1,383 @@
#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
source "$(dirname -- "$0")/../load.sh" || exit 1
require: git pman pman.conf
git_cleancheckout_DIRTY="\
Vous avez des modifications locales.
Enregistrez ces modifications avant de fusionner la branche"
function _merge_action() {
enote "\
Ce script va
- fusionner la branche ${COULEUR_BLEUE}$MergeSrc${COULEUR_NORMALE} dans ${COULEUR_ROUGE}$MergeDest${COULEUR_NORMALE}${Push:+
- pousser les branches modifiées}"
ask_yesno "Voulez-vous continuer?" O || die
local script=".git/pman-merge.sh"
local -a push_branches delete_branches
local hook
local comment=
local or_die=" || exit 1"
_mscript_start
_scripta <<EOF
################################################################################
# merge
if [ -n "\$merge" ]; then
esection "Fusionner la branche"
EOF
hook="BEFORE_MERGE_$MERGE_SRC"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_mscript_merge_branch
hook="AFTER_MERGE_$MERGE_SRC"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF
fi
EOF
if [ -n "$ShouldDelete" ]; then
_scripta <<EOF
################################################################################
# delete
if [ -n "\$delete" ]; then
esection "Supprimer la branche"
EOF
_mscript_delete_branch
hook="AFTER_DELETE_$MERGE_SRC"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF
fi
EOF
fi
_scripta <<EOF
################################################################################
# push
if [ -n "\$push" ]; then
esection "Pousser les branches"
EOF
hook="BEFORE_PUSH_$MERGE_DEST"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_script_push_branches
if [ ${#delete_branches[*]} -gt 0 ]; then
_scripta <<<"if [ -n \"\$delete\" ]; then"
push_branches=("${delete_branches[@]}")
_script_push_branches
_scripta <<<fi
fi
hook="AFTER_PUSH_$MERGE_DEST"; [ -n "${!hook}" ] && _scripta <<EOF
(
${!hook}
)$or_die
EOF
_scripta <<EOF
fi
EOF
[ -n "$Delete" -o -z "$ShouldDelete" ] && Deleted=1 || Deleted=
[ -n "$ShouldDelete" -a -n "$Delete" ] && ShouldDelete=
[ -n "$ShouldPush" -a -n "$Push" ] && ShouldPush=
if [ -n "$_Fake" ]; then
cat "$script"
elif ! "$script" merge ${Delete:+delete} ${Push:+push}; then
eimportant "\
Le script $script a été lancé avec les arguments 'merge${Delete:+ delete}${Push:+ push}'
En cas d'erreur de merge, veuillez corriger les erreurs puis continuer avec
git merge --continue
Sinon, veuillez consulter le script et/ou le relancer
./$script${Delete:+ delete}${Push:+ push}"
die
elif [ -n "$Deleted" -a -n "$Push" ]; then
[ -n "$_KeepScript" ] || rm "$script"
[ -n "$AfterMerge" ] && eval "$AfterMerge"
else
local msg="\
Le script $script a été lancé avec les arguments 'merge${Delete:+ delete}${Push:+ push}'
Vous pouvez consulter le script et/ou le relancer
./$script${ShouldDelete:+ delete}${ShouldPush:+ push}"
[ -n "$AfterMerge" ] && msg="$msg
Il y a aussi les commandes supplémentaires suivantes:
${AfterMerge//
/
}"
einfo "$msg"
fi
}
function merge_action() {
[ -n "$REF_UNIQUE" ] || resolve_unique_branch "$@"
ensure_merge_branches -a
if [ -n "$PREL_MERGE" ]; then
[ -n "$ForceMerge" ] || die "$MergeSrc: cette branche doit être fusionnée dans $MergeDest avec prel"
fi
if [ -n "$DELETE_MERGED" ]; then
ShouldDelete=1
[ -n "$AfterMerge" ] || setx AfterMerge=qvals git checkout -q "$MergeDest"
else
ShouldDelete=
Delete=
[ -n "$AfterMerge" ] || setx AfterMerge=qvals git checkout -q "$MergeSrc"
fi
[ -z "$_Fake" ] && git_ensure_cleancheckout
if ! array_contains LocalBranches "$MergeSrc" && array_contains AllBranches "$MergeSrc"; then
enote "$MergeSrc: une branche du même nom existe dans l'origine"
fi
if ! array_contains LocalBranches "$MergeDest" && array_contains AllBranches "$MergeDest"; then
enote "$MergeDest: une branche du même nom existe dans l'origine"
fi
array_contains LocalBranches "$MergeSrc" || die "$MergeSrc: branche locale introuvable"
array_contains LocalBranches "$MergeDest" || die "$MergeDest: branche locale introuvable"
resolve_should_push
_merge_action "$@"
}
function rebase_action() {
die "non implémenté"
}
################################################################################
chdir=
Origin=
ConfigBranch=
ConfigFile=
_Fake=
_KeepScript=
action=checkout
ShowLevel=0
TechMerge=
SquashMsg=
Push=1
Delete=1
AfterMerge=
loaded_config=
merge_dir=
if [ "$MYNAME" == ptool ]; then
if [ "$1" == --help ]; then
exit_with eecho "$MYNAME: gérer les branches d'un projet
USAGE
$MYNAME [-t|-f] REF args...
OPTIONS
REF
-f, --merge-from REF
spécifier la branche de référence et indiquer que la fusion se fait dans
le sens REF --> DEST. DEST est calculé en fonction de REF
-t, --merge-to REF
spécifier la branche de référence et indiquer que la fusion se fait dans
le sens SRC --> REF. SRC est calculé en fonction de REF"
fi
ref="$1"; shift
merge_dir=to
[ -n "$ref" ] || die "vous spécifier la branche de référence"
case "$ref" in
-f|--merge-from)
ref="$1"; shift
merge_dir=from
;;
-f*)
ref="${ref#-f}"
merge_dir=from
;;
-t|--merge-to)
ref="$1"; shift
merge_dir=to
;;
-t*)
ref="${ref#-t}"
merge_dir=to
;;
esac
REF_BRANCH="${ref^^}"
array_contains PMAN_BRANCHES "$REF_BRANCH" || die "$ref: invalid branch"
else
REF_BRANCH="PMAN_TOOL_${MYNAME^^}"; REF_BRANCH="${!REF_BRANCH}"
fi
if check_gitdir; then
load_branches all
load_config
set_pman_vars "$merge_dir"
load_branches current
loaded_config=1
else
set_pman_vars "$merge_dir"
fi
RefDesc=
MergeSrcDesc=
MergeDestDesc=
if [ -n "$REF_BRANCH" ]; then
RefDesc="${COULEUR_BLANCHE}<$REF_BRANCH>"
[ -n "$RefBranch" -a -n "$REF_UNIQUE" ] && RefDesc="$RefDesc ($RefBranch)"
RefDesc="$RefDesc${COULEUR_NORMALE}"
fi
if [ -n "$MERGE_SRC" ]; then
MergeSrcDesc="${COULEUR_BLEUE}<$MERGE_SRC>"
[ -n "$MergeSrc" -a -n "$REF_UNIQUE" ] && MergeSrcDesc="$MergeSrcDesc ($MergeSrc)"
MergeSrcDesc="$MergeSrcDesc${COULEUR_NORMALE}"
fi
if [ -n "$MERGE_DEST" ]; then
MergeDestDesc="${COULEUR_ROUGE}<$MERGE_DEST>"
[ -n "$MergeDest" -a -n "$REF_UNIQUE" ] && MergeDestDesc="$MergeDestDesc ($MergeDest)"
MergeDestDesc="$MergeDestDesc${COULEUR_NORMALE}"
fi
if [ -n "$REF_UNIQUE" ]
then purpose="gérer la branche $RefDesc"
else purpose="gérer les branches $RefDesc"
fi
usage="--checkout"
variables=
chdir_def=(chdir= "répertoire dans lequel se placer avant de lancer les opérations")
origin_def=(Origin= "++origine à partir de laquelle les branches distantes sont considérées")
config_branch_def=(ConfigBranch= "++branche à partir de laquelle charger la configuration")
config_file_def=(ConfigFile= "++\
fichier de configuration des branches. le fichier .pman.conf dans le répertoire
du dépôt est utilisé par défaut s'il existe. cette option est prioritaire sur
--config-branch")
fake_def=(_Fake=1 "++option non documentée")
keep_script_def=(_KeepScript=1 "++option non documentée")
dump_action_def=(action=dump "++afficher les noms des branches")
checkout_action_def=('$:' "++non applicable")
show_action_def=('$:' "++non applicable")
rebase_action_def=('$:' "++non applicable")
merge_action_def=('$:' "++non applicable")
tech_merge_def=('$:' "++non applicable")
squash_def=('$:' "++non applicable")
force_merge_def=('$:' "++non applicable")
no_push_def=('$:' "++non applicable")
push_def=('$:' "++non applicable")
no_delete_def=('$:' "++non applicable")
delete_def=('$:' "++non applicable")
after_merge_def=('$:' "++non applicable")
if [ -n "$RefBranch" -a -n "$REF_UNIQUE" ]; then
checkout_action_def=(action=checkout "++\
créer le cas échéant la branche $RefDesc et basculer vers elle.
c'est l'option par défaut")
elif [ -z "$REF_UNIQUE" ]; then
checkout_action_def=(action=checkout "\
créer le cas échéant la branche $RefDesc et basculer vers elle.
c'est l'option par défaut")
else
checkout_action_def=(action=checkout "\
créer la branche $MergeDestDesc et basculer vers elle.
c'est l'option par défaut")
fi
if [ -n "$MERGE_SRC" -a -n "$MERGE_DEST" ]; then
if [ -n "$REF_UNIQUE" ]
then usage="${usage}|--show|--merge"
else usage="${usage} $REF_BRANCH
--show|--merge"
fi
if [ "$REF_BRANCH" != "$MERGE_SRC" ]
then bewareDir="
NB: la fusion se fait dans le sens inverse"
else bewareDir=
fi
variables="Les variables supplémentaires suivantes peuvent être définies:
BEFORE_MERGE_${MERGE_SRC}
AFTER_MERGE_${MERGE_SRC}"
show_action_def=('$action=show; inc@ ShowLevel' "\
lister ce qui serait fusionné dans la branche $MergeDestDesc")
rebase_action_def=('$:' "++non implémenté")
# rebase_action_def=(action=rebase "\
#lancer git rebase -i sur la branche $MergeSrcDesc. cela permet de réordonner
#les commits pour nettoyer l'historique avant la fusion")
merge_action_def=(action=merge "\
fusionner la branche $MergeSrcDesc dans la branche $MergeDestDesc$bewareDir")
tech_merge_def=('$action=merge; TechMerge=1' "++option non documentée")
squash_def=('$action=merge; res@ SquashMsg' "fusionner les modifications de la branche comme un seul commit")
[ -n "$PREL_MERGE" ] && force_merge_def=(ForceMerge=1 "++\
forcer la fusion pour une branche qui devrait être traitée par prel")
no_push_def=(Push= "ne pas pousser les branches vers leur origine après la fusion")
push_def=(Push=1 "++\
pousser les branches vers leur origine après la fusion.
c'est l'option par défaut")
if [ -n "$DELETE_MERGED" ]; then
variables="${variables}
AFTER_DELETE_${MERGE_SRC}"
no_delete_def=(Delete= "\
ne pas supprimer la branche $MergeSrcDesc après la fusion dans la branche
$MergeDestDesc. cette option ne devrait pas être utilisée avec --squash")
delete_def=(Delete=1 "++\
supprimer la branche $MergeSrcDesc après la fusion dans la branche
$MergeDestDesc.
c'est l'option par défaut")
fi
[ -n "$MERGE_DEST" ] && variables="${variables}
BEFORE_PUSH_${MERGE_DEST}
AFTER_PUSH_${MERGE_DEST}"
after_merge_def=(AfterMerge= "évaluer le script spécifié après une fusion *réussie*")
fi
args=(
"$purpose"
"\
$usage
CONFIGURATION
Le fichier .pman.conf contient la configuration des branches.
$variables"
-d:,--chdir:BASEDIR "${chdir_def[@]}"
-O:,--origin "${origin_def[@]}"
-B:,--config-branch "${config_branch_def[@]}"
-c:,--config-file:CONFIG "${config_file_def[@]}"
--fake "${fake_def[@]}"
--keep-script "${keep_script_def[@]}"
--dump "${dump_action_def[@]}"
--checkout "${checkout_action_def[@]}"
-w,--show "${show_action_def[@]}"
-b,--rebase "${rebase_action_def[@]}"
-m,--merge "${merge_action_def[@]}"
--tech-merge "${tech_merge_def[@]}"
-s:,--squash:COMMIT_MSG "${squash_def[@]}"
-f,--force-merge "${force_merge_def[@]}"
-n,--no-push "${no_push_def[@]}"
--push "${push_def[@]}"
-k,--no-delete "${no_delete_def[@]}"
--delete "${delete_def[@]}"
-a:,--after-merge "${after_merge_def[@]}"
)
parse_args "$@"; set -- "${args[@]}"
if [ -z "$loaded_config" -o -n "$chdir" -o -n "$ConfigFile" -o -n "$ConfigBranch" ]; then
# charger la configuration
ensure_gitdir "$chdir"
load_branches all
load_config
set_pman_vars "$merge_dir"
load_branches current
fi
resolve_should_push quiet
"${action}_action" "$@"

View File

@ -1,60 +0,0 @@
#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
source "$(dirname -- "$0")/../load.sh" || exit 1
require: git pman pman.conf
git_cleancheckout_DIRTY="\
Vous avez des modifications locales.
Enregistrez ces modifications avant de créer une nouvelle branche"
chdir=
Origin=
ConfigBranch=
ConfigFile=
[ -z "$PMAN_NO_PUSH" ] && Push=1 || Push=
args=(
"créer une branche de feature"
"<feature>"
-d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
-O:,--origin Origin= "++\
origine à partir de laquelle les branches distantes sont considérées"
-B:,--config-branch ConfigBranch= "++\
branche à partir de laquelle charger la configuration"
-c:,--config-file:CONFIG ConfigFile= "++\
fichier de configuration des branches. cette option est prioritaire sur --config-branch
par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe"
-n,--no-push Push= "\
ne pas pousser les branches vers leur origine après la fusion"
--push Push=1 "++\
pousser les branches vers leur origine après la fusion.
c'est l'option par défaut"
)
parse_args "$@"; set -- "${args[@]}"
# charger la configuration
ensure_gitdir "$chdir"
load_branches all
load_config "$MYNAME"
load_branches current
branch="$1"
if [ -z "$branch" -a ${#FeatureBranches[*]} -eq 1 ]; then
branch="${FeatureBranches[0]}"
fi
[ -n "$branch" ] || die "Vous devez spécifier la branche à créer"
branch="$FEATURE${branch#$FEATURE}"
resolve_should_push
git_ensure_cleancheckout
if array_contains AllBranches "$branch"; then
git checkout -q "$branch"
else
# si la branche source n'existe pas, la créer
args=(--origin "$Origin")
if [ -n "$ConfigFile" ]; then args+=(--config-file "$ConfigFile")
elif [ -n "$ConfigBranch" ]; then args+=(--config-branch "$ConfigBranch")
fi
[ -z "$Push" ] && args+=(--no-push)
exec "$MYDIR/pman" "${args[@]}" "$branch"
fi

1
bin/pwip Symbolic link
View File

@ -0,0 +1 @@
ptool

View File

@ -19,6 +19,7 @@ while true; do
fi
cd ..
done
cd "$owd"
export RUNPHP_MOUNT=
if [ "$MYNAME" == composer ]; then

1
bin/sqlite.capacitor.php Symbolic link
View File

@ -0,0 +1 @@
runphp

1
bin/yml2json.php Symbolic link
View File

@ -0,0 +1 @@
runphp

View File

@ -18,7 +18,10 @@
"nulib/php": "*"
},
"require": {
"symfony/yaml": "^7.1",
"symfony/yaml": "^7.3",
"symfony/expression-language": "^7.3",
"phpmailer/phpmailer": "^6.8",
"league/commonmark": "^2.7",
"ext-json": "*",
"php": "^8.2"
},
@ -35,7 +38,8 @@
},
"autoload": {
"psr-4": {
"nulib\\": "php/src"
"nulib\\": "php/src",
"cli\\": "php/cli"
}
},
"autoload-dev": {
@ -43,6 +47,15 @@
"nulib\\": "php/tests"
}
},
"bin": [
"php/bin/cachectl.php",
"php/bin/dumpser.php",
"php/bin/json2yml.php",
"php/bin/yml2json.php",
"php/bin/sqlite.capacitor.php",
"php/bin/mysql.capacitor.php",
"php/bin/pgsql.capacitor.php"
],
"config": {
"vendor-dir": "php/vendor"
},

1404
composer.lock generated

File diff suppressed because it is too large Load Diff

7
php/bin/cachectl.php Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use cli\CachectlApp;
CachectlApp::run();

7
php/bin/dumpser.php Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use cli\DumpserApp;
DumpserApp::run();

7
php/bin/json2yml.php Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use cli\Json2yamlApp;
Json2yamlApp::run();

7
php/bin/mysql.capacitor.php Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use cli\MysqlCapacitorApp;
MysqlCapacitorApp::run();

7
php/bin/pgsql.capacitor.php Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use cli\PgsqlCapacitorApp;
PgsqlCapacitorApp::run();

7
php/bin/sqlite.capacitor.php Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use cli\SqliteCapacitorApp;
SqliteCapacitorApp::run();

7
php/bin/yml2json.php Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use cli\Yaml2jsonApp;
Yaml2jsonApp::run();

View File

@ -0,0 +1,111 @@
<?php
namespace cli;
use nulib\A;
use nulib\app\cli\Application;
use nulib\db\Capacitor;
use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage;
use nulib\ext\yaml;
use nulib\file\Stream;
use nulib\output\msg;
abstract class AbstractCapacitorApp extends Application {
const ACTION_RESET = 0, ACTION_QUERY = 1, ACTION_SQL = 2;
protected ?string $tableName = null;
protected ?string $channelClass = null;
protected int $action = self::ACTION_QUERY;
protected bool $recreate = true;
protected static function isa_cond(string $arg, ?array &$ms=null): bool {
return preg_match('/^(.+?)\s*(=|<>|<|>|<=|>=|(?:is\s+)?null|(?:is\s+)?not\s+null)\s*(.*)$/', $arg, $ms);
}
protected function storageCtl(CapacitorStorage $storage): void {
$args = $this->args;
$channelClass = $this->channelClass;
$tableName = $this->tableName;
if ($channelClass === null && $tableName === null) {
$name = A::shift($args);
if ($name !== null) {
if (!$storage->channelExists($name, $row)) {
self::die("$name: nom de canal de données introuvable");
}
if ($row["class_name"] !== "class@anonymous") $channelClass = $row["class_name"];
else $tableName = $row["table_name"];
}
}
if ($channelClass !== null) {
$channelClass = str_replace("/", "\\", $channelClass);
$channel = new $channelClass;
} elseif ($tableName !== null) {
$channel = new class($tableName) extends CapacitorChannel {
function __construct(?string $name=null) {
parent::__construct($name);
$this->tableName = $name;
}
};
} else {
$found = false;
foreach ($storage->getChannels() as $row) {
msg::print($row["name"]);
$found = true;
}
if ($found) self::exit();
self::die("Vous devez spécifier le canal de données");
}
$capacitor = new Capacitor($storage, $channel);
switch ($this->action) {
case self::ACTION_RESET:
$capacitor->reset($this->recreate);
break;
case self::ACTION_QUERY:
if (!$args) {
# lister les id
$out = new Stream(STDOUT);
$primaryKeys = $storage->getPrimaryKeys($channel);
$rows = $storage->db()->all([
"select",
"cols" => $primaryKeys,
"from" => $channel->getTableName(),
]);
$out->fputcsv($primaryKeys);
foreach ($rows as $row) {
$rowIds = $storage->getRowIds($channel, $row);
$out->fputcsv($rowIds);
}
} else {
# afficher les lignes correspondantes
if (count($args) == 1 && !self::isa_cond($args[0])) {
$filter = $args[0];
} else {
$filter = [];
$ms = null;
foreach ($args as $arg) {
if (self::isa_cond($arg, $ms)) {
$filter[$ms[1]] = [$ms[2], $ms[3]];
} else {
$filter[$arg] = ["not null"];
}
}
}
$first = true;
$capacitor->each($filter, function ($row) use (&$first) {
if ($first) $first = false;
else echo "---\n";
yaml::dump($row);
});
}
break;
case self::ACTION_SQL:
echo $capacitor->getCreateSql()."\n";
break;
}
}
}

122
php/cli/BgLauncherApp.php Normal file
View File

@ -0,0 +1,122 @@
<?php
namespace cli;
use nulib\app\app;
use nulib\app\cli\Application;
use nulib\app\RunFile;
use nulib\ExitError;
use nulib\ext\yaml;
use nulib\os\path;
use nulib\os\proc\Cmd;
use nulib\os\sh;
use nulib\output\msg;
class BgLauncherApp extends Application {
const ACTION_INFOS = 0, ACTION_START = 1, ACTION_STOP = 2;
const ARGS = [
"purpose" => "lancer un script en tâche de fond",
"usage" => "ApplicationClass args...",
"sections" => [
parent::VERBOSITY_SECTION,
],
["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS,
"help" => "Afficher des informations sur la tâche",
],
["-s", "--start", "name" => "action", "value" => self::ACTION_START,
"help" => "Démarrer la tâche",
],
["-k", "--stop", "name" => "action", "value" => self::ACTION_STOP,
"help" => "Arrêter la tâche",
],
];
protected int $action = self::ACTION_START;
static function show_infos(RunFile $runfile, ?int $level=null): void {
msg::print($runfile->getDesc(), $level);
msg::print(yaml::with(["data" => $runfile->read()]), ($level ?? 0) - 1);
}
function main() {
$args = $this->args;
$appClass = $args[0] ?? null;
if ($appClass === null) {
self::die("Vous devez spécifier la classe de l'application");
}
$appClass = $args[0] = str_replace("/", "\\", $appClass);
if (!class_exists($appClass)) {
self::die("$appClass: classe non trouvée");
}
$useRunfile = constant("$appClass::USE_RUNFILE");
if (!$useRunfile) {
self::die("Cette application ne supporte le lancement en tâche de fond");
}
$runfile = app::with($appClass)->getRunfile();
switch ($this->action) {
case self::ACTION_START:
$argc = count($args);
$appClass::_manage_runfile($argc, $args, $runfile);
if ($runfile->warnIfLocked()) self::exit(app::EC_LOCKED);
array_splice($args, 0, 0, [
PHP_BINARY,
path::abspath(NULIB_APP_app_launcher),
]);
app::params_putenv();
self::_start($args, $runfile);
break;
case self::ACTION_STOP:
self::_stop($runfile);
self::show_infos($runfile, -1);
break;
case self::ACTION_INFOS:
self::show_infos($runfile);
break;
}
}
public static function _start(array $args, Runfile $runfile): void {
$pid = pcntl_fork();
if ($pid == -1) {
# parent, impossible de forker
throw new ExitError(app::EC_FORK_PARENT, "Unable to fork");
} elseif (!$pid) {
# child, fork ok
$runfile->wfPrepare($pid);
$outfile = $runfile->getOutfile() ?? "/tmp/NULIB_APP_app_console.out";
$exitcode = app::EC_FORK_CHILD;
try {
# rediriger STDIN, STDOUT et STDERR
fclose(fopen($outfile, "wb")); // vider le fichier
fclose(STDIN); $in = fopen("/dev/null", "rb");
fclose(STDOUT); $out = fopen($outfile, "ab");
fclose(STDERR); $err = fopen($outfile, "ab");
# puis lancer la commande
$cmd = new Cmd($args);
$cmd->addSource("/g/init.env");
$cmd->addRedir("both", $outfile, true);
$cmd->fork_exec($exitcode, false);
sh::_waitpid(-$pid, $exitcode);
} finally {
$runfile->wfReaped($exitcode);
}
}
}
public static function _stop(Runfile $runfile): bool {
$data = $runfile->read();
$pid = $runfile->_getCid($data);
msg::action("stop $pid");
if ($runfile->wfKill($reason)) {
msg::asuccess();
return true;
} else {
msg::afailure($reason);
return false;
}
}
}

132
php/cli/CachectlApp.php Normal file
View File

@ -0,0 +1,132 @@
<?php
namespace cli;
use Exception;
use nulib\app\cli\Application;
use nulib\cache\CacheFile;
use nulib\ext\yaml;
use nulib\os\path;
use nulib\output\msg;
class CachectlApp extends Application {
const ACTION_READ = 10, ACTION_INFOS = 20, ACTION_CLEAN = 30;
const ACTION_UPDATE = 40, ACTION_UPDATE_ADD = 41, ACTION_UPDATE_SUB = 42, ACTION_UPDATE_SET = 43;
const ARGS = [
"merge" => parent::ARGS,
"purpose" => "gestion de fichiers cache",
["-r", "--read", "name" => "action", "value" => self::ACTION_READ,
"help" => "Afficher le contenu d'un fichier cache",
],
["-d::", "--data",
"help" => "Identifiant de la donnée à afficher",
],
["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS,
"help" => "Afficher des informations sur le fichier cache",
],
["-k", "--clean", "name" => "action", "value" => self::ACTION_CLEAN,
"help" => "Supprimer le fichier cache s'il a expiré",
],
["-a", "--add-duration", "args" => 1,
"action" => [null, "->setActionUpdate", self::ACTION_UPDATE_ADD],
"help" => "Ajouter le nombre de secondes spécifié à la durée du cache",
],
["-b", "--sub-duration", "args" => 1,
"action" => [null, "->setActionUpdate", self::ACTION_UPDATE_SUB],
"help" => "Enlever le nombre de secondes spécifié à la durée du cache",
],
#XXX pas encore implémenté
//["-s", "--set-duration", "args" => 1,
// "action" => [null, "->setActionUpdate", self::ACTION_UPDATE_SET],
// "help" => "Mettre à jour la durée du cache à la valeur spécifiée",
//],
];
protected $action = self::ACTION_READ;
protected $updateAction, $updateDuration;
protected $data = null;
function setActionUpdate(int $action, $updateDuration): void {
$this->action = self::ACTION_UPDATE;
switch ($action) {
case self::ACTION_UPDATE_SUB:
$this->updateAction = CacheFile::UPDATE_SUB;
break;
case self::ACTION_UPDATE_SET:
$this->updateAction = CacheFile::UPDATE_SET;
break;
case self::ACTION_UPDATE_ADD:
$this->updateAction = CacheFile::UPDATE_ADD;
break;
}
$this->updateDuration = $updateDuration;
}
protected function findCaches(string $dir, ?array &$files): void {
foreach (glob("$dir/*") as $file) {
if (is_dir($file)) {
$this->findCaches($file, $files);
} elseif (is_file($file) && fnmatch("*.cache", $file)) {
$files[] = $file;
}
}
}
function main() {
$files = [];
foreach ($this->args as $arg) {
if (is_dir($arg)) {
$this->findCaches($arg, $files);
} elseif (is_file($arg)) {
$files[] = $arg;
} else {
msg::warning("$arg: fichier introuvable");
}
}
$showSection = count($files) > 1;
foreach ($files as $file) {
switch ($this->action) {
case self::ACTION_READ:
if ($showSection) msg::section($file);
$cache = new CacheFile($file, null, [
"readonly" => true,
"duration" => "INF",
"override_duration" => true,
]);
yaml::dump($cache->get($this->data));
break;
case self::ACTION_INFOS:
if ($showSection) msg::section($file);
$cache = new CacheFile($file, null, [
"readonly" => true,
]);
yaml::dump($cache->getInfos());
break;
case self::ACTION_CLEAN:
msg::action(path::ppath($file));
$cache = new CacheFile($file);
try {
if ($cache->deleteExpired()) msg::asuccess("fichier supprimé");
else msg::adone("fichier non expiré");
} catch (Exception $e) {
msg::afailure($e);
}
break;
case self::ACTION_UPDATE:
msg::action(path::ppath($file));
$cache = new CacheFile($file);
try {
$cache->updateDuration($this->updateDuration, $this->updateAction);
msg::asuccess("fichier mis à jour");
} catch (Exception $e) {
msg::afailure($e);
}
break;
default:
self::die("$this->action: action non implémentée");
}
}
}
}

31
php/cli/DumpserApp.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace cli;
use nulib\app\cli\Application;
use nulib\ext\yaml;
use nulib\file\SharedFile;
use nulib\output\msg;
class DumpserApp extends Application {
const ARGS = [
"merge" => parent::ARGS,
"purpose" => "afficher des données sérialisées",
];
function main() {
$files = [];
foreach ($this->args as $arg) {
if (is_file($arg)) {
$files[] = $arg;
} else {
msg::warning("$arg: fichier invalide ou introuvable");
}
}
$showSection = count($files) > 1;
foreach ($files as $file) {
if ($showSection) msg::section($file);
$sfile = new SharedFile($file);
yaml::dump($sfile->unserialize());
}
}
}

21
php/cli/Json2yamlApp.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace cli;
use nulib\app\cli\Application;
use nulib\ext\json;
use nulib\ext\yaml;
use nulib\os\path;
class Json2yamlApp extends Application {
function main() {
$input = $this->args[0] ?? null;
if ($input === null || $input === "-") {
$output = null;
} else {
$output = path::ensure_ext($input, ".yml", ".json");
}
$data = json::load($input);
yaml::dump($data, $output);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace cli;
use nulib\A;
use nulib\app\config;
use nulib\db\mysql\MysqlStorage;
class MysqlCapacitorApp extends AbstractCapacitorApp {
const ARGS = [
"merge" => parent::ARGS,
"purpose" => "gestion d'un capacitor mysql",
"usage" => [
"DBCONN [channelName | -t table | -c ChannelClass] [--query] key=value...",
"DBCONN [channelName | -t table | -c ChannelClass] --sql-create",
],
["-t:table", "--table-name",
"help" => "nom de la table porteuse du canal de données",
],
["-c:class", "--channel-class",
"help" => "nom de la classe dérivée de CapacitorChannel",
],
["-z", "--reset", "name" => "action", "value" => self::ACTION_RESET,
"help" => "réinitialiser le canal",
],
["-n", "--no-recreate", "name" => "recreate", "value" => false,
"help" => "ne pas recréer la table correspondant au canal"
],
["--query", "name" => "action", "value" => self::ACTION_QUERY,
"help" => "lister les lignes correspondant aux valeurs spécifiées. c'est l'action par défaut",
],
["-s", "--sql-create", "name" => "action", "value" => self::ACTION_SQL,
"help" => "afficher la requête pour créer la table",
],
];
function main() {
$dbconn = A::shift($this->args);
if ($dbconn === null) self::die("Vous devez spécifier la base de données");
$tmp = config::db($dbconn);
if ($tmp === null) self::die("$dbconn: base de données invalide");
$storage = new MysqlStorage($tmp);
$this->storageCtl($storage);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace cli;
use nulib\A;
use nulib\app\config;
use nulib\db\pgsql\PgsqlStorage;
class PgsqlCapacitorApp extends AbstractCapacitorApp {
const ARGS = [
"merge" => parent::ARGS,
"purpose" => "gestion d'un capacitor pgsql",
"usage" => [
"DBCONN [channelName | -t table | -c ChannelClass] [--query] key=value...",
"DBCONN [channelName | -t table | -c ChannelClass] --sql-create",
],
["-t:table", "--table-name",
"help" => "nom de la table porteuse du canal de données",
],
["-c:class", "--channel-class",
"help" => "nom de la classe dérivée de CapacitorChannel",
],
["-z", "--reset", "name" => "action", "value" => self::ACTION_RESET,
"help" => "réinitialiser le canal",
],
["-n", "--no-recreate", "name" => "recreate", "value" => false,
"help" => "ne pas recréer la table correspondant au canal"
],
["--query", "name" => "action", "value" => self::ACTION_QUERY,
"help" => "lister les lignes correspondant aux valeurs spécifiées. c'est l'action par défaut",
],
["-s", "--sql-create", "name" => "action", "value" => self::ACTION_SQL,
"help" => "afficher la requête pour créer la table",
],
];
function main() {
$dbconn = A::shift($this->args);
if ($dbconn === null) self::die("Vous devez spécifier la base de données");
$tmp = config::db($dbconn);
if ($tmp === null) self::die("$dbconn: base de données invalide");
$storage = new PgsqlStorage($tmp);
$this->storageCtl($storage);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace cli;
use nulib\A;
use nulib\db\sqlite\SqliteStorage;
class SqliteCapacitorApp extends AbstractCapacitorApp {
const ARGS = [
"merge" => parent::ARGS,
"purpose" => "gestion d'un capacitor sqlite",
"usage" => [
"DBFILE [channelName | -t table | -c ChannelClass] [--query] key=value...",
"DBFILE [channelName | -t table | -c ChannelClass] --sql-create",
],
["-t:table", "--table-name",
"help" => "nom de la table porteuse du canal de données",
],
["-c:class", "--channel-class",
"help" => "nom de la classe dérivée de CapacitorChannel",
],
["-z", "--reset", "name" => "action", "value" => self::ACTION_RESET,
"help" => "réinitialiser le canal",
],
["-n", "--no-recreate", "name" => "recreate", "value" => false,
"help" => "ne pas recréer la table correspondant au canal"
],
["--query", "name" => "action", "value" => self::ACTION_QUERY,
"help" => "lister les lignes correspondant aux valeurs spécifiées. c'est l'action par défaut",
],
["-s", "--sql-create", "name" => "action", "value" => self::ACTION_SQL,
"help" => "afficher la requête pour créer la table",
],
];
function main() {
$dbfile = A::shift($this->args);
if ($dbfile === null) self::die("Vous devez spécifier la base de données");
if (!file_exists($dbfile)) self::die("$dbfile: fichier introuvable");
$storage = new SqliteStorage($dbfile);
$this->storageCtl($storage);
}
}

21
php/cli/Yaml2jsonApp.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace cli;
use nulib\app\cli\Application;
use nulib\ext\json;
use nulib\ext\yaml;
use nulib\os\path;
class Yaml2jsonApp extends Application {
function main() {
$input = $this->args[0] ?? null;
if ($input === null || $input === "-") {
$output = null;
} else {
$output = path::ensure_ext($input, ".json", [".yml", ".yaml"]);
}
$data = yaml::load($input);
json::dump($data, $output);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace cli;
use nulib\app\app;
use nulib\app\cli\Application;
use nulib\output\msg;
use nulib\php\time\DateTime;
use nulib\text\words;
class _SteamTrainApp extends Application {
const PROJDIR = __DIR__.'/../..';
const TITLE = "Train à vapeur";
const USE_LOGFILE = true;
const USE_RUNFILE = true;
const USE_RUNLOCK = true;
const ARGS = [
"purpose" => self::TITLE,
"description" => <<<EOT
Cette application peut être utilisée pour tester le lancement des tâches de fond
EOT,
["-c:count", "--count",
"help" => "spécifier le nombre d'étapes",
],
["-f", "--force-enabled", "value" => true,
"help" => "lancer la commande même si les tâches planifiées sont désactivées",
],
["-n", "--no-install-signal-handler", "value" => false,
"help" => "ne pas installer le gestionnaire de signaux",
],
];
protected $count = 100;
protected bool $forceEnabled = false;
protected bool $installSignalHandler = true;
function main() {
app::check_bgapplication_enabled($this->forceEnabled);
if ($this->installSignalHandler) app::install_signal_handler();
$count = intval($this->count);
msg::info("Starting train for ".words::q($count, "step#s"));
app::action("Running train...", $count);
for ($i = 1; $i <= $count; $i++) {
msg::print("Tchou-tchou! x $i");
app::step();
sleep(1);
}
msg::info("Stopping train at ".new DateTime());
}
}

View File

@ -1,18 +1,17 @@
<?php
namespace nulib\tools\pman;
namespace cli\pman;
use nulib\cl;
use nulib\exceptions;
use nulib\ext\json;
use nulib\file;
use nulib\os\path;
use nulib\ValueException;
class ComposerFile {
function __construct(string $composerFile=".", bool $ensureExists=true) {
if (is_dir($composerFile)) $composerFile = path::join($composerFile, 'composer.json');
if ($ensureExists && !file_exists($composerFile)) {
$message = path::ppath($composerFile).": fichier introuvable";
throw new ValueException($message);
throw exceptions::invalid_value(path::ppath($composerFile), "ce fichier", "il est introuvable");
}
$this->composerFile = $composerFile;
$this->load();

View File

@ -1,11 +1,11 @@
<?php
namespace nulib\tools\pman;
namespace cli\pman;
use nulib\A;
use nulib\exceptions;
use nulib\ext\yaml;
use nulib\os\path;
use nulib\str;
use nulib\ValueException;
class ComposerPmanFile {
const NAMES = [".composer.pman", ".pman"];
@ -29,8 +29,7 @@ class ComposerPmanFile {
}
}
if ($ensureExists && !file_exists($configFile)) {
$message = path::ppath($configFile).": fichier introuvable";
throw new ValueException($message);
throw exceptions::invalid_value(path::ppath($configFile), "ce fichier", "il est introuvable");
}
$this->configFile = $configFile;
$this->load();
@ -66,9 +65,7 @@ class ComposerPmanFile {
function getProfileConfig(string $profile, ?array $composerRequires=null, ?array $composerRequireDevs=null): array {
$config = $this->data["composer"][$profile] ?? null;
if ($config === null) {
throw new ValueException("$profile: profil invalide");
}
if ($config === null) throw exceptions::invalid_value($profile, "ce profil");
if ($composerRequires !== null) {
$matchRequires = $this->data["composer"]["match_require"];
foreach ($composerRequires as $dep => $version) {

View File

@ -1,5 +1,5 @@
#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
MYDIR="$(dirname -- "$0")"
VENDOR="$MYDIR/../vendor"
VENDOR="$MYDIR/vendor"
"$VENDOR/bin/phpunit" --bootstrap "$VENDOR/autoload.php" "$@" "$MYDIR/tests"

View File

@ -1,7 +1,6 @@
<?php
namespace nulib;
use nulib\php\func;
use Traversable;
/**

View File

@ -1,36 +1,38 @@
<?php
namespace nulib;
use RuntimeException;
/**
* Class AccessException: indiquer que la resource ou l'objet auquel on veut
* accéder n'est pas accessible. il s'agit donc d'une erreur de l'utilisateur
*/
class AccessException extends UserException {
class AccessException extends RuntimeException {
static final function read_only(?string $dest=null, ?string $prefix=null): self {
if ($prefix) $prefix = "$prefix: ";
if ($dest === null) $dest = "this property";
$message = "$dest is read-only";
return new static($prefix.$message);
return new static("$prefix$message");
}
static final function immutable_object(?string $dest=null, ?string $prefix=null): self {
if ($prefix) $prefix = "$prefix: ";
if ($dest === null) $dest = "this object";
$message = "$dest is immutable";
return new static($prefix.$message);
return new static("$prefix$message");
}
static final function not_allowed(?string $action=null, ?string $prefix=null): self {
if ($prefix) $prefix = "$prefix: ";
if ($action === null) $action = "this operation";
$message = "$action is not allowed";
return new static($prefix.$message);
return new static("$prefix$message");
}
static final function not_accessible(?string $dest=null, ?string $prefix=null): self {
if ($prefix) $prefix = "$prefix: ";
if ($dest === null) $dest = "this resource";
$message = "$dest is not accessible";
return new static($prefix.$message);
return new static("$prefix$message");
}
}

View File

@ -38,17 +38,22 @@ class ExceptionShadow {
$this->trace = self::extract_trace($exception->getTrace());
$previous = $exception->getPrevious();
if ($previous !== null) $this->previous = new static($previous);
if ($exception instanceof UserException) {
$this->userMessage = $exception->getUserMessage();
$this->techMessage = $exception->getTechMessage();
} else {
$this->userMessage = null;
$this->techMessage = null;
}
}
/** @var string */
protected $class;
protected string $class;
function getClass(): string {
return $this->class;
}
/** @var string */
protected $message;
protected string $message;
function getMessage(): string {
return $this->message;
@ -61,22 +66,19 @@ class ExceptionShadow {
return $this->code;
}
/** @var string */
protected $file;
protected string $file;
function getFile(): string {
return $this->file;
}
/** @var int */
protected $line;
protected int $line;
function getLine(): int {
return $this->line;
}
/** @var array */
protected $trace;
protected array $trace;
function getTrace(): array {
return $this->trace;
@ -92,10 +94,21 @@ class ExceptionShadow {
return implode("\n", $lines);
}
/** @var ExceptionShadow */
protected $previous;
protected ?ExceptionShadow $previous;
function getPrevious(): ?ExceptionShadow {
return $this->previous;
}
protected ?array $userMessage;
function getUserMessage(): ?array {
return $this->userMessage;
}
protected ?array $techMessage;
function getTechMessage(): ?array {
return $this->techMessage;
}
}

View File

@ -18,8 +18,7 @@ class ExitError extends Error {
return $this->getCode() !== 0;
}
/** @var ?string */
protected $userMessage;
protected ?string $userMessage;
function haveUserMessage(): bool {
return $this->userMessage !== null;

View File

@ -12,12 +12,12 @@ class StateException extends LogicException {
if ($method === null) $method = "this method";
$message = "$method is not implemented";
if ($prefix) $prefix = "$prefix: ";
return new static($prefix.$message);
return new static("$prefix$message");
}
static final function unexpected_state(?string $suffix=null): self {
$message = "unexpected state";
if ($suffix) $suffix = ": $suffix";
return new static($message.$suffix);
return new static("$message$suffix");
}
}

View File

@ -1,90 +1,35 @@
<?php
namespace nulib;
use nulib\php\content\c;
use RuntimeException;
use Throwable;
/**
* Class UserException: une exception qui peut en plus contenir un message
* utilisateur
* Class UserException: une exception qui peut contenir un message utilisateur
* et un message technique
*/
class UserException extends RuntimeException {
/** @param Throwable|ExceptionShadow $e */
static function get_user_message($e): ?string {
if ($e instanceof self) return $e->getUserMessage();
else return null;
function __construct($userMessage, $code=0, ?Throwable $previous=null) {
$this->userMessage = $userMessage = c::resolve($userMessage);
parent::__construct(c::to_string($userMessage), $code, $previous);
}
/** @param Throwable|ExceptionShadow $e */
static final function get_user_summary($e): string {
$parts = [];
$first = true;
while ($e !== null) {
$message = self::get_user_message($e);
if (!$message) $message = "(no message)";
if ($first) $first = false;
else $parts[] = "caused by ";
$parts[] = get_class($e) . ": " . $message;
$e = $e->getPrevious();
}
return implode(", ", $parts);
}
protected ?array $userMessage;
/** @param Throwable|ExceptionShadow $e */
static function get_message($e): ?string {
$message = $e->getMessage();
if (!$message && $e instanceof self) $message = $e->getUserMessage();
return $message;
}
/** @param Throwable|ExceptionShadow $e */
static final function get_summary($e): string {
$parts = [];
$first = true;
while ($e !== null) {
$message = self::get_message($e);
if (!$message) $message = "(no message)";
if ($first) $first = false;
else $parts[] = "caused by ";
if ($e instanceof ExceptionShadow) $class = $e->getClass();
else $class = get_class($e);
$parts[] = "$class: $message";
$e = $e->getPrevious();
}
return implode(", ", $parts);
}
/** @param Throwable|ExceptionShadow $e */
static final function get_traceback($e): string {
$tbs = [];
$previous = false;
while ($e !== null) {
if (!$previous) {
$efile = $e->getFile();
$eline = $e->getLine();
$tbs[] = "at $efile($eline)";
} else {
$tbs[] = "~~ caused by: " . self::get_summary($e);
}
$tbs[] = $e->getTraceAsString();
$e = $e->getPrevious();
$previous = true;
#XXX il faudrait ne pas réinclure les lignes communes aux exceptions qui
# ont déjà été affichées
}
return implode("\n", $tbs);
}
function __construct($userMessage, $techMessage=null, $code=0, ?Throwable $previous=null) {
$this->userMessage = $userMessage;
if ($techMessage === null) $techMessage = $userMessage;
parent::__construct($techMessage, $code, $previous);
}
/** @var ?string */
protected $userMessage;
function getUserMessage(): ?string {
function getUserMessage(): ?array {
return $this->userMessage;
}
protected ?array $techMessage = null;
function getTechMessage(): ?array {
return $this->techMessage;
}
function setTechMessage($techMessage): self {
if ($techMessage !== null) $techMessage = c::resolve($techMessage);
$this->techMessage = $techMessage;
return $this;
}
}

View File

@ -5,72 +5,4 @@ namespace nulib;
* Class ValueException: indiquer qu'une valeur est invalide
*/
class ValueException extends UserException {
private static function value($value): string {
if (is_object($value)) {
return "<".get_class($value).">";
} elseif (is_array($value)) {
$values = $value;
$parts = [];
$index = 0;
foreach ($values as $key => $value) {
if ($key === $index) {
$index++;
$parts[] = self::value($value);
} else {
$parts[] = "$key=>".self::value($value);
}
}
return "[" . implode(", ", $parts) . "]";
} elseif (is_string($value)) {
return $value;
} else {
return var_export($value, true);
}
}
private static function message($value, ?string $message, ?string $kind, ?string $prefix, ?string $suffix): string {
if ($kind === null) $kind = "value";
if ($message === null) $message = "$kind$suffix";
if ($value !== null) {
$value = self::value($value);
if ($prefix) $prefix = "$prefix: $value";
else $prefix = $value;
}
if ($prefix) $prefix = "$prefix: ";
return $prefix.$message;
}
static final function null(?string $kind=null, ?string $prefix=null, ?string $message=null): self {
return new static(self::message(null, $message, $kind, $prefix, " should not be null"));
}
static final function check_null($value, ?string $kind=null, ?string $prefix=null, ?string $message=null) {
if ($value === null) throw static::null($kind, $prefix, $message);
return $value;
}
static final function invalid_kind($value=null, ?string $kind=null, ?string $prefix=null, ?string $message=null): self {
return new static(self::message($value, $message, $kind, $prefix, " is invalid"));
}
static final function invalid_key($value, ?string $prefix=null, ?string $message=null): self {
return self::invalid_kind($value, "key", $prefix, $message);
}
static final function invalid_value($value, ?string $prefix=null, ?string $message=null): self {
return self::invalid_kind($value, "value", $prefix, $message);
}
static final function invalid_type($value, string $expected_type): self {
return new static(self::message($value, null, "type", null, " is invalid, expected $expected_type"));
}
static final function invalid_class($class, string $expected_class): self {
if (is_object($class)) $class = get_class($class);
return new static(self::message($class, null, "class", null, " is invalid, expected $expected_class"));
}
static final function forbidden($value=null, ?string $kind=null, ?string $prefix=null, ?string $message=null): self {
return new static(self::message($value, $message, $kind, $prefix, " is forbidden"));
}
}

View File

@ -2,7 +2,6 @@
namespace nulib\app;
use nulib\A;
use nulib\app;
use nulib\cl;
use nulib\file\SharedFile;
use nulib\os\path;

View File

@ -1,8 +1,5 @@
# nulib\app
* [ ] ajouter des méthodes normalisées `app::get_cachedir()` et
`app::get_cachefile($name)` avec la valeur par défaut
`cachedir = $vardir/cache`
* [ ] `app::action()` et `app::step()` appellent automatiquement
`app::_dispatch_signals()`

655
php/src/app/app.php Normal file
View File

@ -0,0 +1,655 @@
<?php
namespace nulib\app;
use nulib\A;
use nulib\app\cli\Application;
use nulib\app\config\ProfileManager;
use nulib\cl;
use nulib\exceptions;
use nulib\ExitError;
use nulib\os\path;
use nulib\os\sh;
use nulib\php\func;
use nulib\ref\ref_profiles;
use nulib\str;
class app {
private static function isa_Application($app): bool {
if (!is_string($app)) return false;
#XXX support legacy
$legacyApplication = 'nur\cli\Application';
if ($app === $legacyApplication || is_subclass_of($app, $legacyApplication)) return true;
return $app === Application::class
|| is_subclass_of($app, Application::class);
}
private static function get_params($app): array {
if ($app instanceof self) {
$params = $app->getParams();
} elseif ($app instanceof Application) {
$class = get_class($app);
$params = [
"class" => $class,
"projdir" => $app::PROJDIR,
"vendor" => $app::VENDOR,
"projcode" => $app::PROJCODE,
"datadir" => $app::DATADIR,
"etcdir" => $app::ETCDIR,
"vardir" => $app::VARDIR,
"cachedir" => $app::CACHEDIR,
"logdir" => $app::LOGDIR,
"appgroup" => $app::APPGROUP,
"name" => $app::NAME,
"title" => $app::TITLE,
];
} elseif (self::isa_Application($app)) {
$class = $app;
$params = [
"class" => $class,
"projdir" => constant("$app::PROJDIR"),
"vendor" => constant("$app::VENDOR"),
"projcode" => constant("$app::PROJCODE"),
"datadir" => constant("$app::DATADIR"),
"etcdir" => constant("$app::ETCDIR"),
"vardir" => constant("$app::VARDIR"),
"cachedir" => constant("$app::CACHEDIR"),
"logdir" => constant("$app::LOGDIR"),
"appgroup" => constant("$app::APPGROUP"),
"name" => constant("$app::NAME"),
"title" => constant("$app::TITLE"),
];
} elseif (is_array($app)) {
$params = $app;
} else {
throw exceptions::invalid_type($app, "app", Application::class);
}
return $params;
}
protected static ?self $app = null;
/**
* @param Application|string|array $app
* @param Application|string|array|null $proj
*/
static function with($app, $proj=null): self {
$params = self::get_params($app);
$proj ??= self::params_getenv();
$proj ??= self::$app;
$proj_params = $proj !== null? self::get_params($proj): null;
if ($proj_params !== null) {
A::merge($params, cl::select($proj_params, [
"projdir",
"vendor",
"projcode",
"cwd",
"datadir",
"etcdir",
"vardir",
"cachedir",
"logdir",
"profile",
"facts",
"debug",
]));
}
return new static($params, $proj_params !== null);
}
static function init($app, $proj=null): void {
self::$app = static::with($app, $proj);
}
static function get(): self {
return self::$app ??= new static(null);
}
static function params_putenv(): void {
$params = serialize(self::get()->getParams());
putenv("NULIB_APP_app_params=$params");
}
static function params_getenv(): ?array {
$params = getenv("NULIB_APP_app_params");
if ($params === false) return null;
return unserialize($params);
}
static function get_profile(?bool &$productionMode=null): string {
return self::get()->getProfile($productionMode);
}
static function is_production_mode(): bool {
return self::get()->isProductionMode();
}
static function is_prod(): bool {
return self::get_profile() === ref_profiles::PROD;
}
static function is_test(): bool {
return self::get_profile() === ref_profiles::TEST;
}
static function is_devel(): bool {
return self::get_profile() === ref_profiles::DEVEL;
}
static function set_profile(?string $profile=null, ?bool $productionMode=null): void {
self::get()->setProfile($profile, $productionMode);
}
const FACT_WEB_APP = "web-app";
const FACT_CLI_APP = "cli-app";
static final function is_fact(string $fact, $value=true): bool {
return self::get()->isFact($fact, $value);
}
static final function set_fact(string $fact, $value=true): void {
self::get()->setFact($fact, $value);
}
static function is_debug(): bool {
return self::get()->isDebug();
}
static function set_debug(?bool $debug=true): void {
self::get()->setDebug($debug);
}
/**
* @var array répertoires vendor exprimés relativement à PROJDIR
*/
const DEFAULT_VENDOR = [
"bindir" => "vendor/bin",
"autoload" => "vendor/autoload.php",
];
function __construct(?array $params, bool $useProjParams=false) {
if ($useProjParams) {
[
"projdir" => $projdir,
"vendor" => $vendor,
"projcode" => $projcode,
"datadir" => $datadir,
"etcdir" => $etcdir,
"vardir" => $vardir,
"cachedir" => $cachedir,
"logdir" => $logdir,
] = $params;
$cwd = $params["cwd"] ?? null;
$datadirIsDefined = true;
} else {
# projdir
$projdir = $params["projdir"] ?? null;
if ($projdir === null) {
global $_composer_autoload_path, $_composer_bin_dir;
$autoload = $_composer_autoload_path ?? null;
$bindir = $_composer_bin_dir ?? null;
if ($autoload !== null) {
$vendor = preg_replace('/\/[^\/]+\.php$/', "", $autoload);
$bindir ??= "$vendor/bin";
$projdir = preg_replace('/\/[^\/]+$/', "", $vendor);
$params["vendor"] = [
"autoload" => $autoload,
"bindir" => $bindir,
];
}
}
if ($projdir === null) $projdir = ".";
$projdir = path::abspath($projdir);
# vendor
$vendor = $params["vendor"] ?? self::DEFAULT_VENDOR;
$vendor["bindir"] = path::reljoin($projdir, $vendor["bindir"]);
$vendor["autoload"] = path::reljoin($projdir, $vendor["autoload"]);
# projcode
$projcode = $params["projcode"] ?? null;
if ($projcode === null) {
$projcode = str::without_suffix("-app", path::basename($projdir));
}
$PROJCODE = str_replace("-", "_", strtoupper($projcode));
# cwd
$cwd = $params["cwd"] ?? null;
# datadir
$datadir = getenv("${PROJCODE}_DATADIR");
$datadirIsDefined = $datadir !== false;
if ($datadir === false) $datadir = $params["datadir"] ?? null;
if ($datadir === null) $datadir = "devel";
$datadir = path::reljoin($projdir, $datadir);
# etcdir
$etcdir = getenv("${PROJCODE}_ETCDIR");
if ($etcdir === false) $etcdir = $params["etcdir"] ?? null;
if ($etcdir === null) $etcdir = "etc";
$etcdir = path::reljoin($datadir, $etcdir);
# vardir
$vardir = getenv("${PROJCODE}_VARDIR");
if ($vardir === false) $vardir = $params["vardir"] ?? null;
if ($vardir === null) $vardir = "var";
$vardir = path::reljoin($datadir, $vardir);
# cachedir
$cachedir = getenv("${PROJCODE}_CACHEDIR");
if ($cachedir === false) $cachedir = $params["cachedir"] ?? null;
if ($cachedir === null) $cachedir = "cache";
$cachedir = path::reljoin($vardir, $cachedir);
# logdir
$logdir = getenv("${PROJCODE}_LOGDIR");
if ($logdir === false) $logdir = $params["logdir"] ?? null;
if ($logdir === null) $logdir = "log";
$logdir = path::reljoin($datadir, $logdir);
}
# cwd
$cwd ??= getcwd();
# profile
$this->profileManager = new ProfileManager([
"app" => true,
"name" => $projcode,
"default_profile" => $datadirIsDefined? "prod": "devel",
"profile" => $params["profile"] ?? null,
]);
# $facts
$this->facts = $params["facts"] ?? null;
# debug
$this->debug = $params["debug"] ?? null;
$this->projdir = $projdir;
$this->vendor = $vendor;
$this->projcode = $projcode;
$this->cwd = $cwd;
$this->datadir = $datadir;
$this->etcdir = $etcdir;
$this->vardir = $vardir;
$this->cachedir = $cachedir;
$this->logdir = $logdir;
# name, title
$appgroup = $params["appgroup"] ?? null;
$name = $params["name"] ?? $params["class"] ?? null;
if ($name === null) {
$name = $projcode;
} else {
# si $name est une classe, enlever le package et normaliser i.e
# my\package\MyApplication --> my-application.php
$name = preg_replace('/.*\\\\/', "", $name);
$name = str::camel2us($name, false, "-");
$name = str::without_suffix("-app", $name);
}
$this->appgroup = $appgroup;
$this->name = $name;
$this->title = $params["title"] ?? null;
}
#############################################################################
# Paramètres partagés par tous les scripts d'un projet (et les scripts lancés
# à partir d'une application de ce projet)
protected string $projdir;
function getProjdir(): string {
return $this->projdir;
}
protected array $vendor;
function getVendorBindir(): string {
return $this->vendor["bindir"];
}
function getVendorAutoload(): string {
return $this->vendor["autoload"];
}
protected string $projcode;
function getProjcode(): string {
return $this->projcode;
}
protected string $cwd;
function getCwd(): string {
return $this->cwd;
}
protected string $datadir;
function getDatadir(): string {
return $this->datadir;
}
protected string $etcdir;
function getEtcdir(): string {
return $this->etcdir;
}
protected string $vardir;
function getVardir(): string {
return $this->vardir;
}
protected string $cachedir;
function getCachedir(): string {
return $this->cachedir;
}
protected string $logdir;
function getLogdir(): string {
return $this->logdir;
}
protected ProfileManager $profileManager;
function getProfile(?bool &$productionMode=null): string {
return $this->profileManager->getProfile($productionMode);
}
function isProductionMode(): bool {
return $this->profileManager->isProductionMode();
}
function setProfile(?string $profile, ?bool $productionMode=null): void {
$this->profileManager->setProfile($profile, $productionMode);
}
protected ?array $facts;
function isFact(string $fact, $value=true): bool {
return ($this->facts[$fact] ?? false) === $value;
}
function setFact(string $fact, $value=true): void {
$this->facts[$fact] = $value;
}
protected ?bool $debug;
function isDebug(): bool {
$debug = $this->debug;
if ($debug === null) {
$debug = defined("DEBUG")? DEBUG: null;
$DEBUG = getenv("DEBUG");
$debug ??= $DEBUG !== false? $DEBUG: null;
$debug ??= config::k("debug");
$debug ??= false;
$this->debug = $debug;
}
return $debug;
}
function setDebug(bool $debug=true): void {
$this->debug = $debug;
}
/**
* @param ?string|false $profile
*
* false === pas de profil
* null === profil par défaut
*/
function withProfile(string $file, $profile): string {
if ($profile !== false) {
$profile ??= $this->getProfile();
[$dir, $filename] = path::split($file);
$basename = path::basename($filename);
$ext = path::ext($file);
$file = path::join($dir, "$basename.$profile$ext");
}
return $file;
}
function findFile(array $dirs, array $names, $profile=null): string {
# d'abord chercher avec le profil
if ($profile !== false) {
foreach ($dirs as $dir) {
foreach ($names as $name) {
$file = path::join($dir, $name);
$file = $this->withProfile($file, $profile);
if (file_exists($file)) return $file;
}
}
}
# puis sans profil
foreach ($dirs as $dir) {
foreach ($names as $name) {
$file = path::join($dir, $name);
if (file_exists($file)) return $file;
}
}
# la valeur par défaut est avec profil
return $this->withProfile(path::join($dirs[0], $names[0]), $profile);
}
function fencedJoin(string $basedir, ?string ...$paths): string {
$path = path::reljoin($basedir, ...$paths);
if (!path::is_within($path, $basedir)) {
throw exceptions::invalid_value($path, "path");
}
return $path;
}
#############################################################################
# Paramètres spécifiques à cette application
protected ?string $appgroup;
function getAppgroup(): ?string {
return $this->appgroup;
}
protected string $name;
function getName(): ?string {
return $this->name;
}
protected ?string $title;
function getTitle(): ?string {
return $this->title;
}
#############################################################################
# Méthodes outils
/** recréer le tableau des paramètres */
function getParams(): array {
return [
"projdir" => $this->projdir,
"vendor" => $this->vendor,
"projcode" => $this->projcode,
"cwd" => $this->cwd,
"datadir" => $this->datadir,
"etcdir" => $this->etcdir,
"vardir" => $this->vardir,
"cachedir" => $this->cachedir,
"logdir" => $this->logdir,
"profile" => $this->getProfile(),
"facts" => $this->facts,
"debug" => $this->debug,
"appgroup" => $this->appgroup,
"name" => $this->name,
"title" => $this->title,
];
}
/**
* obtenir le chemin vers le fichier de configuration. par défaut, retourner
* une valeur de la forme "$ETCDIR/$name[.$profile].conf"
*/
function getEtcfile(?string $name=null, $profile=null): string {
$name ??= "{$this->name}.conf";
return $this->findFile([$this->etcdir], [$name], $profile);
}
/**
* obtenir le chemin vers le fichier de travail. par défaut, retourner une
* valeur de la forme "$VARDIR/$appgroup/$name[.$profile].tmp"
*/
function getVarfile(?string $name=null, $profile=null): string {
$name ??= "{$this->name}.tmp";
$file = $this->fencedJoin($this->vardir, $this->appgroup, $name);
$file = $this->withProfile($file, $profile);
sh::mkdirof($file);
return $file;
}
/**
* obtenir le chemin vers le fichier de cache. par défaut, retourner une
* valeur de la forme "$CACHEDIR/$appgroup/$name[.$profile].cache"
*/
function getCachefile(?string $name=null, $profile=null): string {
$name ??= "{$this->name}.cache";
$file = $this->fencedJoin($this->cachedir, $this->appgroup, $name);
$file = $this->withProfile($file, $profile);
sh::mkdirof($file);
return $file;
}
/**
* obtenir le chemin vers le fichier de log. par défaut, retourner une
* valeur de la forme "$LOGDIR/$appgroup/$name.log" (sans le profil, parce
* qu'il s'agit du fichier de log par défaut)
*
* Si $name est spécifié, la valeur retournée sera de la forme
* "$LOGDIR/$appgroup/$basename[.$profile].$ext"
*/
function getLogfile(?string $name=null, $profile=null): string {
if ($name === null) {
$name = "{$this->name}.log";
$profile ??= false;
}
$logfile = $this->fencedJoin($this->logdir, $this->appgroup, $name);
$logfile = $this->withProfile($logfile, $profile);
sh::mkdirof($logfile);
return $logfile;
}
/**
* obtenir le chemin absolu vers un fichier de travail
* - si le chemin est absolu, il est inchangé
* - sinon le chemin est exprimé par rapport à $vardir/$appgroup
*
* is $ensureDir, créer le répertoire du fichier s'il n'existe pas déjà
*
* la différence avec {@link self::getVarfile()} est que le fichier peut
* au final être situé ailleurs que dans $vardir. de plus, il n'y a pas de
* valeur par défaut pour $file
*/
function getWorkfile(string $file, $profile=null, bool $ensureDir=true): string {
$file = path::reljoin($this->vardir, $this->appgroup, $file);
$file = $this->withProfile($file, $profile);
if ($ensureDir) sh::mkdirof($file);
return $file;
}
/**
* obtenir le chemin absolu vers un fichier spécifié par l'utilisateur.
* - si le chemin commence par /, il est laissé en l'état
* - si le chemin commence par ./ ou ../, il est exprimé par rapport à $cwd
* - sinon le chemin est exprimé par rapport à $vardir/$appgroup
*
* la différence est avec {@link self::getVarfile()} est que le fichier peut
* au final être situé ailleurs que dans $vardir. de plus, il n'y a pas de
* valeur par défaut pour $file
*/
function getUserfile(string $file): string {
if (path::is_qualified($file)) {
return path::reljoin($this->cwd, $file);
} else {
return path::reljoin($this->vardir, $this->appgroup, $file);
}
}
protected ?RunFile $runfile = null;
function getRunfile(): RunFile {
$name = $this->name;
$runfile = $this->getWorkfile($name);
$logfile = $this->getLogfile("$name.out", false);
return $this->runfile ??= new RunFile($name, $runfile, $logfile);
}
protected ?array $lockFiles = null;
function getLockfile(?string $name=null): LockFile {
$this->lockFiles[$name] ??= $this->getRunfile()->getLockFile($name, $this->title);
return $this->lockFiles[$name];
}
#############################################################################
const EC_FORK_CHILD = 250;
const EC_FORK_PARENT = 251;
const EC_DISABLED = 252;
const EC_LOCKED = 253;
const EC_BAD_COMMAND = 254;
const EC_UNEXPECTED = 255;
#############################################################################
static bool $dispach_signals = false;
static function install_signal_handler(bool $allow=true): void {
if (!$allow) return;
$signalHandler = function(int $signo, $siginfo) {
throw new ExitError(128 + $signo);
};
pcntl_signal(SIGHUP, $signalHandler);
pcntl_signal(SIGINT, $signalHandler);
pcntl_signal(SIGQUIT, $signalHandler);
pcntl_signal(SIGTERM, $signalHandler);
self::$dispach_signals = true;
}
static function _dispatch_signals() {
if (self::$dispach_signals) pcntl_signal_dispatch();
}
#############################################################################
static ?func $bgapplication_enabled = null;
/**
* spécifier la fonction permettant de vérifier si l'exécution de tâches
* de fond est autorisée. Si cette méthode n'est pas utilisée, par défaut,
* les tâches planifiées sont autorisées
*
* si $func===true, spécifier une fonction qui retourne toujours vrai
* si $func===false, spécifiée une fonction qui retourne toujours faux
* sinon, $func doit être une fonction valide
*/
static function set_bgapplication_enabled($func): void {
if (is_bool($func)) {
$enabled = $func;
$func = function () use ($enabled) {
return $enabled;
};
}
self::$bgapplication_enabled = func::with($func);
}
/**
* Si les exécutions en tâche de fond sont autorisée, retourner. Sinon
* afficher une erreur et quitter l'application
*/
static function check_bgapplication_enabled(bool $forceEnabled=false): void {
if (self::$bgapplication_enabled === null || $forceEnabled) return;
if (!self::$bgapplication_enabled->invoke()) {
throw new ExitError(self::EC_DISABLED, "Planifications désactivées. La tâche n'a pas été lancée");
}
}
#############################################################################
static function action(?string $title, ?int $maxSteps=null): void {
self::get()->getRunfile()->action($title, $maxSteps);
}
static function step(int $nbSteps=1): void {
self::get()->getRunfile()->step($nbSteps);
}
}

View File

@ -0,0 +1,110 @@
<?php
namespace nulib\app\args;
use stdClass;
abstract class AbstractArgsParser {
protected function notEnoughArgs(int $needed, ?string $arg=null): ArgsException {
if ($arg !== null) $arg .= ": ";
$reason = $arg._exceptions::missing_value_message($needed);
return _exceptions::missing_value(null, null, $reason);
}
protected function checkEnoughArgs(?string $option, int $count): void {
if ($count > 0) throw $this->notEnoughArgs($count, $option);
}
protected function tooManyArgs(int $count, int $expected, ?string $arg=null): ArgsException {
if ($arg !== null) $arg .= ": ";
$reason = $arg._exceptions::unexpected_value_message($count - $expected);
return _exceptions::unexpected_value(null, null, $reason);
}
protected function invalidArg(string $arg): ArgsException {
return _exceptions::invalid_value($arg);
}
protected function ambiguousArg(string $arg, array $candidates): ArgsException {
$candidates = implode(", ", $candidates);
return new ArgsException("$arg: cet argument est ambigû (les valeurs possibles sont $candidates)");
}
/**
* consommer les arguments de $src en avançant l'index $srci et provisionner
* $dest à partir de $desti. si $desti est plus grand que 0, celà veut dire
* que $dest a déjà commencé à être provisionné, et qu'il faut continuer.
*
* $destmin est le nombre minimum d'arguments à consommer. $destmax est le
* nombre maximum d'arguments à consommer.
*
* $srci est la position de l'élément courant à consommer le cas échéant
* retourner le nombre d'arguments qui manquent (ou 0 si tous les arguments
* ont été consommés)
*
* pour les arguments optionnels, ils sont consommés tant qu'il y en a de
* disponible, ou jusqu'à la présence de '--'. Si $keepsep, l'argument '--'
* est gardé dans la liste des arguments optionnels.
*/
protected static function consume_args($src, &$srci, &$dest, $desti, $destmin, $destmax, bool $keepsep): int {
$srcmax = count($src);
# arguments obligatoires
while ($desti < $destmin) {
if ($srci < $srcmax) {
$dest[] = $src[$srci];
} else {
# pas assez d'arguments
return $destmin - $desti;
}
$srci++;
$desti++;
}
# arguments facultatifs
$eoo = false; // l'option a-t-elle été terminée?
while ($desti < $destmax && $srci < $srcmax) {
$opt = $src[$srci];
$srci++;
$desti++;
if ($opt === "--") {
# fin des arguments facultatifs en entrée
$eoo = true;
if ($keepsep) $dest[] = "--";
break;
}
$dest[] = $opt;
}
if (!$eoo && $desti < $destmax) {
# pas assez d'arguments en entrée, terminer avec "--"
if ($keepsep) $dest[] = "--";
}
return 0;
}
abstract function normalize(array $args): array;
/** @var object|array objet destination */
protected $dest;
protected function setDest(&$dest): void {
$this->dest =& $dest;
}
protected function unsetDest(): void {
unset($this->dest);
}
abstract function process(array $args);
function parse(&$dest, array $args=null): void {
if ($args === null) {
global $argv;
$args = array_slice($argv, 1);
}
$args = $this->normalize($args);
$dest ??= new stdClass();
$this->setDest($dest);
$this->process($args);
$this->unsetDest();
}
abstract function actionPrintHelp(string $arg): void;
}

643
php/src/app/args/Aodef.php Normal file
View File

@ -0,0 +1,643 @@
<?php
namespace nulib\app\args;
use nulib\A;
use nulib\cl;
use nulib\php\akey;
use nulib\php\func;
use nulib\php\oprop;
use nulib\php\types\varray;
use nulib\php\types\vbool;
use nulib\php\valx;
use nulib\str;
/**
* Class Aodef: une définition d'un argument
*
* il y a 3 temps dans l'initialisation de l'objet:
* - constructeur: accumuler les informations
* - setup1($extends): calculer les options effectives. $extends permet de
* cibler les définitions qui étendent une définition existante
* - setup2(): calculer les arguments et les actions
*/
class Aodef {
const TYPE_SHORT = 0, TYPE_LONG = 1, TYPE_COMMAND = 2;
const ARGS_NONE = 0, ARGS_MANDATORY = 1, ARGS_OPTIONAL = 2;
function __construct(array $def) {
$this->origDef = $def;
$this->mergeParse($def);
//$this->debugTrace("construct");
}
protected array $origDef;
public bool $show = true;
public ?bool $disabled = null;
public ?bool $isRemains = null;
public ?string $extends = null;
protected ?array $_removes = null;
protected ?array $_adds = null;
protected ?array $_args = null;
public ?string $argsdesc = null;
public ?bool $ensureArray = null;
public $action = null;
public ?func $func = null;
public ?bool $inverse = null;
public $value = null;
public ?string $name = null;
public ?string $property = null;
public ?string $key = null;
public ?string $help = null;
protected ?array $_options = [];
public bool $haveShortOptions = false;
public bool $haveLongOptions = false;
public bool $isCommand = false;
public bool $isHelp = false;
public bool $haveArgs = false;
public ?int $minArgs = null;
public ?int $maxArgs = null;
protected function mergeParse(array $def): void {
$merges = $defs["merges"] ?? null;
$merge = $defs["merge"] ?? null;
if ($merge !== null) $merges[] = $merge;
if ($merges !== null) {
foreach ($merges as $merge) {
if ($merge !== null) $this->mergeParse($merge);
}
}
$this->parse($def);
$merge = $defs["merge_after"] ?? null;
if ($merge !== null) $this->mergeParse($merge);
}
private static function verifix_args(?array &$options): ?array {
$args = null;
if ($options !== null) {
foreach ($options as &$option) {
if (preg_match('/^(.*:)([^:].*)$/', $option, $ms)) {
$option = $ms[1];
$args ??= explode(",", $ms[2]);
}
}; unset($option);
}
return $args;
}
protected function parse(array $def): void {
[$options, $params] = cl::split_assoc($def);
$this->show ??= $params["show"] ?? true;
$this->extends ??= $params["extends"] ?? null;
$args ??= $params["args"] ?? null;
$args ??= $params["arg"] ?? null;
if ($args === true) $args = 1;
elseif ($args === "*") $args = [null];
elseif ($args === "+") $args = ["value", null];
if (is_int($args)) $args = array_fill(0, $args, "value");
$this->disabled = vbool::withn($params["disabled"] ?? null);
$adds = varray::withn($params["add"] ?? null);
A::merge($this->_adds, $adds);
A::merge($this->_adds, $options);
$args ??= self::verifix_args($this->_adds);
$removes = varray::withn($params["remove"] ?? null);
A::merge($this->_removes, $removes);
self::verifix_args($this->_adds);
$this->_args ??= cl::withn($args);
$this->argsdesc ??= $params["argsdesc"] ?? null;
$this->ensureArray ??= $params["ensure_array"] ?? null;
$this->action = $params["action"] ?? null;
$this->inverse ??= $params["inverse"] ?? null;
$this->value ??= $params["value"] ?? null;
$this->name ??= $params["name"] ?? null;
$this->property ??= $params["property"] ?? null;
$this->key ??= $params["key"] ?? null;
$this->help ??= $params["help"] ?? null;
}
function isExtends(): bool {
return $this->extends !== null;
}
function setup1(bool $extends=false, ?Aolist $aolist=null): void {
if (!$extends && !$this->isExtends()) {
$this->processOptions();
} elseif ($extends && $this->isExtends()) {
$this->processExtends($aolist);
}
$this->initRemains();
//$this->debugTrace("setup1");
}
protected function processExtends(Aolist $argdefs): void {
$option = $this->extends;
if ($option === null) {
throw _exceptions::null_value("extends", "il doit spécifier l'argument destination");
}
$dest = $argdefs->get($option);
if ($dest === null) {
throw _exceptions::invalid_value($option, "extends", "il doit spécifier un argument valide");
}
if ($this->ensureArray !== null) $dest->ensureArray = $this->ensureArray;
if ($this->action !== null) $dest->action = $this->action;
if ($this->inverse !== null) $dest->inverse = $this->inverse;
if ($this->value !== null) $dest->value = $this->value;
if ($this->name !== null) $dest->name = $this->name;
if ($this->property !== null) $dest->property = $this->property;
if ($this->key !== null) $dest->key = $this->key;
A::merge($dest->_removes, $this->_removes);
A::merge($dest->_adds, $this->_adds);
$dest->processOptions();
}
function buildOptions(?array $options): array {
$result = [];
if ($options !== null) {
foreach ($options as $option) {
if (substr($option, 0, 2) === "--") {
$type = self::TYPE_LONG;
if (preg_match('/^--([^:-][^:]*)(::?)?$/', $option, $ms)) {
$name = $ms[1];
$args = $ms[2] ?? null;
$option = "--$name";
} else {
throw _exceptions::invalid_value($option, "cette option longue");
}
} elseif (substr($option, 0, 1) === "-") {
$type = self::TYPE_SHORT;
if (preg_match('/^-([^:-])(::?)?$/', $option, $ms)) {
$name = $ms[1];
$args = $ms[2] ?? null;
$option = "-$name";
} else {
throw _exceptions::invalid_value($option, " cette option courte");
}
} else {
$type = self::TYPE_COMMAND;
if (preg_match('/^([^:-][^:]*)$/', $option, $ms)) {
$name = $ms[1];
$args = null;
$option = "$name";
} else {
throw _exceptions::invalid_value($option, "cette commande");
}
}
if ($args === ":") {
$argsType = self::ARGS_MANDATORY;
} elseif ($args === "::") {
$argsType = self::ARGS_OPTIONAL;
} else {
$argsType = self::ARGS_NONE;
}
$result[$option] = [
"name" => $name,
"option" => $option,
"type" => $type,
"args_type" => $argsType,
];
}
}
return $result;
}
protected function initRemains(): void {
if ($this->isRemains === null) {
$options = array_fill_keys(array_keys($this->_options), true);
foreach (array_keys($this->buildOptions($this->_removes)) as $option) {
unset($options[$option]);
}
foreach (array_keys($this->buildOptions($this->_adds)) as $option) {
unset($options[$option]);
}
if (!$options) $this->isRemains = true;
}
}
/** traiter le paramètre parent */
protected function processOptions(): void {
$this->removeOptions($this->_removes);
$this->_removes = null;
$this->addOptions($this->_adds);
$this->_adds = null;
}
function addOptions(?array $options): void {
// les options pouvant être numériques (e.g "-1"), utiliser A::merge2
A::merge2($this->_options, $this->buildOptions($options));
$this->updateType();
}
function removeOptions(?array $options): void {
foreach ($this->buildOptions($options) as $option) {
unset($this->_options[$option["option"]]);
}
$this->updateType();
}
function removeOption(string $option): void {
unset($this->_options[$option]);
}
/** mettre à jour le type d'option */
protected function updateType(): void {
$haveShortOptions = false;
$haveLongOptions = false;
$isCommand = false;
$isHelp = false;
foreach ($this->_options as $option) {
switch ($option["type"]) {
case self::TYPE_SHORT:
$haveShortOptions = true;
break;
case self::TYPE_LONG:
$haveLongOptions = true;
break;
case self::TYPE_COMMAND:
$isCommand = true;
break;
}
switch ($option["option"]) {
case "--help":
case "--help++":
$isHelp = true;
break;
}
}
$this->haveShortOptions = $haveShortOptions;
$this->haveLongOptions = $haveLongOptions;
$this->isCommand = $isCommand;
$this->isHelp = $isHelp;
}
function setup2(): void {
$this->processArgs();
$this->processAction();
$this->afterSetup();
//$this->debugTrace("setup2");
}
/**
* traiter les informations concernant les arguments puis calculer les nombres
* minimum et maximum d'arguments que prend l'option
*/
protected function processArgs(): void {
$args = $this->_args;
if ($this->isRemains) {
$args ??= [null];
$haveArgs = boolval($args);
} elseif ($args === null) {
$haveArgs = false;
$optionalArgs = null;
foreach ($this->_options as $option) {
switch ($option["args_type"]) {
case self::ARGS_NONE:
break;
case self::ARGS_MANDATORY:
$haveArgs = true;
$optionalArgs = false;
break;
case self::ARGS_OPTIONAL:
$haveArgs = true;
$optionalArgs ??= true;
break;
}
}
$optionalArgs ??= false;
if ($haveArgs) {
$args = ["value"];
if ($optionalArgs) $args = [$args];
}
} else {
$haveArgs = boolval($args);
}
if ($this->isRemains) $desc = "remaining args";
else $desc = cl::first($this->_options)["option"];
$args ??= [];
$argsdesc = [];
$reqs = [];
$haveNull = false;
$optArgs = null;
foreach ($args as $arg) {
if (is_string($arg)) {
$reqs[] = $arg;
$argsdesc[] = strtoupper($arg);
} elseif (is_array($arg)) {
$optArgs = $arg;
break;
} elseif ($arg === null) {
$haveNull = true;
break;
} else {
throw _exceptions::invalid_value("$desc: $arg");
}
}
$opts = [];
$optArgsdesc = null;
$lastarg = "VALUE";
if ($optArgs !== null) {
$haveOpt = false;
foreach ($optArgs as $arg) {
if (is_string($arg)) {
$haveOpt = true;
$opts[] = $arg;
$lastarg = strtoupper($arg);
$optArgsdesc[] = $lastarg;
} elseif ($arg === null) {
$haveNull = true;
break;
} else {
throw _exceptions::invalid_value("$desc: $arg");
}
}
if (!$haveOpt) $haveNull = true;
}
if ($haveNull) $optArgsdesc[] = "${lastarg}s...";
if ($optArgsdesc !== null) {
$argsdesc[] = "[".implode(" ", $optArgsdesc)."]";
}
$minArgs = count($reqs);
if ($haveNull) $maxArgs = PHP_INT_MAX;
else $maxArgs = $minArgs + count($opts);
$this->haveArgs = $haveArgs;
$this->minArgs = $minArgs;
$this->maxArgs = $maxArgs;
$this->argsdesc ??= implode(" ", $argsdesc);
}
private static function get_longest(array $options, int $type): ?string {
$longest = null;
$maxlen = 0;
foreach ($options as $option) {
if ($option["type"] !== $type) continue;
$name = $option["name"];
$len = strlen($name);
if ($len > $maxlen) {
$longest = $name;
$maxlen = $len;
}
}
return $longest;
}
protected function processAction(): void {
$this->ensureArray ??= $this->isRemains || $this->maxArgs > 1;
$action = $this->action;
$func = $this->func;
if ($action === null) {
if ($this->isCommand) $action = "--set-command";
elseif ($this->isRemains) $action = "--set-args";
elseif ($this->isHelp) $action = "--show-help";
elseif ($this->haveArgs) $action = "--set";
elseif ($this->value !== null) $action = "--set";
else $action = "--inc";
}
if (is_string($action) && substr($action, 0, 2) === "--") {
# fonction interne
} else {
$func = func::with($action);
$action = "--func";
}
$this->action = $action;
$this->func = $func;
$name = $this->name;
$property = $this->property;
$key = $this->key;
if ($action !== "--func" && !$this->isRemains &&
$name === null && $property === null && $key === null
) {
# si on ne précise pas le nom de la propriété, la dériver à partir du
# nom de l'option la plus longue
$longest = self::get_longest($this->_options, self::TYPE_LONG);
$longest ??= self::get_longest($this->_options, self::TYPE_COMMAND);
$longest ??= self::get_longest($this->_options, self::TYPE_SHORT);
if ($longest !== null) {
$longest = preg_replace('/[^A-Za-z0-9]+/', "_", $longest);
# les options --no-name mettent à jour la valeur $name et inversent
# le traitement
if ($longest !== "no_" && str::del_prefix($longest, "no_")) {
$this->inverse ??= true;
}
if (preg_match('/^[0-9]/', $longest)) {
# le nom de la propriété ne doit pas commencer par un chiffre
$longest = "p$longest";
}
$name = $longest;
}
} elseif ($name === null && $property !== null) {
$name = $property;
} elseif ($name === null && $key !== null) {
$name = $key;
}
$this->name = $name;
}
protected function afterSetup(): void {
$this->disabled ??= false;
$this->ensureArray ??= false;
$this->inverse ??= false;
if (str::del_prefix($this->help, "++")) {
$this->show = false;
}
}
function getOptions(): array {
if ($this->disabled) return [];
else return array_keys($this->_options);
}
function isEmpty(): bool {
return $this->disabled || (!$this->_options && !$this->isRemains);
}
function printHelp(?array $what=null): void {
$showDef = $what["show"] ?? $this->show;
if (!$showDef || $this->isRemains) return;
$prefix = $what["prefix"] ?? null;
if ($prefix !== null) echo $prefix;
$showOptions = $what["options"] ?? true;
if ($showOptions) {
echo " ";
echo implode(", ", array_keys($this->_options));
if ($this->haveArgs) {
echo " ";
echo $this->argsdesc;
}
echo "\n";
}
$showHelp = $what["help"] ?? true;
if ($this->help && $showHelp) {
echo str::indent($this->help, " ");
echo "\n";
}
}
function action(&$dest, $value, ?string $arg, AbstractArgsParser $parser): void {
if ($this->ensureArray) {
varray::ensure($value);
} elseif (is_array($value)) {
$count = count($value);
if ($count == 0) $value = null;
elseif ($count == 1) $value = $value[0];
}
switch ($this->action) {
case "--set": $this->actionSet($dest, $value); break;
case "--inc": $this->actionInc($dest); break;
case "--dec": $this->actionDec($dest); break;
case "--add": $this->actionAdd($dest, $value); break;
case "--adds": $this->actionAdds($dest, $value); break;
case "--merge": $this->actionMerge($dest, $value); break;
case "--merges": $this->actionMerges($dest, $value); break;
case "--func": $this->func->bind($dest)->invoke([$value, $arg, $this]); break;
case "--set-args": $this->actionSetArgs($dest, $value); break;
case "--set-command": $this->actionSetCommand($dest, $value); break;
case "--show-help": $parser->actionPrintHelp($arg); break;
default: throw _exceptions::invalid_value($this->action, null, "action non supportée");
}
}
function actionSet(&$dest, $value): void {
if ($this->property !== null) {
oprop::set($dest, $this->property, $value);
} elseif ($this->key !== null) {
akey::set($dest, $this->key, $value);
} elseif ($this->name !== null) {
valx::set($dest, $this->name, $value);
}
}
function actionInc(&$dest): void {
if ($this->property !== null) {
if ($this->inverse) oprop::dec($dest, $this->property);
else oprop::inc($dest, $this->property);
} elseif ($this->key !== null) {
if ($this->inverse) akey::dec($dest, $this->key);
else akey::inc($dest, $this->key);
} elseif ($this->name !== null) {
if ($this->inverse) valx::dec($dest, $this->name);
else valx::inc($dest, $this->name);
}
}
function actionDec(&$dest): void {
if ($this->property !== null) {
if ($this->inverse) oprop::inc($dest, $this->property);
else oprop::dec($dest, $this->property);
} elseif ($this->key !== null) {
if ($this->inverse) akey::inc($dest, $this->key);
else akey::dec($dest, $this->key);
} elseif ($this->name !== null) {
if ($this->inverse) valx::inc($dest, $this->name);
else valx::dec($dest, $this->name);
}
}
function actionAdd(&$dest, $value): void {
if ($this->property !== null) {
oprop::append($dest, $this->property, $value);
} elseif ($this->key !== null) {
akey::append($dest, $this->key, $value);
} elseif ($this->name !== null) {
valx::append($dest, $this->name, $value);
}
}
function actionAdds(&$dest, $value): void {
if ($this->property !== null) {
foreach (cl::with($value) as $value) {
oprop::append($dest, $this->property, $value);
}
} elseif ($this->key !== null) {
foreach (cl::with($value) as $value) {
akey::append($dest, $this->key, $value);
}
} elseif ($this->name !== null) {
foreach (cl::with($value) as $value) {
valx::append($dest, $this->name, $value);
}
}
}
function actionMerge(&$dest, $value): void {
if ($this->property !== null) {
oprop::merge($dest, $this->property, $value);
} elseif ($this->key !== null) {
akey::merge($dest, $this->key, $value);
} elseif ($this->name !== null) {
valx::merge($dest, $this->name, $value);
}
}
function actionMerges(&$dest, $value): void {
if ($this->property !== null) {
foreach (cl::with($value) as $value) {
oprop::merge($dest, $this->property, $value);
}
} elseif ($this->key !== null) {
foreach (cl::with($value) as $value) {
akey::merge($dest, $this->key, $value);
}
} elseif ($this->name !== null) {
foreach (cl::with($value) as $value) {
valx::merge($dest, $this->name, $value);
}
}
}
function actionSetArgs(&$dest, $value): void {
if ($this->property !== null) {
oprop::set($dest, $this->property, $value);
} elseif ($this->key !== null) {
akey::set($dest, $this->key, $value);
} elseif ($this->name !== null) {
valx::set($dest, $this->name, $value);
}
}
function actionSetCommand(&$dest, $value): void {
if ($this->property !== null) {
oprop::set($dest, $this->property, $value);
} elseif ($this->key !== null) {
akey::set($dest, $this->key, $value);
} elseif ($this->name !== null) {
valx::set($dest, $this->name, $value);
}
}
function __toString(): string {
$options = implode(",", $this->getOptions());
$args = $this->haveArgs? " ({$this->minArgs}-{$this->maxArgs})": false;
return "$options$args";
}
private function debugTrace(string $message): void {
$options = implode(",", cl::split_assoc($this->origDef)[0] ?? []);
echo "$options $message\n";
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace nulib\app\args;
use nulib\A;
/**
* Class Aogroup: groupe d'arguments fonctionnant ensemble
*/
class Aogroup extends Aolist {
function __construct(array $defs, bool $setup=false) {
$marker = A::pop($defs, 0);
if ($marker !== "group") {
throw _exceptions::missing_value(null, null, "ce n'est pas un groupe valide");
}
# réordonner les clés numériques
$defs = array_merge($defs);
parent::__construct($defs, $setup);
}
function printHelp(?array $what=null): void {
$showGroup = $what["show"] ?? true;
if (!$showGroup) return;
$prefix = $what["prefix"] ?? null;
if ($prefix !== null) echo $prefix;
$firstAodef = null;
foreach ($this->all() as $aodef) {
$firstAodef ??= $aodef;
$aodef->printHelp(["help" => false]);
}
if ($firstAodef !== null) {
$firstAodef->printHelp(["options" => false]);
}
}
}

268
php/src/app/args/Aolist.php Normal file
View File

@ -0,0 +1,268 @@
<?php
namespace nulib\app\args;
use nulib\cl;
use nulib\str;
use const true;
/**
* Class Aodefs: une liste d'objets Aodef
*/
abstract class Aolist {
function __construct(array $defs, bool $setup=true) {
$this->origDefs = $defs;
$this->initDefs($defs, $setup);
}
protected array $origDefs;
protected ?array $aomain;
protected ?array $aosections;
protected ?array $aospecials;
public ?Aodef $remainsArgdef = null;
function initDefs(array $defs, bool $setup=true): void {
$this->mergeParse($defs, $aobjects);
$this->aomain = $aobjects["main"] ?? null;
$this->aosections = $aobjects["sections"] ?? null;
$this->aospecials = $aobjects["specials"] ?? null;
if ($setup) $this->setup();
}
protected function mergeParse(array $defs, ?array &$aobjects, bool $parse=true): void {
$aobjects ??= [];
$merges = $defs["merges"] ?? null;
$merge = $defs["merge"] ?? null;
if ($merge !== null) $merges[] = $merge;
if ($merges !== null) {
foreach ($merges as $merge) {
$this->mergeParse($merge, $aobjects, false);
$this->parse($merge, $aobjects);
}
}
if ($parse) $this->parse($defs, $aobjects);
$merge = $defs["merge_after"] ?? null;
if ($merge !== null) {
$this->mergeParse($merge, $aobjects, false);
$this->parse($merge, $aobjects);
}
}
protected function parse(array $defs, array &$aobjects): void {
[$defs, $params] = cl::split_assoc($defs);
if ($defs !== null) {
$aomain =& $aobjects["main"];
foreach ($defs as $def) {
$first = $def[0] ?? null;
if ($first === "group") {
$aobject = new Aogroup($def);
} else {
$aobject = new Aodef($def);
}
$aomain[] = $aobject;
}
}
$sections = $params["sections"] ?? null;
if ($sections !== null) {
$aosections =& $aobjects["sections"];
$index = 0;
foreach ($sections as $key => $section) {
if ($key === $index) {
$index++;
$aosections[] = new Aosection($section);
} else {
/** @var Aosection $aosection */
$aosection = $aosections[$key] ?? null;
if ($aosection === null) {
$aosections[$key] = new Aosection($section);
} else {
#XXX il faut implémenter la fusion en cas de section existante
# pour le moment, la liste existante est écrasée
$aosection->initDefs($section);
}
}
}
}
$this->parseParams($params);
}
protected function parseParams(?array $params): void {
}
function all(?array $what=null): iterable {
$returnsAodef = $what["aodef"] ?? true;
$returnsAolist = $what["aolist"] ?? false;
$returnExtends = $what["extends"] ?? false;
$withSpecials = $what["aospecials"] ?? true;
# lister les sections avant, pour que les options de la section principale
# soient prioritaires
$aosections = $this->aosections;
if ($aosections !== null) {
/** @var Aosection $aobject */
foreach ($aosections as $aosection) {
if ($returnsAolist) {
yield $aosection;
} elseif ($returnsAodef) {
yield from $aosection->all($what);
}
}
}
$aomain = $this->aomain;
if ($aomain !== null) {
/** @var Aodef $aobject */
foreach ($aomain as $aobject) {
if ($aobject instanceof Aodef) {
if ($returnsAodef) {
if ($returnExtends) {
if ($aobject->isExtends()) yield $aobject;
} else {
if (!$aobject->isExtends()) yield $aobject;
}
}
} elseif ($aobject instanceof Aolist) {
if ($returnsAolist) {
yield $aobject;
} elseif ($returnsAodef) {
yield from $aobject->all($what);
}
}
}
}
$aospecials = $this->aospecials;
if ($withSpecials && $aospecials !== null) {
/** @var Aodef $aobject */
foreach ($aospecials as $aobject) {
yield $aobject;
}
}
}
protected function filter(callable $callback): void {
$aomain = $this->aomain;
if ($aomain !== null) {
$filtered = [];
/** @var Aodef $aobject */
foreach ($aomain as $aobject) {
if ($aobject instanceof Aolist) {
$aobject->filter($callback);
}
if (call_user_func($callback, $aobject)) {
$filtered[] = $aobject;
}
}
$this->aomain = $filtered;
}
$aosections = $this->aosections;
if ($aosections !== null) {
$filtered = [];
/** @var Aosection $aosection */
foreach ($aosections as $aosection) {
$aosection->filter($callback);
if (call_user_func($callback, $aosection)) {
$filtered[] = $aosection;
}
}
$this->aosections = $filtered;
}
}
protected function setup(): void {
# calculer les options
foreach ($this->all() as $aodef) {
$aodef->setup1();
}
/** @var Aodef $aodef */
foreach ($this->all(["extends" => true]) as $aodef) {
$aodef->setup1(true, $this);
}
# ne garder que les objets non vides
$this->filter(function($aobject): bool {
if ($aobject instanceof Aodef) {
return !$aobject->isEmpty();
} elseif ($aobject instanceof Aolist) {
return !$aobject->isEmpty();
} else {
return false;
}
});
# puis calculer nombre d'arguments et actions
foreach ($this->all() as $aodef) {
$aodef->setup2();
}
}
function isEmpty(): bool {
foreach ($this->all() as $aobject) {
return false;
}
return true;
}
function get(string $option): ?Aodef {
return null;
}
function actionPrintHelp(string $arg): void {
$this->printHelp([
"show_all" => $arg === "--help++",
]);
}
function printHelp(?array $what=null): void {
$show = $what["show_all"] ?? false;
if (!$show) $show = null;
$aosections = $this->aosections;
if ($aosections !== null) {
/** @var Aosection $aosection */
foreach ($aosections as $aosection) {
$aosection->printHelp(cl::merge($what, [
"show" => $show,
"prefix" => "\n",
]));
}
}
$aomain = $this->aomain;
if ($aomain !== null) {
echo "\nOPTIONS\n";
foreach ($aomain as $aobject) {
$aobject->printHelp(cl::merge($what, [
"show" => $show,
]));
}
}
}
function __toString(): string {
$items = [];
$what = [
"aodef" => true,
"aolist" => true,
];
foreach ($this->all($what) as $aobject) {
if ($aobject instanceof Aodef) {
$items[] = strval($aobject);
} elseif ($aobject instanceof Aogroup) {
$items[] = implode("\n", [
"group",
str::indent(strval($aobject)),
]);
} elseif ($aobject instanceof Aosection) {
$items[] = implode("\n", [
"section",
str::indent(strval($aobject)),
]);
} else {
$items[] = false;
}
}
return implode("\n", $items);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace nulib\app\args;
use nulib\php\types\vbool;
/**
* Class Aosection: un regroupement d'arguments pour améliorer la mise en forme
* de l'affichage de l'aide
*/
class Aosection extends Aolist {
function __construct(array $defs, bool $setup=false) {
parent::__construct($defs, $setup);
}
public bool $show = true;
public ?string $prefix = null;
public ?string $title = null;
public ?string $description = null;
public ?string $suffix = null;
protected function parseParams(?array $params): void {
$this->show = vbool::with($params["show"] ?? true);
$this->prefix ??= $params["prefix"] ?? null;
$this->title ??= $params["title"] ?? null;
$this->description ??= $params["description"] ?? null;
$this->suffix ??= $params["suffix"] ?? null;
}
function printHelp(?array $what=null): void {
$showSection = $what["show"] ?? $this->show;
if (!$showSection) return;
$prefix = $what["prefix"] ?? null;
if ($prefix !== null) echo $prefix;
if ($this->prefix) echo "{$this->prefix}\n";
if ($this->title) echo "{$this->title}\n";
if ($this->description) echo "\n{$this->description}\n";
/** @var Aodef|Aolist $aobject */
foreach ($this->all(["aolist" => true]) as $aobject) {
$aobject->printHelp();
}
if ($this->suffix) echo "{$this->suffix}\n";
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace nulib\app\args;
use nulib\UserException;
class ArgsException extends UserException {
}

View File

@ -0,0 +1,185 @@
<?php
namespace nulib\app\args;
use nulib\cl;
use nulib\php\types\vbool;
use nulib\str;
use const true;
/**
* Class SimpleArgdefs: une définition simple des arguments et des options
* valides d'un programme: les commandes ne sont pas supportées, ni les suites
* de commandes
*
* i.e
* -x --long est supporté
* cmd -a -b n'est PAS supporté
* cmd1 -x // cmd2 -y n'est PAS supporté
*/
class SimpleAolist extends Aolist {
public ?string $prefix = null;
public ?string $name = null;
public ?string $purpose = null;
public $usage = null;
public ?string $description = null;
public ?string $suffix = null;
public ?string $commandname = null;
public ?string $commandproperty = null;
public ?string $commandkey = null;
public ?string $argsname = null;
public ?string $argsproperty = null;
public ?string $argskey = null;
public ?bool $autohelp = null;
public ?bool $autoremains = null;
protected array $index;
protected function parseParams(?array $params): void {
# méta-informations
$this->prefix ??= $params["prefix"] ?? null;
$this->name ??= $params["name"] ?? null;
$this->purpose ??= $params["purpose"] ?? null;
$this->usage ??= $params["usage"] ?? null;
$this->description ??= $params["description"] ?? null;
$this->suffix ??= $params["suffix"] ?? null;
$this->commandname ??= $params["commandname"] ?? null;
$this->commandproperty ??= $params["commandproperty"] ?? null;
$this->commandkey ??= $params["commandkey"] ?? null;
$this->argsname ??= $params["argsname"] ?? null;
$this->argsproperty ??= $params["argsproperty"] ?? null;
$this->argskey ??= $params["argskey"] ?? null;
$this->autohelp ??= vbool::withn($params["autohelp"] ?? null);
$this->autoremains ??= vbool::withn($params["autoremains"] ?? null);
}
/** @return string[] */
function getOptions(): array {
return array_keys($this->index);
}
protected function indexAodefs(): void {
$this->index = [];
foreach ($this->all() as $aodef) {
$options = $aodef->getOptions();
foreach ($options as $option) {
/** @var Aodef $prevAodef */
$prevAodef = $this->index[$option] ?? null;
if ($prevAodef !== null) $prevAodef->removeOption($option);
$this->index[$option] = $aodef;
}
}
}
protected function setup(): void {
# calculer les options pour les objets déjà fusionnés
/** @var Aodef $aodef */
foreach ($this->all() as $aodef) {
$aodef->setup1();
}
# puis traiter les extensions d'objets et calculer les options pour ces
# objets sur la base de l'index que l'on crée une première fois
$this->indexAodefs();
/** @var Aodef $aodef */
foreach ($this->all(["extends" => true]) as $aodef) {
$aodef->setup1(true, $this);
}
# ne garder que les objets non vides
$this->filter(function($aobject) {
if ($aobject instanceof Aodef) {
return !$aobject->isEmpty();
} elseif ($aobject instanceof Aolist) {
return !$aobject->isEmpty();
} else {
return false;
}
});
# rajouter remains et help si nécessaire
$this->aospecials = [];
$helpArgdef = null;
$remainsArgdef = null;
/** @var Aodef $aodef */
foreach ($this->all() as $aodef) {
if ($aodef->isHelp) $helpArgdef = $aodef;
if ($aodef->isRemains) $remainsArgdef = $aodef;
}
$this->autohelp ??= true;
if ($helpArgdef === null && $this->autohelp) {
$helpArgdef = new Aodef([
"--help", "--help++",
"action" => "--show-help",
"help" => "Afficher l'aide",
]);
$helpArgdef->setup1();
$this->aospecials[] = $helpArgdef;
}
$this->autoremains ??= true;
if ($remainsArgdef === null && $this->autoremains) {
$remainsArgdef = new Aodef([
"args" => [null],
"action" => "--set-args",
"name" => $this->argsname ?? "args",
"property" => $this->argsproperty,
"key" => $this->argskey,
]);
$remainsArgdef->setup1();
$this->aospecials[] = $remainsArgdef;
}
$this->remainsArgdef = $remainsArgdef;
# puis calculer nombre d'arguments et actions
$this->indexAodefs();
/** @var Aodef $aodef */
foreach ($this->all() as $aodef) {
$aodef->setup2();
}
}
function get(string $option): ?Aodef {
return $this->index[$option] ?? null;
}
function printHelp(?array $what = null): void {
$showList = $what["show"] ?? true;
if (!$showList) return;
$prefix = $what["prefix"] ?? null;
if ($prefix !== null) echo $prefix;
if ($this->prefix) echo "{$this->prefix}\n";
if ($this->purpose) {
echo "{$this->name}: {$this->purpose}\n";
} elseif (!$this->prefix) {
# s'il y a un préfixe sans purpose, il remplace purpose
echo "{$this->name}\n";
}
if ($this->usage) {
echo "\nUSAGE\n";
foreach (cl::with($this->usage) as $usage) {
echo " {$this->name} $usage\n";
}
}
if ($this->description) echo "\n{$this->description}\n";
parent::printHelp($what);
if ($this->suffix) echo "{$this->suffix}\n";
}
function __toString(): string {
return implode("\n", [
"objects:",
str::indent(parent::__toString()),
"index:",
str::indent(implode("\n", array_keys($this->index))),
]);
}
}

View File

@ -0,0 +1,247 @@
<?php
namespace nulib\app\args;
use nulib\cl;
use nulib\ExitError;
use nulib\StateException;
class SimpleArgsParser extends AbstractArgsParser {
function __construct(array $defs) {
global $argv;
$defs["name"] ??= basename($argv[0]);
$this->aolist = new SimpleAolist($defs);
}
protected SimpleAolist $aolist;
protected function getArgdef(string $option): ?Aodef {
return $this->aolist->get($option);
}
protected function getOptions(): array {
return $this->aolist->getOptions();
}
function normalize(array $args): array {
$i = 0;
$max = count($args);
$options = [];
$remains = [];
$parseOpts = true;
while ($i < $max) {
$arg = $args[$i++];
if (!$parseOpts) {
# le reste n'est que des arguments
$remains[] = $arg;
continue;
}
if ($arg === "--") {
# fin des options
$parseOpts = false;
continue;
}
if (substr($arg, 0, 2) === "--") {
#######################################################################
# option longue
$pos = strpos($arg, "=");
if ($pos !== false) {
# option avec valeur
$option = substr($arg, 0, $pos);
$value = substr($arg, $pos + 1);
} else {
# option sans valeur
$option = $arg;
$value = null;
}
$argdef = $this->getArgdef($option);
if ($argdef === null) {
# chercher une correspondance
$len = strlen($option);
$candidates = [];
foreach ($this->getOptions() as $candidate) {
if (substr($candidate, 0, $len) === $option) {
$candidates[] = $candidate;
}
}
switch (count($candidates)) {
case 0: throw $this->invalidArg($option);
case 1: $option = $candidates[0]; break;
default: throw $this->ambiguousArg($option, $candidates);
}
$argdef = $this->getArgdef($option);
}
if ($argdef->haveArgs) {
$minArgs = $argdef->minArgs;
$maxArgs = $argdef->maxArgs;
$values = [];
if ($value !== null) {
$values[] = $value;
$offset = 1;
} elseif ($minArgs == 0) {
# cas particulier: la première valeur doit être collée à l'option
# si $maxArgs == 1
$offset = $maxArgs == 1 ? 1 : 0;
} else {
$offset = 0;
}
$this->checkEnoughArgs($option,
self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true));
if ($minArgs == 0 && $maxArgs == 1) {
# cas particulier: la première valeur doit être collée à l'option
if (count($values) > 0) {
$options[] = "$option=$values[0]";
$values = array_slice($values, 1);
} else {
$options[] = $option;
}
} else {
$options[] = $option;
}
$options = array_merge($options, $values);
} elseif ($value !== null) {
throw $this->tooManyArgs(1, 0, $option);
} else {
$options[] = $option;
}
} elseif (substr($arg, 0, 1) === "-") {
#######################################################################
# option courte
$pos = 1;
$len = strlen($arg);
while ($pos < $len) {
$option = "-".substr($arg, $pos, 1);
$argdef = $this->getArgdef($option);
if ($argdef === null) throw $this->invalidArg($option);
if ($argdef->haveArgs) {
$minArgs = $argdef->minArgs;
$maxArgs = $argdef->maxArgs;
$values = [];
if ($len > $pos + 1) {
$values[] = substr($arg, $pos + 1);
$offset = 1;
$pos = $len;
} elseif ($minArgs == 0) {
# cas particulier: la première valeur doit être collée à l'option
# si $maxArgs == 1
$offset = $maxArgs == 1 ? 1 : 0;
} else {
$offset = 0;
}
$this->checkEnoughArgs($option,
self::consume_args($args, $i, $values, $offset, $minArgs, $maxArgs, true));
if ($minArgs == 0 && $maxArgs == 1) {
# cas particulier: la première valeur doit être collée à l'option
if (count($values) > 0) {
$options[] = "$option$values[0]";
$values = array_slice($values, 1);
} else {
$options[] = $option;
}
} else {
$options[] = $option;
}
$options = array_merge($options, $values);
} else {
$options[] = $option;
}
$pos++;
}
} else {
#XXX implémenter les commandes
#######################################################################
# argument
$remains[] = $arg;
}
}
return array_merge($options, ["--"], $remains);
}
function process(array $args) {
$i = 0;
$max = count($args);
# d'abord traiter les options
while ($i < $max) {
$arg = $args[$i++];
if ($arg === "--") {
# fin des options
break;
}
if (preg_match('/^(--[^=]+)(?:=(.*))?/', $arg, $ms)) {
# option longue
} elseif (preg_match('/^(-.)(.+)?/', $arg, $ms)) {
# option courte
} else {
# commande
throw StateException::unexpected_state("commands are not supported");
}
$option = $ms[1];
$ovalue = $ms[2] ?? null;
$argdef = $this->getArgdef($option);
if ($argdef === null) throw StateException::unexpected_state();
$defvalue = $argdef->value;
if ($argdef->haveArgs) {
$minArgs = $argdef->minArgs;
$maxArgs = $argdef->maxArgs;
if ($minArgs == 0 && $maxArgs == 1) {
# argument facultatif
if ($ovalue !== null) $value = [$ovalue];
else $value = cl::with($defvalue);
$offset = 1;
} else {
$value = [];
$offset = 0;
}
self::consume_args($args, $i, $value, $offset, $minArgs, $maxArgs, false);
} else {
$value = $defvalue;
}
$this->action($value, $arg, $argdef);
}
# construire la liste des arguments qui restent
$args = array_slice($args, $i);
$i = 0;
$max = count($args);
$argdef = $this->aolist->remainsArgdef;
if ($argdef !== null && $argdef->haveArgs) {
$minArgs = $argdef->minArgs;
$maxArgs = $argdef->maxArgs;
if ($maxArgs == PHP_INT_MAX) {
# cas particulier: si le nombre d'arguments restants est non borné,
# les prendre tous sans distinction ni traitement de '--'
$value = $args;
# mais tester tout de même s'il y a le minimum requis d'arguments
$this->checkEnoughArgs(null, $minArgs - $max);
} else {
$value = [];
$this->checkEnoughArgs(null,
self::consume_args($args, $i, $value, 0, $minArgs, $maxArgs, false));
if ($i <= $max - 1) throw $this->tooManyArgs($max, $i);
}
$this->action($value, null, $argdef);
} elseif ($i <= $max - 1) {
throw $this->tooManyArgs($max, $i);
}
}
function action($value, ?string $arg, Aodef $argdef) {
$argdef->action($this->dest, $value, $arg, $this);
}
public function actionPrintHelp(string $arg): void {
$this->aolist->actionPrintHelp($arg);
throw new ExitError(0);
}
function showDebugInfos() {
echo $this->aolist."\n"; #XXX
}
}

20
php/src/app/args/TODO.md Normal file
View File

@ -0,0 +1,20 @@
# nulib\app\args
* [ ] transformer un schéma en définition d'arguments, un tableau en liste d'arguments, et vice-versa
* [ ] faire une implémentation ArgsParser qui supporte les commandes, et les options dynamiques
* commandes:
`program [options] command [options]`
* multi-commandes:
`program [options] command [options] // command [options] // ...`
* dynamique: la liste des options et des commandes supportées est calculée dynamiquement
## support des commandes
faire une interface Runnable qui représente un composant pouvant être exécuté.
Application implémente Runnable, mais l'analyse des arguments peut retourner une
autre instance de runnable pour faciliter l'implémentation de différents
sous-outils
## BUGS
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

View File

@ -0,0 +1,10 @@
<?php
namespace nulib\app\args;
use nulib\exceptions;
class _exceptions extends exceptions {
const EXCEPTION = ArgsException::class;
const WORD = "masc:l'argument#s";
}

View File

@ -0,0 +1,354 @@
<?php
namespace nulib\app\cli;
use Exception;
use nulib\app\app;
use nulib\app\args\AbstractArgsParser;
use nulib\app\args\ArgsException;
use nulib\app\args\SimpleArgsParser;
use nulib\app\config;
use nulib\app\RunFile;
use nulib\ExitError;
use nulib\ext\yaml;
use nulib\output\con;
use nulib\output\log;
use nulib\output\msg;
use nulib\output\say;
use nulib\output\std\ConsoleMessenger;
use nulib\output\std\LogMessenger;
use nulib\output\std\ProxyMessenger;
use nulib\ref\ref_profiles;
/**
* Class Application: application de base
*/
abstract class Application {
/** @var string répertoire du projet (celui qui contient composer.json */
const PROJDIR = null;
/**
* @var array répertoires vendor exprimés relativement à PROJDIR
*
* les clés suivantes doivent être présentes dans le tableau:
* - autoload (chemin vers vendor/autoload.php)
* - bindir (chemin vers vendor/bin)
*/
const VENDOR = null;
/**
* @var string code du projet, utilisé pour dériver le noms de certains des
* paramètres extraits de l'environnement, e.g MY_APP_DATADIR si le projet a
* pour code my-app
*
* si non définie, cette valeur est calculée automatiquement à partir de
* self::PROJDIR sans le suffixe "-app"
*/
const PROJCODE = null;
/**
* @var string|null identifiant d'un groupe auquel l'application appartient.
* les applications du même groupe enregistrent leur fichiers de controle au
* même endroit $VARDIR/$APPGROUP
*/
const APPGROUP = null;
/**
* @var string code de l'application, utilisé pour inférer le nom de certains
* fichiers spécifiques à l'application.
*
* si non définie, cette valeur est calculée automatiquement à partir de
* static::class
*/
const NAME = null;
/** @var string description courte de l'application */
const TITLE = null;
const DATADIR = null;
const ETCDIR = null;
const VARDIR = null;
const CACHEDIR = null;
const LOGDIR = null;
/** @var bool faut-il activer automatiquement l'écriture dans les logs */
const USE_LOGFILE = null;
/** @var bool faut-il maintenir un fichier de suivi du process? */
const USE_RUNFILE = false;
/**
* @var bool faut-il empêcher deux instances de cette application de se lancer
* en même temps?
*
* nécessite USE_RUNFILE==true
*/
const USE_RUNLOCK = false;
/** @var bool faut-il installer le gestionnaire de signaux? */
const INSTALL_SIGNAL_HANDLER = false;
private static function _info(string $message, int $ec=0): int {
fwrite(STDERR, "INFO: $message\n");
return $ec;
}
private static function _error(string $message, int $ec=1): int {
fwrite(STDERR, "ERROR: $message\n");
return $ec;
}
static function _manage_runfile(int &$argc, array &$argv, RunFile $runfile): void {
if ($argc <= 1 || $argv[1] !== "//") return;
array_splice($argv, 1, 1); $argc--;
$ec = 0;
switch ($argv[1] ?? "infos") {
case "help":
self::_info(<<<EOT
Valid commands:
infos
dump
reset
release
start
kill
EOT);
break;
case "infos":
case "i":
$desc = $runfile->getDesc();
echo implode("\n", $desc["message"])."\n";
$ec = $desc["exitcode"] ?? 0;
break;
case "dump":
case "d":
yaml::dump($runfile->read());
break;
case "reset":
case "z":
if (!$runfile->isRunning()) $runfile->reset();
else $ec = self::_error("cannot reset while running");
break;
case "release":
case "rl":
$runfile->release();
break;
case "start":
case "s":
array_splice($argv, 1, 1); $argc--;
return;
case "kill":
case "k":
if ($runfile->isRunning()) $runfile->wfKill();
else $ec = self::_error("not running");
break;
default:
$ec = self::_error("$argv[1]: unexpected command", app::EC_BAD_COMMAND);
}
exit($ec);
}
static function run(?Application $app=null): void {
$unlock = false;
$stop = false;
$shutdown = function () use (&$unlock, &$stop) {
if ($unlock) {
app::get()->getRunfile()->release();
$unlock = false;
}
if ($stop) {
app::get()->getRunfile()->wfStop();
$stop = false;
}
};
register_shutdown_function($shutdown);
app::install_signal_handler(static::INSTALL_SIGNAL_HANDLER);
try {
static::_initialize_app();
$useRunfile = static::USE_RUNFILE;
$useRunlock = static::USE_RUNLOCK;
if ($useRunfile) {
$runfile = app::get()->getRunfile();
global $argc, $argv;
self::_manage_runfile($argc, $argv, $runfile);
if ($useRunlock && $runfile->warnIfLocked()) exit(app::EC_LOCKED);
$runfile->wfStart();
$stop = true;
if ($useRunlock) {
$runfile->lock();
$unlock = true;
}
}
if ($app === null) $app = new static();
static::_configure_app($app);
static::_start_app($app);
} catch (ExitError $e) {
if ($e->haveUserMessage()) msg::error($e->getUserMessage());
exit($e->getCode());
} catch (Exception $e) {
msg::error($e);
exit(app::EC_UNEXPECTED);
}
}
protected static function _initialize_app(): void {
app::init(static::class);
app::set_fact(app::FACT_CLI_APP);
$con = new ConsoleMessenger([
"min_level" => msg::DEBUG,
]);
say::set_messenger($con);
msg::set_messenger($con);
}
protected static function _configure_app(Application $app): void {
config::configure(config::CONFIGURE_INITIAL_ONLY);
$con = con::set_messenger(new ConsoleMessenger([
"min_level" => con::NORMAL,
]));
say::set_messenger($con, true);
msg::set_messenger($con, true);
if (static::USE_LOGFILE) {
$log = log::set_messenger(new LogMessenger([
"output" => app::get()->getLogfile(),
"min_level" => msg::MINOR,
]));
} else {
$log = log::set_messenger(new ProxyMessenger());
}
msg::set_messenger($log);
$app->parseArgs();
config::configure();
}
protected static function _start_app(Application $app): void {
$retcode = $app->main();
if (is_int($retcode)) exit($retcode);
elseif (is_bool($retcode)) exit($retcode? 0: 1);
elseif ($retcode !== null) exit(strval($retcode));
}
/**
* sortir de l'application avec un code d'erreur, qui est 0 par défaut (i.e
* pas d'erreur)
*
* équivalent à lancer l'exception {@link ExitError}
*/
protected static final function exit(int $exitcode=0, $message=null) {
throw new ExitError($exitcode, $message);
}
/**
* sortir de l'application avec un code d'erreur, qui vaut 1 par défaut (i.e
* une erreur s'est produite)
*
* équivalent à lancer l'exception {@link ExitError}
*/
protected static final function die($message=null, int $exitcode=1) {
throw new ExitError($exitcode, $message);
}
const PROFILE_SECTION = [
"title" => "PROFIL D'EXECUTION",
"show" => false,
["-c", "--config", "--app-config",
"args" => "file", "argsdesc" => "CONFIG.yml",
"action" => [config::class, "load_config"],
"help" => "spécifier un fichier de configuration",
],
["group",
["-g", "--profile", "--app-profile",
"args" => 1, "argsdesc" => "PROFILE",
"action" => [app::class, "set_profile"],
"help" => "spécifier le profil d'exécution",
],
["-P", "--prod", "action" => [app::class, "set_profile", ref_profiles::PROD]],
["-T", "--test", "action" => [app::class, "set_profile", ref_profiles::TEST]],
["--devel", "action" => [app::class, "set_profile", ref_profiles::DEVEL]],
],
];
const VERBOSITY_SECTION = [
"title" => "NIVEAU D'INFORMATION",
"show" => false,
["group",
["-V", "--verbosity",
"args" => "verbosity", "argsdesc" => "silent|quiet|verbose|debug",
"action" => [con::class, "set_verbosity"],
"help" => "Spécifier le niveau d'informations affiché sur la console",
],
["-q", "--quiet", "action" => [con::class, "set_verbosity", "quiet"]],
["-v", "--verbose", "action" => [con::class, "set_verbosity", "verbose"]],
["-D", "--debug", "action" => [con::class, "set_verbosity", "debug"]],
],
["group",
["--color",
"action" => [con::class, "set_color", true],
"help" => "Afficher (resp. ne pas afficher) la sortie en couleur par défaut",
],
["--no-color", "action" => [con::class, "set_color", false]],
],
["group",
["-L", "--logfile",
"args" => "output",
"action" => [log::class, "set_output"],
"help" => "Logger les messages de l'application dans le fichier spécifié",
],
["--lV", "--lverbosity",
"args" => "verbosity", "argsdesc" => "silent|quiet|verbose|debug",
"action" => [log::class, "set_verbosity"],
"help" => "Spécifier le niveau des informations ajoutées dans les logs",
],
["--lq", "--lquiet", "action" => [log::class, "set_verbosity", "quiet"]],
["--lv", "--lverbose", "action" => [log::class, "set_verbosity", "verbose"]],
["--lD", "--ldebug", "action" => [log::class, "set_verbosity", "debug"]],
],
];
const ARGS = [
"sections" => [
self::PROFILE_SECTION,
self::VERBOSITY_SECTION,
],
];
protected function getArgsParser(): AbstractArgsParser {
return new SimpleArgsParser(static::ARGS);
}
/** @throws ArgsException */
function parseArgs(array $args=null): void {
$this->getArgsParser()->parse($this, $args);
}
const PROFILE_COLORS = [
ref_profiles::PROD => "@r",
ref_profiles::TEST => "@g",
ref_profiles::DEVEL => "@w",
];
const DEFAULT_PROFILE_COLOR = "y";
/** retourner le profil courant en couleur */
static function get_profile(?string $profile=null): string {
if ($profile === null) $profile = app::get_profile();
foreach (static::PROFILE_COLORS as $text => $color) {
if (strpos($profile, $text) !== false) {
return $color? "<color $color>$profile</color>": $profile;
}
}
$color = static::DEFAULT_PROFILE_COLOR;
return $color? "<color $color>$profile</color>": $profile;
}
protected ?array $args = null;
abstract function main();
static function runfile(): RunFile {
return app::with(static::class)->getRunfile();
}
}

View File

@ -3,7 +3,7 @@
# les constantes suivantes doivent être définies AVANT de chager ce script:
# - NULIB_APP_app_params : paramètres du projet
use nulib\app;
use nulib\app\app;
use nulib\os\path;
if ($argc <= 1) die("invalid arguments");

56
php/src/app/config.php Normal file
View File

@ -0,0 +1,56 @@
<?php
namespace nulib\app;
use nulib\app\config\ConfigManager;
use nulib\app\config\JsonConfig;
use nulib\app\config\YamlConfig;
use nulib\exceptions;
use nulib\os\path;
/**
* Class config: gestion de la configuration de l'application
*/
class config {
protected static ConfigManager $config;
static function init_configurator($configurators): void {
self::$config->addConfigurator($configurators);
}
# certains types de configurations sont normalisés
/** ne configurer que le minimum pour que l'application puisse s'initialiser */
const CONFIGURE_INITIAL_ONLY = ["include" => "initial"];
/** ne configurer que les routes */
const CONFIGURE_ROUTES_ONLY = ["include" => "routes"];
/** configurer uniquement ce qui ne nécessite pas d'avoir une session */
const CONFIGURE_NO_SESSION = ["exclude" => "session"];
static function configure(?array $params=null): void {
self::$config->configure($params);
}
static final function add($config, string ...$profiles): void { self::$config->addConfig($config, $profiles); }
static final function load_config($file): void {
$ext = path::ext($file);
if ($ext === ".yml" || $ext === ".yaml") {
$config = new YamlConfig($file);
} elseif ($ext === ".json") {
$config = new JsonConfig($file);
} else {
throw exceptions::invalid_value($file, "config file");
}
self::add($config);
}
static final function get(string $pkey, $default=null, ?string $profile=null) { return self::$config->getValue($pkey, $default, $profile); }
static final function k(string $pkey, $default=null) { return self::$config->getValue("app.$pkey", $default); }
static final function db(string $pkey, $default=null) { return self::$config->getValue("dbs.$pkey", $default); }
static final function m(string $pkey, $default=null) { return self::$config->getValue("msgs.$pkey", $default); }
static final function l(string $pkey, $default=null) { return self::$config->getValue("mails.$pkey", $default); }
}
new class extends config {
function __construct() {
self::$config = new ConfigManager();
}
};

View File

@ -0,0 +1,50 @@
<?php
namespace nulib\app\config;
use nulib\cl;
class ArrayConfig implements IConfig {
protected function APP(): array {
return static::APP;
} const APP = [];
protected function DBS(): array {
return static::DBS;
} const DBS = [];
protected function MSGS(): array {
return static::MSGS;
} const MSGS = [];
protected function MAILS(): array {
return static::MAILS;
} const MAILS = [];
function __construct(?array $config) {
foreach (self::CONFIG_KEYS as $key) {
switch ($key) {
case "app": $default = $this->APP(); break;
case "dbs": $default = $this->DBS(); break;
case "msgs": $default = $this->MSGS(); break;
case "mails": $default = $this->MAILS(); break;
default: $default = [];
}
$config[$key] ??= $default;
}
$this->config = $config;
}
protected array $config;
function has(string $pkey, string $profile): bool {
return cl::phas($this->config, $pkey);
}
function get(string $pkey, string $profile) {
return cl::pget($this->config, $pkey);
}
function set(string $pkey, $value, string $profile): void {
cl::pset($this->config, $pkey, $value);
}
}

View File

@ -0,0 +1,148 @@
<?php
namespace nulib\app\config;
use nulib\A;
use nulib\app\app;
use nulib\cl;
use nulib\exceptions;
use nulib\php\func;
use ReflectionClass;
class ConfigManager {
protected array $configurators = [];
/** ajouter une classe ou un objet à la liste des configurateurs */
function addConfigurator($configurators): void {
A::merge($this->configurators, cl::with($configurators));
}
protected array $configured = [];
/**
* configurer les objets et les classes qui ne l'ont pas encore été. la liste
* des objets et des classes à configurer est fournie en appelant la méthode
* {@link addConfigurator()}
*
* par défaut, la configuration se fait en appelant toutes les méthodes
* publiques des objets et toutes les méthodes statiques des classes qui
* commencent par 'configure', e.g 'configureThis()' ou 'configure_db()',
* si elles n'ont pas déjà été appelées
*
* Il est possible de modifier la liste des méthodes appelées avec le tableau
* $params, qui doit être conforme au schema de {@link func::CALL_ALL_SCHEMA}
*/
function configure(?array $params=null): void {
$params["prefix"] ??= "configure";
foreach ($this->configurators as $key => $configurator) {
$configured =& $this->configured[$key];
/** @var func[] $methods */
$methods = func::get_all($configurator, $params);
foreach ($methods as $method) {
$name = $method->getName() ?? "(no name)";
$done = $configured[$name] ?? false;
if (!$done) {
$method->invoke();
$configured[$name] = true;
}
}
}
}
#############################################################################
protected $cache = [];
protected function resetCache(): void {
$this->cache = [];
}
protected function cacheHas(string $pkey, string $profile) {
return array_key_exists("$profile.$pkey", $this->cache);
}
protected function cacheGet(string $pkey, string $profile) {
return cl::get($this->cache, "$profile.$pkey");
}
protected function cacheSet(string $pkey, $value, string $profile): void {
$this->cache["$profile.$pkey"] = $value;
}
protected array $profileConfigs = [];
/**
* Ajouter une configuration valide pour le(s) profil(s) spécifié(s)
*
* $config est un objet ou une classe qui définit une ou plusieurs des
* constantes APP, DBS, MSGS, MAILS
*
* si !$inProfiles, la configuration est valide dans tous les profils
*/
function addConfig($config, ?array $inProfiles=null): void {
if (is_string($config)) {
$c = new ReflectionClass($config);
if ($c->implementsInterface(IConfig::class)) {
$config = $c->newInstance();
} else {
$config = [];
foreach (IConfig::CONFIG_KEYS as $key) {
$config[$key] = cl::with($c->getConstant(strtoupper($key)));
}
$config = new ArrayConfig($config);
}
} elseif (is_array($config)) {
$config = new ArrayConfig($config);
} elseif (!($config instanceof IConfig)) {
throw exceptions::invalid_type($config, "config", ["array", IConfig::class]);
}
if (!$inProfiles) $inProfiles = [IConfig::PROFILE_ALL];
foreach ($inProfiles as $profile) {
$this->profileConfigs[$profile][] = $config;
}
$this->resetCache();
}
function _getValue(string $pkey, $default, string $inProfile) {
$profiles = [$inProfile];
if ($inProfile !== IConfig::PROFILE_ALL) $profiles[] = IConfig::PROFILE_ALL;
foreach ($profiles as $profile) {
/** @var IConfig[] $configs */
$configs = $this->profileConfigs[$profile] ?? [];
foreach (array_reverse($configs) as $config) {
if ($config->has($pkey, $profile)) {
return $config->get($pkey, $profile);
}
}
}
return $default;
}
/**
* obtenir la valeur au chemin de clé $pkey dans le profil spécifié
*
* le $inProfile===null, prendre le profil par défaut.
*/
function getValue(string $pkey, $default=null, ?string $inProfile=null) {
$inProfile ??= app::get_profile();
if ($this->cacheHas($pkey, $inProfile)) {
return $this->cacheGet($pkey, $inProfile);
}
$value = $this->_getValue($pkey, $default, $inProfile);
$this->cacheSet($pkey, $value, $inProfile);
return $value;
}
function setValue(string $pkey, $value, ?string $inProfile=null): void {
$inProfile ??= app::get_profile();
/** @var IConfig[] $configs */
$configs =& $this->profileConfigs[$inProfile];
if ($configs === null) $key = 0;
else $key = array_key_last($configs);
$configs[$key] ??= new ArrayConfig([]);
$configs[$key]->set($pkey, $value, $inProfile);
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace nulib\app\config;
use nulib\cl;
use nulib\ext\json;
use nulib\file;
use nulib\str;
/**
* Class EnvConfig: configuration extraite depuis les variables d'environnement
*
* les variables doivent être de la forme {PREFIX}_{PROFILE}_{PKEY} :
* - PREFIX vaut CONFIG, FILE_CONFIG, JSON_CONFIG ou JSON_FILE_CONFIG
* - PROFILE est le profil dans lequel la configuration est valide, ou ALL si
* la valeur doit être valide dans tous les profils
* - PKEY est le chemin de clé dans lequel les caractères '.' sont remplacés
* par '__'
*
* par exemple, la valeur dbs.my_auth.type du profil par défaut est pris dans
* la variable 'CONFIG_ALL_dbs__my_auth__type'. pour le profil prod c'est la
* variable 'CONFIG_prod_dbs__my_auth__type'
*
* pour représenter le tableau suivant:
* [ "type" => "mysql", "name" => "mysql:host=authdb;dbname=auth;charset=utf8",
* "user" => "auth_int", "pass" => "auth" ]
* situé au chemin de clé dbs.auth dans le profil prod, on peut par exemple
* définir les variables suivantes:
* CONFIG_prod_dbs__auth__type="mysql"
* CONFIG_prod_dbs__auth__name="mysql:host=authdb;dbname=auth;charset=utf8"
* CONFIG_prod_dbs__auth__user="auth_int"
* CONFIG_prod_dbs__auth__pass="auth"
* ou alternativement:
* JSON_CONFIG_prod_dbs__auth='{"type":"mysql","name":"mysql:host=authdb;dbname=auth;charset=utf8","user":"auth_int","pass":"auth"}'
*
* Les préfixes supportés sont, dans l'ordre de précédence:
* - JSON_FILE_CONFIG -- une valeur au format JSON inscrite dans un fichier
* - JSON_CONFIG -- une valeur au format JSON
* - FILE_CONFIG -- une valeur inscrite dans un fichier
* - CONFIG -- une valeur scalaire
*/
class EnvConfig implements IConfig{
protected ?array $profileConfigs = null;
/** analyser $name et retourner [$pkey, $profile] */
private static function parse_pkey_profile($name): array {
$i = strpos($name, "_");
if ($i === false) return [false, false];
$profile = substr($name, 0, $i);
if ($profile === "ALL") $profile = IConfig::PROFILE_ALL;
$name = substr($name, $i + 1);
$pkey = str_replace("__", ".", $name);
return [$pkey, $profile];
}
function loadEnvConfig(): void {
if ($this->profileConfigs !== null) return;
$json_files = [];
$jsons = [];
$files = [];
$vars = [];
foreach (getenv() as $name => $value) {
if (str::starts_with("JSON_FILE_CONFIG_", $name)) {
$json_files[str::without_prefix("JSON_FILE_CONFIG_", $name)] = $value;
} elseif (str::starts_with("JSON_CONFIG_", $name)) {
$jsons[str::without_prefix("JSON_CONFIG_", $name)] = $value;
} elseif (str::starts_with("FILE_CONFIG_", $name)) {
$files[str::without_prefix("FILE_CONFIG_", $name)] = $value;
} elseif (str::starts_with("CONFIG_", $name)) {
$vars[str::without_prefix("CONFIG_", $name)] = $value;
}
}
$profileConfigs = [];
foreach ($json_files as $name => $file) {
[$pkey, $profile] = self::parse_pkey_profile($name);
$value = json::load($file);
cl::pset($profileConfigs, "$profile.$pkey", $value);
}
foreach ($jsons as $name => $json) {
[$pkey, $profile] = self::parse_pkey_profile($name);
$value = json::decode($json);
cl::pset($profileConfigs, "$profile.$pkey", $value);
}
foreach ($files as $name => $file) {
[$pkey, $profile] = self::parse_pkey_profile($name);
$value = file::reader($file)->getContents();
cl::pset($profileConfigs, "$profile.$pkey", $value);
}
foreach ($vars as $name => $value) {
[$pkey, $profile] = self::parse_pkey_profile($name);
cl::pset($profileConfigs, "$profile.$pkey", $value);
}
$this->profileConfigs = $profileConfigs;
}
function has(string $pkey, string $profile): bool {
$this->loadEnvConfig();
$config = $this->profileConfigs[$profile] ?? null;
return cl::phas($config, $pkey);
}
function get(string $pkey, string $profile) {
$this->loadEnvConfig();
$config = $this->profileConfigs[$profile] ?? null;
return cl::pget($config, $pkey);
}
function set(string $pkey, $value, string $profile): void {
$this->loadEnvConfig();
$config =& $this->profileConfigs[$profile];
cl::pset($config, $pkey, $value);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace nulib\app\config;
/**
* Interface IConfig: un objet fournissant une configuration
*/
interface IConfig {
/**
* @var string profil indiquant qu'une configuration est valide dans tous les
* profils
*/
const PROFILE_ALL = "-ALL-";
const CONFIG_KEYS = [
"app",
"dbs", "msgs", "mails",
];
function has(string $pkey, string $profile): bool;
function get(string $pkey, string $profile);
function set(string $pkey, $value, string $profile): void;
}

View File

@ -0,0 +1,13 @@
<?php
namespace nulib\app\config;
use nulib\ext\json;
/**
* Class JsonConfig: une configuration chargée depuis un fichier JSON
*/
class JsonConfig extends ArrayConfig {
function __construct(string $input) {
parent::__construct(json::load($input));
}
}

View File

@ -0,0 +1,140 @@
<?php
namespace nulib\app\config;
use nulib\app\app;
use nulib\app\config;
use nulib\ref\ref_profiles;
/**
* Class ProfileManager: gestionnaire de profils
*/
class ProfileManager {
/**
* @var string code du système dont on gère le profil
*
* ce code est utilisé pour dériver le nom du paramètre dans la configuration
* ainsi que la variable d'environnement depuis laquelle est chargée la valeur
* du profil
*/
const NAME = null;
/** @var array|null liste des profils valides */
const PROFILES = null;
/** @var array profils dont le mode production doit être actif */
const PRODUCTION_MODES = ref_profiles::PRODUCTION_MODES;
/**
* @var array mapping profil d'application --> profil effectif
*
* ce mapping est utilisé quand il faut calculer le profil courant s'il n'a
* pas été spécifié par l'utilisateur. il permet de faire correspondre le
* profil courant de l'application avec le profil effectif à sélectionner
*/
const PROFILE_MAP = null;
function __construct(?array $params=null) {
$this->isAppProfile = $params["app"] ?? false;
$this->profiles = static::PROFILES;
$this->productionModes = static::PRODUCTION_MODES;
$this->profileMap = static::PROFILE_MAP;
$name = $params["name"] ?? static::NAME;
if ($name === null) {
$this->configKey = null;
$this->envKeys = ["APP_PROFILE"];
} else {
$configKey = "${name}_profile";
$envKey = strtoupper($configKey);
if ($this->isAppProfile) {
$this->configKey = null;
$this->envKeys = [$envKey, "APP_PROFILE"];
} else {
$this->configKey = $configKey;
$this->envKeys = [$envKey];
}
}
$this->defaultProfile = $params["default_profile"] ?? null;
$profile = $params["profile"] ?? null;
$productionMode = $params["production_mode"] ?? null;
$productionMode ??= $this->productionModes[$profile] ?? false;
$this->profile = $profile;
$this->productionMode = $productionMode;
}
/**
* @var bool cet objet est-il utilisé pour gérer le profil de l'application?
*/
protected bool $isAppProfile;
protected ?array $profiles;
protected ?array $productionModes;
protected ?array $profileMap;
protected function mapProfile(?string $profile): ?string {
return $this->profileMap[$profile] ?? $profile;
}
protected ?string $configKey;
function getConfigProfile(): ?string {
if ($this->configKey === null) return null;
return config::k($this->configKey);
}
protected array $envKeys;
function getEnvProfile(): ?string {
foreach ($this->envKeys as $envKey) {
$profile = getenv($envKey);
if ($profile !== false) return $profile;
}
return null;
}
protected ?string $defaultProfile;
function getDefaultProfile(): ?string {
return $this->defaultProfile;
}
function setDefaultProfile(?string $profile): void {
$this->defaultProfile = $profile;
}
protected ?string $profile;
protected bool $productionMode;
protected function resolveProfile(): void {
$profile ??= $this->getenvProfile();
$profile ??= $this->getConfigProfile();
$profile ??= $this->getDefaultProfile();
if ($this->isAppProfile) {
$profile ??= $this->profiles[0] ?? ref_profiles::PROD;
} else {
$profile ??= $this->mapProfile(app::get_profile());
}
$this->profile = $profile;
$this->productionMode = $this->productionModes[$profile] ?? false;
}
function getProfile(?bool &$productionMode=null): string {
if ($this->profile === null) $this->resolveProfile();
$productionMode = $this->productionMode;
return $this->profile;
}
function isProductionMode(): bool {
return $this->productionMode;
}
function setProfile(?string $profile=null, ?bool $productionMode=null): void {
if ($profile === null) $this->profile = null;
$profile ??= $this->getProfile($productionMode);
$productionMode ??= $this->productionModes[$profile] ?? false;
$this->profile = $profile;
$this->productionMode = $productionMode;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace nulib\app\config;
use nulib\ext\yaml;
/**
* Class YamlConfig: une configuration chargée depuis un fichier yaml
*/
class YamlConfig extends ArrayConfig {
function __construct(string $input) {
parent::__construct(yaml::load($input));
}
}

50
php/src/cache/CacheData.php vendored Normal file
View File

@ -0,0 +1,50 @@
<?php
namespace nulib\cache;
use nulib\php\func;
/**
* Class CacheData: gestion d'une donnée mise en cache
*/
abstract class CacheData {
function __construct(?string $name, $compute) {
$this->name = $name ?? "";
$this->compute = func::withn($compute ?? static::COMPUTE);
}
protected string $name;
function getName() : string {
return $this->name;
}
protected ?func $compute;
/** calculer la donnée */
function compute() {
$compute = $this->compute;
$data = $compute !== null? $compute->invoke(): null;
return $data;
}
/**
* le cache est-il externe? si non, utiliser {@link setDatafile()} pour
* spécifier le fichier destination de la valeur
*/
abstract function isExternal(): bool;
/** spécifier le chemin du cache à partir du fichier de base */
abstract function setDatafile(?string $basefile): void;
/** indiquer si le cache existe */
abstract function exists(): bool;
/** charger la donnée depuis le cache */
abstract function load();
/** sauvegarder la donnée dans le cache et la retourner */
abstract function save($data);
/** supprimer le cache */
abstract function delete();
}

Some files were not shown because too many files have changed in this diff Show More