<pman>Intégration de la branche dev74
This commit is contained in:
		
						commit
						7420704880
					
				
							
								
								
									
										2
									
								
								.idea/nulib-base.iml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								.idea/nulib-base.iml
									
									
									
										generated
									
									
									
								
							| @ -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" /> | ||||
|  | ||||
							
								
								
									
										30
									
								
								.idea/php-docker-settings.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										30
									
								
								.idea/php-docker-settings.xml
									
									
									
										generated
									
									
									
								
							| @ -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
									
									
									
								
							
							
						
						
									
										77
									
								
								.idea/php.xml
									
									
									
										generated
									
									
									
								
							| @ -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"> | ||||
|  | ||||
| @ -4,7 +4,7 @@ UPSTREAM=dev74 | ||||
| DEVELOP=dev82 | ||||
| FEATURE=wip82/ | ||||
| RELEASE=rel82- | ||||
| MAIN=dist82 | ||||
| MAIN=main82 | ||||
| TAG_SUFFIX=p82 | ||||
| HOTFIX=hotf82- | ||||
| DIST= | ||||
|  | ||||
							
								
								
									
										4
									
								
								TODO.md
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								TODO.md
									
									
									
									
									
								
							| @ -1,3 +1,7 @@ | ||||
| # nulib | ||||
| 
 | ||||
| * [wip](wip/TODO.md) | ||||
| 
 | ||||
| # nulib/bash | ||||
| 
 | ||||
| * [nulib/bash](bash/TODO.md) | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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
									
								
							
							
						
						
									
										147
									
								
								bash/src/install.sh
									
									
									
									
									
										Normal 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" | ||||
| } | ||||
| @ -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/ | ||||
|  | ||||
							
								
								
									
										428
									
								
								bash/src/pman.sh
									
									
									
									
									
								
							
							
						
						
									
										428
									
								
								bash/src/pman.sh
									
									
									
									
									
								
							| @ -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 "$@" | ||||
| } | ||||
|  | ||||
| @ -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- | ||||
|  | ||||
| @ -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
									
								
							
							
						
						
									
										12
									
								
								bash/src/pman84.conf.sh
									
									
									
									
									
										Normal 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= | ||||
| @ -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
									
								
							
							
						
						
									
										1
									
								
								bin/.cachectl.php
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| ../php/bin/cachectl.php | ||||
							
								
								
									
										1
									
								
								bin/.dumpser.php
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/.dumpser.php
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| ../php/bin/dumpser.php | ||||
							
								
								
									
										1
									
								
								bin/.json2yml.php
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/.json2yml.php
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| ../php/bin/json2yml.php | ||||
							
								
								
									
										1
									
								
								bin/.mysql.capacitor.php
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/.mysql.capacitor.php
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| ../php/bin/mysql.capacitor.php | ||||
| @ -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(); | ||||
| 
 | ||||
| @ -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
									
								
							
							
						
						
									
										1
									
								
								bin/.pgsql.capacitor.php
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| ../php/bin/pgsql.capacitor.php | ||||
							
								
								
									
										1
									
								
								bin/.sqlite.capacitor.php
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/.sqlite.capacitor.php
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| ../php/bin/sqlite.capacitor.php | ||||
							
								
								
									
										1
									
								
								bin/.yml2json.php
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/.yml2json.php
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| ../php/bin/yml2json.php | ||||
| @ -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"} "$@" | ||||
|  | ||||
| @ -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
									
								
							
							
						
						
									
										85
									
								
								bin/ff
									
									
									
									
									
										Executable 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
									
								
							
							
						
						
									
										1
									
								
								bin/json2yml.php
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| runphp | ||||
							
								
								
									
										1
									
								
								bin/mysql.capacitor.php
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/mysql.capacitor.php
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| runphp | ||||
| @ -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
									
								
							
							
						
						
									
										1
									
								
								bin/pcomp-local_deps.php
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| runphp | ||||
							
								
								
									
										1
									
								
								bin/pcomp-select_profile.php
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/pcomp-select_profile.php
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| runphp | ||||
							
								
								
									
										1
									
								
								bin/pgsql.capacitor.php
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/pgsql.capacitor.php
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| runphp | ||||
							
								
								
									
										192
									
								
								bin/pinit
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										192
									
								
								bin/pinit
									
									
									
									
									
										Executable 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 | ||||
							
								
								
									
										71
									
								
								bin/prel
									
									
									
									
									
								
							
							
						
						
									
										71
									
								
								bin/prel
									
									
									
									
									
								
							| @ -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
									
								
							
							
						
						
									
										383
									
								
								bin/ptool
									
									
									
									
									
										Executable 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" "$@" | ||||
							
								
								
									
										60
									
								
								bin/pwip
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								bin/pwip
									
									
									
									
									
								
							| @ -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 | ||||
| @ -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
									
								
							
							
						
						
									
										1
									
								
								bin/sqlite.capacitor.php
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| runphp | ||||
							
								
								
									
										1
									
								
								bin/yml2json.php
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/yml2json.php
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| runphp | ||||
| @ -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
									
									
									
								
							
							
						
						
									
										1404
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										7
									
								
								php/bin/cachectl.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										7
									
								
								php/bin/cachectl.php
									
									
									
									
									
										Executable 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
									
								
							
							
						
						
									
										7
									
								
								php/bin/dumpser.php
									
									
									
									
									
										Executable 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
									
								
							
							
						
						
									
										7
									
								
								php/bin/json2yml.php
									
									
									
									
									
										Executable 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
									
								
							
							
						
						
									
										7
									
								
								php/bin/mysql.capacitor.php
									
									
									
									
									
										Executable 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
									
								
							
							
						
						
									
										7
									
								
								php/bin/pgsql.capacitor.php
									
									
									
									
									
										Executable 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
									
								
							
							
						
						
									
										7
									
								
								php/bin/sqlite.capacitor.php
									
									
									
									
									
										Executable 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
									
								
							
							
						
						
									
										7
									
								
								php/bin/yml2json.php
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,7 @@ | ||||
| #!/usr/bin/php
 | ||||
| <?php | ||||
| require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php'; | ||||
| 
 | ||||
| use cli\Yaml2jsonApp; | ||||
| 
 | ||||
| Yaml2jsonApp::run(); | ||||
							
								
								
									
										111
									
								
								php/cli/AbstractCapacitorApp.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								php/cli/AbstractCapacitorApp.php
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										122
									
								
								php/cli/BgLauncherApp.php
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										132
									
								
								php/cli/CachectlApp.php
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										31
									
								
								php/cli/DumpserApp.php
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										21
									
								
								php/cli/Json2yamlApp.php
									
									
									
									
									
										Normal 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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										45
									
								
								php/cli/MysqlCapacitorApp.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								php/cli/MysqlCapacitorApp.php
									
									
									
									
									
										Normal 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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										45
									
								
								php/cli/PgsqlCapacitorApp.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								php/cli/PgsqlCapacitorApp.php
									
									
									
									
									
										Normal 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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										43
									
								
								php/cli/SqliteCapacitorApp.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								php/cli/SqliteCapacitorApp.php
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										21
									
								
								php/cli/Yaml2jsonApp.php
									
									
									
									
									
										Normal 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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										53
									
								
								php/cli/_SteamTrainApp.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								php/cli/_SteamTrainApp.php
									
									
									
									
									
										Normal 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()); | ||||
|   } | ||||
| } | ||||
| @ -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(); | ||||
| @ -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) { | ||||
| @ -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" | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| <?php | ||||
| namespace nulib; | ||||
| 
 | ||||
| use nulib\php\func; | ||||
| use Traversable; | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -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"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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")); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -2,7 +2,6 @@ | ||||
| namespace nulib\app; | ||||
| 
 | ||||
| use nulib\A; | ||||
| use nulib\app; | ||||
| use nulib\cl; | ||||
| use nulib\file\SharedFile; | ||||
| use nulib\os\path; | ||||
|  | ||||
| @ -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
									
								
							
							
						
						
									
										655
									
								
								php/src/app/app.php
									
									
									
									
									
										Normal 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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										110
									
								
								php/src/app/args/AbstractArgsParser.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								php/src/app/args/AbstractArgsParser.php
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										643
									
								
								php/src/app/args/Aodef.php
									
									
									
									
									
										Normal 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"; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										36
									
								
								php/src/app/args/Aogroup.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								php/src/app/args/Aogroup.php
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										268
									
								
								php/src/app/args/Aolist.php
									
									
									
									
									
										Normal 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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										45
									
								
								php/src/app/args/Aosection.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								php/src/app/args/Aosection.php
									
									
									
									
									
										Normal 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"; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										7
									
								
								php/src/app/args/ArgsException.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								php/src/app/args/ArgsException.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| <?php | ||||
| namespace nulib\app\args; | ||||
| 
 | ||||
| use nulib\UserException; | ||||
| 
 | ||||
| class ArgsException extends UserException { | ||||
| } | ||||
							
								
								
									
										185
									
								
								php/src/app/args/SimpleAolist.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								php/src/app/args/SimpleAolist.php
									
									
									
									
									
										Normal 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))), | ||||
|     ]); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										247
									
								
								php/src/app/args/SimpleArgsParser.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								php/src/app/args/SimpleArgsParser.php
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										20
									
								
								php/src/app/args/TODO.md
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										10
									
								
								php/src/app/args/_exceptions.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								php/src/app/args/_exceptions.php
									
									
									
									
									
										Normal 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"; | ||||
| } | ||||
							
								
								
									
										354
									
								
								php/src/app/cli/Application.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										354
									
								
								php/src/app/cli/Application.php
									
									
									
									
									
										Normal 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(); | ||||
|   } | ||||
| } | ||||
| @ -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
									
								
							
							
						
						
									
										56
									
								
								php/src/app/config.php
									
									
									
									
									
										Normal 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(); | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										50
									
								
								php/src/app/config/ArrayConfig.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								php/src/app/config/ArrayConfig.php
									
									
									
									
									
										Normal 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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										148
									
								
								php/src/app/config/ConfigManager.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								php/src/app/config/ConfigManager.php
									
									
									
									
									
										Normal 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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										112
									
								
								php/src/app/config/EnvConfig.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								php/src/app/config/EnvConfig.php
									
									
									
									
									
										Normal 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} où: | ||||
|  * - 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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										24
									
								
								php/src/app/config/IConfig.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								php/src/app/config/IConfig.php
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
							
								
								
									
										13
									
								
								php/src/app/config/JsonConfig.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								php/src/app/config/JsonConfig.php
									
									
									
									
									
										Normal 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)); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										140
									
								
								php/src/app/config/ProfileManager.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								php/src/app/config/ProfileManager.php
									
									
									
									
									
										Normal 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; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										13
									
								
								php/src/app/config/YamlConfig.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								php/src/app/config/YamlConfig.php
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										50
									
								
								php/src/cache/CacheData.php
									
									
									
									
										vendored
									
									
										Normal 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
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user