From fb01429970cf2a967c054506f9e9d76a6d06b35a Mon Sep 17 00:00:00 2001
From: Jephte Clain
Date: Fri, 28 Feb 2025 20:08:17 +0400
Subject: [PATCH] importation initiale dev74
---
.gitattributes | 1 +
.gitignore | 12 +
.idea/codeception.xml | 12 +
.idea/inspectionProfiles/Project_Default.xml | 14 +
.idea/modules.xml | 8 +
.idea/nulib.iml | 12 +
.idea/php-docker-settings.xml | 38 +
.idea/php-test-framework.xml | 15 +
.idea/php.xml | 63 +
.idea/phpspec.xml | 10 +
.idea/phpunit.xml | 10 +
.idea/vcs.xml | 6 +
.pman.conf | 11 +
.runphp.conf | 8 +
.udir | 30 +
TODO.md | 15 +
awk/src/base.array.awk | 157 ++
awk/src/base.awk | 5 +
awk/src/base.core.awk | 141 ++
awk/src/base.date.awk | 52 +
awk/src/base.tools.awk | 20 +
awk/src/csv.awk | 201 ++
awk/src/enc.base64.awk | 57 +
bash/TODO.md | 45 +
bash/src/TEMPLATE | 4 +
bash/src/_output_color.sh | 77 +
bash/src/_output_vanilla.sh | 66 +
bash/src/base.args.sh | 486 ++++
bash/src/base.array.sh | 360 +++
bash/src/base.bool.sh | 50 +
bash/src/base.core.sh | 458 ++++
bash/src/base.init.sh | 53 +
bash/src/base.input.sh | 581 +++++
bash/src/base.num.sh | 30 +
bash/src/base.output.sh | 601 +++++
bash/src/base.path.sh | 304 +++
bash/src/base.sh | 22 +
bash/src/base.split.sh | 188 ++
bash/src/base.str.sh | 140 ++
bash/src/base.tools.sh | 101 +
bash/src/donk.build.sh | 4 +
bash/src/donk.common.sh | 3 +
bash/src/donk.help.sh | 41 +
bash/src/fndate.sh | 45 +
bash/src/git.sh | 226 ++
bash/src/nulib.sh | 1 +
bash/src/pman.conf.sh | 21 +
bash/src/pman.sh | 416 ++++
bash/src/pman74.conf.sh | 16 +
bash/src/pman82.conf.sh | 16 +
bash/src/pretty.sh | 194 ++
bash/src/sysinfos.sh | 4 +
bash/src/template.sh | 225 ++
bash/src/tests.sh | 160 ++
bash/tests/.gitignore | 1 +
bash/tests/_template-dest.txt | 21 +
bash/tests/_template-source.txt | 23 +
bash/tests/_template-source_envs | 19 +
bash/tests/_template-values.env | 12 +
bash/tests/test-args-autodebug.sh | 17 +
bash/tests/test-args-autohelp.sh | 40 +
bash/tests/test-args-base.sh | 187 ++
bash/tests/test-args-help.sh | 18 +
bash/tests/test-input.sh | 18 +
bash/tests/test-output.sh | 169 ++
bash/tests/test-template.sh | 29 +
bin/_merge82 | 4 +
bin/_runphp_build-all | 30 +
bin/composer | 1 +
bin/nlman | 69 +
bin/nlshell | 38 +
bin/p | 67 +
bin/pdev | 197 ++
bin/pman | 250 ++
bin/prel | 239 ++
bin/runphp | 51 +
bin/templ.md | 38 +
bin/templ.sh | 56 +
bin/templ.sql | 40 +
bin/templ.yml | 38 +
composer.json | 47 +
composer.lock | 2021 +++++++++++++++++
dockerfiles/Dockerfile.adminer | 31 +
dockerfiles/Dockerfile.adminer+ic | 40 +
dockerfiles/Dockerfile.mariadb10 | 19 +
dockerfiles/Dockerfile.php-apache | 31 +
dockerfiles/Dockerfile.php-apache+ic | 44 +
dockerfiles/Dockerfile.php-cli | 30 +
dockerfiles/Dockerfile.php-cli+ic | 43 +
dockerfiles/Dockerfile.postgres15 | 17 +
lib/completion.d/pman | 37 +
lib/profile.d/nulib | 2 +
lib/setup.sh | 11 +
lib/uinst/conf | 6 +
lib/uinst/rootconf | 5 +
load.sh | 182 ++
php/run-tests | 5 +
php/src/A.php | 235 ++
php/src/AccessException.php | 36 +
php/src/ExceptionShadow.php | 101 +
php/src/ExitError.php | 31 +
php/src/IArrayWrapper.php | 11 +
php/src/NoMoreDataException.php | 10 +
php/src/StateException.php | 23 +
php/src/StopException.php | 12 +
php/src/UserException.php | 90 +
php/src/ValueException.php | 76 +
php/src/app/LockFile.php | 89 +
php/src/app/RunFile.php | 496 ++++
php/src/app/args.php | 39 +
php/src/app/cli/include-launcher.php | 29 +
php/src/cl.php | 769 +++++++
php/src/cv.php | 234 ++
php/src/db/Capacitor.php | 173 ++
php/src/db/CapacitorChannel.php | 407 ++++
php/src/db/CapacitorStorage.php | 632 ++++++
php/src/db/IDatabase.php | 19 +
php/src/db/ITransactor.php | 30 +
php/src/db/TODO.md | 7 +
php/src/db/_private/Tbindings.php | 36 +
php/src/db/_private/Tcreate.php | 39 +
php/src/db/_private/Tdelete.php | 38 +
php/src/db/_private/Tgeneric.php | 18 +
php/src/db/_private/Tinsert.php | 82 +
php/src/db/_private/Tselect.php | 168 ++
php/src/db/_private/Tupdate.php | 40 +
php/src/db/_private/Tvalues.php | 35 +
php/src/db/_private/_base.php | 285 +++
php/src/db/_private/_create.php | 12 +
php/src/db/_private/_delete.php | 11 +
php/src/db/_private/_generic.php | 7 +
php/src/db/_private/_insert.php | 13 +
php/src/db/_private/_select.php | 17 +
php/src/db/_private/_update.php | 14 +
php/src/db/cache/CacheChannel.php | 116 +
php/src/db/cache/RowsChannel.php | 51 +
php/src/db/cache/cache.php | 37 +
php/src/db/conds.php | 48 +
php/src/db/mysql/Mysql.php | 14 +
php/src/db/mysql/MysqlStorage.php | 46 +
php/src/db/mysql/_query_base.php | 52 +
php/src/db/mysql/_query_create.php | 10 +
php/src/db/mysql/_query_delete.php | 10 +
php/src/db/mysql/_query_generic.php | 10 +
php/src/db/mysql/_query_insert.php | 10 +
php/src/db/mysql/_query_select.php | 10 +
php/src/db/mysql/_query_update.php | 10 +
php/src/db/mysql/query.php | 12 +
php/src/db/pdo/Pdo.php | 277 +++
php/src/db/pdo/_config.php | 36 +
php/src/db/pdo/_query_base.php | 76 +
php/src/db/pdo/_query_create.php | 10 +
php/src/db/pdo/_query_delete.php | 10 +
php/src/db/pdo/_query_generic.php | 10 +
php/src/db/pdo/_query_insert.php | 10 +
php/src/db/pdo/_query_select.php | 10 +
php/src/db/pdo/_query_update.php | 10 +
php/src/db/sqlite/Sqlite.php | 335 +++
php/src/db/sqlite/SqliteException.php | 18 +
php/src/db/sqlite/SqliteStorage.php | 97 +
php/src/db/sqlite/_config.php | 36 +
php/src/db/sqlite/_migration.php | 55 +
php/src/db/sqlite/_query_base.php | 62 +
php/src/db/sqlite/_query_create.php | 10 +
php/src/db/sqlite/_query_delete.php | 10 +
php/src/db/sqlite/_query_generic.php | 10 +
php/src/db/sqlite/_query_insert.php | 10 +
php/src/db/sqlite/_query_select.php | 10 +
php/src/db/sqlite/_query_update.php | 10 +
php/src/ext/JsonException.php | 20 +
php/src/ext/json.php | 67 +
php/src/ext/yaml.php | 35 +
php/src/file.php | 91 +
php/src/file/FileReader.php | 51 +
php/src/file/FileWriter.php | 31 +
php/src/file/IReader.php | 43 +
php/src/file/IWriter.php | 30 +
php/src/file/MemoryStream.php | 21 +
php/src/file/SharedFile.php | 15 +
php/src/file/Stream.php | 500 ++++
php/src/file/TStreamFilter.php | 49 +
php/src/file/TempStream.php | 28 +
php/src/file/TmpfileWriter.php | 99 +
php/src/file/_File.php | 39 +
php/src/file/_IFile.php | 57 +
php/src/file/csv/CsvBuilder.php | 34 +
php/src/file/csv/CsvReader.php | 41 +
php/src/file/csv/csv_flavours.php | 59 +
php/src/file/tab/AbstractBuilder.php | 177 ++
php/src/file/tab/AbstractReader.php | 129 ++
php/src/file/tab/IBuilder.php | 14 +
php/src/file/tab/IReader.php | 7 +
php/src/file/tab/TAbstractBuilder.php | 56 +
php/src/file/tab/TAbstractReader.php | 55 +
php/src/file/web/Upload.php | 123 +
php/src/os/EOFException.php | 14 +
php/src/os/IOException.php | 42 +
php/src/os/README.md | 7 +
php/src/os/TODO.md | 8 +
php/src/os/path.php | 318 +++
php/src/os/proc/AbstractCmd.php | 201 ++
php/src/os/proc/AbstractCmdList.php | 53 +
php/src/os/proc/Cmd.php | 19 +
php/src/os/proc/CmdAnd.php | 13 +
php/src/os/proc/CmdOr.php | 13 +
php/src/os/proc/CmdPipe.php | 81 +
php/src/os/proc/ICmd.php | 82 +
php/src/os/sh.php | 363 +++
php/src/output/IMessenger.php | 107 +
php/src/output/TODO.md | 35 +
php/src/output/_messenger.php | 74 +
php/src/output/console.php | 28 +
php/src/output/log.php | 28 +
php/src/output/msg.php | 76 +
php/src/output/out.php | 34 +
php/src/output/say.php | 28 +
php/src/output/std/ProxyMessenger.php | 121 +
php/src/output/std/StdMessenger.php | 722 ++++++
php/src/output/std/StdOutput.php | 248 ++
php/src/output/std/_IMessenger.php | 19 +
php/src/php/ICloseable.php | 10 +
php/src/php/README.md | 5 +
php/src/php/akey.php | 99 +
php/src/php/coll/AutoArray.php | 44 +
php/src/php/coll/BaseArray.php | 117 +
php/src/php/content/IContent.php | 11 +
php/src/php/content/IPrintable.php | 10 +
php/src/php/content/Printer.php | 31 +
php/src/php/content/README.md | 77 +
php/src/php/content/c.php | 178 ++
php/src/php/func.php | 646 ++++++
php/src/php/iter/AbstractIterator.php | 154 ++
php/src/php/mprop.php | 122 +
php/src/php/nur_func.php | 453 ++++
php/src/php/oprop.php | 152 ++
php/src/php/time/Date.php | 20 +
php/src/php/time/DateInterval.php | 59 +
php/src/php/time/DateTime.php | 343 +++
php/src/php/time/Delay.php | 174 ++
php/src/php/time/Elapsed.php | 174 ++
php/src/php/valm.php | 84 +
php/src/php/valx.php | 84 +
php/src/ref/cli/ref_args.php | 85 +
php/src/ref/file/csv/ref_csv.php | 32 +
php/src/ref/php/ref_func.php | 12 +
php/src/ref/schema/ref_analyze.php | 25 +
php/src/ref/schema/ref_schema.php | 58 +
php/src/ref/schema/ref_types.php | 10 +
php/src/ref/web/ref_mimetypes.php | 12 +
php/src/str.php | 421 ++++
php/src/text/Word.php | 279 +++
php/src/text/words.php | 19 +
php/src/tools/BgLauncherApp.php | 124 +
php/src/tools/SteamTrainApp.php | 53 +
php/src/txt.php | 294 +++
php/src/web/curl/CurlException.php | 22 +
php/src/web/curl/curl.php | 59 +
php/src/web/http.php | 144 ++
php/src/web/params/F.php | 76 +
php/src/web/params/G.php | 32 +
php/src/web/params/P.php | 33 +
php/src/web/params/R.php | 23 +
php/src/web/uploads.php | 61 +
php/support/copy-templates | 39 +
php/support/template-.launcher.php | 12 +
php/support/template-_bg_launcher.php | 18 +
php/support/template-_wrapper.sh | 132 ++
php/tests/app/argsTest.php | 26 +
php/tests/appTest.php | 132 ++
php/tests/db/_private/_baseTest.php | 22 +
php/tests/db/sqlite/.gitignore | 1 +
php/tests/db/sqlite/SqliteStorageTest.php | 344 +++
php/tests/db/sqlite/SqliteTest.php | 146 ++
php/tests/db/sqlite/_queryTest.php | 125 +
php/tests/file/base/FileReaderTest.php | 63 +
php/tests/file/base/impl/avec_bom.csv | 2 +
php/tests/file/base/impl/avec_bom.txt | 1 +
php/tests/file/base/impl/msexcel.csv | 2 +
php/tests/file/base/impl/ooffice.csv | 2 +
php/tests/file/base/impl/sans_bom.txt | 1 +
php/tests/file/base/impl/weird.tsv | 2 +
php/tests/php/access/KeyAccessTest.php | 67 +
php/tests/php/access/ValueAccessTest.php | 70 +
php/tests/php/content/cTest.php | 40 +
php/tests/php/content/impl/AContent.php | 10 +
php/tests/php/content/impl/APrintable.php | 10 +
php/tests/php/content/impl/ATag.php | 23 +
php/tests/php/content/impl/html.php | 14 +
php/tests/php/funcTest.php | 1167 ++++++++++
php/tests/php/nur_funcTest.php | 292 +++
php/tests/php/time/DateTest.php | 85 +
php/tests/php/time/DateTimeTest.php | 109 +
php/tests/php/time/DelayTest.php | 83 +
php/tests/schema/_scalar/ScalarSchemaTest.php | 64 +
php/tests/schema/types/boolTest.php | 111 +
php/tests/schema/types/floatTest.php | 139 ++
php/tests/schema/types/intTest.php | 139 ++
php/tests/schema/types/strTest.php | 123 +
php/tests/schema/types/unionTest.php | 29 +
php/tests/strTest.php | 36 +
php/tests/web/uploadsTest.php | 200 ++
runphp/Dockerfile.runphp | 30 +
runphp/Dockerfile.runphp+ic | 43 +
runphp/build | 240 ++
runphp/dot-build.env.dist | 21 +
runphp/dot-dkbuild.env.dist | 12 +
runphp/dot-runphp.conf | 8 +
runphp/runphp | 641 ++++++
runphp/runphp.userconf.local | 3 +
runphp/template.sh | 22 +
runphp/update-runphp.sh | 125 +
sbin/composer.phar | Bin 0 -> 2384623 bytes
wip/_pci | 30 +
wip/_pp | 22 +
wip/_pu | 147 ++
wip/donk | 25 +
wip/pman.md | 14 +
317 files changed, 30781 insertions(+)
create mode 100644 .gitattributes
create mode 100644 .gitignore
create mode 100644 .idea/codeception.xml
create mode 100644 .idea/inspectionProfiles/Project_Default.xml
create mode 100644 .idea/modules.xml
create mode 100644 .idea/nulib.iml
create mode 100644 .idea/php-docker-settings.xml
create mode 100644 .idea/php-test-framework.xml
create mode 100644 .idea/php.xml
create mode 100644 .idea/phpspec.xml
create mode 100644 .idea/phpunit.xml
create mode 100644 .idea/vcs.xml
create mode 100644 .pman.conf
create mode 100644 .runphp.conf
create mode 100644 .udir
create mode 100644 TODO.md
create mode 100644 awk/src/base.array.awk
create mode 100644 awk/src/base.awk
create mode 100644 awk/src/base.core.awk
create mode 100644 awk/src/base.date.awk
create mode 100644 awk/src/base.tools.awk
create mode 100644 awk/src/csv.awk
create mode 100644 awk/src/enc.base64.awk
create mode 100644 bash/TODO.md
create mode 100644 bash/src/TEMPLATE
create mode 100644 bash/src/_output_color.sh
create mode 100644 bash/src/_output_vanilla.sh
create mode 100644 bash/src/base.args.sh
create mode 100644 bash/src/base.array.sh
create mode 100644 bash/src/base.bool.sh
create mode 100644 bash/src/base.core.sh
create mode 100644 bash/src/base.init.sh
create mode 100644 bash/src/base.input.sh
create mode 100644 bash/src/base.num.sh
create mode 100644 bash/src/base.output.sh
create mode 100644 bash/src/base.path.sh
create mode 100644 bash/src/base.sh
create mode 100644 bash/src/base.split.sh
create mode 100644 bash/src/base.str.sh
create mode 100644 bash/src/base.tools.sh
create mode 100644 bash/src/donk.build.sh
create mode 100644 bash/src/donk.common.sh
create mode 100644 bash/src/donk.help.sh
create mode 100644 bash/src/fndate.sh
create mode 100644 bash/src/git.sh
create mode 120000 bash/src/nulib.sh
create mode 100644 bash/src/pman.conf.sh
create mode 100644 bash/src/pman.sh
create mode 100644 bash/src/pman74.conf.sh
create mode 100644 bash/src/pman82.conf.sh
create mode 100644 bash/src/pretty.sh
create mode 100644 bash/src/sysinfos.sh
create mode 100644 bash/src/template.sh
create mode 100644 bash/src/tests.sh
create mode 100644 bash/tests/.gitignore
create mode 100644 bash/tests/_template-dest.txt
create mode 100644 bash/tests/_template-source.txt
create mode 100755 bash/tests/_template-source_envs
create mode 100644 bash/tests/_template-values.env
create mode 100755 bash/tests/test-args-autodebug.sh
create mode 100755 bash/tests/test-args-autohelp.sh
create mode 100755 bash/tests/test-args-base.sh
create mode 100755 bash/tests/test-args-help.sh
create mode 100755 bash/tests/test-input.sh
create mode 100755 bash/tests/test-output.sh
create mode 100755 bash/tests/test-template.sh
create mode 100755 bin/_merge82
create mode 100755 bin/_runphp_build-all
create mode 120000 bin/composer
create mode 100755 bin/nlman
create mode 100755 bin/nlshell
create mode 100755 bin/p
create mode 100755 bin/pdev
create mode 100755 bin/pman
create mode 100755 bin/prel
create mode 100755 bin/runphp
create mode 100755 bin/templ.md
create mode 100755 bin/templ.sh
create mode 100755 bin/templ.sql
create mode 100755 bin/templ.yml
create mode 100644 composer.json
create mode 100644 composer.lock
create mode 100644 dockerfiles/Dockerfile.adminer
create mode 100644 dockerfiles/Dockerfile.adminer+ic
create mode 100644 dockerfiles/Dockerfile.mariadb10
create mode 100644 dockerfiles/Dockerfile.php-apache
create mode 100644 dockerfiles/Dockerfile.php-apache+ic
create mode 100644 dockerfiles/Dockerfile.php-cli
create mode 100644 dockerfiles/Dockerfile.php-cli+ic
create mode 100644 dockerfiles/Dockerfile.postgres15
create mode 100644 lib/completion.d/pman
create mode 100644 lib/profile.d/nulib
create mode 100755 lib/setup.sh
create mode 100644 lib/uinst/conf
create mode 100644 lib/uinst/rootconf
create mode 100644 load.sh
create mode 100755 php/run-tests
create mode 100644 php/src/A.php
create mode 100644 php/src/AccessException.php
create mode 100644 php/src/ExceptionShadow.php
create mode 100644 php/src/ExitError.php
create mode 100644 php/src/IArrayWrapper.php
create mode 100644 php/src/NoMoreDataException.php
create mode 100644 php/src/StateException.php
create mode 100644 php/src/StopException.php
create mode 100644 php/src/UserException.php
create mode 100644 php/src/ValueException.php
create mode 100644 php/src/app/LockFile.php
create mode 100644 php/src/app/RunFile.php
create mode 100644 php/src/app/args.php
create mode 100644 php/src/app/cli/include-launcher.php
create mode 100644 php/src/cl.php
create mode 100644 php/src/cv.php
create mode 100644 php/src/db/Capacitor.php
create mode 100644 php/src/db/CapacitorChannel.php
create mode 100644 php/src/db/CapacitorStorage.php
create mode 100644 php/src/db/IDatabase.php
create mode 100644 php/src/db/ITransactor.php
create mode 100644 php/src/db/TODO.md
create mode 100644 php/src/db/_private/Tbindings.php
create mode 100644 php/src/db/_private/Tcreate.php
create mode 100644 php/src/db/_private/Tdelete.php
create mode 100644 php/src/db/_private/Tgeneric.php
create mode 100644 php/src/db/_private/Tinsert.php
create mode 100644 php/src/db/_private/Tselect.php
create mode 100644 php/src/db/_private/Tupdate.php
create mode 100644 php/src/db/_private/Tvalues.php
create mode 100644 php/src/db/_private/_base.php
create mode 100644 php/src/db/_private/_create.php
create mode 100644 php/src/db/_private/_delete.php
create mode 100644 php/src/db/_private/_generic.php
create mode 100644 php/src/db/_private/_insert.php
create mode 100644 php/src/db/_private/_select.php
create mode 100644 php/src/db/_private/_update.php
create mode 100644 php/src/db/cache/CacheChannel.php
create mode 100644 php/src/db/cache/RowsChannel.php
create mode 100644 php/src/db/cache/cache.php
create mode 100644 php/src/db/conds.php
create mode 100644 php/src/db/mysql/Mysql.php
create mode 100644 php/src/db/mysql/MysqlStorage.php
create mode 100644 php/src/db/mysql/_query_base.php
create mode 100644 php/src/db/mysql/_query_create.php
create mode 100644 php/src/db/mysql/_query_delete.php
create mode 100644 php/src/db/mysql/_query_generic.php
create mode 100644 php/src/db/mysql/_query_insert.php
create mode 100644 php/src/db/mysql/_query_select.php
create mode 100644 php/src/db/mysql/_query_update.php
create mode 100644 php/src/db/mysql/query.php
create mode 100644 php/src/db/pdo/Pdo.php
create mode 100644 php/src/db/pdo/_config.php
create mode 100644 php/src/db/pdo/_query_base.php
create mode 100644 php/src/db/pdo/_query_create.php
create mode 100644 php/src/db/pdo/_query_delete.php
create mode 100644 php/src/db/pdo/_query_generic.php
create mode 100644 php/src/db/pdo/_query_insert.php
create mode 100644 php/src/db/pdo/_query_select.php
create mode 100644 php/src/db/pdo/_query_update.php
create mode 100644 php/src/db/sqlite/Sqlite.php
create mode 100644 php/src/db/sqlite/SqliteException.php
create mode 100644 php/src/db/sqlite/SqliteStorage.php
create mode 100644 php/src/db/sqlite/_config.php
create mode 100644 php/src/db/sqlite/_migration.php
create mode 100644 php/src/db/sqlite/_query_base.php
create mode 100644 php/src/db/sqlite/_query_create.php
create mode 100644 php/src/db/sqlite/_query_delete.php
create mode 100644 php/src/db/sqlite/_query_generic.php
create mode 100644 php/src/db/sqlite/_query_insert.php
create mode 100644 php/src/db/sqlite/_query_select.php
create mode 100644 php/src/db/sqlite/_query_update.php
create mode 100644 php/src/ext/JsonException.php
create mode 100644 php/src/ext/json.php
create mode 100644 php/src/ext/yaml.php
create mode 100644 php/src/file.php
create mode 100644 php/src/file/FileReader.php
create mode 100644 php/src/file/FileWriter.php
create mode 100644 php/src/file/IReader.php
create mode 100644 php/src/file/IWriter.php
create mode 100644 php/src/file/MemoryStream.php
create mode 100644 php/src/file/SharedFile.php
create mode 100644 php/src/file/Stream.php
create mode 100644 php/src/file/TStreamFilter.php
create mode 100644 php/src/file/TempStream.php
create mode 100644 php/src/file/TmpfileWriter.php
create mode 100644 php/src/file/_File.php
create mode 100644 php/src/file/_IFile.php
create mode 100644 php/src/file/csv/CsvBuilder.php
create mode 100644 php/src/file/csv/CsvReader.php
create mode 100644 php/src/file/csv/csv_flavours.php
create mode 100644 php/src/file/tab/AbstractBuilder.php
create mode 100644 php/src/file/tab/AbstractReader.php
create mode 100644 php/src/file/tab/IBuilder.php
create mode 100644 php/src/file/tab/IReader.php
create mode 100644 php/src/file/tab/TAbstractBuilder.php
create mode 100644 php/src/file/tab/TAbstractReader.php
create mode 100644 php/src/file/web/Upload.php
create mode 100644 php/src/os/EOFException.php
create mode 100644 php/src/os/IOException.php
create mode 100644 php/src/os/README.md
create mode 100644 php/src/os/TODO.md
create mode 100644 php/src/os/path.php
create mode 100644 php/src/os/proc/AbstractCmd.php
create mode 100644 php/src/os/proc/AbstractCmdList.php
create mode 100644 php/src/os/proc/Cmd.php
create mode 100644 php/src/os/proc/CmdAnd.php
create mode 100644 php/src/os/proc/CmdOr.php
create mode 100644 php/src/os/proc/CmdPipe.php
create mode 100644 php/src/os/proc/ICmd.php
create mode 100644 php/src/os/sh.php
create mode 100644 php/src/output/IMessenger.php
create mode 100644 php/src/output/TODO.md
create mode 100644 php/src/output/_messenger.php
create mode 100644 php/src/output/console.php
create mode 100644 php/src/output/log.php
create mode 100644 php/src/output/msg.php
create mode 100644 php/src/output/out.php
create mode 100644 php/src/output/say.php
create mode 100644 php/src/output/std/ProxyMessenger.php
create mode 100644 php/src/output/std/StdMessenger.php
create mode 100644 php/src/output/std/StdOutput.php
create mode 100644 php/src/output/std/_IMessenger.php
create mode 100644 php/src/php/ICloseable.php
create mode 100644 php/src/php/README.md
create mode 100644 php/src/php/akey.php
create mode 100644 php/src/php/coll/AutoArray.php
create mode 100644 php/src/php/coll/BaseArray.php
create mode 100644 php/src/php/content/IContent.php
create mode 100644 php/src/php/content/IPrintable.php
create mode 100644 php/src/php/content/Printer.php
create mode 100644 php/src/php/content/README.md
create mode 100644 php/src/php/content/c.php
create mode 100644 php/src/php/func.php
create mode 100644 php/src/php/iter/AbstractIterator.php
create mode 100644 php/src/php/mprop.php
create mode 100644 php/src/php/nur_func.php
create mode 100644 php/src/php/oprop.php
create mode 100644 php/src/php/time/Date.php
create mode 100644 php/src/php/time/DateInterval.php
create mode 100644 php/src/php/time/DateTime.php
create mode 100644 php/src/php/time/Delay.php
create mode 100644 php/src/php/time/Elapsed.php
create mode 100644 php/src/php/valm.php
create mode 100644 php/src/php/valx.php
create mode 100644 php/src/ref/cli/ref_args.php
create mode 100644 php/src/ref/file/csv/ref_csv.php
create mode 100644 php/src/ref/php/ref_func.php
create mode 100644 php/src/ref/schema/ref_analyze.php
create mode 100644 php/src/ref/schema/ref_schema.php
create mode 100644 php/src/ref/schema/ref_types.php
create mode 100644 php/src/ref/web/ref_mimetypes.php
create mode 100644 php/src/str.php
create mode 100644 php/src/text/Word.php
create mode 100644 php/src/text/words.php
create mode 100644 php/src/tools/BgLauncherApp.php
create mode 100644 php/src/tools/SteamTrainApp.php
create mode 100644 php/src/txt.php
create mode 100644 php/src/web/curl/CurlException.php
create mode 100644 php/src/web/curl/curl.php
create mode 100644 php/src/web/http.php
create mode 100644 php/src/web/params/F.php
create mode 100644 php/src/web/params/G.php
create mode 100644 php/src/web/params/P.php
create mode 100644 php/src/web/params/R.php
create mode 100644 php/src/web/uploads.php
create mode 100755 php/support/copy-templates
create mode 100644 php/support/template-.launcher.php
create mode 100644 php/support/template-_bg_launcher.php
create mode 100644 php/support/template-_wrapper.sh
create mode 100644 php/tests/app/argsTest.php
create mode 100644 php/tests/appTest.php
create mode 100644 php/tests/db/_private/_baseTest.php
create mode 100644 php/tests/db/sqlite/.gitignore
create mode 100644 php/tests/db/sqlite/SqliteStorageTest.php
create mode 100644 php/tests/db/sqlite/SqliteTest.php
create mode 100644 php/tests/db/sqlite/_queryTest.php
create mode 100644 php/tests/file/base/FileReaderTest.php
create mode 100644 php/tests/file/base/impl/avec_bom.csv
create mode 100644 php/tests/file/base/impl/avec_bom.txt
create mode 100644 php/tests/file/base/impl/msexcel.csv
create mode 100644 php/tests/file/base/impl/ooffice.csv
create mode 100644 php/tests/file/base/impl/sans_bom.txt
create mode 100644 php/tests/file/base/impl/weird.tsv
create mode 100644 php/tests/php/access/KeyAccessTest.php
create mode 100644 php/tests/php/access/ValueAccessTest.php
create mode 100644 php/tests/php/content/cTest.php
create mode 100644 php/tests/php/content/impl/AContent.php
create mode 100644 php/tests/php/content/impl/APrintable.php
create mode 100644 php/tests/php/content/impl/ATag.php
create mode 100644 php/tests/php/content/impl/html.php
create mode 100644 php/tests/php/funcTest.php
create mode 100644 php/tests/php/nur_funcTest.php
create mode 100644 php/tests/php/time/DateTest.php
create mode 100644 php/tests/php/time/DateTimeTest.php
create mode 100644 php/tests/php/time/DelayTest.php
create mode 100644 php/tests/schema/_scalar/ScalarSchemaTest.php
create mode 100644 php/tests/schema/types/boolTest.php
create mode 100644 php/tests/schema/types/floatTest.php
create mode 100644 php/tests/schema/types/intTest.php
create mode 100644 php/tests/schema/types/strTest.php
create mode 100644 php/tests/schema/types/unionTest.php
create mode 100644 php/tests/strTest.php
create mode 100644 php/tests/web/uploadsTest.php
create mode 100644 runphp/Dockerfile.runphp
create mode 100644 runphp/Dockerfile.runphp+ic
create mode 100755 runphp/build
create mode 100644 runphp/dot-build.env.dist
create mode 100644 runphp/dot-dkbuild.env.dist
create mode 100644 runphp/dot-runphp.conf
create mode 100755 runphp/runphp
create mode 100644 runphp/runphp.userconf.local
create mode 100755 runphp/template.sh
create mode 100755 runphp/update-runphp.sh
create mode 100755 sbin/composer.phar
create mode 100755 wip/_pci
create mode 100755 wip/_pp
create mode 100755 wip/_pu
create mode 100755 wip/donk
create mode 100644 wip/pman.md
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..ef72495
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+/sbin/composer.phar -delta
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..62033b5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+/.composer.lock.runphp
+
+.~lock*#
+.*.swp
+/vendor/
+
+/.idea/shelf/
+/.idea/workspace.xml
+/.idea/httpRequests/
+/.idea/dataSources/
+/.idea/dataSources.local.xml
+/.phpunit.result.cache
diff --git a/.idea/codeception.xml b/.idea/codeception.xml
new file mode 100644
index 0000000..330f2dd
--- /dev/null
+++ b/.idea/codeception.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..f0e209f
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..0bb2642
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/nulib.iml b/.idea/nulib.iml
new file mode 100644
index 0000000..2f97961
--- /dev/null
+++ b/.idea/nulib.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/php-docker-settings.xml b/.idea/php-docker-settings.xml
new file mode 100644
index 0000000..d039669
--- /dev/null
+++ b/.idea/php-docker-settings.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/php-test-framework.xml b/.idea/php-test-framework.xml
new file mode 100644
index 0000000..23ef5c6
--- /dev/null
+++ b/.idea/php-test-framework.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/php.xml b/.idea/php.xml
new file mode 100644
index 0000000..7498661
--- /dev/null
+++ b/.idea/php.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/phpspec.xml b/.idea/phpspec.xml
new file mode 100644
index 0000000..33107fd
--- /dev/null
+++ b/.idea/phpspec.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/phpunit.xml b/.idea/phpunit.xml
new file mode 100644
index 0000000..4f8104c
--- /dev/null
+++ b/.idea/phpunit.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.pman.conf b/.pman.conf
new file mode 100644
index 0000000..ba5edad
--- /dev/null
+++ b/.pman.conf
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+UPSTREAM=
+DEVELOP=dev74
+FEATURE=wip74/
+RELEASE=rel74-
+MAIN=dist74
+TAG_SUFFIX=p74
+HOTFIX=hotf74-
+DIST=
+NOAUTO=
diff --git a/.runphp.conf b/.runphp.conf
new file mode 100644
index 0000000..75c5696
--- /dev/null
+++ b/.runphp.conf
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+# Chemin vers runphp, e.g sbin/runphp
+RUNPHP=
+
+# Si RUNPHP n'est pas défini, les variables suivantes peuvent être définies
+DIST=d11
+#REGISTRY=pubdocker.univ-reunion.fr/dist
diff --git a/.udir b/.udir
new file mode 100644
index 0000000..1f19bde
--- /dev/null
+++ b/.udir
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+# Utiliser 'udir --help-vars' pour une description de la signification des
+# variables suivantes:
+udir_desc="librairies de base pour scripts bash, awk, php, python"
+udir_note=""
+udir_types=(uinst)
+uinc=release
+uinc_options=()
+uinc_args=()
+preconfig_scripts=()
+configure_variables=(dest)
+configure_dest_for=(lib/profile.d/nulib)
+config_scripts=(lib/uinst/conf)
+install_profiles=true
+profiledir=lib/profile.d
+bashrcdir=lib/bashrc.d
+defaultdir=lib/default
+workdir_rsync_options=()
+workdir_excludes=()
+workdir_includes=()
+copy_files=true
+destdir=/opt
+destdir_override_userhost=
+destdir_ssh=
+destdir_force_remote=
+srcdir=.
+files=()
+owner=root:
+modes=(u=rwX,g=rX,o=rX)
+root_scripts=(lib/uinst/rootconf)
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..5ea289c
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,15 @@
+# nulib
+
+* runners
+ * [ ] rnlphp -- lancer un programme php avec la bonne version (+docker le cas échéant)
+ * [ ] utilisable en shebang
+ * [ ] utilisable en tant que lien: lance `../php/bin/$MYNAME.php`
+ * [ ] frontend pour composer
+ * [ ] rnljava -- lancer un programme java avec la bonne version (+docker le cas échéant)
+ * [ ] frontend pour maven
+ * [ ] rnlawk -- lancer un script awk
+ * [ ] rnlpy3 -- lancer un script python3
+ * [ ] rnlsh -- lancer un shell avec les librairies bash / lancer un script
+* MYTRUEDIR, MYTRUENAME, MYTRUESELF -- résoudre les liens symboliques
+
+-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary
\ No newline at end of file
diff --git a/awk/src/base.array.awk b/awk/src/base.array.awk
new file mode 100644
index 0000000..bd5ac32
--- /dev/null
+++ b/awk/src/base.array.awk
@@ -0,0 +1,157 @@
+# -*- coding: utf-8 mode: awk -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+function mkindices(values, indices, i, j) {
+ array_new(indices)
+ j = 1
+ for (i in values) {
+ indices[j++] = int(i)
+ }
+ return asort(indices)
+}
+function array_new(dest) {
+ dest[0] = 0 # forcer awk à considérer dest comme un tableau
+ delete dest
+}
+function array_newsize(dest, size, i) {
+ dest[0] = 0 # forcer awk à considérer dest comme un tableau
+ delete dest
+ size = int(size)
+ for (i = 1; i <= size; i++) {
+ dest[i] = ""
+ }
+}
+function array_len(values, count, i) {
+ # length(array) a un bug sur awk 3.1.5
+ # cette version est plus lente mais fonctionne toujours
+ count = 0
+ for (i in values) {
+ count++
+ }
+ return count
+}
+function array_copy(dest, src, count, indices, i) {
+ array_new(dest)
+ count = mkindices(src, indices)
+ for (i = 1; i <= count; i++) {
+ dest[indices[i]] = src[indices[i]]
+ }
+}
+function array_getlastindex(src, count, indices) {
+ count = mkindices(src, indices)
+ if (count == 0) return 0
+ return indices[count]
+}
+function array_add(dest, value, lastindex) {
+ lastindex = array_getlastindex(dest)
+ dest[lastindex + 1] = value
+}
+function array_deli(dest, i, l) {
+ i = int(i)
+ if (i == 0) return
+ l = array_len(dest)
+ while (i < l) {
+ dest[i] = dest[i + 1]
+ i++
+ }
+ delete dest[l]
+}
+function array_del(dest, value, ignoreCase, i) {
+ do {
+ i = key_index(value, dest, ignoreCase)
+ if (i != 0) array_deli(dest, i)
+ } while (i != 0)
+}
+function array_extend(dest, src, count, lastindex, indices, i) {
+ lastindex = array_getlastindex(dest)
+ count = mkindices(src, indices)
+ for (i = 1; i <= count; i++) {
+ dest[lastindex + i] = src[indices[i]]
+ }
+}
+function array_fill(dest, i) {
+ array_new(dest)
+ for (i = 1; i <= NF; i++) {
+ dest[i] = $i
+ }
+}
+function array_getline(src, count, indices, i, j) {
+ $0 = ""
+ count = mkindices(src, indices)
+ for (i = 1; i <= count; i++) {
+ j = indices[i]
+ $j = src[j]
+ }
+}
+function array_appendline(src, count, indices, i, nf, j) {
+ count = mkindices(src, indices)
+ nf = NF
+ for (i = 1; i <= count; i++) {
+ j = nf + indices[i]
+ $j = src[indices[i]]
+ }
+}
+function in_array(value, values, ignoreCase, i) {
+ if (ignoreCase) {
+ value = tolower(value)
+ for (i in values) {
+ if (tolower(values[i]) == value) return 1
+ }
+ } else {
+ for (i in values) {
+ if (values[i] == value) return 1
+ }
+ }
+ return 0
+}
+function key_index(value, values, ignoreCase, i) {
+ if (ignoreCase) {
+ value = tolower(value)
+ for (i in values) {
+ if (tolower(values[i]) == value) return int(i)
+ }
+ } else {
+ for (i in values) {
+ if (values[i] == value) return int(i)
+ }
+ }
+ return 0
+}
+function array2s(values, prefix, sep, suffix, noindices, first, i, s) {
+ if (!prefix) prefix = "["
+ if (!sep) sep = ", "
+ if (!suffix) suffix = "]"
+ s = prefix
+ first = 1
+ for (i in values) {
+ if (first) first = 0
+ else s = s sep
+ if (!noindices) s = s "[" i "]="
+ s = s values[i]
+ }
+ s = s suffix
+ return s
+}
+function array2so(values, prefix, sep, suffix, noindices, count, indices, i, s) {
+ if (!prefix) prefix = "["
+ if (!sep) sep = ", "
+ if (!suffix) suffix = "]"
+ s = prefix
+ count = mkindices(values, indices)
+ for (i = 1; i <= count; i++) {
+ if (i > 1) s = s sep
+ if (!noindices) s = s "[" indices[i] "]="
+ s = s values[indices[i]]
+ }
+ s = s suffix
+ return s
+}
+function array_join(values, sep, prefix, suffix, count, indices, i, s) {
+ s = prefix
+ count = mkindices(values, indices)
+ for (i = 1; i <= count; i++) {
+ if (i > 1) s = s sep
+ s = s values[indices[i]]
+ }
+ s = s suffix
+ return s
+}
diff --git a/awk/src/base.awk b/awk/src/base.awk
new file mode 100644
index 0000000..65c79fe
--- /dev/null
+++ b/awk/src/base.awk
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 mode: awk -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+@include "base.core.awk"
+@include "base.array.awk"
+@include "base.date.awk"
+@include "base.tools.awk"
diff --git a/awk/src/base.core.awk b/awk/src/base.core.awk
new file mode 100644
index 0000000..49a4b58
--- /dev/null
+++ b/awk/src/base.core.awk
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 mode: awk -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+function num(s) {
+ if (s ~ /^[0-9]+$/) return int(s)
+ else return s
+}
+function ord(s, i) {
+ s = substr(s, 1, 1)
+ i = index(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", s)
+ if (i != 0) i += 32 - 1
+ return i
+}
+function hex(i, s) {
+ s = sprintf("%x", i)
+ if (length(s) < 2) s = "0" s
+ return s
+}
+function qhtml(s) {
+ gsub(/&/, "\\&", s)
+ gsub(/"/, "\\"", s)
+ gsub(/>/, "\\>", s)
+ gsub(/, "\\<", s)
+ return s
+}
+function unquote_html(s) {
+ gsub(/</, "<", s)
+ gsub(/>/, ">", s)
+ gsub(/"/, "\"", s)
+ gsub(/&/, "\\&", s)
+ return s
+}
+function qawk(s) {
+ gsub(/\\/, "\\\\", s)
+ gsub(/"/, "\\\"", s)
+ gsub(/\n/, "\\n", s)
+ return "\"" s "\""
+}
+function qval(s) {
+ gsub(/'/, "'\\''", s)
+ return "'" s "'"
+}
+function sqval(s) {
+ return " " qval(s)
+}
+function qvals( i, line) {
+ line = ""
+ for (i = 1; i <= NF; i++) {
+ if (i > 1) line = line " "
+ line = line qval($i)
+ }
+ return line
+}
+function sqvals() {
+ return " " qvals()
+}
+function qarr(values, prefix, i, count, line) {
+ line = prefix
+ count = array_len(values)
+ for (i = 1; i <= count; i++) {
+ if (i > 1 || line != "") line = line " "
+ line = line qval(values[i])
+ }
+ return line
+}
+function qregexp(s) {
+ gsub(/[[\\.^$*+?()|{]/, "\\\\&", s)
+ return s
+}
+function qsubrepl(s) {
+ gsub(/\\/, "\\\\", s)
+ gsub(/&/, "\\\\&", s)
+ return s
+}
+function qgrep(s) {
+ gsub(/[[\\.^$*]/, "\\\\&", s)
+ return s
+}
+function qegrep(s) {
+ gsub(/[[\\.^$*+?()|{]/, "\\\\&", s)
+ return s
+}
+function qsql(s, suffix) {
+ gsub(/'/, "''", s)
+ return "'" s "'" (suffix != ""? " " suffix: "")
+}
+function cqsql(s, suffix) {
+ return "," qsql(s, suffix)
+}
+function unquote_mysqlcsv(s) {
+ gsub(/\\n/, "\n", s)
+ gsub(/\\t/, "\t", s)
+ gsub(/\\0/, "\0", s)
+ gsub(/\\\\/, "\\", s)
+ return s
+}
+function sval(s) {
+ if (s == "") return s
+ else return " " s
+}
+function cval(s, suffix) {
+ suffix = suffix != ""? " " suffix: ""
+ if (s == "") return s
+ else return "," s suffix
+}
+
+function printto(s, output) {
+ if (output == "") {
+ print s
+ } else if (output ~ /^>>/) {
+ sub(/^>>/, "", output)
+ print s >>output
+ } else if (output ~ /^>/) {
+ sub(/^>/, "", output)
+ print s >output
+ } else if (output ~ /^\|&/) {
+ sub(/^\|&/, "", output)
+ print s |&output
+ } else if (output ~ /^\|/) {
+ sub(/^\|/, "", output)
+ print s |output
+ } else {
+ print s >output
+ }
+}
+function find_line(input, field, value, orig, line) {
+ orig = $0
+ line = ""
+ while ((getline 0) {
+ if ($field == value) {
+ line = $0
+ break
+ }
+ }
+ close(input)
+ $0 = orig
+ return line
+}
+function merge_line(input, field, key, line) {
+ line = find_line(input, field, $key)
+ if (line != "") $0 = $0 FS line
+}
diff --git a/awk/src/base.date.awk b/awk/src/base.date.awk
new file mode 100644
index 0000000..48e3eff
--- /dev/null
+++ b/awk/src/base.date.awk
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 mode: awk -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+function date__parse_fr(date, parts, y, m, d) {
+ if (match(date, /([0-9][0-9]?)\/([0-9][0-9]?)\/([0-9][0-9][0-9][0-9])/, parts)) {
+ y = int(parts[3])
+ m = int(parts[2])
+ d = int(parts[1])
+ return mktime(sprintf("%04i %02i %02i 00 00 00 +0400", y, m, d))
+ } else if (match(date, /([0-9][0-9]?)\/([0-9][0-9]?)\/([0-9][0-9])/, parts)) {
+ basey = int(strftime("%Y")); basey = basey - basey % 100
+ y = basey + int(parts[3])
+ m = int(parts[2])
+ d = int(parts[1])
+ return mktime(sprintf("%04i %02i %02i 00 00 00 +0400", y, m, d))
+ }
+ return -1
+}
+function date__parse_mysql(date, parts, y, m, d) {
+ if (match(date, /([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])/, parts)) {
+ y = int(parts[1])
+ m = int(parts[2])
+ d = int(parts[3])
+ return mktime(sprintf("%04i %02i %02i 00 00 00 +0400", y, m, d))
+ }
+ return -1
+}
+function date__parse_any(date, serial) {
+ serial = date__parse_fr(date)
+ if (serial == -1) serial = date__parse_mysql(date)
+ return serial
+}
+function date_serial(date) {
+ return date__parse_any(date)
+}
+function date_parse(date, serial) {
+ serial = date__parse_any(date)
+ if (serial == -1) return date
+ return strftime("%d/%m/%Y", serial)
+}
+function date_monday(date, serial, dow) {
+ serial = date__parse_any(date)
+ if (serial == -1) return date
+ dow = strftime("%u", serial)
+ serial -= (dow - 1) * 86400
+ return strftime("%d/%m/%Y", serial)
+}
+function date_add(date, nbdays, serial) {
+ serial = date__parse_any(date)
+ if (serial == -1) return date
+ serial += nbdays * 86400
+ return strftime("%d/%m/%Y", serial)
+}
diff --git a/awk/src/base.tools.awk b/awk/src/base.tools.awk
new file mode 100644
index 0000000..64f6d89
--- /dev/null
+++ b/awk/src/base.tools.awk
@@ -0,0 +1,20 @@
+BEGIN {
+ srand()
+}
+
+function get_random_password( password, max, LETTERS) {
+ LETTERS = "AZERTYUIOPQSDFGHJKLMWXCVBNazertyuiopqsdfghjklmwxcvbn0123456789"
+ max = length(LETTERS)
+ password = ""
+ for (i = 0; i < 16; i++) {
+ password = password substr(LETTERS, int(rand() * max), 1)
+ }
+ return password
+}
+
+function should_generate_password() {
+ return $0 ~ /XXXRANDOMXXX/
+}
+function generate_password() {
+ sub(/XXXRANDOMXXX/, get_random_password())
+}
diff --git a/awk/src/csv.awk b/awk/src/csv.awk
new file mode 100644
index 0000000..c58e41b
--- /dev/null
+++ b/awk/src/csv.awk
@@ -0,0 +1,201 @@
+# -*- coding: utf-8 mode: awk -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+@include "base.core.awk"
+@include "base.array.awk"
+
+function csv__parse_quoted(line, destl, colsep, qchar, echar, pos, tmpl, nextc, resl) {
+ line = substr(line, 2)
+ resl = ""
+ while (1) {
+ pos = index(line, qchar)
+ if (pos == 0) {
+ # chaine mal terminee
+ resl = resl line
+ destl[0] = ""
+ destl[1] = 0
+ return resl
+ }
+ if (echar != "" && pos > 1) {
+ # tenir compte du fait qu"un caratère peut être mis en échappement
+ prevc = substr(line, pos - 1, 1)
+ quotec = substr(line, pos, 1)
+ nextc = substr(line, pos + 1, 1)
+ if (prevc == echar) {
+ # qchar en échappement
+ tmpl = substr(line, 1, pos - 2)
+ resl = resl tmpl quotec
+ line = substr(line, pos + 1)
+ continue
+ }
+ tmpl = substr(line, 1, pos - 1)
+ if (nextc == colsep || nextc == "") {
+ # fin de champ ou fin de ligne
+ resl = resl tmpl
+ destl[0] = substr(line, pos + 2)
+ destl[1] = nextc == colsep
+ return resl
+ } else {
+ # erreur de syntaxe: guillemet non mis en échappement
+ # ignorer cette erreur et prendre le guillemet quand meme
+ resl = resl tmpl quotec
+ line = substr(line, pos + 1)
+ }
+ } else {
+ # pas d"échappement pour qchar. il est éventuellement doublé
+ tmpl = substr(line, 1, pos - 1)
+ quotec = substr(line, pos, 1)
+ nextc = substr(line, pos + 1, 1)
+ if (nextc == colsep || nextc == "") {
+ # fin de champ ou fin de ligne
+ resl = resl tmpl
+ destl[0] = substr(line, pos + 2)
+ destl[1] = nextc == colsep
+ return resl
+ } else if (nextc == qchar) {
+ # qchar en echappement
+ resl = resl tmpl quotec
+ line = substr(line, pos + 2)
+ } else {
+ # erreur de syntaxe: guillemet non mis en échappement
+ # ignorer cette erreur et prendre le guillemet quand meme
+ resl = resl tmpl quotec
+ line = substr(line, pos + 1)
+ }
+ }
+ }
+}
+function csv__parse_unquoted(line, destl, colsep, qchar, echar, pos) {
+ pos = index(line, colsep)
+ if (pos == 0) {
+ destl[0] = ""
+ destl[1] = 0
+ return line
+ } else {
+ destl[0] = substr(line, pos + 1)
+ destl[1] = 1
+ return substr(line, 1, pos - 1)
+ }
+}
+function csv__array_parse(fields, line, nbfields, colsep, qchar, echar, shouldparse, destl, i) {
+ array_new(fields)
+ array_new(destl)
+ i = 1
+ shouldparse = 0
+ # shouldparse permet de gérer le cas où un champ vide est en fin de ligne.
+ # en effet, après "," il faut toujours parser, même si line==""
+ while (shouldparse || line != "") {
+ if (index(line, qchar) == 1) {
+ value = csv__parse_quoted(line, destl, colsep, qchar, echar)
+ line = destl[0]
+ shouldparse = destl[1]
+ } else {
+ value = csv__parse_unquoted(line, destl, colsep, qchar, echar)
+ line = destl[0]
+ shouldparse = destl[1]
+ }
+ fields[i] = value
+ i = i + 1
+ }
+ if (nbfields) {
+ nbfields = int(nbfields)
+ i = array_len(fields)
+ while (i < nbfields) {
+ i++
+ fields[i] = ""
+ }
+ }
+ return array_len(fields)
+}
+BEGIN {
+ DEFAULT_COLSEP = ","
+ DEFAULT_QCHAR = "\""
+ DEFAULT_ECHAR = ""
+}
+function array_parsecsv2(fields, line, nbfields, colsep, qchar, echar) {
+ return csv__array_parse(fields, line, nbfields, colsep, qchar, echar)
+}
+function array_parsecsv(fields, line, nbfields, colsep, qchar, echar) {
+ if (colsep == "") colsep = DEFAULT_COLSEP
+ if (qchar == "") qchar = DEFAULT_QCHAR
+ if (echar == "") echar = DEFAULT_ECHAR
+ return csv__array_parse(fields, line, nbfields, colsep, qchar, echar)
+}
+function parsecsv(line, fields) {
+ array_parsecsv(fields, line)
+ array_getline(fields)
+ return NF
+}
+function getlinecsv(file, fields) {
+ if (file) {
+ getline 1) line = line colsep
+ if (qchar != "" && index(value, qchar) != 0) {
+ if (echar != "") gsub(qchar, quote_subrepl(echar) "&", value);
+ else gsub(qchar, "&&", value);
+ }
+ if (qchar != "" && (index(value, mvsep) != 0 || index(value, colsep) != 0 || index(value, qchar) != 0 || csv__should_quote(value))) {
+ line = line qchar value qchar
+ } else {
+ line = line value
+ }
+ }
+ return line
+}
+function array_formatcsv(fields) {
+ return array_formatcsv2(fields, ",", ";", "\"", "")
+}
+function array_printcsv(fields, output) {
+ printto(array_formatcsv(fields), output)
+}
+function get_formatcsv( fields) {
+ array_fill(fields)
+ return array_formatcsv(fields)
+}
+function formatcsv() {
+ $0 = get_formatcsv()
+}
+function printcsv(output, fields) {
+ array_fill(fields)
+ array_printcsv(fields, output)
+}
+function array_findcsv(fields, input, field, value, nbfields, orig, found, i) {
+ array_new(orig)
+ array_fill(orig)
+ array_new(fields)
+ found = 0
+ while ((getline 0) {
+ array_parsecsv(fields, $0, nbfields)
+ if (fields[field] == value) {
+ found = 1
+ break
+ }
+ }
+ close(input)
+ array_getline(orig)
+ if (!found) {
+ delete fields
+ if (nbfields) {
+ nbfields = int(nbfields)
+ i = array_len(fields)
+ while (i < nbfields) {
+ i++
+ fields[i] = ""
+ }
+ }
+ }
+ return found
+}
diff --git a/awk/src/enc.base64.awk b/awk/src/enc.base64.awk
new file mode 100644
index 0000000..3ce38e2
--- /dev/null
+++ b/awk/src/enc.base64.awk
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 mode: awk -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+function base64__and(var, x, l_res, l_i) {
+ l_res = 0
+ for (l_i = 0; l_i < 8; l_i++) {
+ if (var%2 == 1 && x%2 == 1) l_res = l_res/2 + 128
+ else l_res /= 2
+ var = int(var/2)
+ x = int(x/2)
+ }
+ return l_res
+}
+# Rotate bytevalue left x times
+function base64__lshift(var, x) {
+ while(x > 0) {
+ var *= 2
+ x--
+ }
+ return var
+}
+# Rotate bytevalue right x times
+function base64__rshift(var, x) {
+ while(x > 0) {
+ var = int(var/2)
+ x--
+ }
+ return var
+}
+BEGIN {
+ BASE64__BYTES = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+}
+function b64decode(src, result, base1, base2, base3, base4) {
+ result = ""
+ while (length(src) > 0) {
+ # Specify byte values
+ base1 = substr(src, 1, 1)
+ base2 = substr(src, 2, 1)
+ base3 = substr(src, 3, 1); if (base3 == "") base3 = "="
+ base4 = substr(src, 4, 1); if (base4 == "") base4 = "="
+ # Now find numerical position in BASE64 string
+ byte1 = index(BASE64__BYTES, base1) - 1
+ if (byte1 < 0) byte1 = 0
+ byte2 = index(BASE64__BYTES, base2) - 1
+ if (byte2 < 0) byte2 = 0
+ byte3 = index(BASE64__BYTES, base3) - 1
+ if (byte3 < 0) byte3 = 0
+ byte4 = index(BASE64__BYTES, base4) - 1
+ if (byte4 < 0) byte4 = 0
+ # Reconstruct ASCII string
+ result = result sprintf( "%c", base64__lshift(base64__and(byte1, 63), 2) + base64__rshift(base64__and(byte2, 48), 4) )
+ if (base3 != "=") result = result sprintf( "%c", base64__lshift(base64__and(byte2, 15), 4) + base64__rshift(base64__and(byte3, 60), 2) )
+ if (base4 != "=") result = result sprintf( "%c", base64__lshift(base64__and(byte3, 3), 6) + byte4 )
+ # Decrease incoming string with 4
+ src = substr(src, 5)
+ }
+ return result
+}
diff --git a/bash/TODO.md b/bash/TODO.md
new file mode 100644
index 0000000..9eca406
--- /dev/null
+++ b/bash/TODO.md
@@ -0,0 +1,45 @@
+# nulib/bash
+
+## template
+
+* [x] pour tout fichier source `.file.template`, considérer avant
+ `file.template.local` s'il existe, ce qui permet à un utilisateur de
+ remplacer le modèle livré.
+ cela a-t-il du sens de supporter aussi file.dist.local? vu que ça ne sert
+ qu'une seule fois? ça ne mange pas de pain...
+
+## args
+
+* [x] support des couples d'options --option et --no-option qui mettent à jour
+ tous les deux la variables option. ceci:
+ ~~~
+ --option .
+ --no-option .
+ ~~~
+ est équivalent à ceci:
+ ~~~
+ --option '$inc@ option'
+ --no-option '$dec@ option'
+ ~~~
+ dec@ est une nouvelle fonction qui décrémente et remplace par une chaine vide
+ quand on arrive à zéro
+* [x] args: support des noms d'argument pour améliorer l'affichage de l'aide.
+ par exemple la définition
+ ~~~
+ -f:file,--input input= "spécifier le fichier en entrée"
+ ~~~
+ donnera cette aide:
+ ~~~
+ -f, --input FILE
+ spécifier le fichier
+ ~~~
+* [ ] args: après le support des noms d'arguments, ajouter la génération
+ automatique de l'auto-complétion basée sur ces informations. certains noms
+ seraient normalisés: `file` pour un fichier, `dir` pour un répertoire, `env`
+ pour une variable d'environnement, etc.
+ on pourrait même considérer mettre des patterns pour la sélection, e.g
+ ~~~
+ "-C,--config:file:*.conf *.cnf" input= "spécifier le fichier de configuration"
+ ~~~
+
+-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary
\ No newline at end of file
diff --git a/bash/src/TEMPLATE b/bash/src/TEMPLATE
new file mode 100644
index 0000000..aba64fa
--- /dev/null
+++ b/bash/src/TEMPLATE
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: TEMPLATE "DESCRIPTION"
+
diff --git a/bash/src/_output_color.sh b/bash/src/_output_color.sh
new file mode 100644
index 0000000..ce0dd77
--- /dev/null
+++ b/bash/src/_output_color.sh
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+function __esection() {
+ local -a lines
+ local lsep prefix="$(__edate)$(__eindent0)"
+ local length="${COLUMNS:-80}"
+ setx lsep=__complete "$prefix" "$length" -
+
+ tooenc "$COULEUR_BLEUE$lsep$COULEUR_NORMALE"
+ [ -n "$*" ] || return 0
+ length=$((length - 1))
+ setx -a lines=echo "$1"
+ for line in "${lines[@]}"; do
+ setx line=__complete "$prefix- $line" "$length"
+ tooenc "$COULEUR_BLEUE$line-$COULEUR_NORMALE"
+ done
+ tooenc "$COULEUR_BLEUE$lsep$COULEUR_NORMALE"
+}
+function __etitle() {
+ local -a lines; local maxlen=0
+ local prefix="$(__edate)$(__eindent0)"
+
+ setx -a lines=echo "$1"
+ for line in "${lines[@]}"; do
+ [ ${#line} -gt $maxlen ] && maxlen=${#line}
+ tooenc "${prefix}${COULEUR_BLEUE}T $line$COULEUR_NORMALE"
+ done
+ maxlen=$((maxlen + 2))
+ tooenc "${prefix}${COULEUR_BLEUE}T$(__complete "" $maxlen -)${COULEUR_NORMALE}"
+}
+function __edesc() {
+ local -a lines
+ local prefix="$(__edate)$(__eindent0)"
+
+ setx -a lines=echo "$1"
+ for line in "${lines[@]}"; do
+ tooenc "${prefix}${COULEUR_BLEUE}>${COULEUR_NORMALE} $line"
+ done
+}
+function __ebanner() {
+ local -a lines
+ local lsep prefix="$(__edate)$(__eindent0)"
+ local length="${COLUMNS:-80}"
+ setx lsep=__complete "$prefix" "$length" =
+
+ tooenc "$COULEUR_ROUGE$lsep"
+ length=$((length - 1))
+ setx -a lines=echo "$1"
+ for line in "" "${lines[@]}" ""; do
+ setx line=__complete "$prefix= $line" "$length"
+ tooenc "$line="
+ done
+ tooenc "$lsep$COULEUR_NORMALE"
+}
+function __eimportant() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}!${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __eattention() { tooenc "$(__edate)$(__eindent0)${COULEUR_JAUNE}*${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __eerror() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}E${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __ewarn() { tooenc "$(__edate)$(__eindent0)${COULEUR_JAUNE}W${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __enote() { tooenc "$(__edate)$(__eindent0)${COULEUR_VERTE}N${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __einfo() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLEUE}I${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __edebug() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLANCHE}D${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+
+function __estep() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __estepe() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __estepw() { tooenc "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __estepn() { tooenc "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __estepi() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __estep_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __estepe_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __estepw_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __estepn_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __estepi_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+
+function __action() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __asuccess() { tooenc "$(__edate)$(__eindent0)${COULEUR_VERTE}✔${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __afailure() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}✘${COULEUR_NORMALE} $(__eindent "$1" " ")"; }
+function __adone() { tooenc "$(__edate)$(__eindent0)$(__eindent "$1")"; }
diff --git a/bash/src/_output_vanilla.sh b/bash/src/_output_vanilla.sh
new file mode 100644
index 0000000..c37509d
--- /dev/null
+++ b/bash/src/_output_vanilla.sh
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+function __esection() {
+ local -a lines
+ local lsep prefix="$(__edate)$(__eindent0)"
+ local length="${COLUMNS:-80}"
+ setx lsep=__complete "$prefix" "$length" -
+
+ tooenc "$lsep"
+ [ -n "$*" ] || return 0
+ length=$((length - 1))
+ setx -a lines=echo "$1"
+ for line in "${lines[@]}"; do
+ setx line=__complete "$prefix- $line" "$length"
+ tooenc "$line-"
+ done
+ tooenc "$lsep"
+}
+function __etitle() {
+ local p="TITLE: " i=" "
+ tooenc "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")"
+}
+function __edesc() {
+ local p="DESC: " i=" "
+ tooenc "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")"
+}
+function __ebanner() {
+ local -a lines
+ local lsep prefix="$(__edate)$(__eindent0)"
+ local length="${COLUMNS:-80}"
+ setx lsep=__complete "$prefix" "$length" =
+
+ tooenc "$lsep"
+ length=$((length - 1))
+ setx -a lines=echo "$1"
+ for line in "" "${lines[@]}" ""; do
+ setx line=__complete "$prefix= $line" "$length"
+ tooenc "$line="
+ done
+ tooenc "$lsep"
+}
+function __eimportant() { tooenc "$(__edate)$(__eindent0)IMPORTANT! $(__eindent "$1" " ")"; }
+function __eattention() { tooenc "$(__edate)$(__eindent0)ATTENTION! $(__eindent "$1" " ")"; }
+function __eerror() { tooenc "$(__edate)$(__eindent0)ERROR: $(__eindent "$1" " ")"; }
+function __ewarn() { tooenc "$(__edate)$(__eindent0)WARNING: $(__eindent "$1" " ")"; }
+function __enote() { tooenc "$(__edate)$(__eindent0)NOTE: $(__eindent "$1" " ")"; }
+function __einfo() { tooenc "$(__edate)$(__eindent0)INFO: $(__eindent "$1" " ")"; }
+function __edebug() { tooenc "$(__edate)$(__eindent0)DEBUG: $(__eindent "$1" " ")"; }
+function __eecho() { tooenc "$(__edate)$(__eindent0)$(__eindent "$1")"; }
+function __eecho_() { tooenc_ "$(__edate)$(__eindent0)$(__eindent "$1")"; }
+
+function __estep() { tooenc "$(__edate)$(__eindent0). $(__eindent "$1" " ")"; }
+function __estepe() { tooenc "$(__edate)$(__eindent0).E $(__eindent "$1" " ")"; }
+function __estepw() { tooenc "$(__edate)$(__eindent0).W $(__eindent "$1" " ")"; }
+function __estepn() { tooenc "$(__edate)$(__eindent0).N $(__eindent "$1" " ")"; }
+function __estepi() { tooenc "$(__edate)$(__eindent0).I $(__eindent "$1" " ")"; }
+function __estep_() { tooenc_ "$(__edate)$(__eindent0). $(__eindent "$1" " ")"; }
+function __estepe_() { tooenc_ "$(__edate)$(__eindent0).E $(__eindent "$1" " ")"; }
+function __estepw_() { tooenc_ "$(__edate)$(__eindent0).W $(__eindent "$1" " ")"; }
+function __estepn_() { tooenc_ "$(__edate)$(__eindent0).N $(__eindent "$1" " ")"; }
+function __estepi_() { tooenc_ "$(__edate)$(__eindent0).I $(__eindent "$1" " ")"; }
+
+function __action() { tooenc "$(__edate)$(__eindent0)ACTION: $(__eindent "$1" " ")"; }
+function __asuccess() { tooenc "$(__edate)$(__eindent0)(OK) $(__eindent "$1" " ")"; }
+function __afailure() { tooenc "$(__edate)$(__eindent0)(KO) $(__eindent "$1" " ")"; }
+function __adone() { tooenc "$(__edate)$(__eindent0)$(__eindent "$1")"; }
diff --git a/bash/src/base.args.sh b/bash/src/base.args.sh
new file mode 100644
index 0000000..393a7c5
--- /dev/null
+++ b/bash/src/base.args.sh
@@ -0,0 +1,486 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: base.args "Fonctions de base: analyse d'arguments"
+
+function: local_args "Afficher des commandes pour rendre locales des variables utilisées par parse_args()
+
+Cela permet d'utiliser parse_args() à l'intérieur d'une fonction."
+function local_args() {
+ echo "local -a args"
+ echo "local NULIB_ARGS_ONERROR_RETURN=1"
+ echo "local NULIB_VERBOSITY=\"\$NULIB_VERBOSITY\""
+ echo "local NULIB_INTERACTION=\"\$NULIB_INTERACTION\""
+}
+
+function: parse_args "Analyser les arguments de la ligne de commande à partir des définitions du tableau args
+
+Cette fonction s'utilise ainsi:
+~~~"'
+args=(
+ [desc]
+ [usage]
+ [+|-]
+ -o,--longopt action [optdesc]
+ -a:,--mandarg: action [optdesc]
+ -b::,--optarg:: action [optdesc]
+)
+parse_args "$@"; set -- "${args[@]}"
+~~~'"
+
+au retour de la fonction, args contient les arguments qui n'ont pas été traités
+automatiquement.
+
+les options --help et --help++ sont automatiquement gérées. avec --help, seules
+les options standards sont affichées. --help++ affiche toutes les options. les
+descriptions sont utilisées pour l'affichage de l'aide. une option avancée est
+identifiée par une description qui commence par ++
+
+desc
+: description de l'objet du script ou de la fonction. cette valeur est
+ facultative
+
+usage
+: description des arguments du script, sans le nom du script. par exemple la
+ valeur '[options] FILE' générera le texte d'aide suivant:
+ ~~~
+ USAGE
+ \$MYNAME [options] FILE
+ ~~~
+ Peut contenir autant de lignes que nécessaire. Chaque ligne est préfixée du
+ nom du script, jusqu'à la première ligne vide. Ensuite, les lignes sont
+ affichées telles quelles.
+ Le premier espace est ignoré, ce qui permet de spécifier des USAGEs avec une
+ option, e.g ' -c VALUE'
+
++|-
+: méthode d'analyse des arguments.
+ * Par défaut, les options sont valides n'importe où sur la ligne de commande.
+ * Avec '+', l'analyse s'arrête au premier argument qui n'est pas une option.
+ * Avec '-', les options sont valides n'importe où sur la ligne de commande,
+ mais les arguments ne sont pas réordonnés, et apparaissent dans l'ordre de
+ leur mention. IMPORTANT: dans ce cas, aucun argument ni option n'est traité,
+ c'est à la charge de l'utilisateur. Au retour de la fonction, args contient
+ l'ensemble des arguments tels qu'analysés par getopt
+
+-o, --longopt
+: option sans argument
+
+-a:, --mandarg:
+: option avec argument obligatoire
+
+ l'option peut être suivi d'une valeur qui décrit l'argument attendu e.g
+ -a:file pour un fichier. cette valeur est mise en majuscule lors de
+ l'affichage de l'aide. pour le moment, cette valeur n'est pas signifiante.
+
+-b::, --optarg::
+: option avec argument facultatif
+
+ l'option peut être suivi d'une valeur qui décrit l'argument attendu e.g
+ -b::file pour un fichier. cette valeur est mise en majuscule lors de
+ l'affichage de l'aide. pour le moment, cette valeur n'est pas signifiante.
+
+action
+: action à effectuer si cette option est utilisée. plusieurs syntaxes sont valides:
+ * 'NAME' met à jour la variable en fonction du type d'argument: l'incrémenter
+ pour une option sans argument, lui donner la valeur spécifiée pour une
+ option avec argument, ajouter la valeur spécifiée au tableau si l'option est
+ spécifiée plusieurs fois.
+ la valeur spéciale '.' calcule une valeur de NAME en fonction du nom de
+ l'option la plus longue. par exemple, les deux définitions suivantes sont
+ équivalentes:
+ ~~~
+ -o,--short,--very-long .
+ -o,--short,--very-long very_long
+ ~~~
+ De plus, la valeur spéciale '.' traite les options de la forme --no-opt
+ comme l'inverse des option --opt. par exemple, les deux définitions
+ suivantes sont équivalentes:
+ ~~~
+ --opt . --no-opt .
+ --opt opt --no-opt '\$dec@ opt'
+ ~~~
+ * 'NAME=VALUE' pour une option sans argument, forcer la valeur spécifiée; pour
+ une option avec argument, prendre la valeur spécifiée comme valeur par
+ défaut si la valeur de l'option est vide
+ * '\$CMD' CMD est évalué avec eval *dès* que l'option est rencontrée.
+ les valeurs suivantes sont initialisées:
+ * option_ est l'option utilisée, e.g --long-opt
+ * value_ est la valeur de l'option
+ les fonctions suivantes sont définies:
+ * 'inc@ NAME' incrémente la variable NAME -- c'est le comportement de set@ si
+ l'option est sans argument
+ * 'dec@ NAME' décrémente la variable NAME, et la rend vide quand le compte
+ arrive à zéro
+ * 'res@ NAME VALUE' initialise la variable NAME avec la valeur de l'option (ou
+ VALUE si la valeur de l'option est vide) -- c'est le comportement de set@
+ si l'option prend un argument
+ * 'add@ NAME VALUE' ajoute la valeur de l'option (ou VALUE si la valeur de
+ l'option est vide) au tableau NAME.
+ * 'set@ NAME' met à jour la variable NAME en fonction de la définition de
+ l'option (avec ou sans argument, ajout ou non à un tableau)
+
+optdesc
+: description de l'option. cette valeur est facultative. si la description
+ commence par ++, c'est une option avancée qui n'est pas affichée par défaut."
+function parse_args() {
+ eval "$NULIB__DISABLE_SET_X"
+ local __r=
+ local __DIE='[ -n "$NULIB_ARGS_ONERROR_RETURN" ] && return 1 || die'
+
+ if ! is_array args; then
+ eerror "Invalid args definition: args must be defined"
+ __r=1
+ fi
+ # distinguer les descriptions des définition d'arguments
+ local __USAGE __DESC
+ local -a __DEFS __ARGS
+ __ARGS=("$@")
+ set -- "${args[@]}"
+ [ "${1#-}" == "$1" ] && { __DESC="$1"; shift; }
+ [ "${1#-}" == "$1" ] && { __USAGE="$1"; shift; }
+ if [ -n "$__r" ]; then
+ :
+ elif [ $# -gt 0 -a "$1" != "+" -a "${1#-}" == "$1" ]; then
+ eerror "Invalid args definition: third arg must be an option"
+ __r=1
+ else
+ __DEFS=("$@")
+ __parse_args || __r=1
+ fi
+ eval "$NULIB__ENABLE_SET_X"
+ if [ -n "$__r" ]; then
+ eval "$__DIE"
+ fi
+}
+function __parse_args() {
+ ## tout d'abord, construire la liste des options
+ local __AUTOH=1 __AUTOHELP=1 # faut-il gérer automatiquement l'affichage de l'aide?
+ local __AUTOD=1 __AUTODEBUG=1 # faut-il rajouter les options -D et --debug
+ local __ADVHELP # y a-t-il des options avancées?
+ local __popt __sopts __lopts
+ local -a __defs
+ set -- "${__DEFS[@]}"
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ +) __popt="$1"; shift; continue;;
+ -) __popt="$1"; shift; continue;;
+ -*) IFS=, read -a __defs <<<"$1"; shift;;
+ *) eerror "Invalid arg definition: expected option, got '$1'"; eval "$__DIE";;
+ esac
+ # est-ce que l'option prend un argument?
+ local __def __witharg __valdesc
+ __witharg=
+ for __def in "${__defs[@]}"; do
+ if [ "${__def%::*}" != "$__def" ]; then
+ [ "$__witharg" != : ] && __witharg=::
+ elif [ "${__def%:*}" != "$__def" ]; then
+ __witharg=:
+ fi
+ done
+ # définitions __sopts et __lopts
+ for __def in "${__defs[@]}"; do
+ __def="${__def%%:*}"
+ if [[ "$__def" == --* ]]; then
+ # --longopt
+ __def="${__def#--}"
+ __lopts="$__lopts${__lopts:+,}$__def$__witharg"
+ elif [[ "$__def" == -* ]] && [ ${#__def} -eq 2 ]; then
+ # -o
+ __def="${__def#-}"
+ __sopts="$__sopts$__def$__witharg"
+ [ "$__def" == h ] && __AUTOH=
+ [ "$__def" == D ] && __AUTOD=
+ else
+ # -longopt ou longopt
+ __def="${__def#-}"
+ __lopts="$__lopts${__lopts:+,}$__def$__witharg"
+ fi
+ [ "$__def" == help -o "$__def" == help++ ] && __AUTOHELP=
+ [ "$__def" == debug ] && __AUTODEBUG=
+ done
+ # sauter l'action
+ shift
+ # sauter la description le cas échéant
+ if [ "${1#-}" == "$1" ]; then
+ [ "${1#++}" != "$1" ] && __ADVHELP=1
+ shift
+ fi
+ done
+
+ [ -n "$__AUTOH" ] && __sopts="${__sopts}h"
+ [ -n "$__AUTOHELP" ] && __lopts="$__lopts${__lopts:+,}help,help++"
+ [ -n "$__AUTOD" ] && __sopts="${__sopts}D"
+ [ -n "$__AUTODEBUG" ] && __lopts="$__lopts${__lopts:+,}debug"
+
+ __sopts="$__popt$__sopts"
+ local -a __getopt_args
+ __getopt_args=(-n "$MYNAME" ${__sopts:+-o "$__sopts"} ${__lopts:+-l "$__lopts"} -- "${__ARGS[@]}")
+
+ ## puis analyser et normaliser les arguments
+ if args="$(getopt -q "${__getopt_args[@]}")"; then
+ eval "set -- $args"
+ else
+ # relancer pour avoir le message d'erreur
+ LANG=C getopt "${__getopt_args[@]}" 2>&1 1>/dev/null
+ eval "$__DIE"
+ fi
+
+ ## puis traiter les options
+ local __defname __resvalue __decvalue __defvalue __add __action option_ name_ value_
+ function inc@() {
+ eval "[ -n \"\$$1\" ] || let $1=0"
+ eval "let $1=$1+1"
+ }
+ function dec@() {
+ eval "[ -n \"\$$1\" ] && let $1=$1-1"
+ eval "[ \"\$$1\" == 0 ] && $1="
+ }
+ function res@() {
+ local __value="${value_:-$2}"
+ eval "$1=\"\$__value\""
+ }
+ function add@() {
+ local __value="${value_:-$2}"
+ eval "$1+=(\"\$__value\")"
+ }
+ function set@() {
+ if [ -n "$__resvalue" ]; then
+ res@ "$@"
+ elif [ -n "$__witharg" ]; then
+ if is_array "$1"; then
+ add@ "$@"
+ elif ! is_defined "$1"; then
+ # première occurrence: variable
+ res@ "$@"
+ else
+ # deuxième occurence: tableau
+ [ -z "${!1}" ] && eval "$1=()"
+ add@ "$@"
+ fi
+ elif [ -n "$__decvalue" ]; then
+ dec@ "$@"
+ else
+ inc@ "$@"
+ fi
+ }
+ function showhelp@() {
+ local help="$MYNAME" showadv="$1"
+ if [ -n "$__DESC" ]; then
+ help="$help: $__DESC"
+ fi
+
+ local first usage nl=$'\n'
+ local prefix=" $MYNAME "
+ local usages="${__USAGE# }"
+ [ -n "$usages" ] || usages="[options]"
+ help="$help
+
+USAGE"
+ first=1
+ while [ -n "$usages" ]; do
+ usage="${usages%%$nl*}"
+ if [ "$usage" != "$usages" ]; then
+ usages="${usages#*$nl}"
+ else
+ usages=
+ fi
+ if [ -n "$first" ]; then
+ first=
+ [ -z "$usage" ] && continue
+ else
+ [ -z "$usage" ] && prefix=
+ fi
+ help="$help
+$prefix$usage"
+ done
+
+ set -- "${__DEFS[@]}"
+ first=1
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ +) shift; continue;;
+ -) shift; continue;;
+ -*) IFS=, read -a __defs <<<"$1"; shift;;
+ esac
+ if [ -n "$first" ]; then
+ first=
+ help="$help${nl}${nl}OPTIONS"
+ if [ -n "$__AUTOHELP" -a -n "$__ADVHELP" ]; then
+ help="$help
+ --help++
+ Afficher l'aide avancée"
+ fi
+ fi
+ # est-ce que l'option prend un argument?
+ __witharg=
+ __valdesc=value
+ for __def in "${__defs[@]}"; do
+ if [ "${__def%::*}" != "$__def" ]; then
+ [ "$__witharg" != : ] && __witharg=::
+ [ -n "${__def#*::}" ] && __valdesc="[${__def#*::}]"
+ elif [ "${__def%:*}" != "$__def" ]; then
+ __witharg=:
+ [ -n "${__def#*:}" ] && __valdesc="${__def#*:}"
+ fi
+ done
+ # description de l'option
+ local first=1 thelp tdesc
+ for __def in "${__defs[@]}"; do
+ __def="${__def%%:*}"
+ if [[ "$__def" == --* ]]; then
+ : # --longopt
+ elif [[ "$__def" == -* ]] && [ ${#__def} -eq 2 ]; then
+ : # -o
+ else
+ # -longopt ou longopt
+ __def="--${__def#-}"
+ fi
+ if [ -n "$first" ]; then
+ first=
+ thelp="${nl} "
+ else
+ thelp="$thelp, "
+ fi
+ thelp="$thelp$__def"
+ done
+ [ -n "$__witharg" ] && thelp="$thelp ${__valdesc^^}"
+ # sauter l'action
+ shift
+ # prendre la description le cas échéant
+ if [ "${1#-}" == "$1" ]; then
+ tdesc="$1"
+ if [ "${tdesc#++}" != "$tdesc" ]; then
+ # option avancée
+ if [ -n "$showadv" ]; then
+ tdesc="${tdesc#++}"
+ else
+ thelp=
+ fi
+ fi
+ [ -n "$thelp" ] && thelp="$thelp${nl} ${tdesc//$nl/$nl }"
+ shift
+ fi
+ [ -n "$thelp" ] && help="$help$thelp"
+ done
+ uecho "$help"
+ exit 0
+ }
+ if [ "$__popt" != - ]; then
+ while [ $# -gt 0 ]; do
+ if [ "$1" == -- ]; then
+ shift
+ break
+ fi
+ [[ "$1" == -* ]] || break
+ option_="$1"; shift
+ __parse_opt "$option_"
+ if [ -n "$__witharg" ]; then
+ # l'option prend un argument
+ value_="$1"; shift
+ else
+ # l'option ne prend pas d'argument
+ value_=
+ fi
+ eval "$__action"
+ done
+ fi
+ unset -f inc@ res@ add@ set@ showhelp@
+ args=("$@")
+}
+function __parse_opt() {
+ # $1 est l'option spécifiée
+ local option_="$1"
+ set -- "${__DEFS[@]}"
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ +) shift; continue;;
+ -) shift; continue;;
+ -*) IFS=, read -a __defs <<<"$1"; shift;;
+ esac
+ # est-ce que l'option prend un argument?
+ __witharg=
+ for __def in "${__defs[@]}"; do
+ if [ "${__def%::*}" != "$__def" ]; then
+ [ "$__witharg" != : ] && __witharg=::
+ elif [ "${__def%:*}" != "$__def" ]; then
+ __witharg=:
+ fi
+ done
+ # nom le plus long
+ __defname=
+ local __found=
+ for __def in "${__defs[@]}"; do
+ __def="${__def%%:*}"
+ [ "$__def" == "$option_" ] && __found=1
+ if [[ "$__def" == --* ]]; then
+ # --longopt
+ __def="${__def#--}"
+ [ ${#__def} -gt ${#__defname} ] && __defname="$__def"
+ elif [[ "$__def" == -* ]] && [ ${#__def} -eq 2 ]; then
+ # -o
+ __def="${__def#-}"
+ [ ${#__def} -gt ${#__defname} ] && __defname="$__def"
+ else
+ # -longopt ou longopt
+ __def="${__def#-}"
+ [ ${#__def} -gt ${#__defname} ] && __defname="$__def"
+ fi
+ done
+ __defname="${__defname//-/_}"
+ # analyser l'action
+ __decvalue=
+ if [ "${1#\$}" != "$1" ]; then
+ name_="$__defname"
+ __resvalue=
+ __defvalue=
+ __action="${1#\$}"
+ else
+ if [ "$1" == . ]; then
+ name_="$__defname"
+ __resvalue=
+ __defvalue=
+ if [ "${name_#no_}" != "$name_" ]; then
+ name_="${name_#no_}"
+ __decvalue=1
+ fi
+ elif [[ "$1" == *=* ]]; then
+ name_="${1%%=*}"
+ __resvalue=1
+ __defvalue="${1#*=}"
+ else
+ name_="$1"
+ __resvalue=
+ __defvalue=
+ fi
+ __action="$(qvals set@ "$name_" "$__defvalue")"
+ fi
+ shift
+ # sauter la description le cas échéant
+ [ "${1#-}" == "$1" ] && shift
+
+ [ -n "$__found" ] && return 0
+ done
+ if [ -n "$__AUTOH" -a "$option_" == -h ]; then
+ __action="showhelp@"
+ return 0
+ fi
+ if [ -n "$__AUTOHELP" ]; then
+ if [ "$option_" == --help ]; then
+ __action="showhelp@"
+ return 0
+ elif [ "$option_" == --help++ ]; then
+ __action="showhelp@ ++"
+ return 0
+ fi
+ fi
+ if [ -n "$__AUTOD" -a "$option_" == -D ]; then
+ __action=set_debug
+ return 0
+ fi
+ if [ -n "$__AUTODEBUG" -a "$option_" == --debug ]; then
+ __action=set_debug
+ return 0
+ fi
+ # ici, l'option n'a pas été trouvée, on ne devrait pas arriver ici
+ eerror "Unexpected option '$option_'"; eval "$__DIE"
+}
diff --git a/bash/src/base.array.sh b/bash/src/base.array.sh
new file mode 100644
index 0000000..9c47e71
--- /dev/null
+++ b/bash/src/base.array.sh
@@ -0,0 +1,360 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: base.array "Fonctions de base: gestion des variables tableaux"
+
+function: array_count "retourner le nombre d'éléments du tableau \$1"
+function array_count() {
+ eval "echo \${#$1[*]}"
+}
+
+function: array_isempty "tester si le tableau \$1 est vide"
+function array_isempty() {
+ eval "[ \${#$1[*]} -eq 0 ]"
+}
+
+function: array_new "créer un tableau vide dans la variable \$1"
+function array_new() {
+ eval "$1=()"
+}
+
+function: array_copy "copier le contenu du tableau \$2 dans le tableau \$1"
+function array_copy() {
+ eval "$1=(\"\${$2[@]}\")"
+}
+
+function: array_add "ajouter les valeurs \$2..@ à la fin du tableau \$1"
+function array_add() {
+ local __aa_a="$1"; shift
+ eval "$__aa_a+=(\"\$@\")"
+}
+
+function: array_ins "insérer les valeurs \$2..@ au début du tableau \$1"
+function array_ins() {
+ local __aa_a="$1"; shift
+ eval "$__aa_a=(\"\$@\" \"\${$__aa_a[@]}\")"
+}
+
+function: array_del "supprimer *les* valeurs \$2 du tableau \$1"
+function array_del() {
+ local __ad_v
+ local -a __ad_vs
+ eval '
+for __ad_v in "${'"$1"'[@]}"; do
+ if [ "$__ad_v" != "$2" ]; then
+ __ad_vs=("${__ad_vs[@]}" "$__ad_v")
+ fi
+done'
+ array_copy "$1" __ad_vs
+}
+
+function: array_addu "ajouter la valeur \$2 au tableau \$1, si la valeur n'y est pas déjà
+
+Retourner vrai si la valeur a été ajoutée"
+function array_addu() {
+ local __as_v
+ eval '
+for __as_v in "${'"$1"'[@]}"; do
+ [ "$__as_v" == "$2" ] && return 1
+done'
+ array_add "$1" "$2"
+ return 0
+}
+
+function: array_insu "insérer la valeur \$2 au début du tableau tableau \$1, si la valeur n'y est pas déjà
+
+Retourner vrai si la valeur a été ajoutée."
+function array_insu() {
+ local __as_v
+ eval '
+for __as_v in "${'"$1"'[@]}"; do
+ [ "$__as_v" == "$2" ] && return 1
+done'
+ array_ins "$1" "$2"
+ return 0
+}
+
+function: array_fillrange "Initialiser le tableau \$1 avec les nombres de \$2(=1) à \$3(=10) avec un step de \$4(=1)"
+function array_fillrange() {
+ local -a __af_vs
+ local __af_i="${2:-1}" __af_to="${3:-10}" __af_step="${4:-1}"
+ while [ "$__af_i" -le "$__af_to" ]; do
+ __af_vs=("${__af_vs[@]}" "$__af_i")
+ __af_i=$(($__af_i + $__af_step))
+ done
+ array_copy "$1" __af_vs
+}
+
+function: array_eq "tester l'égalité des tableaux \$1 et \$2"
+function array_eq() {
+ local -a __ae_a1 __ae_a2
+ array_copy __ae_a1 "$1"
+ array_copy __ae_a2 "$2"
+ [ ${#__ae_a1[*]} -eq ${#__ae_a2[*]} ] || return 1
+ local __ae_v __ae_i=0
+ for __ae_v in "${__ae_a1[@]}"; do
+ [ "$__ae_v" == "${__ae_a2[$__ae_i]}" ] || return 1
+ __ae_i=$(($__ae_i + 1))
+ done
+ return 0
+}
+
+function: array_contains "tester si le tableau \$1 contient la valeur \$2"
+function array_contains() {
+ local __ac_v
+ eval '
+for __ac_v in "${'"$1"'[@]}"; do
+ [ "$__ac_v" == "$2" ] && return 0
+done'
+ return 1
+}
+
+function: array_icontains "tester si le tableau \$1 contient la valeur \$2, sans tenir compte de la casse"
+function array_icontains() {
+ local __ac_v
+ eval '
+for __ac_v in "${'"$1"'[@]}"; do
+ [ "${__ac_v,,} == "${2,,}" ] && return 0
+done'
+ return 1
+}
+
+function: array_find "si le tableau \$1 contient la valeur \$2, afficher l'index de la valeur. Si le tableau \$3 est spécifié, afficher la valeur à l'index dans ce tableau"
+function array_find() {
+ local __af_i __af_v
+ __af_i=0
+ eval '
+for __af_v in "${'"$1"'[@]}"; do
+ if [ "$__af_v" == "$2" ]; then
+ if [ -n "$3" ]; then
+ recho "${'"$3"'[$__af_i]}"
+ else
+ echo "$__af_i"
+ fi
+ return 0
+ fi
+ __af_i=$(($__af_i + 1))
+done'
+ return 1
+}
+
+function: array_reverse "Inverser l'ordre des élément du tableau \$1"
+function array_reverse() {
+ local -a __ar_vs
+ local __ar_v
+ array_copy __ar_vs "$1"
+ array_new "$1"
+ for __ar_v in "${__ar_vs[@]}"; do
+ array_ins "$1" "$__ar_v"
+ done
+}
+
+function: array_replace "dans le tableau \$1, remplacer toutes les occurences de \$2 par \$3..*"
+function array_replace() {
+ local __ar_sn="$1"; shift
+ local __ar_f="$1"; shift
+ local -a __ar_s __ar_d
+ local __ar_v
+ array_copy __ar_s "$__ar_sn"
+ for __ar_v in "${__ar_s[@]}"; do
+ if [ "$__ar_v" == "$__ar_f" ]; then
+ __ar_d=("${__ar_d[@]}" "$@")
+ else
+ __ar_d=("${__ar_d[@]}" "$__ar_v")
+ fi
+ done
+ array_copy "$__ar_sn" __ar_d
+}
+
+function: array_each "Pour chacune des valeurs ITEM du tableau \$1, appeler la fonction \$2 avec les arguments (\$3..@ ITEM)"
+function array_each() {
+ local __ae_v
+ local -a __ae_a
+ array_copy __ae_a "$1"; shift
+ for __ae_v in "${__ae_a[@]}"; do
+ "$@" "$__ae_v"
+ done
+}
+
+function: array_map "Pour chacune des valeurs ITEM du tableau \$1, appeler la fonction \$2 avec les arguments (\$3..@ ITEM), et remplacer la valeur par le résultat de la fonction"
+function array_map() {
+ local __am_v
+ local -a __am_a __am_vs
+ local __am_an="$1"; shift
+ local __am_f="$1"; shift
+ array_copy __am_a "$__am_an"
+ for __am_v in "${__am_a[@]}"; do
+ __am_vs=("${__am_vs[@]}" "$("$__am_f" "$@" "$__am_v")")
+ done
+ array_copy "$__am_an" __am_vs
+}
+
+function: array_first "afficher la première valeur du tableau \$1"
+function array_first() {
+ eval "recho \"\${$1[@]:0:1}\""
+}
+
+function: array_last "afficher la dernière valeur du tableau \$1"
+function array_last() {
+ eval "recho \"\${$1[@]: -1:1}\""
+}
+
+function: array_copy_firsts "copier toutes les valeurs du tableau \$2(=\$1) dans le tableau \$1, excepté la dernière"
+function array_copy_firsts() {
+ eval "$1=(\"\${${2:-$1}[@]:0:\$((\${#${2:-$1}[@]}-1))}\")"
+}
+
+function: array_copy_lasts "copier toutes les valeurs du tableau \$2(=\$1) dans le tableau \$1, excepté la première"
+function array_copy_lasts() {
+ eval "$1=(\"\${${2:-$1}[@]:1}\")"
+}
+
+function: array_extend "ajouter le contenu du tableau \$2 au tableau \$1"
+function array_extend() {
+ eval "$1=(\"\${$1[@]}\" \"\${$2[@]}\")"
+}
+
+function: array_extendu "ajouter chacune des valeurs du tableau \$2 au tableau \$1, si ces valeurs n'y sont pas déjà
+
+Retourner vrai si au moins une valeur a été ajoutée"
+function array_extendu() {
+ local __ae_v __ae_s=1
+ eval '
+for __ae_v in "${'"$2"'[@]}"; do
+ array_addu "$1" "$__ae_v" && __ae_s=0
+done'
+ return "$__ae_s"
+}
+
+function: array_extend_firsts "ajouter toutes les valeurs du tableau \$2 dans le tableau \$1, excepté la dernière"
+function array_extend_firsts() {
+ eval "$1=(\"\${$1[@]}\" \"\${$2[@]:0:\$((\${#$2[@]}-1))}\")"
+}
+
+function: array_extend_lasts "ajouter toutes les valeurs du tableau \$2 dans le tableau \$1, excepté la première"
+function array_extend_lasts() {
+ eval "$1=(\"\${$1[@]}\" \"\${$2[@]:1}\")"
+}
+
+function: array_xsplit "créer le tableau \$1 avec chaque élément de \$2 (un ensemble d'éléments séparés par \$3, qui vaut ':' par défaut)"
+function array_xsplit() {
+ eval "$1=($(recho_ "$2" | lawk -v RS="${3:-:}" '
+{
+ gsub(/'\''/, "'\'\\\\\'\''")
+ print "'\''" $0 "'\''"
+}'))" #"
+}
+
+function: array_xsplitc "variante de array_xsplit() où le séparateur est ',' par défaut"
+function array_xsplitc() {
+ array_xsplit "$1" "$2" "${3:-,}"
+}
+
+function: array_split "créer le tableau \$1 avec chaque élément de \$2 (un ensemble d'éléments séparés par \$3, qui vaut ':' par défaut)
+
+Les éléments vides sont ignorés. par exemple \"a::b\" est équivalent à \"a:b\""
+function array_split() {
+ eval "$1=($(recho_ "$2" | lawk -v RS="${3:-:}" '
+/^$/ { next }
+{
+ gsub(/'\''/, "'\'\\\\\'\''")
+ print "'\''" $0 "'\''"
+}'))" #"
+}
+
+function: array_splitc "variante de array_split() où le séparateur est ',' par défaut"
+function array_splitc() {
+ array_split "$1" "$2" "${3:-,}"
+}
+
+function: array_xsplitl "créer le tableau \$1 avec chaque ligne de \$2"
+function array_xsplitl() {
+ eval "$1=($(recho_ "$2" | nl2lf | lawk '
+{
+ gsub(/'\''/, "'\'\\\\\'\''")
+ print "'\''" $0 "'\''"
+}'))" #"
+}
+
+function: array_splitl "créer le tableau \$1 avec chaque ligne de \$2
+
+Les lignes vides sont ignorés."
+function array_splitl() {
+ eval "$1=($(recho_ "$2" | nl2lf | lawk '
+/^$/ { next }
+{
+ gsub(/'\''/, "'\'\\\\\'\''")
+ print "'\''" $0 "'\''"
+}'))" #"
+}
+
+function: array_join "afficher le contenu du tableau \$1 sous forme d'une liste de valeurs séparées par \$2 (qui vaut ':' par défaut)
+
+* Si \$1==\"@\", alors les éléments du tableaux sont les arguments de la fonction à partir de \$3
+* Si \$1!=\"@\" et que le tableau est vide, afficher \$3
+* Si \$1!=\"@\", \$4 et \$5 sont des préfixes et suffixes à rajouter à chaque élément"
+function array_join() {
+ local __aj_an __aj_l __aj_j __aj_s="${2:-:}" __aj_pf __aj_sf
+ if [ "$1" == "@" ]; then
+ __aj_an="\$@"
+ shift; shift
+ else
+ __aj_an="\${$1[@]}"
+ __aj_pf="$4"
+ __aj_sf="$5"
+ fi
+ eval '
+for __aj_l in "'"$__aj_an"'"; do
+ __aj_j="${__aj_j:+$__aj_j'"$__aj_s"'}$__aj_pf$__aj_l$__aj_sf"
+done'
+ if [ -n "$__aj_j" ]; then
+ recho "$__aj_j"
+ elif [ "$__aj_an" != "\$@" -a -n "$3" ]; then
+ recho "$3"
+ fi
+}
+
+function: array_joinc "afficher les éléments du tableau \$1 séparés par ','"
+function array_joinc() {
+ array_join "$1" , "$2" "$3" "$4"
+}
+
+function: array_joinl "afficher les éléments du tableau \$1 à raison d'un élément par ligne"
+function array_joinl() {
+ array_join "$1" "
+" "$2" "$3" "$4"
+}
+
+function: array_mapjoin "map le tableau \$1 avec la fonction \$2, puis afficher le résultat en séparant chaque élément par \$3
+
+Les arguments et la sémantique sont les mêmes que pour array_join() en
+tenant compte de l'argument supplémentaire \$2 qui est la fonction pour
+array_map() (les autres arguments sont décalés en conséquence)"
+function array_mapjoin() {
+ local __amj_src="$1" __amj_func="$2" __amj_sep="$3"
+ shift; shift; shift
+ if [ "$__amj_src" == "@" ]; then
+ local -a __amj_tmpsrc
+ __amj_tmpsrc=("$@")
+ __amj_src=__amj_tmpsrc
+ set --
+ fi
+ local -a __amj_tmp
+ array_copy __amj_tmp "$__amj_src"
+ array_map __amj_tmp "$__amj_func"
+ array_join __amj_tmp "$__amj_sep" "$@"
+}
+
+function: array_fix_paths "Corriger les valeurs du tableau \$1. Les valeurs contenant le séparateur \$2(=':') sont séparées en plusieurs valeurs.
+
+Par exemple avec le tableau input=(a b:c), le résultat est input=(a b c)"
+function array_fix_paths() {
+ local __afp_an="$1" __afp_s="${2:-:}"
+ local -a __afp_vs
+ local __afp_v
+ array_copy __afp_vs "$__afp_an"
+ array_new "$__afp_an"
+ for __afp_v in "${__afp_vs[@]}"; do
+ array_split __afp_v "$__afp_v" "$__afp_s"
+ array_extend "$__afp_an" __afp_v
+ done
+}
diff --git a/bash/src/base.bool.sh b/bash/src/base.bool.sh
new file mode 100644
index 0000000..8ed84a7
--- /dev/null
+++ b/bash/src/base.bool.sh
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: base.bool "Fonctions de base: valeurs booléennes"
+
+function: is_yes 'retourner vrai si $1 est une valeur "oui"'
+function is_yes() {
+ case "${1,,}" in
+ o|oui|y|yes|v|vrai|t|true|on) return 0;;
+ esac
+ isnum "$1" && [ "$1" -ne 0 ] && return 0
+ return 1
+}
+
+function: is_no 'retourner vrai si $1 est une valeur "non"'
+function is_no() {
+ case "${1,,}" in
+ n|non|no|f|faux|false|off) return 0;;
+ esac
+ isnum "$1" && [ "$1" -eq 0 ] && return 0
+ return 1
+}
+
+function: normyesval 'remplacer les valeurs des variables $1..* par la valeur normalisée de leur valeur "oui"'
+function normyesval() {
+ while [ $# -gt 0 ]; do
+ is_yes "${!1}" && _setv "$1" 1 || _setv "$1" ""
+ shift
+ done
+}
+
+function: setb 'Lancer la commande $2..@ en supprimant la sortie standard. Si la commande
+retourne vrai, assigner la valeur 1 à la variable $1. Sinon, lui assigner la
+valeur ""
+note: en principe, la syntaxe est "setb var cmd args...". cependant, la
+syntaxe "setb var=cmd args..." est supportée aussi'
+function setb() {
+ local __s_var="$1"; shift
+ if [[ "$__s_var" == *=* ]]; then
+ set -- "${__s_var#*=}" "$@"
+ __s_var="${__s_var%%=*}"
+ fi
+ local __s_r
+ if "$@" >/dev/null; then
+ eval "$__s_var=1"
+ else
+ __s_r=$?
+ eval "$__s_var="
+ return $__s_r
+ fi
+}
diff --git a/bash/src/base.core.sh b/bash/src/base.core.sh
new file mode 100644
index 0000000..36f5979
--- /dev/null
+++ b/bash/src/base.core.sh
@@ -0,0 +1,458 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: base.core "Fonctions de base: fondement"
+
+function: echo_ "afficher la valeur \$* sans passer à la ligne"
+function echo_() { echo -n "$*"; }
+
+function: recho "afficher une valeur brute.
+
+contrairement à la commande echo, ne reconnaitre aucune option (i.e. -e, -E, -n
+ne sont pas signifiants)"
+function recho() {
+ if [[ "${1:0:2}" == -[eEn] ]]; then
+ local first="${1:1}"; shift
+ echo -n -
+ echo "$first" "$@"
+ else
+ echo "$@"
+ fi
+}
+
+function: recho_ "afficher une valeur brute, sans passer à la ligne.
+
+contrairement à la commande echo, ne reconnaitre aucune option (i.e. -e, -E, -n
+ne sont pas signifiants)"
+function recho_() {
+ if [[ "${1:0:2}" == -[eEn] ]]; then
+ local first="${1:1}"; shift
+ echo -n -
+ echo -n "$first" "$@"
+ else
+ echo -n "$@"
+ fi
+}
+
+function: _qval "Dans la chaine \$*, remplacer:
+~~~
+\\ par \\\\
+\" par \\\"
+\$ par \\\$
+\` par \\\`
+~~~
+
+Cela permet de quoter une chaine à mettre entre guillements.
+
+note: la protection de ! n'est pas effectuée, parce que le comportement du shell
+est incohérent entre le shell interactif et les scripts. Pour une version plus
+robuste, il est nécessaire d'utiliser un programme externe tel que sed ou awk"
+function _qval() {
+ local s="$*"
+ s="${s//\\/\\\\}"
+ s="${s//\"/\\\"}"
+ s="${s//\$/\\\$}"
+ s="${s//\`/\\\`}"
+ recho_ "$s"
+}
+
+function: should_quote "Tester si la chaine \$* doit être mise entre quotes"
+function should_quote() {
+ # pour optimiser, toujours mettre entre quotes si plusieurs arguments sont
+ # spécifiés ou si on spécifie une chaine vide ou de plus de 80 caractères
+ [ $# -eq 0 -o $# -gt 1 -o ${#1} -eq 0 -o ${#1} -gt 80 ] && return 0
+ # sinon, tester si la chaine contient des caractères spéciaux
+ local s="$*"
+ s="${s//[a-zA-Z0-9]/}"
+ s="${s//,/}"
+ s="${s//./}"
+ s="${s//+/}"
+ s="${s//\//}"
+ s="${s//-/}"
+ s="${s//_/}"
+ s="${s//=/}"
+ [ -n "$s" ]
+}
+
+function: qval "Afficher la chaine \$* quotée avec \""
+function qval() {
+ echo -n \"
+ _qval "$@"
+ echo \"
+}
+
+function: qvalm "Afficher la chaine \$* quotée si nécessaire avec \""
+function qvalm() {
+ if should_quote "$@"; then
+ echo -n \"
+ _qval "$@"
+ echo \"
+ else
+ recho "$@"
+ fi
+}
+
+function: qvalr "Afficher la chaine \$* quotée si nécessaire avec \", sauf si elle est vide"
+function qvalr() {
+ if [ -z "$*" ]; then
+ :
+ elif should_quote "$@"; then
+ echo -n \"
+ _qval "$@"
+ echo \"
+ else
+ recho "$@"
+ fi
+}
+
+function: qvals "Afficher chaque argument de cette fonction quotée le cas échéant avec \", chaque valeur étant séparée par un espace"
+function qvals() {
+ local arg first=1
+ for arg in "$@"; do
+ [ -z "$first" ] && echo -n " "
+ if should_quote "$arg"; then
+ echo -n \"
+ _qval "$arg"
+ echo -n \"
+ else
+ recho_ "$arg"
+ fi
+ first=
+ done
+ [ -z "$first" ] && echo
+}
+
+function: qwc "Dans la chaine \$*, remplacer:
+~~~
+ \\ par \\\\
+\" par \\\"
+\$ par \\\$
+\` par \\\`
+~~~
+puis quoter la chaine avec \", sauf les wildcards *, ? et [class]
+
+Cela permet de quoter une chaine permettant de glober des fichiers, e.g
+~~~
+eval \"ls \$(qwc \"\$value\")\"
+~~~
+
+note: la protection de ! n'est pas effectuée, parce que le comportement du shell
+est incohérent entre le shell interactif et les scripts. Pour une version plus
+robuste, il est nécessaire d'utiliser un programme externe tel que sed ou awk"
+function qwc() {
+ local s="$*"
+ s="${s//\\/\\\\}"
+ s="${s//\"/\\\"}"
+ s="${s//\$/\\\$}"
+ s="${s//\`/\\\`}"
+ local r a b c
+ while [ -n "$s" ]; do
+ a=; b=; c=
+ a=; [[ "$s" == *\** ]] && { a="${s%%\**}"; a=${#a}; }
+ b=; [[ "$s" == *\?* ]] && { b="${s%%\?*}"; b=${#b}; }
+ c=; [[ "$s" == *\[* ]] && { c="${s%%\[*}"; c=${#c}; }
+ if [ -z "$a" -a -z "$b" -a -z "$c" ]; then
+ r="$r\"$s\""
+ break
+ fi
+ if [ -n "$a" ]; then
+ [ -n "$b" ] && [ $a -lt $b ] && b=
+ [ -n "$c" ] && [ $a -lt $c ] && c=
+ fi
+ if [ -n "$b" ]; then
+ [ -n "$a" ] && [ $b -lt $a ] && a=
+ [ -n "$c" ] && [ $b -lt $c ] && c=
+ fi
+ if [ -n "$c" ]; then
+ [ -n "$a" ] && [ $c -lt $a ] && a=
+ [ -n "$b" ] && [ $c -lt $b ] && b=
+ fi
+ if [ -n "$a" ]; then # PREFIX*
+ a="${s%%\**}"
+ s="${s#*\*}"
+ [ -n "$a" ] && r="$r\"$a\""
+ r="$r*"
+ elif [ -n "$b" ]; then # PREFIX?
+ a="${s%%\?*}"
+ s="${s#*\?}"
+ [ -n "$a" ] && r="$r\"$a\""
+ r="$r?"
+ elif [ -n "$c" ]; then # PREFIX[class]
+ a="${s%%\[*}"
+ b="${s#*\[}"; b="${b%%\]*}"
+ s="${s:$((${#a} + ${#b} + 2))}"
+ [ -n "$a" ] && r="$r\"$a\""
+ r="$r[$b]"
+ fi
+ done
+ recho_ "$r"
+}
+
+function: qlines "Traiter chaque ligne de l'entrée standard pour en faire des chaines quotées avec '"
+function qlines() {
+ sed "s/'/'\\\\''/g; s/.*/'&'/g"
+}
+
+function: setv "initialiser la variable \$1 avec la valeur \$2..*
+
+note: en principe, la syntaxe est 'setv var values...'. cependant, la syntaxe 'setv var=values...' est supportée aussi"
+function setv() {
+ local s__var="$1"; shift
+ if [[ "$s__var" == *=* ]]; then
+ set -- "${s__var#*=}" "$@"
+ s__var="${s__var%%=*}"
+ fi
+ eval "$s__var=\"\$*\""
+}
+
+function: _setv "Comme la fonction setv() mais ne supporte que la syntaxe '_setv var values...'
+
+Cette fonction est légèrement plus rapide que setv()"
+function _setv() {
+ local s__var="$1"; shift
+ eval "$s__var=\"\$*\""
+}
+
+function: echo_setv "Afficher la commande qui serait lancée par setv \"\$@\""
+function echo_setv() {
+ local s__var="$1"; shift
+ if [[ "$s__var" == *=* ]]; then
+ set -- "${s__var#*=}" "$@"
+ s__var="${s__var%%=*}"
+ fi
+ echo "$s__var=$(qvalr "$*")"
+}
+
+function: echo_setv2 "Afficher la commande qui recrée la variable \$1.
+
+Equivalent à
+~~~
+echo_setv \"\$1=\${!1}\"
+~~~
+
+Si d'autres arguments que le nom de la variable sont spécifiés, cette fonction
+se comporte comme echo_setv()"
+function echo_setv2() {
+ local s__var="$1"; shift
+ if [[ "$s__var" == *=* ]]; then
+ set -- "${s__var#*=}" "$@"
+ s__var="${s__var%%=*}"
+ fi
+ if [ $# -eq 0 ]; then
+ echo_setv "$s__var" "${!s__var}"
+ else
+ echo_setv "$s__var" "$@"
+ fi
+}
+
+function: seta "initialiser le tableau \$1 avec les valeurs \$2..@
+
+note: en principe, la syntaxe est 'seta array values...'. cependant, la syntaxe
+'seta array=values...' est supportée aussi"
+function seta() {
+ local s__array="$1"; shift
+ if [[ "$s__array" == *=* ]]; then
+ set -- "${s__array#*=}" "$@"
+ s__array="${s__array%%=*}"
+ fi
+ eval "$s__array=(\"\$@\")"
+}
+
+function: _seta "Comme la fonction seta() mais ne supporte que la syntaxe '_seta array values...'
+
+Cette fonction est légèrement plus rapide que seta()"
+function _seta() {
+ local s__array="$1"; shift
+ eval "$s__array=(\"\$@\")"
+}
+
+function: echo_seta "Afficher la commande qui serait lancée par seta \"\$@\""
+function echo_seta() {
+ local s__var="$1"; shift
+ if [[ "$s__var" == *=* ]]; then
+ set -- "${s__var#*=}" "$@"
+ s__var="${s__var%%=*}"
+ fi
+ echo "$s__var=($(qvals "$@"))"
+}
+
+function: echo_seta2 "Afficher la commande qui recrée le tableau \$1
+
+Si d'autres arguments que le nom de tableau sont spécifiés, cette fonction se
+comporte comme echo_seta()"
+function echo_seta2() {
+ local s__var="$1"; shift
+ if [[ "$s__var" == *=* ]]; then
+ set -- "${s__var#*=}" "$@"
+ s__var="${s__var%%=*}"
+ elif [ $# -eq 0 ]; then
+ eval "set -- \"\${$s__var[@]}\""
+ fi
+ echo "$s__var=($(qvals "$@"))"
+}
+
+function: setx "Initialiser une variable avec le résultat d'une commande
+
+* syntaxe 1: initialiser la variable \$1 avec le résultat de la commande \"\$2..@\"
+ ~~~
+ setx var cmd
+ ~~~
+ note: en principe, la syntaxe est 'setx var cmd args...'. cependant, la syntaxe
+ 'setx var=cmd args...' est supportée aussi
+
+* syntaxe 2: initialiser le tableau \$1 avec le résultat de la commande
+ \"\$2..@\", chaque ligne du résultat étant un élément du tableau
+ ~~~
+ setx -a array cmd
+ ~~~
+ note: en principe, la syntaxe est 'setx -a array cmd args...'. cependant, la
+ syntaxe 'setx -a array=cmd args...' est supportée aussi"
+function setx() {
+ if [ "$1" == -a ]; then
+ shift
+ local s__array="$1"; shift
+ if [[ "$s__array" == *=* ]]; then
+ set -- "${s__array#*=}" "$@"
+ s__array="${s__array%%=*}"
+ fi
+ eval "$s__array=($("$@" | qlines))"
+ else
+ local s__var="$1"; shift
+ if [[ "$s__var" == *=* ]]; then
+ set -- "${s__var#*=}" "$@"
+ s__var="${s__var%%=*}"
+ fi
+ eval "$s__var="'"$("$@")"'
+ fi
+}
+
+function: _setvx "Comme la fonction setx() mais ne supporte que l'initialisation d'une variable scalaire avec la syntaxe '_setvx var cmd args...' pour gagner (un peu) en rapidité d'exécution."
+function _setvx() {
+ local s__var="$1"; shift
+ eval "$s__var="'"$("$@")"'
+}
+
+function: _setax "Comme la fonction setx() mais ne supporte que l'initialisation d'un tableau avec la syntaxe '_setax array cmd args...' pour gagner (un peu) en rapidité d'exécution."
+function _setax() {
+ local s__array="$1"; shift
+ eval "$s__array=($("$@" | qlines))"
+}
+
+function: is_defined "tester si la variable \$1 est définie"
+function is_defined() {
+ [ -n "$(declare -p "$1" 2>/dev/null)" ]
+}
+
+function: is_array "tester si la variable \$1 est un tableau"
+function is_array() {
+ [[ "$(declare -p "$1" 2>/dev/null)" =~ declare\ -[^\ ]*a[^\ ]*\ ]]
+}
+
+function: array_local "afficher les commandes pour faire une copie dans la variable locale \$1 du tableau \$2"
+function array_local() {
+ if [ "$1" == "$2" ]; then
+ declare -p "$1" 2>/dev/null || echo "local -a $1"
+ else
+ echo "local -a $1; $1=(\"\${$2[@]}\")"
+ fi
+}
+
+function: upvar "Implémentation de upvar() de http://www.fvue.nl/wiki/Bash:_Passing_variables_by_reference
+
+USAGE
+~~~
+local varname && upvar varname values...
+~~~
+* @param varname Variable name to assign value to
+* @param values Value(s) to assign. If multiple values (> 1), an array is
+ assigned, otherwise a single value is assigned."
+function upvar() {
+ if unset -v "$1"; then
+ if [ $# -lt 2 ]; then
+ eval "$1=\"\$2\""
+ else
+ eval "$1=(\"\${@:2}\")"
+ fi
+ fi
+}
+
+function: array_upvar "Comme upvar() mais force la création d'un tableau, même s'il y a que 0 ou 1 argument"
+function array_upvar() {
+ unset -v "$1" && eval "$1=(\"\${@:2}\")"
+}
+
+function: upvars "Implémentation modifiée de upvars() de http://www.fvue.nl/wiki/Bash:_Passing_variables_by_reference
+
+Par rapport à l'original, il n'est plus nécessaire de préfixer une variable
+scalaire avec -v, et -a peut être spécifié sans argument.
+
+USAGE
+~~~
+local varnames... && upvars [varname value | -aN varname values...]...
+~~~
+* @param -a assigns remaining values to varname as array
+* @param -aN assigns next N values to varname as array. Returns 1 if wrong
+ number of options occurs"
+function upvars() {
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ -a)
+ unset -v "$2" && eval "$2=(\"\${@:3}\")"
+ break
+ ;;
+ -a*)
+ unset -v "$2" && eval "$2=(\"\${@:3:${1#-a}}\")"
+ shift $((${1#-a} + 2)) || return 1
+ ;;
+ *)
+ unset -v "$1" && eval "$1=\"\$2\""
+ shift; shift
+ ;;
+ esac
+ done
+}
+
+function: set_debug "Passer en mode DEBUG"
+function set_debug() {
+ export NULIB_DEBUG=1
+}
+
+function: is_debug "Tester si on est en mode DEBUG"
+function is_debug() {
+ [ -n "$NULIB_DEBUG" ]
+}
+
+function: lawk "Lancer GNUawk avec la librairie 'base'"
+function lawk() {
+ gawk -i base.awk "$@"
+}
+
+function: cawk "Lancer GNUawk avec LANG=C et la librairie 'base'
+
+Le fait de forcer la valeur de LANG permet d'éviter les problèmes avec la locale"
+function cawk() {
+ LANG=C gawk -i base.awk "$@"
+}
+
+function: lsort "Lancer sort avec support de la locale courante"
+function: csort "Lancer sort avec LANG=C pour désactiver le support de la locale
+
+Avec LANG!=C, sort utilise les règles de la locale pour le tri, et par
+exemple, avec LANG=fr_FR.UTF-8, la locale indique que les ponctuations doivent
+être ignorées."
+function lsort() { sort "$@"; }
+function csort() { LANG=C sort "$@"; }
+
+function: lgrep "Lancer grep avec support de la locale courante"
+function: cgrep "Lancer grep avec LANG=C pour désactiver le support de la locale"
+function lgrep() { grep "$@"; }
+function cgrep() { LANG=C grep "$@"; }
+
+function: lsed "Lancer sed avec support de la locale courante"
+function: csed "Lancer sed avec LANG=C pour désactiver le support de la locale"
+function lsed() { sed "$@"; }
+function csed() { LANG=C sed "$@"; }
+
+function: ldiff "Lancer diff avec support de la locale courante"
+function: cdiff "Lancer diff avec LANG=C pour désactiver le support de la locale"
+function ldiff() { diff "$@"; }
+function cdiff() { LANG=C diff "$@"; }
diff --git a/bash/src/base.init.sh b/bash/src/base.init.sh
new file mode 100644
index 0000000..de5ae8c
--- /dev/null
+++ b/bash/src/base.init.sh
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: base.init "Fonctions de base: initialiser l'environnement"
+
+if [ -z "$NULIB_NO_INIT_ENV" ]; then
+ # Emplacement du script courant
+ if [ "$0" == "-bash" ]; then
+ MYNAME=
+ MYDIR=
+ MYSELF=
+ elif [ ! -f "$0" -a -f "${0#-}" ]; then
+ MYNAME="$(basename -- "${0#-}")"
+ MYDIR="$(dirname -- "${0#-}")"
+ MYDIR="$(cd "$MYDIR"; pwd)"
+ MYSELF="$MYDIR/$MYNAME"
+ else
+ MYNAME="$(basename -- "$0")"
+ MYDIR="$(dirname -- "$0")"
+ MYDIR="$(cd "$MYDIR"; pwd)"
+ MYSELF="$MYDIR/$MYNAME"
+ fi
+ [ -n "$NULIBDIR" ] || NULIBDIR="$MYDIR"
+
+ # Repertoire temporaire
+ [ -z "$TMPDIR" -a -d "$HOME/tmp" ] && TMPDIR="$HOME/tmp"
+ [ -z "$TMPDIR" ] && TMPDIR="${TMP:-${TEMP:-/tmp}}"
+ export TMPDIR
+
+ # User
+ [ -z "$USER" -a -n "$LOGNAME" ] && export USER="$LOGNAME"
+
+ # Le fichier nulibrc doit être chargé systématiquement
+ [ -f /etc/debian_chroot ] && NULIB_CHROOT=1
+ [ -f /etc/nulibrc ] && . /etc/nulibrc
+ [ -f ~/.nulibrc ] && . ~/.nulibrc
+
+ # Type de système sur lequel tourne le script
+ UNAME_SYSTEM=`uname -s`
+ [ "${UNAME_SYSTEM#CYGWIN}" != "$UNAME_SYSTEM" ] && UNAME_SYSTEM=Cygwin
+ [ "${UNAME_SYSTEM#MINGW32}" != "$UNAME_SYSTEM" ] && UNAME_SYSTEM=Mingw
+ UNAME_MACHINE=`uname -m`
+ if [ -n "$NULIB_CHROOT" ]; then
+ # Dans un chroot, il est possible de forcer les valeurs
+ [ -n "$NULIB_UNAME_SYSTEM" ] && eval "UNAME_SYSTEM=$NULIB_UNAME_SYSTEM"
+ [ -n "$NULIB_UNAME_MACHINE" ] && eval "UNAME_MACHINE=$NULIB_UNAME_MACHINE"
+ fi
+
+ # Nom d'hôte respectivement avec et sans domaine
+ # contrairement à $HOSTNAME, cette valeur peut être spécifiée, comme par ruinst
+ [ -n "$MYHOST" ] || MYHOST="$HOSTNAME"
+ [ -n "$MYHOSTNAME" ] || MYHOSTNAME="${HOSTNAME%%.*}"
+ export MYHOST MYHOSTNAME
+fi
diff --git a/bash/src/base.input.sh b/bash/src/base.input.sh
new file mode 100644
index 0000000..9e9acf0
--- /dev/null
+++ b/bash/src/base.input.sh
@@ -0,0 +1,581 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: base.input "Fonctions de base: saisie"
+
+function toienc() {
+# $1 étant une variable contenant une chaine encodée dans l'encoding d'entrée $2
+# (qui vaut par défaut $NULIB_INPUT_ENCODING), transformer cette chaine en
+# utf-8
+ local __var="$1" __from="${2:-$NULIB_INPUT_ENCODING}"
+ if [ "$__from" != "NULIB__UTF8" ]; then
+ _setv "$__var" "$(iconv -f "$__from" -t utf-8 <<<"${!__var}")"
+ fi
+}
+
+function uread() {
+# Lire une valeur sur stdin et la placer dans la variable $1. On assume que la
+# valeur en entrée est encodée dans $NULIB_INPUT_ENCODING
+ [ $# -gt 0 ] || set -- REPLY
+ local __var
+ read "$@"
+ for __var in "$@"; do
+ [ -z "$__var" -o "${__var:0:1}" == "-" ] && continue # ignorer les options
+ toienc "$__var"
+ done
+}
+
+function set_interaction() { :;}
+function is_interaction() { return 1; }
+function check_interaction() { return 0; }
+function get_interaction_option() { :;}
+
+function ask_yesno() {
+# Afficher le message $1 suivi de [oN] ou [On] suivant que $2 vaut O ou N, puis
+# lire la réponse. Retourner 0 si la réponse est vrai, 1 sinon.
+# Si $1 est une option, elle est utilisée avec check_interaction pour savoir si
+# on est en mode interactif ou non. A ce moment-là, les valeurs sont décalées
+# ($2=message, $3=default)
+# Si $2 vaut C, la valeur par défaut est N si on est interactif, O sinon
+# Si $2 vaut X, la valeur par défaut est O si on est interactif, N sinon
+ local interactive=1
+ if [[ "$1" == -* ]]; then
+ if [ "$1" != -- ]; then
+ check_interaction "$1" || interactive=
+ fi
+ shift
+ else
+ check_interaction -c || interactive=
+ fi
+ local default="${2:-N}"
+ if [ "$default" == "C" ]; then
+ [ -n "$interactive" ] && default=N || default=O
+ elif [ "$default" == "X" ]; then
+ [ -n "$interactive" ] && default=O || default=N
+ fi
+ if [ -n "$interactive" ]; then
+ local message="$1"
+ local prompt="[oN]"
+ local r
+ is_yes "$default" && prompt="[On]"
+ if [ -n "$message" ]; then
+ __eecho_ "$message" 1>&2
+ else
+ __eecho_ "Voulez-vous continuer?" 1>&2
+ fi
+ tooenc_ " $prompt " 1>&2
+ uread r
+ is_yes "${r:-$default}"
+ else
+ is_yes "$default"
+ fi
+}
+
+function ask_any() {
+# Afficher le message $1 suivi du texte "[$2]" (qui vaut par défaut +Oq), puis
+# lire la réponse. Les lettres de la chaine de format $2 sont numérotées de 0 à
+# $((${#2} - 1)). Le code de retour est le numéro de la lettre qui a été
+# sélectionnée. Cette fonction est une généralisation de ask_yesno() pour
+# n'importe quel ensemble de lettres.
+# La première lettre en majuscule est la lettre sélectionnée par défaut.
+# La lettre O matche toutes les lettres qui signifient oui: o, y, 1, v, t
+# La lettre N matche toutes les lettres qui signifient non: n, f, 0
+# Il y a des raccourcis:
+# +O --> On
+# +N --> oN
+# +C --> oN si on est en mode interactif, On sinon
+# +X --> On si on est en mode interactifn oN sinon
+# Si $1 est une option, elle est utilisée avec check_interaction pour savoir si
+# on est en mode interactif ou non. A ce moment-là, les valeurs sont décalées
+# ($2=message, $3=format)
+ local interactive=1
+ if [[ "$1" == -* ]]; then
+ if [ "$1" != -- ]; then
+ check_interaction "$1" || interactive=
+ fi
+ shift
+ else
+ check_interaction -c || interactive=
+ fi
+ local format="${2:-+Oq}"
+ format="${format/+O/On}"
+ format="${format/+N/oN}"
+ if [ -n "$interactive" ]; then
+ format="${format/+C/oN}"
+ format="${format/+X/On}"
+ else
+ format="${format/+C/On}"
+ format="${format/+X/oN}"
+ fi
+ local i count="${#format}"
+
+ if [ -n "$interactive" ]; then
+ local message="${1:-Voulez-vous continuer?}"
+ local prompt="[$format]"
+ local r f lf defi
+ while true; do
+ __eecho_ "$message $prompt " 1>&2
+ uread r
+ r="$(strlower "${r:0:1}")"
+ i=0; defi=
+ while [ $i -lt $count ]; do
+ f="${format:$i:1}"
+ lf="$(strlower "$f")"
+ [ "$r" == "$lf" ] && return $i
+ if [ -z "$defi" ]; then
+ [ -z "${f/[A-Z]/}" ] && defi="$i"
+ fi
+ if [ "$lf" == o ]; then
+ case "$r" in o|y|1|v|t) return $i;; esac
+ elif [ "$lf" == n ]; then
+ case "$r" in n|f|0) return $i;; esac
+ fi
+ i=$(($i + 1))
+ done
+ [ -z "$r" ] && return ${defi:-0}
+ done
+ else
+ i=0
+ while [ $i -lt $count ]; do
+ f="${format:$i:1}"
+ [ -z "${f/[A-Z]/}" ] && return $i
+ i=$(($i + 1))
+ done
+ return 0
+ fi
+}
+
+function read_value() {
+# Afficher le message $1 suivi de la valeur par défaut [$3] si elle est non
+# vide, puis lire la valeur donnée par l'utilisateur. Cette valeur doit être non
+# vide si $4(=O) est vrai. La valeur saisie est placée dans la variable
+# $2(=value)
+# Si $1 est une option, elle est utilisée avec check_interaction pour savoir si
+# on est en mode interactif ou non. A ce moment-là, les valeurs sont décalées
+# ($2=message, $3=variable, $4=default, $5=required)
+# En mode non interactif, c'est la valeur par défaut qui est sélectionnée. Si
+# l'utilisateur requière que la valeur soit non vide et que la valeur par défaut
+# est vide, afficher un message d'erreur et retourner faux
+# read_password() est comme read_value(), mais la valeur saisie n'est pas
+# affichée, ce qui la rend appropriée pour la lecture d'un mot de passe.
+ local -a __rv_opts; local __rv_readline=1 __rv_showdef=1 __rv_nl=
+ __rv_opts=()
+ [ -n "$NULIB_NO_READLINE" ] && __rv_readline=
+ __rv_read "$@"
+}
+
+function read_password() {
+ local -a __rv_opts __rv_readline= __rv_showdef= __rv_nl=1
+ __rv_opts=(-s)
+ __rv_read "$@"
+}
+
+function __rv_read() {
+ local __rv_int=1
+ if [[ "$1" == -* ]]; then
+ if [ "$1" != -- ]; then
+ check_interaction "$1" || __rv_int=
+ fi
+ shift
+ else
+ check_interaction -c || __rv_int=
+ fi
+ local __rv_msg="$1" __rv_v="${2:-value}" __rv_d="$3" __rv_re="${4:-O}"
+ if [ -z "$__rv_int" ]; then
+ # En mode non interactif, retourner la valeur par défaut
+ if is_yes "$__rv_re" && [ -z "$__rv_d" ]; then
+ eerror "La valeur par défaut de $__rv_v doit être non vide"
+ return 1
+ fi
+ _setv "$__rv_v" "$__rv_d"
+ return 0
+ fi
+
+ local __rv_r
+ while true; do
+ if [ -n "$__rv_msg" ]; then
+ __eecho_ "$__rv_msg" 1>&2
+ else
+ __eecho_ "Entrez la valeur" 1>&2
+ fi
+ if [ -n "$__rv_readline" ]; then
+ tooenc_ ": " 1>&2
+ uread -e ${__rv_d:+-i"$__rv_d"} "${__rv_opts[@]}" __rv_r
+ else
+ if [ -n "$__rv_d" ]; then
+ if [ -n "$__rv_showdef" ]; then
+ tooenc_ " [$__rv_d]" 1>&2
+ else
+ tooenc_ " [****]" 1>&2
+ fi
+ fi
+ tooenc_ ": " 1>&2
+ uread "${__rv_opts[@]}" __rv_r
+ [ -n "$__rv_nl" ] && echo
+ fi
+ __rv_r="${__rv_r:-$__rv_d}"
+ if [ -n "$__rv_r" ] || ! is_yes "$__rv_re"; then
+ _setv "$__rv_v" "$__rv_r"
+ return 0
+ fi
+ done
+}
+
+function simple_menu() {
+# Afficher un menu simple dont les éléments sont les valeurs du tableau
+# $2(=options). L'option choisie est placée dans la variable $1(=option)
+# -t TITLE: spécifier le titre du menu
+# -m YOUR_CHOICE: spécifier le message d'invite pour la sélection de l'option
+# -d DEFAULT: spécifier l'option par défaut. Par défaut, prendre la valeur
+# actuelle de la variable $1(=option)
+ eval "$(local_args)"
+ local __sm_title= __sm_yourchoice= __sm_default=
+ args=(
+ -t:,--title __sm_title=
+ -m:,--prompt __sm_yourchoice=
+ -d:,--default __sm_default=
+ )
+ parse_args "$@"; set -- "${args[@]}"
+
+ local __sm_option_var="${1:-option}" __sm_options_var="${2:-options}"
+ local __sm_option __sm_options
+ __sm_options="$__sm_options_var[*]"
+ if [ -z "${!__sm_options}" ]; then
+ eerror "Le tableau $__sm_options_var doit être non vide"
+ return 1
+ fi
+ [ -z "$__sm_default" ] && __sm_default="${!__sm_option_var}"
+
+ array_copy __sm_options "$__sm_options_var"
+ local __sm_c=0 __sm_i __sm_choice
+ while true; do
+ if [ "$__sm_c" == "0" ]; then
+ # Afficher le menu
+ [ -n "$__sm_title" ] && __eecho "=== $__sm_title ===" 1>&2
+ __sm_i=1
+ for __sm_option in "${__sm_options[@]}"; do
+ if [ "$__sm_option" == "$__sm_default" ]; then
+ __eecho "$__sm_i*- $__sm_option" 1>&2
+ else
+ __eecho "$__sm_i - $__sm_option" 1>&2
+ fi
+ let __sm_i=$__sm_i+1
+ done
+ fi
+
+ # Afficher les choix
+ if [ -n "$__sm_yourchoice" ]; then
+ __eecho_ "$__sm_yourchoice" 1>&2
+ else
+ __eecho_ "Entrez le numéro de l'option choisie" 1>&2
+ fi
+ tooenc_ ": " 1>&2
+ uread __sm_choice
+
+ # Valeur par défaut
+ if [ -z "$__sm_choice" -a -n "$__sm_default" ]; then
+ __sm_option="$__sm_default"
+ break
+ fi
+ # Vérifier la saisie
+ if [ -n "$__sm_choice" -a -z "${__sm_choice//[0-9]/}" ]; then
+ if [ "$__sm_choice" -gt 0 -a "$__sm_choice" -le "${#__sm_options[*]}" ]; then
+ __sm_option="${__sm_options[$(($__sm_choice - 1))]}"
+ break
+ else
+ eerror "Numéro d'option incorrect"
+ fi
+ else
+ eerror "Vous devez saisir le numéro de l'option choisie"
+ fi
+
+ let __sm_c=$__sm_c+1
+ if [ "$__sm_c" -eq 5 ]; then
+ # sauter une ligne toutes les 4 tentatives
+ tooenc "" 1>&2
+ __sm_c=0
+ fi
+ done
+ _setv "$__sm_option_var" "$__sm_option"
+}
+
+function actions_menu() {
+# Afficher un menu dont les éléments sont les valeurs du tableau $4(=options),
+# et une liste d'actions tirées du tableau $3(=actions). L'option choisie est
+# placée dans la variable $2(=option). L'action choisie est placée dans la
+# variable $1(=action)
+# Un choix est saisi sous la forme [action]num_option
+# -t TITLE: spécifier le titre du menu
+# -m OPT_YOUR_CHOICE: spécifier le message d'invite pour la sélection de
+# l'action et de l'option
+# -M ACT_YOUR_CHOICE: spécifier le message d'invite dans le cas où aucune option
+# n'est disponible. Dans ce cas, seules les actions vides sont possibles.
+# -e VOID_ACTION: spécifier qu'une action est vide, c'est à dire qu'elle ne
+# requière pas d'être associée à une option. Par défaut, la dernière action
+# est classée dans cette catégorie puisque c'est l'action "quitter"
+# -d DEFAULT_ACTION: choisir l'action par défaut. par défaut, c'est la première
+# action.
+# -q QUIT_ACTION: choisir l'option "quitter" qui provoque la sortie du menu sans
+# choix. par défaut, c'est la dernière action.
+# -o DEFAULT_OPTION: choisir l'option par défaut. par défaut, prendre la valeur
+# actuelle de la variable $2(=option)
+ eval "$(local_args)"
+ local -a __am_action_descs __am_options __am_void_actions
+ local __am_tmp __am_select_action __am_select_option __am_title __am_optyc __am_actyc
+ local __am_default_action=auto __am_quit_action=auto
+ local __am_default_option=
+ args=(
+ -t:,--title __am_title=
+ -m:,--prompt __am_optyc=
+ -M:,--no-prompt __am_actyc=
+ -e: __am_void_actions
+ -d: __am_default_action=
+ -q: __am_quit_action=
+ -o: __am_default_option=
+ )
+ parse_args "$@"; set -- "${args[@]}"
+
+ __am_tmp="${1:-action}"; __am_select_action="${!__am_tmp}"
+ __am_tmp="${2:-option}"; __am_select_option="${!__am_tmp}"
+ [ -n "$__am_default_option" ] && __am_select_option="$__am_default_option"
+ array_copy __am_action_descs "${3:-actions}"
+ array_copy __am_options "${4:-options}"
+
+ eerror_unless [ ${#__am_action_descs[*]} -gt 0 ] "Vous devez spécifier le tableau des actions" || return
+ __actions_menu || return 1
+ _setv "${1:-action}" "$__am_select_action"
+ _setv "${2:-option}" "$__am_select_option"
+}
+
+function __actions_menu() {
+ local title="$__am_title"
+ local optyc="$__am_optyc" actyc="$__am_actyc"
+ local default_action="$__am_default_action"
+ local quit_action="$__am_quit_action"
+ local select_action="$__am_select_action"
+ local select_option="$__am_select_option"
+ local -a action_descs options void_actions
+ array_copy action_descs __am_action_descs
+ array_copy options __am_options
+ array_copy void_actions __am_void_actions
+
+ # Calculer la liste des actions valides
+ local no_options
+ array_isempty options && no_options=1
+
+ local -a actions
+ local tmp action name
+ for tmp in "${action_descs[@]}"; do
+ splitfsep2 "$tmp" : action name
+ [ -n "$action" ] || action="${name:0:1}"
+ action="$(strlower "$action")"
+ array_addu actions "$action"
+ done
+
+ # Calculer l'action par défaut
+ if [ "$default_action" == auto ]; then
+ # si action par défaut non spécifiée, alors prendre la première action
+ default_action="$select_action"
+ if [ -n "$default_action" ]; then
+ array_contains actions "$default_action" || default_action=
+ fi
+ [ -n "$default_action" ] || default_action="${actions[0]}"
+ fi
+ default_action="${default_action:0:1}"
+ default_action="$(strlower "$default_action")"
+
+ # Calculer l'action quitter par défaut
+ if [ "$quit_action" == auto ]; then
+ # si action par défaut non spécifiée, alors prendre la dernière action,
+ # s'il y a au moins 2 actions
+ if [ ${#actions[*]} -gt 1 ]; then
+ quit_action="${actions[@]:$((-1)):1}"
+ array_addu void_actions "$quit_action"
+ fi
+ fi
+ quit_action="${quit_action:0:1}"
+ quit_action="$(strlower "$quit_action")"
+
+ # Calculer la ligne des actions à afficher
+ local action_title
+ for tmp in "${action_descs[@]}"; do
+ splitfsep2 "$tmp" : action name
+ [ -n "$action" ] || action="${name:0:1}"
+ [ -n "$name" ] || name="$action"
+ action="$(strlower "$action")"
+ if [ -n "$no_options" ]; then
+ if ! array_contains void_actions "$action"; then
+ array_del actions "$action"
+ continue
+ fi
+ fi
+ [ "$action" == "$default_action" ] && name="$name*"
+ action_title="${action_title:+$action_title/}$name"
+ done
+ if [ -n "$default_action" ]; then
+ # si action par défaut invalide, alors pas d'action par défaut
+ array_contains actions "$default_action" || default_action=
+ fi
+ if [ -n "$quit_action" ]; then
+ # si action quitter invalide, alors pas d'action quitter
+ array_contains actions "$quit_action" || quit_action=
+ fi
+
+ # Type de menu
+ if [ -n "$no_options" ]; then
+ if array_isempty void_actions; then
+ eerror "Aucune option n'est définie. Il faut définir le tableau des actions vides"
+ return 1
+ fi
+ __void_actions_menu
+ else
+ __options_actions_menu
+ fi
+}
+
+function __void_actions_menu() {
+ local c=0 choice
+ while true; do
+ if [ $c -eq 0 ]; then
+ [ -n "$title" ] && __etitle "$title" 1>&2
+ __eecho_ "=== Actions disponibles: " 1>&2
+ tooenc "$action_title" 1>&2
+ fi
+ if [ -n "$actyc" ]; then
+ __eecho_ "$actyc" 1>&2
+ elif [ -n "$optyc" ]; then
+ __eecho_ "$optyc" 1>&2
+ else
+ __eecho_ "Entrez l'action à effectuer" 1>&2
+ fi
+ tooenc_ ": " 1>&2
+ uread choice
+ if [ -z "$choice" -a -n "$default_action" ]; then
+ select_action="$default_action"
+ break
+ fi
+
+ # vérifier la saisie
+ choice="${choice:0:1}"
+ choice="$(strlower "$choice")"
+ if array_contains actions "$choice"; then
+ select_action="$choice"
+ break
+ elif [ -n "$choice" ]; then
+ eerror "$choice: action incorrecte"
+ else
+ eerror "vous devez saisir l'action à effectuer"
+ fi
+ let c=$c+1
+ if [ $c -eq 5 ]; then
+ # sauter une ligne toutes les 4 tentatives
+ tooenc "" 1>&2
+ c=0
+ fi
+ done
+ __am_select_action="$select_action"
+ __am_select_option=
+}
+
+function __options_actions_menu() {
+ local c=0 option choice action option
+ while true; do
+ if [ $c -eq 0 ]; then
+ [ -n "$title" ] && __etitle "$title" 1>&2
+ i=1
+ for option in "${options[@]}"; do
+ if [ "$option" == "$select_option" ]; then
+ tooenc "$i*- $option" 1>&2
+ else
+ tooenc "$i - $option" 1>&2
+ fi
+ let i=$i+1
+ done
+ __estepn_ "Actions disponibles: " 1>&2
+ tooenc "$action_title" 1>&2
+ fi
+ if [ -n "$optyc" ]; then
+ __eecho_ "$optyc" 1>&2
+ else
+ __eecho_ "Entrez l'action et le numéro de l'option choisie" 1>&2
+ fi
+ tooenc_ ": " 1>&2
+ uread choice
+
+ # vérifier la saisie
+ if [ -z "$choice" -a -n "$default_action" ]; then
+ action="$default_action"
+ if array_contains void_actions "$action"; then
+ select_action="$action"
+ select_option=
+ break
+ elif [ -n "$select_option" ]; then
+ select_action="$action"
+ break
+ fi
+ fi
+ action="${choice:0:1}"
+ action="$(strlower "$action")"
+ if array_contains actions "$action"; then
+ # on commence par un code d'action valide. cool :-)
+ if array_contains void_actions "$action"; then
+ select_action="$action"
+ select_option=
+ break
+ else
+ option="${choice:1}"
+ option="${option// /}"
+ if [ -z "$option" -a -n "$select_option" ]; then
+ select_action="$action"
+ break
+ elif [ -z "$option" ]; then
+ eerror "vous devez saisir le numéro de l'option"
+ elif isnum "$option"; then
+ if [ $option -gt 0 -a $option -le ${#options[*]} ]; then
+ select_action="$action"
+ select_option="${options[$(($option - 1))]}"
+ break
+ fi
+ else
+ eerror "$option: numéro d'option incorrecte"
+ fi
+ fi
+ elif isnum "$choice"; then
+ # on a simplement donné un numéro d'option
+ action="$default_action"
+ if [ -n "$action" ]; then
+ if array_contains void_actions "$action"; then
+ select_action="$action"
+ select_option=
+ break
+ else
+ option="${choice// /}"
+ if [ -z "$option" ]; then
+ eerror "vous devez saisir le numéro de l'option"
+ elif isnum "$option"; then
+ if [ $option -gt 0 -a $option -le ${#options[*]} ]; then
+ select_action="$action"
+ select_option="${options[$(($option - 1))]}"
+ break
+ fi
+ else
+ eerror "$option: numéro d'option incorrecte"
+ fi
+ fi
+ else
+ eerror "Vous devez spécifier l'action à effectuer"
+ fi
+ elif [ -n "$choice" ]; then
+ eerror "$choice: action et/ou option incorrecte"
+ else
+ eerror "vous devez saisir l'action à effectuer"
+ fi
+ let c=$c+1
+ if [ $c -eq 5 ]; then
+ # sauter une ligne toutes les 4 tentatives
+ tooenc "" 1>&2
+ c=0
+ fi
+ done
+ __am_select_action="$select_action"
+ __am_select_option="$select_option"
+}
diff --git a/bash/src/base.num.sh b/bash/src/base.num.sh
new file mode 100644
index 0000000..b54e0ee
--- /dev/null
+++ b/bash/src/base.num.sh
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: base.num "Fonctions de base: gestion des valeurs numériques"
+
+function: isnum 'retourner vrai si $1 est une valeur numérique entière (positive ou négative)'
+function isnum() {
+ [ ${#1} -gt 0 ] || return 1
+ local v="${1#-}"
+ [ ${#v} -gt 0 ] || return 1
+ v="${v//[0-9]/}"
+ [ -z "$v" ]
+}
+
+function: ispnum 'retourner vrai si $1 est une valeur numérique entière positive'
+function ispnum() {
+ [ ${#1} -gt 0 ] || return 1
+ [ -z "${1//[0-9]/}" ]
+}
+
+function: isrnum 'retourner vrai si $1 est une valeur numérique réelle (positive ou négative)
+le séparateur décimal peut être . ou ,'
+function isrnum() {
+ [ ${#1} -gt 0 ] || return 1
+ local v="${1#-}"
+ [ ${#v} -gt 0 ] || return 1
+ v="${v//./}"
+ v="${v//,/}"
+ v="${v//[0-9]/}"
+ [ -z "$v" ]
+}
diff --git a/bash/src/base.output.sh b/bash/src/base.output.sh
new file mode 100644
index 0000000..ae51afd
--- /dev/null
+++ b/bash/src/base.output.sh
@@ -0,0 +1,601 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: base.output "Fonctions de base: affichage"
+nulib__load: _output_vanilla
+
+NULIB__TAB=$'\t'
+NULIB__LATIN1=iso-8859-1
+NULIB__LATIN9=iso-8859-15
+NULIB__UTF8=utf-8
+
+[ -n "$LANG" ] || export LANG=fr_FR.UTF-8
+if [ ! -x "$(which iconv 2>/dev/null)" ]; then
+ function iconv() { cat; }
+fi
+
+function nulib__lang_encoding() {
+ case "${LANG,,}" in
+ *@euro) echo "iso-8859-15";;
+ *.utf-8|*.utf8) echo "utf-8";;
+ *) echo "iso-8859-1";;
+ esac
+}
+
+function nulib__norm_encoding() {
+ local enc="${1,,}"
+ enc="${enc//[-_]/}"
+ case "$enc" in
+ latin|latin1|iso8859|iso88591|8859|88591) echo "iso-8859-1";;
+ latin9|iso885915|885915) echo "iso-8859-15";;
+ utf|utf8) echo "utf-8";;
+ *) echo "$1";;
+ esac
+}
+
+function nulib__init_encoding() {
+ local default_encoding="$(nulib__lang_encoding)"
+ [ -n "$default_encoding" ] || default_encoding=utf-8
+ [ -n "$NULIB_OUTPUT_ENCODING" ] || NULIB_OUTPUT_ENCODING="$default_encoding"
+ NULIB_OUTPUT_ENCODING="$(nulib__norm_encoding "$NULIB_OUTPUT_ENCODING")"
+ [ -n "$NULIB_INPUT_ENCODING" ] || NULIB_INPUT_ENCODING="$NULIB_OUTPUT_ENCODING"
+ NULIB_INPUT_ENCODING="$(nulib__norm_encoding "$NULIB_INPUT_ENCODING")"
+}
+[ -n "$NULIB_LANG" -a -z "$LANG" ] && export NULIB_LANG LANG="$NULIB_LANG"
+nulib__init_encoding
+
+function noerror() {
+# lancer la commande "$@" et masquer son code de retour
+ [ $# -gt 0 ] || set :
+ "$@" || return 0
+}
+
+function noout() {
+# lancer la commande "$@" en supprimant sa sortie standard
+ [ $# -gt 0 ] || return 0
+ "$@" >/dev/null
+}
+
+function noerr() {
+# lancer la commande "$@" en supprimant sa sortie d'erreur
+ [ $# -gt 0 ] || return 0
+ "$@" 2>/dev/null
+}
+
+function isatty() {
+# tester si STDOUT n'est pas une redirection
+ tty -s <&1
+}
+
+function in_isatty() {
+# tester si STDIN n'est pas une redirection
+ tty -s
+}
+
+function out_isatty() {
+# tester si STDOUT n'est pas une redirection. identique à isatty()
+ tty -s <&1
+}
+
+function err_isatty() {
+# tester si STDERR n'est pas une redirection
+ tty -s <&2
+}
+
+################################################################################
+
+function tooenc() {
+# $1 étant une chaine encodée en utf-8, l'afficher dans l'encoding de sortie $2
+# qui vaut par défaut $NULIB_OUTPUT_ENCODING
+ local value="$1" to="${2:-$NULIB_OUTPUT_ENCODING}"
+ if [ "$to" == "$NULIB__UTF8" ]; then
+ recho "$value"
+ else
+ iconv -f -utf-8 -t "$to" <<<"$value"
+ fi
+}
+function uecho() { tooenc "$*"; }
+
+function tooenc_() {
+# $1 étant une chaine encodée en utf-8, l'afficher sans passer à la ligne dans
+# l'encoding de sortie $2 qui vaut par défaut $NULIB_OUTPUT_ENCODING
+ local value="$1" to="${2:-$NULIB_OUTPUT_ENCODING}"
+ if [ "$to" == "$NULIB__UTF8" ]; then
+ recho_ "$value"
+ else
+ recho_ "$value" | iconv -f utf-8 -t "$to"
+ fi
+}
+function uecho_() { tooenc_ "$*"; }
+
+export NULIB_QUIETLOG
+export NULIB__TMPLOG
+function: quietc "\
+N'afficher la sortie de la commande \$@ que si on est en mode DEBUG ou si elle se termine en erreur"
+function quietc() {
+ local r
+ [ -z "$NULIB__TMPLOG" ] && ac_set_tmpfile NULIB__TMPLOG
+ "$@" >&"$NULIB__TMPLOG"; r=$?
+ [ -n "$NULIB_QUIETLOG" ] && cat "$NULIB__TMPLOG" >>"$NULIB_QUIETLOG"
+ [ $r -ne 0 -o -n "$NULIB_DEBUG" ] && cat "$NULIB__TMPLOG"
+ return $r
+}
+
+function: quietc_logto "\
+Si quietc est utilisé, sauvegarder quand même la sortie dans le fichier \$1
+
+Utiliser l'option -a pour ajouter au fichier au lieu de l'écraser e.g
+ quietc_logto -a path/to/logfile
+
+Tous les autres arguments sont du contenu ajoutés au fichier, e.g
+ quietc_logto -a path/to/logfile \"\\
+================================================================================
+= \$(date +%F-%T)\""
+function quietc_logto() {
+ local append
+ if [ "$1" == -a ]; then
+ shift
+ append=1
+ fi
+ NULIB_QUIETLOG="$1"; shift
+ [ -n "$NULIB_QUIETLOG" ] || return
+ if [ -z "$append" ]; then
+ >"$NULIB_QUIETLOG"
+ fi
+ if [ $# -gt 0 ]; then
+ echo "$*" >>"$NULIB_QUIETLOG"
+ fi
+}
+
+function: quietc_echo "Ajouter \$* dans le fichier mentionné par quietc_logto() le cas échéant"
+function quietc_echo() {
+ if [ -n "$NULIB_QUIETLOG" ]; then
+ echo "$*" >>"$NULIB_QUIETLOG"
+ fi
+}
+
+# faut-il dater les messages des fonctions e* et action?
+# Faire NULIB_ELOG_DATE=1 en début de script pour activer cette fonctionnalité
+# faut-il rajouter aussi le nom du script? (nécessite NULIB_ELOG_DATE)
+# Faire NULIB_ELOG_MYNAME=1 en début de script pour activer cette fonctionnalité
+export NULIB_ELOG_DATE NULIB_ELOG_MYNAME
+function __edate() {
+ [ -n "$NULIB_ELOG_DATE" ] || return
+ local prefix="$(date +"[%F %T.%N] ")"
+ [ -n "$NULIB_ELOG_MYNAME" ] && prefix="$prefix$MYNAME "
+ echo "$prefix"
+}
+
+export NULIB_ELOG_OVERWRITE
+function __set_no_colors() { :; }
+function elogto() {
+# Activer NULIB_ELOG_DATE et rediriger STDOUT et STDERR vers le fichier $1
+# Si deux fichiers sont spécifiés, rediriger STDOUT vers $1 et STDERR vers $2
+# Si aucun fichier n'est spécifié, ne pas faire de redirection
+# Si la redirection est activée, forcer l'utilisation de l'encoding UTF8
+# Si NULIB_ELOG_OVERWRITE=1, alors le fichier en sortie est écrasé. Sinon, les
+# lignes en sortie lui sont ajoutées
+ NULIB_ELOG_DATE=1
+ NULIB_ELOG_MYNAME=1
+ if [ -n "$1" -a -n "$2" ]; then
+ LANG=fr_FR.UTF8
+ NULIB_OUTPUT_ENCODING="$NULIB__UTF8"
+ __set_no_colors 1
+ if [ -n "$NULIB_ELOG_OVERWRITE" ]; then
+ exec >"$1" 2>"$2"
+ else
+ exec >>"$1" 2>>"$2"
+ fi
+ elif [ -n "$1" ]; then
+ LANG=fr_FR.UTF8
+ NULIB_OUTPUT_ENCODING="$NULIB__UTF8"
+ __set_no_colors 1
+ if [ -n "$NULIB_ELOG_OVERWRITE" ]; then
+ exec >"$1" 2>&1
+ else
+ exec >>"$1" 2>&1
+ fi
+ fi
+}
+
+# variables utilisées pour l'affichage indenté des messages et des titres
+# NULIB__ESTACK est la liste des invocations de 'etitle' et 'action' en cours
+export NULIB__ESTACK NULIB__INDENT=
+function __eindent0() {
+# afficher le nombre d'espaces correspondant à l'indentation
+ local indent="${NULIB__ESTACK//?/ }"
+ indent="${indent% }$NULIB__INDENT"
+ [ -n "$indent" ] && echo "$indent"
+}
+function __eindent() {
+# indenter les lignes de $1, sauf la première
+ local -a lines; local line first=1
+ local indent="$(__eindent0)$2"
+ setx -a lines=echo "$1"
+ for line in "${lines[@]}"; do
+ if [ -n "$first" ]; then
+ recho "$line"
+ first=
+ else
+ recho "$indent$line"
+ fi
+ done
+}
+
+function __complete() {
+ # compléter $1 avec $3 jusqu'à obtenir une taille de $2 caractères
+ local columns="${COLUMNS:-80}"
+ local line="$1" maxi="${2:-$columns}" sep="${3:- }"
+ while [ ${#line} -lt $maxi ]; do
+ line="$line$sep"
+ done
+ echo "$line"
+}
+
+PRETTYOPTS=()
+function set_verbosity() { :;}
+function check_verbosity() { return 0; }
+function get_verbosity_option() { :;}
+
+# tester respectivement si on doit afficher les messages d'erreur,
+# d'avertissement, d'information, de debug
+function show_error() { return 0; }
+function show_warn() { return 0; }
+function show_info() { return 0; }
+function show_verbose() { return 0; }
+function show_debug() { [ -n "$NULIB_DEBUG" ]; }
+
+# note: toutes les fonctions d'affichage e* écrivent sur stderr
+
+function esection() {
+# Afficher une section. Toutes les indentations sont remises à zéro
+ show_info || return
+ eval "$NULIB__DISABLE_SET_X"
+ NULIB__ESTACK=
+ __esection "$*" 1>&2
+ eval "$NULIB__ENABLE_SET_X"
+}
+
+function etitle() {
+# Afficher le titre $1. Le contenu sous des titres imbriqués est affiché
+# indenté.
+# - si $2..$* est spécifié, c'est une commande qui est lancée dans le contexte
+# du titre, ensuite le titre est automatiquement terminé
+# - sinon il faut terminer le titre explicitement avec eend
+ local title="$1"; shift
+ # etitle
+ NULIB__ESTACK="$NULIB__ESTACK:t"
+ if show_info; then
+ eval "$NULIB__DISABLE_SET_X"
+ __etitle "$title" 1>&2
+ eval "$NULIB__ENABLE_SET_X"
+ fi
+ # commande
+ local r=0
+ if [ $# -gt 0 ]; then
+ "$@"; r=$?
+ eend
+ fi
+ return $r
+}
+
+
+function eend() {
+# Terminer un titre
+ if [ "${NULIB__ESTACK%:t}" != "$NULIB__ESTACK" ]; then
+ NULIB__ESTACK="${NULIB__ESTACK%:t}"
+ fi
+}
+
+function edesc() {
+# Afficher une description sous le titre courant
+ show_info || return
+ eval "$NULIB__DISABLE_SET_X"
+ __edesc "$*" 1>&2
+ eval "$NULIB__ENABLE_SET_X"
+}
+
+function ebanner() {
+# Afficher un message très important encadré, puis attendre 5 secondes
+ show_error || return
+ eval "$NULIB__DISABLE_SET_X"
+ __ebanner "$*" 1>&2
+ eval "$NULIB__ENABLE_SET_X"
+ sleep 5
+}
+
+function eimportant() {
+# Afficher un message très important
+ show_error || return
+ eval "$NULIB__DISABLE_SET_X"
+ __eimportant "$*" 1>&2
+ eval "$NULIB__ENABLE_SET_X"
+}
+function qimportant() { eimportant "$*"; quietc_echo "IMPORTANT: $*"; }
+
+function eattention() {
+# Afficher un message important
+ show_warn || return
+ eval "$NULIB__DISABLE_SET_X"
+ __eattention "$*" 1>&2
+ eval "$NULIB__ENABLE_SET_X"
+}
+function qattention() { eattention "$*"; quietc_echo "ATTENTION: $*"; }
+
+function eerror() {
+# Afficher un message d'erreur
+ show_error || return
+ eval "$NULIB__DISABLE_SET_X"
+ __eerror "$*" 1>&2
+ eval "$NULIB__ENABLE_SET_X"
+}
+function qerror() { eerror "$*"; quietc_echo "ERROR: $*"; }
+
+function eerror_unless() {
+ # Afficher $1 avec eerror() si la commande $2..@ retourne FAUX. dans tous les cas, retourner le code de retour de la commande.
+ local msg="$1"; shift
+ local r=1
+ if [ $# -eq 0 ]; then
+ [ -n "$msg" ] && eerror "$msg"
+ else
+ "$@"; r=$?
+ if [ $r -ne 0 -a -n "$msg" ]; then
+ eerror "$msg"
+ fi
+ fi
+ return $r
+}
+
+function eerror_if() {
+ # Afficher $1 avec eerror() si la commande $2..@ retourne VRAI. dans tous les cas, retourner le code de retour de la commande.
+ local msg="$1"; shift
+ local r=0
+ if [ $# -gt 0 ]; then
+ "$@"; r=$?
+ if [ $r -eq 0 -a -n "$msg" ]; then
+ eerror "$msg"
+ fi
+ fi
+ return $r
+}
+
+function set_die_return() {
+ NULIB__DIE="return 1"
+}
+function die() {
+ [ $# -gt 0 ] && eerror "$@"
+ local die="${NULIB__DIE:-exit 1}"
+ eval "$die" || return
+}
+
+function die_with {
+ [ $# -gt 0 ] && eerror "$1"
+ shift
+ [ $# -gt 0 ] && "$@"
+ local die="${NULIB__DIE:-exit 1}"
+ eval "$die" || return
+}
+
+function die_unless() {
+ # Afficher $1 et quitter le script avec die() si la commande $2..@ retourne FAUX
+ local msg="$1"; shift
+ local die="${NULIB__DIE:-exit 1}"
+ local r=1
+ if [ $# -eq 0 ]; then
+ [ -n "$msg" ] && eerror "$msg"
+ else
+ "$@"; r=$?
+ if [ $r -ne 0 -a -n "$msg" ]; then
+ eerror "$msg"
+ fi
+ fi
+ if [ $r -ne 0 ]; then
+ eval "${die% *} $r" || return $r
+ fi
+ return $r
+}
+
+function die_if() {
+ # Afficher $1 et quitter le script avec die() si la commande $2..@ retourne VRAI. sinon, retourner le code de retour de la commande
+ local msg="$1"; shift
+ local die="${NULIB__DIE:-exit 1}"
+ local r=0
+ if [ $# -gt 0 ]; then
+ "$@"; r=$?
+ if [ $r -eq 0 -a -n "$msg" ]; then
+ eerror "$msg"
+ fi
+ fi
+ if [ $r -eq 0 ]; then
+ eval "${die% *} $r" || return $r
+ fi
+ return $r
+}
+
+function exit_with {
+ if [ $# -gt 0 ]; then "$@"; fi
+ exit $?
+}
+
+function ewarn() {
+# Afficher un message d'avertissement
+ show_warn || return
+ eval "$NULIB__DISABLE_SET_X"
+ __ewarn "$*" 1>&2
+ eval "$NULIB__ENABLE_SET_X"
+}
+function qwarn() { ewarn "$*"; quietc_echo "WARN: $*"; }
+
+function enote() {
+# Afficher un message d'information de même niveau qu'un avertissement
+ show_info || return
+ eval "$NULIB__DISABLE_SET_X"
+ __enote "$*" 1>&2
+ eval "$NULIB__ENABLE_SET_X"
+}
+function qnote() { enote "$*"; quietc_echo "NOTE: $*"; }
+
+function einfo() {
+# Afficher un message d'information
+ show_info || return
+ eval "$NULIB__DISABLE_SET_X"
+ __einfo "$*" 1>&2
+ eval "$NULIB__ENABLE_SET_X"
+}
+function qinfo() { einfo "$*"; quietc_echo "INFO: $*"; }
+
+function eecho() {
+# Afficher un message d'information sans préfixe
+ show_info || return
+ eval "$NULIB__DISABLE_SET_X"
+ __eecho "$*" 1>&2
+ eval "$NULIB__ENABLE_SET_X"
+}
+function qecho() { eecho "$*"; quietc_echo "$*"; }
+
+function eecho_() {
+ show_info || return
+ eval "$NULIB__DISABLE_SET_X"
+ __eecho_ "$*" 1>&2
+ eval "$NULIB__ENABLE_SET_X"
+}
+
+function trace() {
+# Afficher la commande $1..@, la lancer, puis afficher son code d'erreur si une
+# erreur se produit
+ local r cmd="$(qvals "$@")"
+ show_info && { __eecho "\$ $cmd" 1>&2; }
+ "$@"; r=$?
+ if [ $r -ne 0 ]; then
+ if show_info; then
+ __eecho "^ [EC #$r]" 1>&2
+ elif show_error; then
+ __eecho "^ $cmd [EC #$r]" 1>&2
+ fi
+ fi
+ return $r
+}
+
+function trace_error() {
+# Lancer la commande $1..@, puis afficher son code d'erreur si une erreur se
+# produit. La différence avec trace() est que la commande n'est affichée que si
+# une erreur se produit.
+ local r
+ "$@"; r=$?
+ if [ $r -ne 0 ]; then
+ local cmd="$(qvals "$@")"
+ if show_error; then
+ __eecho "^ $cmd [EC #$r]" 1>&2
+ fi
+ fi
+ return $r
+}
+
+function edebug() {
+# Afficher un message de debug
+ show_debug || return
+ eval "$NULIB__DISABLE_SET_X"
+ __edebug "$*" 1>&2
+ eval "$NULIB__ENABLE_SET_X"
+}
+
+# Afficher la description d'une opération. Cette fonction est particulièrement
+# appropriée dans le contexte d'un etitle.
+# Les variantes e (error), w (warning), n (note), i (info) permettent d'afficher
+# des couleurs différentes, mais toutes sont du niveau info.
+function estep() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estep "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; }
+function estepe() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepe "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; }
+function estepw() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepw "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; }
+function estepn() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepn "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; }
+function estepi() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepi "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; }
+function estep_() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estep_ "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; }
+function estepe_() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepe_ "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; }
+function estepw_() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepw_ "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; }
+function estepn_() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepn_ "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; }
+function estepi_() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepi_ "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; }
+
+function qstep() { estep "$*"; quietc_echo "* $*"; }
+
+function action() {
+# commencer l'action $1
+# - si $2..$* est spécifié, c'est une commande qui est lancée dans le contexte
+# de l'action, ensuite l'action est terminée en succès ou en échec suivant le
+# code de retour. ne pas afficher la sortie de la commande comme avec quietc()
+# - sinon il faut terminer le titre explicitement avec eend
+ eval "$NULIB__DISABLE_SET_X"
+ local action="$1"; shift
+ local r=0
+ if [ $# -gt 0 ]; then
+ [ -z "$NULIB__TMPLOG" ] && ac_set_tmpfile NULIB__TMPLOG
+ [ -n "$action" ] && quietc_echo "$(__edate) ACTION: $action:"
+ "$@" >&"$NULIB__TMPLOG"; r=$?
+ [ -n "$NULIB_QUIETLOG" ] && cat "$NULIB__TMPLOG" >>"$NULIB_QUIETLOG"
+ if [ $r -ne 0 -o -n "$NULIB_DEBUG" ]; then
+ NULIB__ESTACK="$NULIB__ESTACK:a"
+ [ -n "$action" ] && action="$action:"
+ __action "$action" 1>&2
+ cat "$NULIB__TMPLOG"
+ aresult $r
+ else
+ if [ $r -eq 0 ]; then
+ [ -n "$action" ] || action="succès"
+ __asuccess "$action" 1>&2
+ else
+ [ -n "$action" ] || action="échec"
+ __afailure "$action" 1>&2
+ fi
+ fi
+ else
+ NULIB__ESTACK="$NULIB__ESTACK:a"
+ [ -n "$action" ] && action="$action:"
+ __action "$action" 1>&2
+ fi
+ eval "$NULIB__ENABLE_SET_X"
+ return $r
+}
+
+function asuccess() {
+# terminer l'action en cours avec le message de succès $*
+ [ "${NULIB__ESTACK%:a}" != "$NULIB__ESTACK" ] || return
+ eval "$NULIB__DISABLE_SET_X"
+ [ -n "$*" ] || set -- "succès"
+ NULIB__INDENT=" " __asuccess "$*" 1>&2
+ NULIB__ESTACK="${NULIB__ESTACK%:a}"
+ eval "$NULIB__ENABLE_SET_X"
+ return 0
+}
+function afailure() {
+# terminer l'action en cours avec le message d'échec $*
+ [ "${NULIB__ESTACK%:a}" != "$NULIB__ESTACK" ] || return
+ eval "$NULIB__DISABLE_SET_X"
+ [ -n "$*" ] || set -- "échec"
+ NULIB__INDENT=" " __afailure "$*" 1>&2
+ NULIB__ESTACK="${NULIB__ESTACK%:a}"
+ eval "$NULIB__ENABLE_SET_X"
+ return 1
+}
+function aresult() {
+# terminer l'action en cours avec un message de succès ou d'échec $2..* en
+# fonction du code de retour $1 (0=succès, sinon échec)
+ [ "${NULIB__ESTACK%:a}" != "$NULIB__ESTACK" ] || return
+ eval "$NULIB__DISABLE_SET_X"
+ local r="${1:-0}"; shift
+ if [ "$r" == 0 ]; then
+ [ -n "$*" ] || set -- "succès"
+ NULIB__INDENT=" " __asuccess "$*" 1>&2
+ else
+ [ -n "$*" ] || set -- "échec"
+ NULIB__INDENT=" " __afailure "$*" 1>&2
+ fi
+ NULIB__ESTACK="${NULIB__ESTACK%:a}"
+ eval "$NULIB__ENABLE_SET_X"
+ return $r
+}
+function adone() {
+# terminer l'action en cours avec le message neutre $*
+ [ "${NULIB__ESTACK%:a}" != "$NULIB__ESTACK" ] || return
+ eval "$NULIB__DISABLE_SET_X"
+ [ -n "$*" ] && NULIB__INDENT=" " __adone "$*" 1>&2
+ NULIB__ESTACK="${NULIB__ESTACK%:a}"
+ eval "$NULIB__ENABLE_SET_X"
+ return 0
+}
diff --git a/bash/src/base.path.sh b/bash/src/base.path.sh
new file mode 100644
index 0000000..8df86b6
--- /dev/null
+++ b/bash/src/base.path.sh
@@ -0,0 +1,304 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: base.path "Fonctions de base: gestion des chemins et des fichiers"
+
+function: in_path "tester l'existence d'un programme dans le PATH"
+function in_path() {
+ [ -n "$1" -a -x "$(which "$1" 2>/dev/null)" ]
+}
+
+function: delpath "supprimer le chemin \$1 de \$2(=PATH)"
+function delpath() {
+ local _qdir="${1//\//\\/}"
+ eval "export ${2:-PATH}; ${2:-PATH}"'="${'"${2:-PATH}"'#$1:}"; '"${2:-PATH}"'="${'"${2:-PATH}"'%:$1}"; '"${2:-PATH}"'="${'"${2:-PATH}"'//:$_qdir:/:}"; [ "$'"${2:-PATH}"'" == "$1" ] && '"${2:-PATH}"'='
+}
+
+function: addpath "Ajouter le chemin \$1 à la fin, dans \$2(=PATH), s'il n'y existe pas déjà"
+function addpath() {
+ local _qdir="${1//\//\\/}"
+ eval "export ${2:-PATH}; "'[ "${'"${2:-PATH}"'#$1:}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'%:$1}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'//:$_qdir:/:}" == "$'"${2:-PATH}"'" -a "$'"${2:-PATH}"'" != "$1" ] && '"${2:-PATH}"'="${'"${2:-PATH}"':+$'"${2:-PATH}"':}$1"'
+}
+
+function: inspathm "Ajouter le chemin \$1 au début, dans \$2(=PATH), s'il n'y existe pas déjà"
+function inspathm() {
+ local _qdir="${1//\//\\/}"
+ eval "export ${2:-PATH}; "'[ "${'"${2:-PATH}"'#$1:}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'%:$1}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'//:$_qdir:/:}" == "$'"${2:-PATH}"'" -a "$'"${2:-PATH}"'" != "$1" ] && '"${2:-PATH}"'="$1${'"${2:-PATH}"':+:$'"${2:-PATH}"'}"'
+}
+
+function: inspath "S'assurer que le chemin \$1 est au début de \$2(=PATH)"
+function inspath() {
+ delpath "$@"
+ inspathm "$@"
+}
+
+function: push_cwd "enregistrer le répertoire courant dans la variable \$2(=cwd) et se placer dans le répertoire \$1"
+function push_cwd() {
+ eval "${2:-cwd}"'="$(pwd)"'
+ cd "$1"
+}
+function: pop_cwd "se placer dans le répertoire \${!\$2}(=\$cwd) puis retourner le code d'erreur \$1(=0)"
+function pop_cwd() {
+ eval 'cd "$'"${2:-cwd}"'"'
+ return "${1:-0}"
+}
+
+################################################################################
+## fichiers temporaires
+
+function: mktempf "générer un fichier temporaire et retourner son nom"
+function mktempf() {
+ mktemp "${1:-"$TMPDIR/tmp.XXXXXX"}"
+}
+
+function: mktempd "générer un répertoire temporaire et retourner son nom"
+function mktempd() {
+ mktemp -d "${1:-"$TMPDIR/tmp.XXXXXX"}"
+}
+
+function ac__forgetall() { NULIB__AC_FILES=(); }
+ac__forgetall
+function ac__trap() {
+ local file
+ for file in "${NULIB__AC_FILES[@]}"; do
+ [ -e "$file" ] && rm -rf "$file" 2>/dev/null
+ done
+ ac__forgetall
+}
+trap ac__trap 1 3 15 EXIT
+
+function: autoclean "\
+Ajouter les fichiers spécifiés à la liste des fichiers à supprimer à la fin du
+programme"
+function autoclean() {
+ local file
+ for file in "$@"; do
+ [ -n "$file" ] && NULIB__AC_FILES=("${NULIB__AC_FILES[@]}" "$file")
+ done
+}
+
+function: ac_cleanall "\
+Supprimer *tous* les fichiers temporaires gérés par autoclean tout de suite."
+function ac_cleanall() {
+ ac__trap
+}
+
+function: ac_clean "\
+Supprimer les fichier temporaires \$1..@ si et seulement s'ils ont été générés
+par ac_set_tmpfile() ou ac_set_tmpdir()"
+function ac_clean() {
+ local file acfile found
+ local -a acfiles
+ for acfile in "${NULIB__AC_FILES[@]}"; do
+ found=
+ for file in "$@"; do
+ if [ "$file" == "$acfile" ]; then
+ found=1
+ [ -e "$file" ] && rm -rf "$file" 2>/dev/null
+ break
+ fi
+ done
+ [ -z "$found" ] && acfiles=("${acfiles[@]}" "$acfile")
+ done
+ NULIB__AC_FILES=("${acfiles[@]}")
+}
+
+function: ac_set_tmpfile "\
+Créer un fichier temporaire avec le motif \$2, l'ajouter à la liste des
+fichiers à supprimer en fin de programme, et mettre sa valeur dans la
+variable \$1
+
+En mode debug, si (\$5 est vide ou \${!5} est une valeur vraie), et si \$3 n'est
+pas vide, prendre ce fichier au lieu de générer un nouveau fichier temporaire.
+Si \$4==keep, ne pas écraser le fichier \$3 s'il existe."
+function ac_set_tmpfile() {
+ local se__d
+ if is_debug; then
+ if [ -n "$5" ]; then
+ is_yes "${!5}" && se__d=1
+ else
+ se__d=1
+ fi
+ fi
+ if [ -n "$se__d" -a -n "$3" ]; then
+ _setv "$1" "$3"
+ [ -f "$3" -a "$4" == keep ] || >"$3"
+ else
+ local se__t="$(mktempf "$2")"
+ autoclean "$se__t"
+ _setv "$1" "$se__t"
+ fi
+}
+
+function: ac_set_tmpdir "\
+Créer un répertoire temporaire avec le motif \$2, l'ajouter à la liste des
+fichiers à supprimer en fin de programme, et mettre sa valeur dans la
+variable \$1
+
+En mode debug, si (\$4 est vide ou \${!4} est une valeur vraie), et si \$3 n'est
+pas vide, prendre ce nom de répertoire au lieu de créer un nouveau répertoire
+temporaire"
+function ac_set_tmpdir() {
+ local sr__d
+ if is_debug; then
+ if [ -n "$4" ]; then
+ is_yes "${!4}" && sr__d=1
+ else
+ sr__d=1
+ fi
+ fi
+ if [ -n "$sr__d" -a -n "$3" ]; then
+ _setv "$1" "$3"
+ mkdir -p "$3"
+ else
+ local sr__t="$(mktempd "$2")"
+ autoclean "$sr__t"
+ _setv "$1" "$sr__t"
+ fi
+}
+
+################################################################################
+## manipulation de chemins
+
+#XXX repris tel quel depuis nutools, à migrer
+
+function normpath() {
+# normaliser le chemin $1, qui est soit absolu, soit relatif à $2 (qui vaut
+# $(pwd) par défaut)
+ local -a parts
+ local part ap
+ IFS=/ read -a parts <<<"$1"
+ if [ "${1#/}" != "$1" ]; then
+ ap=/
+ elif [ -n "$2" ]; then
+ ap="$2"
+ else
+ ap="$(pwd)"
+ fi
+ for part in "${parts[@]}"; do
+ if [ "$part" == "." ]; then
+ continue
+ elif [ "$part" == ".." ]; then
+ ap="${ap%/*}"
+ [ -n "$ap" ] || ap=/
+ else
+ [ "$ap" != "/" ] && ap="$ap/"
+ ap="$ap$part"
+ fi
+ done
+ echo "$ap"
+}
+function __normpath() {
+ # normaliser dans les cas simple le chemin absolu $1. sinon retourner 1.
+ # cette fonction est utilisée par abspath()
+ if [ -d "$1" ]; then
+ if [ -x "$1" ]; then
+ # le cas le plus simple: un répertoire dans lequel on peut entrer
+ (cd "$1"; pwd)
+ return 0
+ fi
+ elif [ -f "$1" ]; then
+ local dn="$(dirname -- "$1")" bn="$(basename -- "$1")"
+ if [ -x "$dn" ]; then
+ # autre cas simple: un fichier situé dans un répertoire dans lequel
+ # on peut entrer
+ (cd "$dn"; echo "$(pwd)/$bn")
+ return 0
+ fi
+ fi
+ return 1
+}
+function abspath() {
+# Retourner un chemin absolu vers $1. Si $2 est non nul et si $1 est un chemin
+# relatif, alors $1 est exprimé par rapport à $2, sinon il est exprimé par
+# rapport au répertoire courant.
+# Si le chemin n'existe pas, il n'est PAS normalisé. Sinon, les meilleurs
+# efforts sont faits pour normaliser le chemin.
+ local ap="$1"
+ if [ "${ap#/}" != "$ap" ]; then
+ # chemin absolu. on peut ignorer $2
+ __normpath "$ap" && return
+ else
+ # chemin relatif. il faut exprimer le chemin par rapport à $2
+ local cwd
+ if [ -n "$2" ]; then
+ cwd="$(abspath "$2")"
+ else
+ cwd="$(pwd)"
+ fi
+ ap="$cwd/$ap"
+ __normpath "$ap" && return
+ fi
+ # dans les cas spéciaux, il faut calculer "manuellement" le répertoire absolu
+ normpath "$ap"
+}
+
+function ppath() {
+# Dans un chemin *absolu*, remplacer "$HOME" par "~" et "$(pwd)/" par "", afin
+# que le chemin soit plus facile à lire. Le répertoire courant est spécifié par
+# $2 ou $(pwd) si $2 est vide
+ local path="$1" cwd="$2"
+
+ path="$(abspath "$path")" # essayer de normaliser le chemin
+ [ -n "$cwd" ] || cwd="$(pwd)"
+
+ [ "$path" == "$cwd" ] && path="."
+ [ "$cwd" != "/" -a "$cwd" != "$HOME" ] && path="${path#$cwd/}"
+ [ "${path#$HOME/}" != "$path" ] && path="~${path#$HOME}"
+
+ echo "$path"
+}
+function ppath2() {
+# Comme ppath() mais afficher '.' comme '. ($dirname)' pour la joliesse
+ local path="$1" cwd="$2"
+
+ path="$(abspath "$path")" # essayer de normaliser le chemin
+ [ -n "$cwd" ] || cwd="$(pwd)"
+
+ if [ "$path" == "$cwd" ]; then
+ path=". ($(basename -- "$path"))"
+ else
+ [ "$cwd" != "/" -a "$cwd" != "$HOME" ] && path="${path#$cwd/}"
+ [ "${path#$HOME/}" != "$path" ] && path="~${path#$HOME}"
+ fi
+
+ echo "$path"
+}
+
+function relpath() {
+# Afficher le chemin relatif de $1 par rapport à $2. Si $2 n'est pas spécifié,
+# on prend le répertoire courant. Si $1 ou $2 ne sont pas des chemins absolus,
+# il sont transformés en chemins absolus par rapport à $3. Si $1==$2, retourner
+# une chaine vide
+ local p="$(abspath "$1" "$3")" cwd="$2"
+ if [ -z "$cwd" ]; then
+ cwd="$(pwd)"
+ else
+ cwd="$(abspath "$cwd" "$3")"
+ fi
+ if [ "$p" == "$cwd" ]; then
+ echo ""
+ elif [ "${p#$cwd/}" != "$p" ]; then
+ echo "${p#$cwd/}"
+ else
+ local rp
+ while [ -n "$cwd" -a "${p#$cwd/}" == "$p" ]; do
+ rp="${rp:+$rp/}.."
+ cwd="${cwd%/*}"
+ done
+ rp="$rp/${p#$cwd/}"
+ # ${rp%//} traite le cas $1==/
+ echo "${rp%//}"
+ fi
+}
+function relpathx() {
+# Comme relpath, mais pour un chemin vers un exécutable qu'il faut lancer:
+# s'assurer qu'il y a une spécification de chemin, e.g. ./script
+ local p="$(relpath "$@")"
+ if [ -z "$p" ]; then
+ echo .
+ elif [ "${p#../}" != "$p" -o "${p#./}" != "$p" ]; then
+ echo "$p"
+ else
+ echo "./$p"
+ fi
+}
diff --git a/bash/src/base.sh b/bash/src/base.sh
new file mode 100644
index 0000000..cd88e49
--- /dev/null
+++ b/bash/src/base.sh
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+# shim pour les fonctions de nulib.sh au cas où ce module n'est pas chargée
+if [ -z "$NULIBDIR" -o "$NULIBDIR" != "$NULIBINIT" ]; then
+ function module:() { :; }
+ function function:() { :; }
+ function require:() { :; }
+fi
+##@include base.init.sh
+##@include base.core.sh
+##@include base.str.sh
+##@include base.num.sh
+##@include base.bool.sh
+##@include base.array.sh
+##@include base.split.sh
+##@include base.path.sh
+##@include base.args.sh
+##@include base.tools.sh
+##@include base.input.sh
+##@include base.output.sh
+module: base "Chargement de tous les modules base.*"
+require: base.init base.core base.str base.num base.bool base.array base.split base.path base.args base.tools base.input base.output
diff --git a/bash/src/base.split.sh b/bash/src/base.split.sh
new file mode 100644
index 0000000..dac4eeb
--- /dev/null
+++ b/bash/src/base.split.sh
@@ -0,0 +1,188 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: base.split "Fonctions de base: analyse et découpage de valeurs"
+
+function: splitfsep "\
+Découper \$1 de la forme first[SEPsecond] entre first, qui est placé dans la
+variable \$3(=first) et second, qui est placée dans la variable \$4(=second). \$2
+est la valeur SEP. Le découpage est faite sur la *première* occurence de SEP."
+function splitfsep() {
+ if [[ "$1" == *"$2"* ]]; then
+ setv "${3:-first}" "${1%%$2*}"
+ setv "${4:-second}" "${1#*$2}"
+ else
+ setv "${3:-first}" "$1"
+ setv "${4:-second}"
+ fi
+}
+
+function: splitfsep2 "\
+Découper \$1 de la forme [firstSEP]second entre first, qui est placé dans la
+variable \$3(=first) et second, qui est placée dans la variable \$4(=second). \$2
+est la valeur SEP. Le découpage est faite sur la *première* occurence de SEP."
+function splitfsep2() {
+ if [[ "$1" == *"$2"* ]]; then
+ setv "${3:-first}" "${1%%$2*}"
+ setv "${4:-second}" "${1#*$2}"
+ else
+ setv "${3:-first}"
+ setv "${4:-second}" "$1"
+ fi
+}
+
+function: splitlsep "\
+Découper \$1 de la forme first[SEPsecond] entre first, qui est placé dans la
+variable \$3(=first) et second, qui est placée dans la variable \$4(=second). \$2
+est la valeur SEP. Le découpage est faite sur la *dernière* occurence de SEP."
+function splitlsep() {
+ if [[ "$1" == *"$2"* ]]; then
+ setv "${3:-first}" "${1%$2*}"
+ setv "${4:-second}" "${1##*$2}"
+ else
+ setv "${3:-first}" "$1"
+ setv "${4:-second}"
+ fi
+}
+
+function: splitlsep2 "\
+Découper \$1 de la forme [firstSEP]second entre first, qui est placé dans la
+variable \$3(=first) et second, qui est placée dans la variable \$4(=second). \$2
+est la valeur SEP. Le découpage est faite sur la *dernière* occurence de SEP."
+function splitlsep2() {
+ if [[ "$1" == *"$2"* ]]; then
+ setv "${3:-first}" "${1%$2*}"
+ setv "${4:-second}" "${1##*$2}"
+ else
+ setv "${3:-first}"
+ setv "${4:-second}" "$1"
+ fi
+}
+
+function: splitvar "\
+Découper \$1 de la forme name[=value] entre le nom, qui est placé dans la
+variable \$2(=name) et la valeur, qui est placée dans la variable \$3(=value)"
+function splitvar() {
+ splitfsep "$1" = "${2:-name}" "${3:-value}"
+}
+
+function: splitpath "\
+Découper \$1 de la forme [dir/]name entre le répertoire, qui est placé dans la
+variable \$2(=dir), et le nom du fichier, qui est placé dans la variable
+\$3(=name)"
+function splitpath() {
+ splitlsep2 "$1" / "${2:-dir}" "${3:-name}"
+}
+
+function: splitname "\
+Découper \$1 de la forme basename[.ext] entre le nom de base du fichier, qui
+est placé dans la variable \$2(=basename) et l'extension, qui est placée dans
+la variable \$3(=ext)
+
+Attention, si \$1 est un chemin, le résultat risque d'être faussé. Par exemple,
+'splitname a.b/c' ne donne pas le résultat escompté."
+function splitname() {
+ splitlsep "$1" . "${2:-basename}" "${3:-ext}"
+}
+
+function: splithost "\
+Découper \$1 de la forme hostname[.domain] entre le nom d'hôte, qui est placé
+dans la variable \$2(=hostname) et le domaine, qui est placée dans la variable
+\$3(=domain)"
+function splithost() {
+ splitfsep "$1" . "${2:-hostname}" "${3:-domain}"
+}
+
+function: splituserhost "\
+Découper \$1 de la forme [user@]host entre le nom de l'utilisateur, qui est placé
+dans la variable \$2(=user) et le nom d'hôte, qui est placée dans la variable
+\$3(=host)"
+function splituserhost() {
+ splitfsep2 "$1" @ "${2:-user}" "${3:-host}"
+}
+
+function: splitpair "\
+Découper \$1 de la forme first[:second] entre la première valeur, qui est placé
+dans la variable \$2(=src) et la deuxième valeur, qui est placée dans la variable
+\$3(=dest)"
+function splitpair() {
+ splitfsep "$1" : "${2:-src}" "${3:-dest}"
+}
+
+function: splitproxy "\
+Découper \$1 de la forme http://[user:password@]host[:port]/ entre les valeurs
+\$2(=host), \$3(=port), \$4(=user), \$5(=password)
+
+S'il n'est pas spécifié, port vaut 3128 par défaut"
+function splitproxy() {
+ local sy__tmp sy__host sy__port sy__creds sy__user sy__password
+
+ sy__tmp="${1#http://}"
+ if [[ "$sy__tmp" == *@* ]]; then
+ sy__creds="${sy__tmp%%@*}"
+ sy__tmp="${sy__tmp#${sy__creds}@}"
+ splitpair "$sy__creds" sy__user sy__password
+ fi
+ sy__tmp="${sy__tmp%%/*}"
+ splitpair "$sy__tmp" sy__host sy__port
+ [ -n "$sy__port" ] || sy__port=3128
+
+ setv "${2:-host}" "$sy__host"
+ setv "${3:-port}" "$sy__port"
+ setv "${4:-user}" "$sy__user"
+ setv "${5:-password}" "$sy__password"
+}
+
+function: spliturl "\
+Découper \$1 de la forme scheme://[user:password@]host[:port]/path entre les
+valeurs \$2(=scheme), \$3(=user), \$4(=password), \$5(=host), \$6(=port), \$7(=path)
+
+S'il n'est pas spécifié, port vaut 80 pour http, 443 pour https, 21 pour ftp"
+function spliturl() {
+ local sl__tmp sl__scheme sl__creds sl__user sl__password sl__host sl__port sl__path
+
+ sl__scheme="${1%%:*}"
+ sl__tmp="${1#${sl__scheme}://}"
+ if [[ "$sl__tmp" == */* ]]; then
+ sl__path="${sl__tmp#*/}"
+ sl__tmp="${sl__tmp%%/*}"
+ fi
+ if [[ "$sl__tmp" == *@* ]]; then
+ sl__creds="${sl__tmp%%@*}"
+ sl__tmp="${sl__tmp#${sl__creds}@}"
+ splitpair "$sl__creds" sl__user sl__password
+ fi
+ splitpair "$sl__tmp" sl__host sl__port
+ if [ -z "$sl__port" ]; then
+ [ "$sl__scheme" == "http" ] && sl__port=80
+ [ "$sl__scheme" == "https" ] && sl__port=443
+ [ "$sl__scheme" == "ftp" ] && sl__port=21
+ fi
+
+ setv "${2:-scheme}" "$sl__scheme"
+ setv "${3:-user}" "$sl__user"
+ setv "${4:-password}" "$sl__password"
+ setv "${5:-host}" "$sl__host"
+ setv "${6:-port}" "$sl__port"
+ setv "${7:-path}" "$sl__path"
+}
+
+function: splitwcs "\
+Découper un nom de chemin \$1 entre la partie sans wildcards, qui est placée dans
+la variables \$2(=basedir), et la partie avec wildcards, qui est placée dans la
+variable \$3(=filespec)"
+function splitwcs() {
+ local ss__p="$1"
+ local ss__dd="${2:-basedir}" ss__df="${3:-filespec}" ss__part ss__d ss__f
+ local -a ss__parts
+ array_split ss__parts "$ss__p" "/"
+ for ss__part in "${ss__parts[@]}"; do
+ if [[ "$ss__part" == *\** ]] || [[ "$ss__part" == *\?* ]] || [ -n "$ss__f" ]; then
+ ss__f="${ss__f:+$ss__f/}$ss__part"
+ else
+ ss__d="${ss__d:+$ss__d/}$ss__part"
+ fi
+ done
+ [ "${ss__p#/}" != "$ss__p" ] && ss__d="/$ss__d"
+ _setv "$ss__dd" "$ss__d"
+ _setv "$ss__df" "$ss__f"
+}
diff --git a/bash/src/base.str.sh b/bash/src/base.str.sh
new file mode 100644
index 0000000..e83e714
--- /dev/null
+++ b/bash/src/base.str.sh
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: base.str "Fonctions de base: gestion des valeurs chaines"
+
+function: strmid "Afficher la plage \$1 de la valeur \$2..*
+
+La plage peut être d'une des formes 'start', '[start]:length'. Si start est
+négatif, le compte est effectué à partir de la fin de la chaine. Si length est
+négatif, il est rajouté à la longueur de la chaine à partir de start"
+function strmid() {
+ local range="$1"; shift
+ local str="$*"
+ if [[ "$range" == *:-* ]]; then
+ local max=${#str}
+ [ $max -eq 0 ] && return
+ local start="${range%%:*}"
+ [ -n "$start" ] || start=0
+ while [ "$start" -lt 0 ]; do
+ start=$(($max$start))
+ done
+ max=$(($max-$start))
+ local length="${range#*:}"
+ while [ "$length" -lt 0 ]; do
+ length=$(($max$length))
+ done
+ range="$start:$length"
+ fi
+ eval 'echo "${str:'" $range"'}"'
+}
+
+function: strrepl "Remplacer dans la valeur \$3..* le motif \$1 par la chaine \$2
+
+\$1 peut commencer par l'un des caractères /, #, % pour indiquer le type de recherche"
+function strrepl() {
+ local pattern="$1"; shift
+ local repl="$1"; shift
+ local str="$*"
+ local cmd='echo "${str/'
+ if [ "${pattern#/}" != "$pattern" ]; then
+ pattern="${pattern#/}"
+ cmd="$cmd/"
+ elif [ "${pattern#\#}" != "$pattern" ]; then
+ pattern="${pattern#\#}"
+ cmd="$cmd#"
+ elif [ "${pattern#%}" != "$pattern" ]; then
+ pattern="${pattern#%}"
+ cmd="$cmd%"
+ fi
+ cmd="$cmd"'$pattern/$repl}"'
+ eval "$cmd"
+}
+
+function: strlcomp "transformer dans le flux en entrée en UTF-8 certains caractères en leur équivalent transformable en latin1.
+
+si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée."
+function strlcomp() {
+ if [ $# -gt 0 ]; then strlcomp <<<"$*"
+ else LANG=fr_FR.UTF-8 sed $'
+s/[\xE2\x80\x90\xE2\x80\x91\xE2\x80\x92\xE2\x80\x93\xE2\x80\x94\xE2\x80\x95]/-/g
+s/[‘’]/\x27/g
+s/[«»“”]/"/g
+s/[\xC2\xA0\xE2\x80\x87\xE2\x80\xAF\xE2\x81\xA0]/ /g
+s/[\xE2\x80\xA6]/.../g
+s/[œ]/oe/g
+s/[Œ]/OE/g
+s/[æ]/ae/g
+s/[Æ]/AE/g
+s/a\xCC\x80/à/g
+s/e\xCC\x81/é/g; s/e\xCC\x80/è/g; s/e\xCC\x82/ê/g; s/e\xCC\x88/ë/g
+s/i\xCC\x88/ï/g; s/i\xCC\x82/î/g
+s/o\xCC\x82/ô/g; s/o\xCC\x88/ö/g
+s/u\xCC\x88/ü/g; s/u\xCC\x82/û/g
+s/c\xCC\xA7/ç/g
+s/A\xCC\x80/À/g
+s/E\xCC\x81/É/g; s/E\xCC\x80/È/g; s/E\xCC\x82/Ê/g; s/E\xCC\x88/Ë/g
+s/I\xCC\x88/Ï/g; s/I\xCC\x82/Î/g
+s/O\xCC\x82/Ô/g; s/O\xCC\x88/Ö/g
+s/U\xCC\x88/Ü/g; s/U\xCC\x82/Û/g
+s/C\xCC\xA7/Ç/g
+'
+ fi
+}
+
+function: strnacc "supprimer les accents dans le flux en entrée en UTF-8
+
+si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée."
+function strnacc() {
+ if [ $# -gt 0 ]; then strnacc <<<"$*"
+ else LANG=fr_FR.UTF-8 sed '
+s/[à]/a/g
+s/[éèêë]/e/g
+s/[ïî]/i/g
+s/[ôö]/o/g
+s/[üû]/u/g
+s/[ç]/c/g
+s/[À]/A/g
+s/[ÉÈÊË]/E/g
+s/[ÏÎ]/I/g
+s/[ÔÖ]/O/g
+s/[ÜÛ]/U/g
+s/[Ç]/C/g
+'
+ fi
+}
+
+function: stripnl "Supprimer dans le flux en entrée les caractères de fin de ligne
+
+si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée."
+function stripnl() {
+ if [ $# -gt 0 ]; then stripnl <<<"$*"
+ else tr -d '\r\n'
+ fi
+}
+
+function: nl2lf "transformer dans le flux en entrée les fins de ligne en LF
+
+si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée."
+function nl2lf() {
+ if [ $# -gt 0 ]; then nl2lf <<<"$*"
+ else lawk 'BEGIN {RS="\r|\r\n|\n"} {print}'
+ fi
+}
+
+function: nl2crlf "transformer dans le flux en entrée les fins de ligne en CRLF
+
+si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée."
+function nl2crlf() {
+ if [ $# -gt 0 ]; then nl2crlf <<<"$*"
+ else lawk 'BEGIN {RS="\r|\r\n|\n"} {print $0 "\r"}'
+ fi
+}
+
+function: nl2cr "transformer dans le flux en entrée les fins de ligne en CR
+
+si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée."
+function nl2cr() {
+ if [ $# -gt 0 ]; then nl2cr <<<"$*"
+ else lawk 'BEGIN {RS="\r|\r\n|\n"; ORS=""} {print $0 "\r"}'
+ fi
+}
diff --git a/bash/src/base.tools.sh b/bash/src/base.tools.sh
new file mode 100644
index 0000000..1a7f800
--- /dev/null
+++ b/bash/src/base.tools.sh
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: base.tools "Fonctions de base: outils divers"
+
+function: mkdirof 'Créer le répertoire correspondant au fichier $1'
+function mkdirof() {
+ mkdir -p "$(dirname -- "$1")"
+}
+
+function __la_cmd() {
+ [ $# -gt 0 ] || set '*'
+ local arg
+ local cmd="/bin/ls -1d"
+ for arg in "$@"; do
+ arg="$(qwc "$arg")"
+ cmd="$cmd $arg"
+ done
+ cmd="$cmd 2>/dev/null"
+ echo "$cmd"
+}
+
+function: ls_all 'Lister les fichiers ou répertoires du répertoire $1, un par ligne
+Les répertoires . et .. sont enlevés de la liste
+$1=un répertoire dont le contenu doit être listé
+$2..@=un ensemble de patterns pour le listage
+
+Seuls les noms des fichiers sont listés. Utiliser l'\''option -p pour inclure
+les chemins'
+function ls_all() {
+ local withp f b
+ if [ "$1" == -p ]; then withp=1; shift; fi
+ b="${1:-.}"; shift
+
+ (
+ cd "$b" || exit
+ eval "$(__la_cmd "$@")" | while read f; do
+ [ "$f" == "." -o "$f" == ".." ] && continue
+ recho "${withp:+$b/}$f"
+ done
+ )
+}
+
+function: ls_files 'Lister les fichiers du répertoire $1, un par ligne
+$1=un répertoire dont le contenu doit être listé.
+$2..@=un ensemble de patterns pour le listage
+
+Seuls les noms des fichiers sont listés. Utiliser l'\''option -p pour inclure
+les chemins'
+function ls_files() {
+ local withp f b
+ if [ "$1" == -p ]; then withp=1; shift; fi
+ b="${1:-.}"; shift
+
+ (
+ cd "$b" || exit
+ eval "$(__la_cmd "$@")" | while read f; do
+ [ -f "$f" ] && recho "${withp:+$b/}$f"
+ done
+ )
+}
+
+function: ls_dirs 'Lister les répertoires du répertoire $1, un par ligne
+Les répertoires . et .. sont enlevés de la liste
+$1=un répertoire dont le contenu doit être listé.
+$2..@=un ensemble de patterns pour le listage
+
+Seuls les noms des répertoires sont listés. Utiliser l'\''option -p pour
+inclure les chemins'
+function ls_dirs() {
+ local withp f b
+ if [ "$1" == -p ]; then withp=1; shift; fi
+ b="${1:-.}"; shift
+
+ (
+ cd "$b" || exit
+ eval "$(__la_cmd "$@")" | while read f; do
+ [ "$f" == "." -o "$f" == ".." ] && continue
+ [ -d "$f" ] && recho "${withp:+$b/}$f"
+ done
+ )
+}
+
+function: quietgrep "tester la présence d'un pattern dans un fichier en mode silencieux"
+function quietgrep() { grep -q "$@" 2>/dev/null; }
+
+function: testsame "tester si deux fichiers sont identiques en mode silencieux"
+function testsame() { diff -q "$@" >&/dev/null; }
+
+function: testdiff "tester si deux fichiers sont différents en mode silencieux"
+function testdiff() { ! diff -q "$@" >&/dev/null; }
+
+function: should_update "faut-il mettre à jour le \$1 qui est construit à partir de \$2..@"
+function should_update() {
+ local dest="$1"; shift
+ local source
+ for source in "$@"; do
+ [ -f "$source" ] || continue
+ [ "$source" -nt "$dest" ] && return 0
+ done
+ return 1
+}
diff --git a/bash/src/donk.build.sh b/bash/src/donk.build.sh
new file mode 100644
index 0000000..a88daa0
--- /dev/null
+++ b/bash/src/donk.build.sh
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: donk.build "construire des images docker"
+require: donk.common
diff --git a/bash/src/donk.common.sh b/bash/src/donk.common.sh
new file mode 100644
index 0000000..79a5efb
--- /dev/null
+++ b/bash/src/donk.common.sh
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: donk.common "fonctions communes"
diff --git a/bash/src/donk.help.sh b/bash/src/donk.help.sh
new file mode 100644
index 0000000..2e0a21b
--- /dev/null
+++ b/bash/src/donk.help.sh
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: donk.help "aide de donk"
+
+DONK_VALID_ACTIONS=(
+ dump:d
+ build:b
+ clean:k
+)
+dump_SUMMARY="afficher les valeurs des variables et ce que ferait l'action build"
+build_SUMMARY="construire les images"
+clean_SUMMARY="nettoyer le projet des fichiers créés avec 'copy gitignore=', en utilisant la commande 'git clean -dX'"
+
+DONK_HELP_SECTIONS=(
+ base:b
+ reference:ref:r
+)
+
+function donk_help() {
+ :
+}
+
+function _donk_show_actions() {
+ local action summary
+ echo "
+ACTIONS"
+ for action in "${DONK_VALID_ACTIONS[@]}"; do
+ IFS=: read -a action <<<"$action"; action="${action[0]}"
+ summary="${action}_SUMMARY"; summary="${!summary}"
+ echo "\
+ $action
+ $summary"
+ done
+}
+
+function _donk_show_help() {
+ if [ -z "$value_" ]; then showhelp@
+ else donk_help "$value_"
+ fi
+ exit $?
+}
diff --git a/bash/src/fndate.sh b/bash/src/fndate.sh
new file mode 100644
index 0000000..7c94e2b
--- /dev/null
+++ b/bash/src/fndate.sh
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: fndate "gestion de fichiers dont le nom contient la date"
+
+function: fndate_verifix "\
+corriger le chemin \$1 pour ajouter le cas échéant une date au nom du fichier
+le fichier n'existe peut-être pas au moment où cette fonction est appelée
+\$2 est l'extension finale du fichier, à ignorer si elle est présente
+ (elle n'est cependant pas ajoutée si elle n'est pas présente)
+\$3 est la date à sélectionner (par défaut c'est la date du jour)
+
+XXX à implémenter:
+- gestion de la date
+- ajout d'un suffixe .N le cas échéant (i.e YYMMDD.NN)
+"
+function fndate_verifix() {
+ local dir filename ext date
+ if [[ "$1" == */* ]]; then
+ dir="$(dirname -- "$1")"
+ filename="$(basename -- "$1")"
+ else
+ dir=
+ filename="$1"
+ fi
+ ext="$2"
+ if [ -n "$ext" ]; then
+ ext=".${2#.}"
+ if [ "${filename%$ext}" != "$filename" ]; then
+ filename="${filename%$ext}"
+ else
+ ext=
+ fi
+ fi
+ date="$3"
+ [ -n "$date" ] || date="$(date +%y%m%d)"
+
+ case "$filename" in
+ ~~-*) filename="$date-${filename#~~-}";;
+ ~~*) filename="$date-${filename#~~}";;
+ *-~~) filename="${filename%-~~}-$date";;
+ *~~) filename="${filename%~~}-$date";;
+ esac
+
+ echo "${dir:+$dir/}$filename$ext"
+}
diff --git a/bash/src/git.sh b/bash/src/git.sh
new file mode 100644
index 0000000..b893864
--- /dev/null
+++ b/bash/src/git.sh
@@ -0,0 +1,226 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+##@require nulib.sh
+##@require base
+module: git "Fonctions pour faciliter l'utilisation de git"
+require: nulib base
+
+function: git_geturl ""
+function git_geturl() {
+ git config --get remote.origin.url
+}
+
+function: git_have_annex ""
+function git_have_annex() {
+ [ -n "$(git config --get annex.uuid)" ]
+}
+
+NULIB_GIT_FUNCTIONS=(
+ git_check_gitvcs git_ensure_gitvcs
+ git_list_branches git_list_rbranches
+ git_have_branch git_have_rbranch
+ git_get_branch git_is_branch
+ git_have_remote git_track_branch
+ git_check_cleancheckout git_ensure_cleancheckout
+ git_is_ancestor git_should_ff git_should_push
+ git_is_merged
+)
+NULIB_GIT_FUNCTIONS_MAP=(
+ cg:git_check_gitvcs eg:git_ensure_gitvcs
+ lbs:git_list_branches rbs:git_list_rbranches
+ hlb:git_have_branch hrb:git_have_rbranch
+ gb:git_get_branch ib:git_is_branch
+ hr:git_have_remote tb:git_track_branch
+ cc:git_check_cleancheckout ec:git_ensure_cleancheckout
+ ia:git_is_ancestor sff:git_should_ff spu:git_should_push
+ im:git_is_merged
+)
+
+function: git_get_toplevel ""
+function git_get_toplevel() {
+ git rev-parse --show-toplevel 2>/dev/null
+}
+
+function: git_check_gitvcs ""
+function git_check_gitvcs() {
+ git rev-parse --show-toplevel >&/dev/null
+}
+
+function: git_ensure_gitvcs ""
+function git_ensure_gitvcs() {
+ git_check_gitvcs || die "$(ppath "$(pwd)" ~): ce répertoire n'est pas un dépôt git" || return
+}
+
+function: git_list_branches ""
+function git_list_branches() {
+ git for-each-ref refs/heads/ --format='%(refname:short)' | csort
+}
+
+function: git_list_rbranches ""
+function git_list_rbranches() {
+ git for-each-ref "refs/remotes/${1:-origin}/" --format='%(refname:short)' | csort
+}
+
+function: git_list_pbranches "lister les branches locales et celles qui existent dans l'origine \$1(=origin) et qui pourraient devenir une branche locale avec la commande git checkout -b"
+function git_list_pbranches() {
+ local prefix="${1:-origin}/"
+ {
+ git for-each-ref refs/heads/ --format='%(refname:short)'
+ git for-each-ref "refs/remotes/$prefix" --format='%(refname:short)' | grep -F "$prefix" | cut -c $((${#prefix} + 1))-
+ } | grep -vF HEAD | csort -u
+}
+
+function: git_have_branch ""
+function git_have_branch() {
+ git_list_branches | grep -qF "$1"
+}
+
+function: git_have_rbranch ""
+function git_have_rbranch() {
+ git_list_rbranches "${2:-origin}" | grep -qF "$1"
+}
+
+function: git_get_branch ""
+function git_get_branch() {
+ git rev-parse --abbrev-ref HEAD 2>/dev/null
+}
+
+function: git_get_branch_remote ""
+function git_get_branch_remote() {
+ local branch="$1"
+ [ -n "$branch" ] || branch="$(git_get_branch)"
+ [ -n "$branch" ] || return
+ git config --get "branch.$branch.remote"
+}
+
+function: git_get_branch_merge ""
+function git_get_branch_merge() {
+ local branch="$1"
+ [ -n "$branch" ] || branch="$(git_get_branch)"
+ [ -n "$branch" ] || return
+ git config --get "branch.$branch.merge"
+}
+
+function: git_get_branch_rbranch ""
+function git_get_branch_rbranch() {
+ local branch="$1" remote="$2" merge
+ [ -n "$branch" ] || branch="$(git_get_branch)"
+ [ -n "$branch" ] || return
+ [ -n "$remote" ] || remote="$(git_get_branch_remote "$branch")"
+ [ -n "$remote" ] || return
+ merge="$(git_get_branch_merge "$branch")"
+ [ -n "$merge" ] || return
+ echo "refs/remotes/$remote/${merge#refs/heads/}"
+}
+
+function: git_is_branch ""
+function git_is_branch() {
+ [ "$(git_get_branch)" == "${1:-master}" ]
+}
+
+function: git_have_remote ""
+function git_have_remote() {
+ [ -n "$(git config --get remote.${1:-origin}.url)" ]
+}
+
+function: git_track_branch ""
+function git_track_branch() {
+ local branch="$1" origin="${2:-origin}"
+ [ -n "$branch" ] || return
+ git_have_remote "$origin" || return
+ [ "$(git config --get branch.$branch.remote)" == "$origin" ] && return
+ if git_have_rbranch "$branch" "$origin"; then
+ if git_have_branch "$branch"; then
+ git branch -u "$origin/$branch" "$branch"
+ else
+ git branch -t "$branch" "$origin/$branch"
+ fi
+ elif git_have_branch "$branch"; then
+ git push -u "$origin" "$branch" || return
+ fi
+}
+
+function: git_ensure_branch "
+@return 0 si la branche a été créée, 1 si elle existait déjà, 2 en cas d'erreur"
+function git_ensure_branch() {
+ local branch="$1" source="${2:-master}" origin="${3:-origin}"
+ [ -n "$branch" ] || return 2
+ git_have_branch "$branch" && return 1
+ if git_have_rbranch "$branch" "$origin"; then
+ # une branche du même nom existe dans l'origine. faire une copie de cette branche
+ git branch -t "$branch" "$origin/$branch" || return 2
+ else
+ # créer une nouvelle branche du nom spécifié
+ git_have_branch "$source" || return 2
+ git branch "$branch" "$source" || return 2
+ if [ -z "$NULIB_GIT_OFFLINE" ]; then
+ git_have_remote "$origin" && git_track_branch "$branch" "$origin"
+ fi
+ fi
+ return 0
+}
+
+function: git_check_cleancheckout "vérifier qu'il n'y a pas de modification locales dans le dépôt correspondant au répertoire courant."
+function git_check_cleancheckout() {
+ [ -z "$(git status --porcelain 2>/dev/null)" ]
+}
+
+git_cleancheckout_VERBOSE=1
+git_cleancheckout_DIRTY="Vous avez des modifications locales. Enregistrez ces modifications avant de continuer"
+function: git_ensure_cleancheckout ""
+function git_ensure_cleancheckout() {
+ git_check_cleancheckout && return
+ [ -n "$git_cleancheckout_VERBOSE" ] &&
+ git status --porcelain 2>/dev/null
+ die "$git_cleancheckout_DIRTY" || return
+}
+
+function git__init_ff() {
+ o="${3:-origin}"
+ b="$1" s="${2:-refs/remotes/$o/$1}"
+ b="$(git rev-parse --verify --quiet "$b")" || return 1
+ s="$(git rev-parse --verify --quiet "$s")" || return 1
+ return 0
+}
+function git__can_ff() {
+ [ "$1" == "$(git merge-base "$1" "$2")" ]
+}
+
+function: git_is_ancestor "vérifier que la branche \$1 est un ancêtre direct de la branche \$2, qui vaut par défaut refs/remotes/\${3:-origin}/\$1
+note: cette fonction retourne vrai si \$1 et \$2 identifient le même commit"
+function git_is_ancestor() {
+ local o b s; git__init_ff "$@" || return
+ git__can_ff "$b" "$s"
+}
+
+function: git_should_ff "vérifier si la branche \$1 devrait être fast-forwardée à partir de la branche d'origine \$2, qui vaut par défaut refs/remotes/\${3:-origin}/\$1
+note: cette fonction est similaire à git_is_ancestor(), mais retourne false si \$1 et \$2 identifient le même commit"
+function git_should_ff() {
+ local o b s; git__init_ff "$@" || return
+ [ "$b" != "$s" ] || return 1
+ git__can_ff "$b" "$s"
+}
+
+function: git_should_push "vérifier si la branche \$1 devrait être poussée vers la branche de même nom dans l'origine \$2(=origin), parce que l'origin peut-être fast-forwardée à partir de cette branche."
+function git_should_push() {
+ git_should_ff "refs/remotes/${2:-origin}/$1" "$1"
+}
+
+function: git_fast_forward "vérifier que la branche courante est bien \$1, puis tester s'il faut la fast-forwarder à partir de la branche d'origine \$2, puis le faire si c'est nécessaire. la branche d'origine \$2 vaut par défaut refs/remotes/origin/\$1"
+function git_fast_forward() {
+ local o b s; git__init_ff "$@" || return
+ [ "$b" != "$s" ] || return 1
+ local head="$(git rev-parse HEAD)"
+ [ "$head" == "$b" ] || return 1
+ git__can_ff "$b" "$s" || return 1
+ git merge --ff-only "$s"
+}
+
+function: git_is_merged "vérifier que les branches \$1 et \$2 ont un ancêtre commun, et que la branche \$1 a été complètement fusionnée dans la branche destination \$2"
+function git_is_merged() {
+ local b="$1" d="$2"
+ b="$(git rev-parse --verify --quiet "$b")" || return 1
+ d="$(git rev-parse --verify --quiet "$d")" || return 1
+ [ -n "$(git merge-base "$b" "$d")" ] || return 1
+ [ -z "$(git rev-list "$d..$b")" ]
+}
diff --git a/bash/src/nulib.sh b/bash/src/nulib.sh
new file mode 120000
index 0000000..562af8d
--- /dev/null
+++ b/bash/src/nulib.sh
@@ -0,0 +1 @@
+../../load.sh
\ No newline at end of file
diff --git a/bash/src/pman.conf.sh b/bash/src/pman.conf.sh
new file mode 100644
index 0000000..b1aec42
--- /dev/null
+++ b/bash/src/pman.conf.sh
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+## configuration par défaut
+
+# branche upstream
+UPSTREAM=
+# branches de développement
+DEVELOP=develop
+FEATURE=wip/
+# branche de préparation de release
+RELEASE=release-
+# branche de release
+MAIN=master
+TAG_PREFIX=
+TAG_SUFFIX=
+# branche de hotfix
+HOTFIX=hotfix-
+# branche de distribution
+DIST=
+# désactiver les releases automatiques?
+NOAUTO=
diff --git a/bash/src/pman.sh b/bash/src/pman.sh
new file mode 100644
index 0000000..b095039
--- /dev/null
+++ b/bash/src/pman.sh
@@ -0,0 +1,416 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+# configuration par défaut
+# doit être identique au contenu de pman.conf
+# les branches sont mergées dans cet ordre:
+# upstream --> develop --> [release -->] main --> dist
+# feature _/ hotfix _/
+UPSTREAM=
+DEVELOP=develop
+FEATURE=wip/
+RELEASE=release-
+MAIN=master
+TAG_PREFIX=
+TAG_SUFFIX=
+HOTFIX=hotfix-
+DIST=
+NOAUTO=
+
+CONFIG_VARS=(
+ UPSTREAM DEVELOP FEATURE RELEASE MAIN TAG_PREFIX TAG_SUFFIX HOTFIX DIST NOAUTO
+)
+
+function _init_changelog() {
+ setx date=date +%d/%m/%Y-%H:%M
+ ac_set_tmpfile changelog
+ echo >"$changelog" "\
+Vérifiez et complétez la liste des changements le cas échéant.
+Un fichier vide annule l'opération
+Ces lignes ne seront pas incluses dans le fichier destination
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"
+}
+
+function _filter_rel() {
+ # enlever les commits "techniques" générés par ce script
+ awk '
+BEGIN { tech = 0 }
+tech == 0 && $0 ~ /^\+.*/ { tech = 1; next }
+tech == 1 && $0 ~ /^\|/ { next }
+tech == 1 && $0 ~ /^\+/ { tech = 0 }
+$0 !~ // { print }
+'
+}
+
+function _filter_changes() {
+ # enlever les commits "inutiles" pour générer le fichier CHANGES.md
+ grep -vE '^([+|] )?[0-9a-f]+ modifs\.mineures sans commentaires$' |
+ grep -vE '^([+|] )?[0-9a-f]+ (cosmetic|typo|bug|fix|maj projet|maj deps)\$'
+}
+
+function _format_md() {
+ awk '
+$0 == "" || $0 ~ /^#/ { print; next }
+$1 == "+" {
+ $1 = "*"
+ $2 = "`" $2 "`"
+ print; next
+}
+$1 == "|" {
+ $1 = " *"
+ $2 = "`" $2 "`"
+ print; next
+}
+{
+ $1 = "* `" $1 "`"
+ print; next
+}
+'
+}
+
+function _list_commits() {
+ local source="${1:-$SrcBranch}" dest="${2:-$DestBranch}" mergebase
+ setx mergebase=git merge-base "$dest" "$source"
+ git log --oneline --graph "$mergebase..$source" |
+ grep -vF '|\' | grep -vF '|/' | sed -r 's/^(\| )+\* +/| /; s/^\* +/+ /' |
+ _filter_rel
+}
+
+function _scripte() {
+ echo >>"$script"
+ echo "$comment$(qvals "$@")" >>"$script"
+}
+
+function _scripta() {
+ [ $# -gt 0 ] && _scripte einfo "$*"
+ cat >>"$script"
+}
+
+function _script_push_branches() {
+ [ ${#push_branches[*]} -gt 0 ] || return
+ [ -n "$Origin" ] || Origin=origin
+ git_have_remote "$Origin" || return
+
+ local branch cmd remote rbranch
+ for branch in "${push_branches[@]}"; do
+ if [[ "$branch" == *:* ]]; then
+ cmd="$(qvals git push "$Origin" "$branch")"
+ else
+ setx remote=git_get_branch_remote "$branch"
+ if [ "$remote" == "$Origin" ]; then
+ setx rbranch=git_get_branch_merge "$branch"
+ if [ -n "$rbranch" ]; then
+ # pousser vers la branche distante existante
+ cmd="$(qvals git push "$Origin" "$branch:${rbranch#refs/heads/}")"
+ else
+ # pas de branche distante: pousser et traquer
+ cmd="$(qvals git push -u "$Origin" "$branch:$branch")"
+ fi
+ elif [ -n "$remote" ]; then
+ # pousser vers un remote différent
+ cmd="$(qvals git push "$Origin" "$branch:$branch")"
+ else
+ # pas de remote: pousser et traquer
+ cmd="$(qvals git push -u "$Origin" "$branch:$branch")"
+ fi
+ fi
+ _scripta <<<"$comment$cmd$or_die"
+ done
+}
+
+function _script_push_tags() {
+ local origin tag
+ _scripte einfo "push tags"
+ for tag in "${push_tags[@]}"; do
+ origin="$Origin"
+ [ -n "$origin" ] || origin=origin
+ _scripta <"$ConfigFile" 2>/dev/null
+ [ -s "$ConfigFile" ] || die "$ConfigBranch: aucune configuration trouvée sur cette branche" || return
+ source "$ConfigFile"
+ fi
+ elif [ -f .pman.conf ]; then
+ ConfigFile="$(pwd)/.pman.conf"
+ source "$ConfigFile"
+ elif [ -n "${MYNAME#prel}" ]; then
+ ConfigFile="$NULIBDIR/bash/src/pman${MYNAME#$1}.conf.sh"
+ source "$ConfigFile"
+ else
+ ConfigFile="$NULIBDIR/bash/src/pman.conf.sh"
+ fi
+}
+
+################################################################################
+# Divers
+
+function _push_branches() {
+ [ ${#push_branches[*]} -gt 0 ] || return
+ [ -n "$Origin" ] || Origin=origin
+ git_have_remote "$Origin" || return
+
+ local -a cmds; local branch cmd remote rbranch
+ for branch in "${push_branches[@]}"; do
+ if [[ "$branch" == *:* ]]; then
+ cmds+=("$(qvals git push "$Origin" "$branch")")
+ else
+ setx remote=git_get_branch_remote "$branch"
+ if [ "$remote" == "$Origin" ]; then
+ setx rbranch=git_get_branch_merge "$branch"
+ if [ -n "$rbranch" ]; then
+ # pousser vers la branche distante existante
+ cmds+=("$(qvals git push "$Origin" "$branch:${rbranch#refs/heads/}")")
+ else
+ # pas de branche distante: pousser et traquer
+ cmds+=("$(qvals git push -u "$Origin" "$branch:$branch")")
+ fi
+ elif [ -n "$remote" ]; then
+ # pousser vers un remote différent
+ cmds+=("$(qvals git push "$Origin" "$branch:$branch")")
+ else
+ # pas de remote: pousser et traquer
+ cmds+=("$(qvals git push -u "$Origin" "$branch:$branch")")
+ fi
+ fi
+ done
+ [ -n "$Push" ] || enote "L'option --no-push étant utilisée, les opérations à effectuer sont simplement affichées"
+ for cmd in "${cmds[@]}"; do
+ einfo "$cmd"
+ if [ -n "$Push" ]; then
+ if ! eval "$cmd"; then
+ ewarn "Une erreur s'est produite, les opérations seront simplement affichées"
+ Push=
+ fi
+ fi
+ done
+}
+
+################################################################################
+# Merge
+
+function _mscript_start() {
+ >"$script"
+ _scripta <"$script"
+ _scripta <>"$changelog" "\
+## 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" <CHANGES.md
+git add CHANGES.md
+EOF
+
+ # mettre à jour la version
+ _scripta "update VERSION.txt" <VERSION.txt
+git add VERSION.txt
+EOF
+
+ # Enregistrer les changements
+ _scripta "commit" <Init changelog & version $Version")
+EOF
+}
+
+function _rscript_merge_release_branch() {
+ local dest="$1" tag="$2"
+
+ # basculer sur la branche
+ _scripta "switch to branch $dest" <Intégration de la branche $ReleaseBranch" --no-ff)$or_die
+EOF
+ array_addu push_branches "$dest"
+
+ # tagger la release
+ if [ -n "$tag" ]; then
+ _scripta "create tag $tag" <"$awkscript"
+ echo '@include "base.tools.awk"'
+ echo 'BEGIN {'
+ for list in "${template_lists[@]}"; do
+ items="${!list}"; read -a items <<<"${items//
+/ }"
+ let i=0
+ echo " $list[0] = 0; delete $list"
+ for item in "${items[@]}"; do
+ item="${item//\\/\\\\}"
+ item="${item//\"/\\\"}"
+ echo " $list[$i] = \"$item\""
+ let i=i+1
+ done
+ done
+ echo '}'
+ echo '{ if (should_generate_password()) { generate_password() } }'
+ for list in "${template_lists[@]}"; do
+ items="${!list}"; read -a items <<<"${items//
+/ }"
+ echo "/@@FOR:$list@@/ {"
+ if [ ${#items[*]} -gt 0 ]; then
+ if [ "${list%S}" != "$list" ]; then item="${list%S}"
+ elif [ "${list%s}" != "$list" ]; then item="${list%s}"
+ else item="$list"
+ fi
+ echo " sub(/@@FOR:$list@@/, \"\")"
+ echo " for (i in $list) {"
+ echo " print gensub(/@@$item@@/, $list[i], \"g\")"
+ echo " }"
+ fi
+ echo " next"
+ echo "}"
+ done
+ echo '{ print }'
+
+ # if, ul, var
+ exec >"$sedscript"
+ for var in "${template_vars[@]}"; do
+ value="${!var}"
+ value="${value//\//\\\/}"
+ value="${value//[/\\[}"
+ value="${value//\*/\\\*}"
+ value="${value//$NL/\\n}"
+ if [ -n "$value" ]; then
+ echo "s/#@@IF:${var}@@//g"
+ echo "s/#@@if:${var}@@//g"
+ echo "/#@@UL:${var}@@/d"
+ echo "s/#@@ul:${var}@@/#/g"
+ echo "s/@@${var}:-([^@]*)@@/${value}/g"
+ echo "s/@@${var}:+([^@]*)@@/\\1/g"
+ else
+ echo "/#@@IF:${var}@@/d"
+ echo "s/#@@if:${var}@@/#/g"
+ echo "s/#@@UL:${var}@@//g"
+ echo "s/#@@ul:${var}@@//g"
+ echo "s/@@${var}:-([^@]*)@@/\\1/g"
+ echo "s/@@${var}:+([^@]*)@@//g"
+ fi
+ echo "s/@@${var}@@/${value}/g"
+ done
+ )
+ #etitle "awkscript" cat "$awkscript"
+ #etitle "sedscript" cat "$sedscript"
+}
+
+function template_process_userfiles() {
+ local awkscript sedscript workfile userfile
+ ac_set_tmpfile awkscript
+ ac_set_tmpfile sedscript
+ template_generate_scripts "$awkscript" "$sedscript" "$@"
+
+ ac_set_tmpfile workfile
+ for userfile in "${userfiles[@]}"; do
+ 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"
+ fi
+ fi
+ done
+
+ ac_clean "$awkscript" "$sedscript" "$workfile"
+}
diff --git a/bash/src/tests.sh b/bash/src/tests.sh
new file mode 100644
index 0000000..86f4dfd
--- /dev/null
+++ b/bash/src/tests.sh
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: tests "tests unitaires"
+require: base
+
+function tests__get_line() {
+ # obtenir le nom du script depuis lequel les fonctions de ce module ont été
+ # appelées
+ local mysource="${BASH_SOURCE[0]}"
+ local i=1
+ while [ "${BASH_SOURCE[$i]}" == "$mysource" ]; do
+ let i=i+1
+ done
+ echo "${BASH_SOURCE[$i]}:${BASH_LINENO[$((i-1))]}"
+}
+
+function tests__set_message() {
+ if [ "$1" == -m ]; then
+ message="$2"
+ return 2
+ elif [[ "$1" == -m* ]]; then
+ message="${1#-m}"
+ return 1
+ else
+ return 0
+ fi
+}
+
+function: assert_ok "faire un test unitaire. la syntaxe est
+ assert_ok [-m message] cmd
+la commande doit retourner vrai pour que le test soit réussi"
+function assert_ok() {
+ local message; tests__set_message "$@" || shift $?
+ "$@" && return 0
+
+ [ -n "$message" ] && message="$message: "
+ message="${message}test failed at $(tests__get_line)"
+ die "$message"
+}
+function assert() { assert_ok "$@"; }
+
+function: assert_ko "faire un test unitaire. la syntaxe est
+ assert_ko [-m message] cmd
+la commande doit retourner faux pour que le test soit réussi"
+function assert_ko() {
+ local message; tests__set_message "$@" || shift $?
+ "$@" || return 0
+
+ [ -n "$message" ] && message="$message: "
+ message="${message}test failed at $(tests__get_line)"
+ die "$message"
+}
+
+function tests__assert() {
+ local message="$1"; shift
+ "assert_${1#assert_}" -m "$message" "${@:2}"
+}
+
+function assert_n() {
+ local message; tests__set_message "$@" || shift $?
+ [ -n "$message" ] || message="$2"
+ [ -n "$message" ] || message="value is empty"
+ tests__assert "$message" ok [ -n "$1" ]
+}
+function assert_z() {
+ local message; tests__set_message "$@" || shift $?
+ [ -n "$message" ] || message="$2"
+ [ -n "$message" ] || message="value is not empty"
+ tests__assert "$message" ok [ -z "$1" ]
+}
+function assert_same() {
+ local message; tests__set_message "$@" || shift $?
+ [ -n "$message" ] || message="$3"
+ [ -n "$message" ] || message="'$1' and '$2' are different"
+ tests__assert "$message" ok [ "$1" == "$2" ]
+}
+function assert_diff() {
+ local message; tests__set_message "$@" || shift $?
+ [ -n "$message" ] || message="$3"
+ [ -n "$message" ] || message="'$1' and '$2' are the same"
+ tests__assert "$message" ok [ "$1" != "$2" ]
+}
+
+function assert_eq() {
+ local message; tests__set_message "$@" || shift $?
+ [ -n "$message" ] || message="$3"
+ [ -n "$message" ] || message="'$1' and '$2' are not equals"
+ tests__assert "'$1' must be a number" ok isnum "$1"
+ tests__assert "$message" ok [ "$1" -eq "$2" ]
+}
+function assert_ne() {
+ local message; tests__set_message "$@" || shift $?
+ [ -n "$message" ] || message="$3"
+ [ -n "$message" ] || message="'$1' and '$2' are equals"
+ tests__assert "'$1' must be a number" ok isnum "$1"
+ tests__assert "$message" ok [ "$1" -ne "$2" ]
+}
+function assert_gt() {
+ local message; tests__set_message "$@" || shift $?
+ [ -n "$message" ] || message="$3"
+ [ -n "$message" ] || message="'$1' is not greater than '$2'"
+ tests__assert "'$1' must be a number" ok isnum "$1"
+ tests__assert "$message" ok [ "$1" -gt "$2" ]
+}
+function assert_ge() {
+ local message; tests__set_message "$@" || shift $?
+ [ -n "$message" ] || message="$3"
+ [ -n "$message" ] || message="'$1' is not greater than or equals to '$2'"
+ tests__assert "'$1' must be a number" ok isnum "$1"
+ tests__assert "$message" ok [ "$1" -ge "$2" ]
+}
+function assert_lt() {
+ local message; tests__set_message "$@" || shift $?
+ [ -n "$message" ] || message="$3"
+ [ -n "$message" ] || message="'$1' is not less than '$2'"
+ tests__assert "'$1' must be a number" ok isnum "$1"
+ tests__assert "$message" ok [ "$1" -lt "$2" ]
+}
+function assert_le() {
+ local message; tests__set_message "$@" || shift $?
+ [ -n "$message" ] || message="$3"
+ [ -n "$message" ] || message="'$1' is not less than or equals to '$2'"
+ tests__assert "'$1' must be a number" ok isnum "$1"
+ tests__assert "$message" ok [ "$1" -le "$2" ]
+}
+
+function assert_is_defined() {
+ local message; tests__set_message "$@" || shift $?
+ [ -n "$message" ] || message="$2"
+ [ -n "$message" ] || message="'$1' is not defined"
+ tests__assert "$message" ok is_defined "$1"
+}
+function assert_not_defined() {
+ local message; tests__set_message "$@" || shift $?
+ [ -n "$message" ] || message="$2"
+ [ -n "$message" ] || message="'$1' is defined"
+ tests__assert "$message" ko is_defined "$1"
+}
+function assert_is_array() {
+ local message; tests__set_message "$@" || shift $?
+ [ -n "$message" ] || message="$2"
+ [ -n "$message" ] || message="'$1' is not an array"
+ tests__assert "$message" ok is_array "$1"
+}
+function assert_not_array() {
+ local message; tests__set_message "$@" || shift $?
+ [ -n "$message" ] || message="$2"
+ [ -n "$message" ] || message="'$1' is an array"
+ tests__assert "$message" ko is_array "$1"
+}
+
+function assert_array_same() {
+ local message; tests__set_message "$@" || shift $?
+ [ -n "$message" ] || message="'$1' is not an array or not equals to (${*:2})"
+
+ assert_is_array "$1" "$message"
+ eval "actual=\"\$(qvals \"\${$1[@]}\")\""; shift
+ eval "expected=\"\$(qvals \"\$@\")\""
+ assert_same "$actual" "$expected" "$message"
+}
diff --git a/bash/tests/.gitignore b/bash/tests/.gitignore
new file mode 100644
index 0000000..3ee43c2
--- /dev/null
+++ b/bash/tests/.gitignore
@@ -0,0 +1 @@
+/template-dest.txt
diff --git a/bash/tests/_template-dest.txt b/bash/tests/_template-dest.txt
new file mode 100644
index 0000000..8de596b
--- /dev/null
+++ b/bash/tests/_template-dest.txt
@@ -0,0 +1,21 @@
+---
+PROFILES vaut prod test
+c'est à dire, si on fait un par ligne:
+- prod
+- test
+---
+PASSWORD is vaeRL6ADYKmWndEA
+---
+hosts:
+- first
+- second
+---
+---
+IF valeur
+if valeur
+#ul valeur
+---
+#if
+UL
+ul
+---
diff --git a/bash/tests/_template-source.txt b/bash/tests/_template-source.txt
new file mode 100644
index 0000000..d98644e
--- /dev/null
+++ b/bash/tests/_template-source.txt
@@ -0,0 +1,23 @@
+---
+PROFILES vaut @@PROFILES@@
+c'est à dire, si on fait un par ligne:
+@@FOR:PROFILES@@- @@PROFILE@@
+---
+PASSWORD is XXXRANDOMXXX
+---
+#@@IF:HOSTS@@hosts:
+@@FOR:HOSTS@@- @@HOST@@
+---
+#@@IF:VALUES@@values:
+@@FOR:VALUES@@- @@VALUE@@
+---
+#@@IF:PLEIN@@IF @@PLEIN@@
+#@@if:PLEIN@@if @@PLEIN@@
+#@@UL:PLEIN@@UL @@PLEIN@@
+#@@ul:PLEIN@@ul @@PLEIN@@
+---
+#@@IF:VIDE@@IF @@VIDE@@
+#@@if:VIDE@@if @@VIDE@@
+#@@UL:VIDE@@UL @@VIDE@@
+#@@ul:VIDE@@ul @@VIDE@@
+---
diff --git a/bash/tests/_template-source_envs b/bash/tests/_template-source_envs
new file mode 100755
index 0000000..e2aadd5
--- /dev/null
+++ b/bash/tests/_template-source_envs
@@ -0,0 +1,19 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+echo "\
+source ./_template-values.env
+template_vars=(
+ PROFILES
+ PASSWORD
+ HOSTS
+ VALUES
+ PLEIN
+ VIDE
+)
+template_lists=(
+ PROFILES
+ HOSTS
+ VALUES
+)
+"
diff --git a/bash/tests/_template-values.env b/bash/tests/_template-values.env
new file mode 100644
index 0000000..03af4ac
--- /dev/null
+++ b/bash/tests/_template-values.env
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+PROFILES="prod test"
+
+HOSTS="
+first
+second
+"
+VALUES=
+
+PLEIN=valeur
+VIDE=
diff --git a/bash/tests/test-args-autodebug.sh b/bash/tests/test-args-autodebug.sh
new file mode 100755
index 0000000..e035ffb
--- /dev/null
+++ b/bash/tests/test-args-autodebug.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+source "$(dirname -- "$0")/../src/nulib.sh" || exit 1
+#NULIB_NO_DISABLE_SET_X=1
+
+args=(
+ "tester autodebug"
+ #-D x=1 "désactiver l'option automatique -D"
+ #--debug x=1 "désactiver l'option automatique --debug"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+if is_debug; then
+ echo "on est en mode debug"
+else
+ echo "on n'est pas en mode debug, relancer avec -D ou --debug"
+fi
diff --git a/bash/tests/test-args-autohelp.sh b/bash/tests/test-args-autohelp.sh
new file mode 100755
index 0000000..bd2cf98
--- /dev/null
+++ b/bash/tests/test-args-autohelp.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+source "$(dirname -- "$0")/../src/nulib.sh" || exit 1
+#NULIB_NO_DISABLE_SET_X=1
+
+args=("tester l'affichage de l'aide")
+
+case "$1" in
+s|std)
+ # NB: seul l'affichage standard est disponible...
+ args+=(
+ -h,--help,--hstd '$showhelp@' "afficher l'aide de base"
+ )
+ shift
+ ;;
+a|adv)
+ # NB: seul l'affichage avancé est disponible...
+ args+=(
+ -H,--help++,--hadv '$showhelp@ ++' "afficher l'aide avancée"
+ )
+ shift
+ ;;
+sa|std+adv)
+ args+=(
+ -h,--help,--hstd '$showhelp@' "afficher l'aide de base"
+ -H,--help++,--hadv '$showhelp@ ++' "afficher l'aide avancée"
+ )
+ shift
+ ;;
+esac
+
+args+=(
+ -a,--std . "cette option apparait dans les options standards"
+ -b,--adv . "++cette option apparait dans les options avancées"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+enote "lancer le script
+- avec --help pour afficher les options standards uniquement
+- avec --help++ pour afficher toutes les options"
diff --git a/bash/tests/test-args-base.sh b/bash/tests/test-args-base.sh
new file mode 100755
index 0000000..b6b45da
--- /dev/null
+++ b/bash/tests/test-args-base.sh
@@ -0,0 +1,187 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+source "$(dirname -- "$0")/../src/nulib.sh" || exit 1
+require: tests
+#NULIB_NO_DISABLE_SET_X=1
+
+function pa() {
+ unset count fixed mopt dmopt oopt doopt autoinc autoval a1 a2 a3 a4
+ count=
+ fixed=
+ efixed=1
+ mopt=
+ dmopt=
+ oopt=
+ doopt=
+ autoinc=
+ autoval=
+ unset a1
+ a2=()
+ a3=
+ a4=x
+ args=(
+ "tester la gestion des arguments"
+ -o,--eopt count "incrémenter count"
+ -f,--fixed fixed=42 "spécifier fixed"
+ -e,--efixed efixed= "spécifier efixed"
+ -a:,--mopt mopt= "spécifier mopt"
+ -A:,--dmopt dmopt=default "spécifier dmopt"
+ -b::,--oopt oopt= "spécifier oopt"
+ -B::,--doopt doopt=default "spécifier doopt"
+ -n,--autoinc . "incrémenter autoinc"
+ -N,--no-autoinc . "décrémenter autoinc"
+ -v:,--autoval . "spécifier autoval"
+ -x: a1 "autoadd a1 qui n'est pas défini"
+ -y: a2 "autoadd a2 qui est défini à ()"
+ -z: a3 "autoadd a3 qui est défini à vide"
+ -t: a4 "autoadd a4 qui est défini à une valeur non vide"
+ -s,--sans-arg '$echo "sans_arg option=$option_, name=$name_, value=$value_"'
+ -S::,--avec-arg '$echo "avec_arg option=$option_, name=$name_, value=$value_"'
+ )
+ parse_args "$@"
+}
+
+pa
+assert_z "$count"
+assert_z "$fixed"
+assert_eq "$efixed" 1
+assert_z "$mopt"
+assert_z "$dmopt"
+assert_z "$oopt"
+assert_z "$doopt"
+assert_z "$autoinc"
+assert_z "$autoval"
+assert_not_defined a1
+assert_is_array a2
+assert_not_array a3
+assert_z "$a3"
+assert_not_array a4
+assert_same x "$a4"
+assert_eq "${#args[*]}" 0
+
+pa -o
+assert_eq "$count" 1
+pa -oo
+assert_eq "$count" 2
+pa -ooo
+assert_eq "$count" 3
+
+pa -f
+assert_eq "$fixed" 42
+pa -ff
+assert_eq "$fixed" 42
+pa -fff
+assert_eq "$fixed" 42
+
+assert_same "$efixed" "1"
+pa -e
+assert_same "$efixed" ""
+pa -ee
+assert_same "$efixed" ""
+pa -eee
+assert_same "$efixed" ""
+
+pa -a ""
+assert_not_array mopt
+assert_same "$mopt" ""
+pa -a abc
+assert_not_array mopt
+assert_same "$mopt" abc
+pa -a abc -a xyz
+assert_not_array mopt
+assert_same "$mopt" xyz
+
+pa -A ""
+assert_not_array dmopt
+assert_same "$dmopt" default
+pa -A abc
+assert_not_array dmopt
+assert_same "$dmopt" abc
+pa -A abc -A xyz
+assert_not_array dmopt
+assert_same "$dmopt" xyz
+
+pa -b
+assert_not_array oopt
+assert_same "$oopt" ""
+pa -babc
+assert_not_array oopt
+assert_same "$oopt" abc
+pa -babc -bxyz
+assert_not_array oopt
+assert_same "$oopt" xyz
+
+pa -B
+assert_not_array doopt
+assert_same "$doopt" default
+pa -Babc
+assert_not_array doopt
+assert_same "$doopt" abc
+pa -Babc -Bxyz
+assert_not_array doopt
+assert_same "$doopt" xyz
+
+pa -n
+assert_eq "$autoinc" 1
+pa -nn
+assert_eq "$autoinc" 2
+pa -nnn
+assert_eq "$autoinc" 3
+
+pa -nN
+assert_z "$autoinc"
+pa -nnN
+assert_eq "$autoinc" 1
+pa -nnnNN
+assert_eq "$autoinc" 1
+
+pa -v ""
+assert_is_array autoval
+assert_array_same autoval ""
+pa -v abc
+assert_is_array autoval
+assert_array_same autoval abc
+pa -v abc -v xyz
+assert_is_array autoval
+assert_array_same autoval abc xyz
+
+pa -xa
+assert_not_array a1
+assert_same "$a1" a
+pa -xa -xb
+assert_is_array a1
+assert_array_same a1 a b
+
+pa -ya
+assert_is_array a2
+assert_array_same a2 a
+pa -ya -yb
+assert_is_array a2
+assert_array_same a2 a b
+
+pa -za
+assert_is_array a3
+assert_array_same a3 a
+pa -za -zb
+assert_is_array a3
+assert_array_same a3 a b
+
+pa -ta
+assert_is_array a4
+assert_array_same a4 x a
+pa -ta -tb
+assert_is_array a4
+assert_array_same a4 x a b
+
+assert_same "$(pa -s)" "sans_arg option=-s, name=sans_arg, value="
+assert_same "$(pa --sans-arg)" "sans_arg option=--sans-arg, name=sans_arg, value="
+
+assert_same "$(pa -S)" "avec_arg option=-S, name=avec_arg, value="
+assert_same "$(pa -Sx)" "avec_arg option=-S, name=avec_arg, value=x"
+assert_same "$(pa --avec-arg)" "avec_arg option=--avec-arg, name=avec_arg, value="
+assert_same "$(pa --avec-arg=x)" "avec_arg option=--avec-arg, name=avec_arg, value=x"
+
+pa x
+assert_array_same args x
+
+enote "tests successful"
diff --git a/bash/tests/test-args-help.sh b/bash/tests/test-args-help.sh
new file mode 100755
index 0000000..646c392
--- /dev/null
+++ b/bash/tests/test-args-help.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+source "$(dirname -- "$0")/../src/nulib.sh" || exit 1
+#NULIB_NO_DISABLE_SET_X=1
+
+args=(
+ "tester l'affichage de l'aide"
+ -f:,--input input= "spécifier le fichier en entrée
+il est possible de spécifier aussi un répertoire auquel cas un fichier par défaut est chargé
+nb: l'aide pour cette option doit faire 3 lignes indentées"
+ -a,--std . "cette option apparait dans les options standards"
+ -b,--adv . "++cette option apparait dans les options avancées"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+enote "lancer le script
+- avec --help pour afficher les options standards uniquement
+- avec --help++ pour afficher toutes les options"
diff --git a/bash/tests/test-input.sh b/bash/tests/test-input.sh
new file mode 100755
index 0000000..5263dc5
--- /dev/null
+++ b/bash/tests/test-input.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+source "$(dirname -- "$0")/../src/nulib.sh" || exit 1
+#NULIB_NO_DISABLE_SET_X=1
+
+args=(
+ "tester diverses fonctions de saisie"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+choices=(first second third)
+choice=
+simple_menu -t "choix sans valeur par défaut" -m "le prompt" choice choices
+enote "vous avez choisi choice=$choice"
+
+choice=second
+simple_menu -t "choix avec valeur par défaut" -m "le prompt" choice choices
+enote "vous avez choisi choice=$choice"
diff --git a/bash/tests/test-output.sh b/bash/tests/test-output.sh
new file mode 100755
index 0000000..56a5812
--- /dev/null
+++ b/bash/tests/test-output.sh
@@ -0,0 +1,169 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+source "$(dirname -- "$0")/../src/nulib.sh" || exit 1
+#NULIB_NO_DISABLE_SET_X=1
+
+Multiline=
+Banner=
+args=(
+ "afficher divers messages avec les fonctions e*"
+ -D,--debug '$set_debug'
+ -d,--date NULIB_ELOG_DATE=1
+ -m,--myname NULIB_ELOG_MYNAME=1
+ -n,--nc,--no-color '$__set_no_colors 1'
+ --ml Multiline=1
+ -b Banner=1
+)
+parse_args "$@"; set -- "${args[@]}"
+
+if [ -n "$Multiline" ]; then
+ ############################################################################
+ [ -n "$Banner" ] && ebanner $'multi-line\nbanner'
+
+ esection $'multi-line\nsection'
+ etitle $'multi-line\ntitle'
+ etitle $'another\ntitle'
+ edesc $'multi-line\ndesc'
+
+ [ -n "$Banner" ] && ebanner $'multi-line\nbanner'
+ eimportant $'multi-line\nimportant'
+ eattention $'multi-line\nattention'
+ eerror $'multi-line\nerror'
+ ewarn $'multi-line\nwarn'
+ enote $'multi-line\nnote'
+ einfo $'multi-line\ninfo'
+ eecho $'multi-line\necho'
+ edebug $'multi-line\ndebug'
+
+ action $'multi-line\naction'
+ asuccess
+
+ action $'multi-line\naction'
+ estep $'multi-line\nstep'
+ afailure
+
+ action $'multi-line\naction'
+ estep $'multi-line\nstep'
+ asuccess $'multi-line\nsuccess'
+
+ action $'multi-line\naction'
+ estep $'multi-line\nstep'
+ adone $'multi-line\nneutral'
+
+ eend
+ eend
+
+else
+ ############################################################################
+ [ -n "$Banner" ] && ebanner "banner"
+ eimportant "important"
+ eattention "attention"
+ eerror "error"
+ ewarn "warn"
+ enote "note"
+ einfo "info"
+ eecho "echo"
+ edebug "debug"
+
+ estep "step"
+ estepe "stepe"
+ estepw "stepw"
+ estepn "stepn"
+ estepi "stepi"
+
+ esection "section"
+ eecho "content"
+
+ etitle "title0"
+ etitle "title1"
+ eecho "print under title1"
+ eend
+ eecho "print under title0"
+ eend
+
+ edesc "action avec step"
+ action "action avec step"
+ estep "step"
+ asuccess "action success"
+
+ action "action avec step"
+ estep "step"
+ afailure "action failure"
+
+ action "action avec step"
+ estep "step"
+ adone "action neutral"
+
+ edesc "actions sans step"
+ action "action sans step"
+ asuccess "action success"
+
+ action "action sans step"
+ afailure "action failure"
+
+ action "action sans step"
+ adone "action neutral"
+
+ edesc "actions imbriquées"
+ action "action0"
+ action "action1"
+ action "action2"
+ asuccess "action2 success"
+ asuccess "action1 success"
+ asuccess "action0 success"
+
+ edesc "action avec step, sans messages"
+ action "action avec step, sans messages, success"
+ estep "step"
+ asuccess
+
+ action "action avec step, sans messages, failure"
+ estep "step"
+ afailure
+
+ action "action avec step, sans messages, done"
+ estep "step"
+ adone
+
+ edesc "action sans step, sans messages"
+ action "action sans step, sans messages, success"
+ asuccess
+
+ action "action sans step, sans messages, failure"
+ afailure
+
+ action "action sans step, sans messages, done"
+ adone
+
+ edesc "actions imbriquées, sans messages"
+ action "action0"
+ action "action1"
+ action "action2"
+ asuccess
+ asuccess
+ asuccess
+
+ function vtrue() {
+ echo "commande qui se termine avec succès"
+ }
+ function vfalse() {
+ echo "commande qui se termine en échec"
+ return 1
+ }
+
+ edesc "action avec commande"
+ action "commande true" vtrue
+ action "commande false" vfalse
+
+ edesc "action avec commande et aresult sans message"
+ action "commande true"
+ vtrue; aresult $?
+ action "commande false"
+ vfalse; aresult $?
+
+ edesc "action avec commande et aresult"
+ action "commande true"
+ vtrue; aresult $? "résultat de la commande"
+ action "commande false"
+ vfalse; aresult $? "résultat de la commande"
+fi
diff --git a/bash/tests/test-template.sh b/bash/tests/test-template.sh
new file mode 100755
index 0000000..ca297c8
--- /dev/null
+++ b/bash/tests/test-template.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+source "$(dirname -- "$0")/../src/nulib.sh" || exit 1
+require: template
+#NULIB_NO_DISABLE_SET_X=1
+
+args=(
+ "description"
+ #"usage"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+function template__source_envs() {
+ eval "$("$MYDIR/_template-source_envs")"
+}
+
+cd "$MYDIR"
+#template_generate_scripts \
+# /tmp/awkscript /tmp/sedscript \
+# template_values.env
+#
+#for i in awk sed; do
+# etitle "$i" cat "/tmp/${i}script"
+#done
+
+template_copy_replace _template-source.txt _template-dest.txt
+template_process_userfiles _template_values.env
+
+cat _template-dest.txt
diff --git a/bin/_merge82 b/bin/_merge82
new file mode 100755
index 0000000..3c89e35
--- /dev/null
+++ b/bin/_merge82
@@ -0,0 +1,4 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+exec "$(dirname -- "$0")/pdev" --tech-merge -Bdev82 dev74 "$@"
diff --git a/bin/_runphp_build-all b/bin/_runphp_build-all
new file mode 100755
index 0000000..c6d0f4f
--- /dev/null
+++ b/bin/_runphp_build-all
@@ -0,0 +1,30 @@
+#!/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
+
+force=
+args=(
+ "Construire toutes les images supportées de runphp"
+ #"usage"
+ -f,--force force=1 "Créer les images même si elles existent déjà"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+export RUNPHP_STANDALONE="$NULIBDIR"
+export RUNPHP_PROJDIR=
+export RUNPHP_REGISTRY=
+export RUNPHP_DIST=
+export RUNPHP_BUILD_FLAVOUR=
+
+runphp=("$MYDIR/../runphp/runphp" --bs)
+[ -z "$force" ] && runphp+=(--ue)
+
+for RUNPHP_DIST in d12 d11; do
+ for RUNPHP_BUILD_FLAVOUR in +ic none; do
+ flavour="$RUNPHP_BUILD_FLAVOUR"
+ [ "$flavour" == none ] && flavour=
+ etitle "$RUNPHP_DIST$flavour"
+ "${runphp[@]}"
+ eend
+ done
+done
diff --git a/bin/composer b/bin/composer
new file mode 120000
index 0000000..42fbf67
--- /dev/null
+++ b/bin/composer
@@ -0,0 +1 @@
+runphp
\ No newline at end of file
diff --git a/bin/nlman b/bin/nlman
new file mode 100755
index 0000000..a6845bd
--- /dev/null
+++ b/bin/nlman
@@ -0,0 +1,69 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+NULIB_NO_IMPORT_DEFAULTS=1
+source "$(dirname -- "$0")/../load.sh" || exit 1
+
+LIST_FUNCS=
+SHOW_MODULE=
+SHOW_FUNCS=()
+function nulib__define_functions() {
+ function nulib_check_loaded() {
+ local module
+ for module in "${NULIB_LOADED_MODULES[@]}"; do
+ [ "$module" == "$1" ] && return 0
+ done
+ return 1
+ }
+ function module:() {
+ if [ -n "$LIST_FUNCS" ]; then
+ esection "@$1: $2"
+ fi
+ local module
+ SHOW_MODULE=
+ for module in "${SHOW_FUNCS[@]}"; do
+ if [ "$module" == "@$1" ]; then
+ SHOW_MODULE=1
+ fi
+ done
+ NULIB_MODULE="$1"
+ if ! nulib_check_loaded "$1"; then
+ NULIB_LOADED_MODULES+=("$1")
+ fi
+ }
+ function function:() {
+ if [ -n "$LIST_FUNCS" ]; then
+ eecho "$1"
+ elif [ -n "$SHOW_MODULE" ]; then
+ eecho "$COULEUR_BLEUE>>> $1 <<<$COULEUR_NORMALE"
+ eecho "$2"
+ else
+ local func
+ for func in "${SHOW_FUNCS[@]}"; do
+ if [ "$func" == "$1" ]; then
+ esection "$1"
+ eecho "$2"
+ fi
+ done
+ fi
+ }
+}
+require: DEFAULTS
+
+modules=()
+args=(
+ "afficher l'aide d'une fonction nulib"
+ "FUNCTIONS|@MODULES..."
+ -m:,--module: modules "charger des modules supplémentaires"
+ -l,--list LIST_FUNCS=1 "lister les fonctions pour lesquelles une aide existe"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+for func in "$@"; do
+ SHOW_FUNCS+=("$func")
+done
+
+NULIB_LOADED_MODULES=(nulib)
+require: DEFAULTS
+for module in "${modules[@]}"; do
+ require: "$module"
+done
diff --git a/bin/nlshell b/bin/nlshell
new file mode 100755
index 0000000..f5f64f6
--- /dev/null
+++ b/bin/nlshell
@@ -0,0 +1,38 @@
+#!/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
+
+force_reload=
+args=(
+ "lancer un shell avec les fonctions de nulib préchargées"
+ -r,--force-reload force_reload=1 "forcer le rechargement des modules"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+ac_set_tmpfile bashrc
+echo >"$bashrc" "\
+if ! grep -q '/etc/bash.bashrc' /etc/profile; then
+ [ -f /etc/bash.bashrc ] && source /etc/bash.bashrc
+fi
+if ! grep -q '~/.bashrc' ~/.bash_profile; then
+ [ -f ~/.bashrc ] && source ~/.bashrc
+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")
+
+if [ -n '$DEFAULT_PS1' ]; then
+ DEFAULT_PS1=$(qval "[nlshell] $DEFAULT_PS1")
+else
+ if [ -z '$PS1' ]; then
+ PS1='\\u@\\h \\w \\$ '
+ fi
+ PS1=\"[nlshell] \$PS1\"
+fi
+
+$(qvals source "$NULIBDIR/load.sh")
+NULIB_FORCE_RELOAD=$(qval "$force_reload")"
+
+"$SHELL" --rcfile "$bashrc" -i -- "$@"
diff --git a/bin/p b/bin/p
new file mode 100755
index 0000000..561d88a
--- /dev/null
+++ b/bin/p
@@ -0,0 +1,67 @@
+#!/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
+
+function git_status() {
+ local status r cwd
+ status="$(git -c color.status=always status "$@" 2>&1)"; r=$?
+ if [ -n "$status" ]; then
+ cwd="$Cwd"
+ [ -n "$cwd" ] || cwd="$(pwd)"
+ setx cwd=ppath2 "$cwd" "$OrigCwd"
+ etitle "$cwd"
+ if [ $r -eq 0 ]; then
+ echo "$status"
+ else
+ eerror "$status"
+ fi
+ eend
+ fi
+}
+
+chdir=
+all=
+args=(
+ "afficher l'état du dépôt"
+ "[-d chdir] [-a patterns...]
+
+Si l'option -a est utilisée, ce script accepte comme arguments une liste de patterns permettant de filtrer les répertoires concernés"
+ -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
+ -a,--all all=1 "faire l'opération sur tous les sous-répertoires de BASEDIR qui sont des dépôts git"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+setx OrigCwd=pwd
+if [ -n "$chdir" ]; then
+ cd "$chdir" || die
+fi
+
+Cwd=
+if [ -n "$all" ]; then
+ # liste de sous répertoires
+ if [ $# -gt 0 ]; then
+ # si on a une liste de patterns, l'utiliser
+ setx -a dirs=ls_dirs . "$@"
+ else
+ dirs=()
+ for dir in */.git; do
+ [ -d "$dir" ] || continue
+ dirs+=("${dir%/.git}")
+ done
+ fi
+ setx cwd=pwd
+ for dir in "${dirs[@]}"; do
+ cd "$dir" || die
+ git_status --porcelain
+ cd "$cwd"
+ done
+else
+ # répertoire courant uniquement
+ setx toplevel=git_get_toplevel
+ [ -n "$toplevel" ] && Cwd="$toplevel"
+
+ args=()
+ isatty || args+=(--porcelain)
+ git_status "${args[@]}"
+fi
diff --git a/bin/pdev b/bin/pdev
new file mode 100755
index 0000000..c077954
--- /dev/null
+++ b/bin/pdev
@@ -0,0 +1,197 @@
+#!/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 show_action() {
+ local commits
+ setx commits=_list_commits
+ if [ -n "$commits" ]; then
+ einfo "Commits à fusionner $SrcBranch --> $DestBranch"
+ eecho "$commits"
+ 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 merge_action() {
+ enote "\
+Ce script va
+- fusionner la branche ${COULEUR_BLEUE}$SrcBranch${COULEUR_NORMALE} dans ${COULEUR_ROUGE}$DestBranch${COULEUR_NORMALE}${Push:+
+- pousser les branches modifiées}"
+ ask_yesno "Voulez-vous continuer?" O || die
+
+ local script=".git/rel-merge.sh"
+ local -a push_branches delete_branches
+ local comment=
+ local or_die=" || exit 1"
+
+ _mscript_start
+ _scripta <.gitignore "\
+.~lock*#
+.*.swp"
+ git add .gitignore
+ fi
+
+ 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_develop_action() {
+ if [ -z "$DevelopBranch" ]; then
+ [ -n "$DEVELOP" ] || die "La branche DEVELOP n'a pas été définie"
+ [ -n "$MAIN" ] || die "La branche MAIN n'a pas été définie"
+ [ -n "$MainBranch" ] || die "$MAIN: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
+
+ enote "Vous allez créer la branche ${COULEUR_VERTE}$DEVELOP${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$MAIN${COULEUR_NORMALE}"
+ ask_yesno "Voulez-vous continuer?" O || die
+
+ local -a push_branches
+
+ einfo "Création de la branche $DEVELOP"
+ git checkout -b "$DEVELOP" "$MAIN" || die
+ push_branches+=("$DEVELOP")
+
+ _push_branches
+ fi
+ git checkout -q "$DEVELOP"
+}
+
+function init_upstream_action() {
+ if [ -z "$UpstreamBranch" ]; then
+ [ -n "$UPSTREAM" ] || die "La branche UPSTREAM n'a pas été définie"
+ [ -n "$DEVELOP" ] || die "La branche DEVELOP n'a pas été définie"
+ [ -n "$DevelopBranch" ] || die "$DEVELOP: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
+
+ enote "Vous allez créer la branche ${COULEUR_VERTE}$UPSTREAM${COULEUR_NORMALE}"
+ ask_yesno "Voulez-vous continuer?" O || die
+
+ local -a push_branches; local config
+
+ # faire une copie de la configuration actuelle
+ ac_set_tmpfile config
+ cp "$ConfigFile" "$config"
+
+ einfo "Création de la branche $UPSTREAM"
+ git checkout --orphan "$UPSTREAM" || die
+ git rm -rf .
+ cp "$config" .pman.conf
+ git add .pman.conf
+ git commit -m "commit initial"
+ push_branches+=("$UPSTREAM")
+
+ einfo "Fusion dans $DEVELOP"
+ git checkout "$DEVELOP"
+ git merge \
+ --no-ff -m "Intégration initiale de la branche $UPSTREAM" \
+ -srecursive -Xours --allow-unrelated-histories \
+ "$UPSTREAM"
+ push_branches+=("$DEVELOP")
+
+ _push_branches
+ fi
+ git checkout -q "$UPSTREAM"
+}
+
+function init_dist_action() {
+ if [ -z "$DistBranch" ]; then
+ [ -n "$DIST" ] || die "La branche DIST n'a pas été définie"
+ [ -n "$MAIN" ] || die "La branche MAIN n'a pas été définie"
+ [ -n "$MainBranch" ] || die "$MAIN: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
+
+ enote "Vous allez créer la branche ${COULEUR_VERTE}$DIST${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$MAIN${COULEUR_NORMALE}"
+ ask_yesno "Voulez-vous continuer?" O || die
+
+ local -a push_branches
+
+ einfo "Création de la branche $DIST"
+ git checkout -b "$DIST" "$MAIN" || die
+ push_branches+=("$DIST")
+
+ _push_branches
+ fi
+ git checkout -q "$DIST"
+}
+
+function init_feature_action() {
+ local branch="${1#$FEATURE}"
+ [ -n "$branch" ] || die "Vous devez définir la nom de la branche à créer"
+ branch="$FEATURE$branch"
+ if ! array_contains AllBranches "$branch"; then
+ [ -n "$DEVELOP" ] || die "La branche DEVELOP n'a pas été définie"
+ [ -n "$DevelopBranch" ] || die "$DEVELOP: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
+
+ enote "Vous allez créer la branche ${COULEUR_VERTE}$branch${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$DEVELOP${COULEUR_NORMALE}"
+ ask_yesno "Voulez-vous continuer?" O || die
+
+ local -a push_branches
+
+ einfo "Création de la branche $branch"
+ git checkout -b "$branch" "$DEVELOP" || die
+ push_branches+=("$branch")
+
+ _push_branches
+ fi
+ git checkout -q "$branch"
+}
+
+function init_action() {
+ local what="${1:-develop}"; shift
+ case "$what" in
+ init|repo|r) init_repo_action "$@";;
+ main|m) git checkout -q "$MAIN";;
+ develop|dev|d) init_develop_action "$@";;
+ upstream|up|u) init_upstream_action "$@";;
+ dist|x) init_dist_action "$@";;
+ *) init_feature_action "$what" "$@";;
+ esac
+}
+
+################################################################################
+# Programme principal
+################################################################################
+
+chdir=
+ConfigBranch=
+ConfigFile=
+action=init
+Origin=
+Push=1
+args=(
+ "gérer un projet git"
+ "repo|develop|upstream|dist
+
+INITIALISATION
+
+Par défaut, le script agit en mode initialisation qui permet de créer et/ou
+configurer certaines branches du dépôt si elles n'existent pas déjà
+
+ repo
+ initialiser un dépôt vide et créer les branches $MAIN et $DEVELOP
+ develop
+ créer la branche $DEVELOP
+ upstream
+ créer la branche ${UPSTREAM:-UPSTREAM} en tant que source de la branche $DEVELOP
+ dist
+ créer la branche ${DIST:-DIST} en tant que destination de la branche $MAIN
+ anything
+ créer la branche ${FEATURE}anything à partir de la branche $DEVELOP"
+ -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
+ -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"
+ -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"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+# charger la configuration
+ensure_gitdir "$chdir"
+load_branches all
+load_config "$MYNAME"
+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
diff --git a/bin/prel b/bin/prel
new file mode 100755
index 0000000..40ff75f
--- /dev/null
+++ b/bin/prel
@@ -0,0 +1,239 @@
+#!/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 release"
+
+function show_action() {
+ local commits
+ setx commits=_list_commits
+ if [ -n "$commits" ]; then
+ einfo "Commits à fusionner $SrcBranch --> $DestBranch"
+ eecho "$commits"
+ 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"
+
+ Tag="$TAG_PREFIX$Version$TAG_SUFFIX"
+ local -a tags
+ setx -a tags=git tag -l "${TAG_PREFIX}*${TAG_SUFFIX}"
+ if [ -z "$ForceCreate" ]; then
+ array_contains tags "$Tag" && die "$Tag: le tag correspondant à la version existe déjà"
+ fi
+}
+
+function create_release_action() {
+ if [ -n "$ReleaseBranch" ]; then
+ Version="${ReleaseBranch#$RELEASE}"
+ merge_release_action "$@"; return $?
+ elif [ -n "$HotfixBranch" ]; then
+ Version="${HotfixBranch#$HOTFIX}"
+ merge_hotfix_action "$@"; return $?
+ fi
+
+ if [ -z "$Version" -a -n "$CurrentVersion" -a -f VERSION.txt ]; then
+ Version="$("
+ -j,--no-autoext autoext= "ne pas rajouter l'extension .yaml ni .yml"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+[ $# -gt 0 ] || die "vous devez spécifier les noms des fichiers à créer"
+
+for file in "$@"; do
+ setx file=fndate_verifix "$file" .md
+ setx filename=basename -- "$file"
+ if [[ "$filename" == *.* ]]; then
+ : # y'a déjà une extension, ne rien faire
+ elif [ -z "$autoext" ]; then
+ : # ne pas rajouter d'extension
+ elif [ -f "$file.markdown" ]; then
+ file="$file.markdown"
+ else
+ file="$file.md"
+ fi
+ [ -e "$file" ] && die "$file: fichier existant"
+ estep "Création de $file"
+
+ echo -n >"$file" "\
+
+
+-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary"
+ "$EDITOR" "$file"
+done
diff --git a/bin/templ.sh b/bin/templ.sh
new file mode 100755
index 0000000..e17b6c5
--- /dev/null
+++ b/bin/templ.sh
@@ -0,0 +1,56 @@
+#!/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: fndate
+
+: ${EDITOR:=vim}
+
+executable=1
+autoext=1
+args=(
+ "créer un nouveau fichier .sh"
+ ""
+ -x,--exec executable=1 "créer un script exécutable"
+ -n,--no-exec executable= "créer un fichier non exécutable"
+ -j,--no-autoext autoext= "ne pas rajouter l'extension .sh"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+[ $# -gt 0 ] || die "vous devez spécifier les noms des fichiers à créer"
+
+for file in "$@"; do
+ setx file=fndate_verifix "$file" .sh
+ setx filename=basename -- "$file"
+ if [[ "$filename" == *.* ]]; then
+ : # y'a déjà une extension, ne rien faire
+ elif [ -z "$autoext" ]; then
+ : # ne pas rajouter d'extension
+ else
+ file="$file.sh"
+ fi
+ [ -e "$file" ] && die "$file: fichier existant"
+ estep "Création de $file"
+
+ if [ -n "$executable" ]; then
+ cat >"$file" <"$file" <"
+ -j,--no-autoext autoext= "ne pas rajouter l'extension .sql"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+[ $# -gt 0 ] || die "vous devez spécifier les noms des fichiers à créer"
+
+for file in "$@"; do
+ setx file=fndate_verifix "$file" .sql
+ setx filename=basename -- "$file"
+ if [[ "$filename" == *.* ]]; then
+ : # y'a déjà une extension, ne rien faire
+ elif [ -z "$autoext" ]; then
+ : # ne pas rajouter d'extension
+ else
+ file="$file.sql"
+ fi
+ [ -e "$file" ] && die "$file: fichier existant"
+ estep "Création de $file"
+
+ cat >"$file" <"
+ -j,--no-autoext autoext= "ne pas rajouter l'extension .yaml ni .yml"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+[ $# -gt 0 ] || die "vous devez spécifier les noms des fichiers à créer"
+
+for file in "$@"; do
+ setx file=fndate_verifix "$file" .yml
+ setx filename=basename -- "$file"
+ if [[ "$filename" == *.* ]]; then
+ : # y'a déjà une extension, ne rien faire
+ elif [ -z "$autoext" ]; then
+ : # ne pas rajouter d'extension
+ elif [ -f "$file.yaml" ]; then
+ file="$file.yaml"
+ else
+ file="$file.yml"
+ fi
+ [ -e "$file" ] && die "$file: fichier existant"
+ estep "Création de $file"
+
+ echo >"$file" "\
+# -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8
+
+"
+ "$EDITOR" +3 "$file"
+done
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..7171bf4
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,47 @@
+{
+ "name": "nulib/php",
+ "type": "library",
+ "description": "fonctions et classes essentielles",
+ "repositories": [
+ {
+ "type": "composer",
+ "url": "https://repos.univ-reunion.fr/composer"
+ }
+ ],
+ "extra": {
+ "branch-alias": {
+ "dev-dev74": "7.4.x-dev",
+ "dev-dev82": "8.2.x-dev"
+ }
+ },
+ "require": {
+ "symfony/yaml": "^5.0",
+ "ext-json": "*",
+ "php": "^7.4"
+ },
+ "require-dev": {
+ "nulib/tests": "^7.4",
+ "ext-posix": "*",
+ "ext-pcntl": "*",
+ "ext-curl": "*",
+ "ext-sqlite3": "*"
+ },
+ "autoload": {
+ "psr-4": {
+ "nulib\\": "php/src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "nulib\\": "php/tests"
+ }
+ },
+ "scripts": {
+ },
+ "authors": [
+ {
+ "name": "Jephte Clain",
+ "email": "Jephte.Clain@univ-reunion.fr"
+ }
+ ]
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..04bbdc9
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,2021 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "ab280aa4a5f5c83fa488537530b29759",
+ "packages": [
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v2.5.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918",
+ "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "2.5-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:11:13+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.31.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-ctype": "*"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
+ {
+ "name": "symfony/yaml",
+ "version": "v5.4.45",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/yaml.git",
+ "reference": "a454d47278cc16a5db371fe73ae66a78a633371e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/a454d47278cc16a5db371fe73ae66a78a633371e",
+ "reference": "a454d47278cc16a5db371fe73ae66a78a633371e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "symfony/console": "<5.3"
+ },
+ "require-dev": {
+ "symfony/console": "^5.3|^6.0"
+ },
+ "suggest": {
+ "symfony/console": "For validating YAML files using the lint command"
+ },
+ "bin": [
+ "Resources/bin/yaml-lint"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Yaml\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Loads and dumps YAML files",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/yaml/tree/v5.4.45"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:11:13+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "doctrine/instantiator",
+ "version": "1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b",
+ "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9 || ^11",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^0.16 || ^1",
+ "phpstan/phpstan": "^1.4",
+ "phpstan/phpstan-phpunit": "^1",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+ "vimeo/psalm": "^4.30 || ^5.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "https://ocramius.github.io/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/instantiator/issues",
+ "source": "https://github.com/doctrine/instantiator/tree/1.5.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-12-30T00:15:36+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.12.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845",
+ "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-11-08T17:47:46+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v5.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "447a020a1f875a434d62f2a401f53b82a396e494"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494",
+ "reference": "447a020a1f875a434d62f2a401f53b82a396e494",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0"
+ },
+ "time": "2024-12-30T11:07:19+00:00"
+ },
+ {
+ "name": "nulib/tests",
+ "version": "7.4",
+ "source": {
+ "type": "git",
+ "url": "https://git.univ-reunion.fr/sda-php/nulib-tests.git",
+ "reference": "9b5c9c295c3dee6fc02ccddbd8a70bca797c8045"
+ },
+ "require": {
+ "php": ">=7.3",
+ "phpunit/phpunit": "^9"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "nulib\\tests\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "nulib\\tests\\": "tests"
+ }
+ },
+ "authors": [
+ {
+ "name": "Jephte Clain",
+ "email": "Jephte.Clain@univ-reunion.fr"
+ }
+ ],
+ "description": "fonctions et classes pour les tests",
+ "time": "2025-01-30T13:18:31+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.2.1"
+ },
+ "time": "2022-02-21T01:04:05+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "9.2.32",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5",
+ "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^4.19.1 || ^5.1.0",
+ "php": ">=7.3",
+ "phpunit/php-file-iterator": "^3.0.6",
+ "phpunit/php-text-template": "^2.0.4",
+ "sebastian/code-unit-reverse-lookup": "^2.0.3",
+ "sebastian/complexity": "^2.0.3",
+ "sebastian/environment": "^5.1.5",
+ "sebastian/lines-of-code": "^1.0.4",
+ "sebastian/version": "^3.0.2",
+ "theseer/tokenizer": "^1.2.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "9.2.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-08-22T04:23:01+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "3.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-12-02T12:48:52+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:58:55+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T05:33:50+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "5.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:16:10+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "9.6.22",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c",
+ "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.5.0 || ^2",
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.12.1",
+ "phar-io/manifest": "^2.0.4",
+ "phar-io/version": "^3.2.1",
+ "php": ">=7.3",
+ "phpunit/php-code-coverage": "^9.2.32",
+ "phpunit/php-file-iterator": "^3.0.6",
+ "phpunit/php-invoker": "^3.1.1",
+ "phpunit/php-text-template": "^2.0.4",
+ "phpunit/php-timer": "^5.0.3",
+ "sebastian/cli-parser": "^1.0.2",
+ "sebastian/code-unit": "^1.0.8",
+ "sebastian/comparator": "^4.0.8",
+ "sebastian/diff": "^4.0.6",
+ "sebastian/environment": "^5.1.5",
+ "sebastian/exporter": "^4.0.6",
+ "sebastian/global-state": "^5.0.7",
+ "sebastian/object-enumerator": "^4.0.4",
+ "sebastian/resource-operations": "^3.0.4",
+ "sebastian/type": "^3.2.1",
+ "sebastian/version": "^3.0.2"
+ },
+ "suggest": {
+ "ext-soap": "To be able to generate mocks based on WSDL files",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.6-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsors.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-12-05T13:48:26+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:27:43+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "1.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:08:54+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:30:19+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "4.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/diff": "^4.0",
+ "sebastian/exporter": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2022-09-14T12:41:17+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-22T06:19:30+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "4.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:30:58+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "5.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:03:51+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "4.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
+ "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-mbstring": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:33:00+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "5.0.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
+ "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:35:11+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "1.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-22T06:20:34+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:12:34+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:14:26+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "4.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
+ "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:07:39+00:00"
+ },
+ {
+ "name": "sebastian/resource-operations",
+ "version": "3.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/resource-operations.git",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides a list of PHP built-in functions that operate on resources",
+ "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+ "support": {
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-14T16:00:52+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "source": "https://github.com/sebastianbergmann/type/tree/3.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:13:03+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:39:44+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.2.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:36:25+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "ext-json": "*",
+ "php": "^7.4"
+ },
+ "platform-dev": {
+ "ext-posix": "*",
+ "ext-pcntl": "*",
+ "ext-curl": "*"
+ },
+ "plugin-api-version": "2.2.0"
+}
diff --git a/dockerfiles/Dockerfile.adminer b/dockerfiles/Dockerfile.adminer
new file mode 100644
index 0000000..688f199
--- /dev/null
+++ b/dockerfiles/Dockerfile.adminer
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+ARG NDIST=12
+ARG REGISTRY=pubdocker.univ-reunion.fr/dist
+
+FROM $REGISTRY/src/base AS base
+FROM $REGISTRY/src/php AS php
+
+################################################################################
+FROM debian:${NDIST}-slim AS builder
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=base /src/ /src/
+RUN /g/build core lite _builder
+RUN /g/build _su-exec_builder
+
+################################################################################
+FROM debian:${NDIST}-slim
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=builder /src/su-exec/su-exec /g/
+RUN /g/build
+
+COPY --from=php /g/ /g/
+RUN /g/build -a @adminer
+
+EXPOSE 80
+ENTRYPOINT ["/g/entrypoint"]
diff --git a/dockerfiles/Dockerfile.adminer+ic b/dockerfiles/Dockerfile.adminer+ic
new file mode 100644
index 0000000..13d4fe0
--- /dev/null
+++ b/dockerfiles/Dockerfile.adminer+ic
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+ARG NDIST=12
+ARG REGISTRY=pubdocker.univ-reunion.fr/dist
+
+FROM $REGISTRY/src/base AS base
+FROM $REGISTRY/src/instantclient AS instantclient
+FROM $REGISTRY/src/php AS php
+
+################################################################################
+FROM debian:${NDIST}-slim AS builder
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=base /src/ /src/
+RUN /g/build core lite _builder
+RUN /g/build _su-exec_builder
+
+COPY --from=instantclient /g/ /g/
+COPY --from=instantclient /src/ /src/
+RUN /g/build _instantclient_builder
+
+################################################################################
+FROM debian:${NDIST}-slim
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=builder /src/su-exec/su-exec /g/
+RUN /g/build
+
+COPY --from=php /g/ /g/
+RUN /g/build -a @adminer
+
+COPY --from=instantclient /g/ /g/
+COPY --from=builder /opt/oracle/ /opt/oracle/
+RUN /g/build instantclient
+
+EXPOSE 80
+ENTRYPOINT ["/g/entrypoint"]
diff --git a/dockerfiles/Dockerfile.mariadb10 b/dockerfiles/Dockerfile.mariadb10
new file mode 100644
index 0000000..f77d113
--- /dev/null
+++ b/dockerfiles/Dockerfile.mariadb10
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+ARG REGISTRY=pubdocker.univ-reunion.fr/dist
+FROM $REGISTRY/src/base AS base
+FROM $REGISTRY/src/mariadb AS mariadb
+FROM $REGISTRY/src/legacytools AS legacytools
+
+FROM mariadb:10
+ARG APT_PROXY TIMEZONE
+ENV APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=mariadb /g/ /g/
+RUN /g/build -a @base @mariadb
+
+COPY --from=legacytools /g/ /g/
+RUN /g/build nutools servertools
+
+EXPOSE 3306
+ENTRYPOINT ["/g/entrypoint"]
diff --git a/dockerfiles/Dockerfile.php-apache b/dockerfiles/Dockerfile.php-apache
new file mode 100644
index 0000000..2a9e467
--- /dev/null
+++ b/dockerfiles/Dockerfile.php-apache
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+ARG NDIST=12
+ARG REGISTRY=pubdocker.univ-reunion.fr/dist
+
+FROM $REGISTRY/src/base AS base
+FROM $REGISTRY/src/php AS php
+
+################################################################################
+FROM debian:${NDIST}-slim AS builder
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=base /src/ /src/
+RUN /g/build core lite _builder
+RUN /g/build _su-exec_builder
+
+################################################################################
+FROM debian:${NDIST}-slim
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=builder /src/su-exec/su-exec /g/
+RUN /g/build
+
+COPY --from=php /g/ /g/
+RUN /g/build -a @apache-php-cas php-utils
+
+EXPOSE 80 443
+ENTRYPOINT ["/g/entrypoint"]
diff --git a/dockerfiles/Dockerfile.php-apache+ic b/dockerfiles/Dockerfile.php-apache+ic
new file mode 100644
index 0000000..cfbbd61
--- /dev/null
+++ b/dockerfiles/Dockerfile.php-apache+ic
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+ARG NDIST=12
+ARG REGISTRY=pubdocker.univ-reunion.fr/dist
+
+FROM $REGISTRY/src/base AS base
+FROM $REGISTRY/src/legacytools AS legacytools
+FROM $REGISTRY/src/instantclient AS instantclient
+FROM $REGISTRY/src/php AS php
+
+################################################################################
+FROM debian:${NDIST}-slim AS builder
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=base /src/ /src/
+RUN /g/build core lite _builder
+RUN /g/build _su-exec_builder
+
+COPY --from=instantclient /g/ /g/
+COPY --from=instantclient /src/ /src/
+RUN /g/build _instantclient_builder
+
+################################################################################
+FROM debian:${NDIST}-slim
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=builder /src/su-exec/su-exec /g/
+RUN /g/build
+
+COPY --from=legacytools /g/ /g/
+RUN /g/build nutools
+
+COPY --from=php /g/ /g/
+RUN /g/build -a @apache-php-cas php-utils
+
+COPY --from=instantclient /g/ /g/
+COPY --from=builder /opt/oracle/ /opt/oracle/
+RUN /g/build instantclient
+
+EXPOSE 80 443
+ENTRYPOINT ["/g/entrypoint"]
diff --git a/dockerfiles/Dockerfile.php-cli b/dockerfiles/Dockerfile.php-cli
new file mode 100644
index 0000000..f6bcfbc
--- /dev/null
+++ b/dockerfiles/Dockerfile.php-cli
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+ARG NDIST=12
+ARG REGISTRY=pubdocker.univ-reunion.fr/dist
+
+FROM $REGISTRY/src/base AS base
+FROM $REGISTRY/src/php AS php
+
+################################################################################
+FROM debian:${NDIST}-slim AS builder
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=base /src/ /src/
+RUN /g/build core lite _builder
+RUN /g/build _su-exec_builder
+
+################################################################################
+FROM debian:${NDIST}-slim
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=builder /src/su-exec/su-exec /g/
+RUN /g/build
+
+COPY --from=php /g/ /g/
+RUN /g/build @php-cli php-utils
+
+ENTRYPOINT ["/g/entrypoint"]
diff --git a/dockerfiles/Dockerfile.php-cli+ic b/dockerfiles/Dockerfile.php-cli+ic
new file mode 100644
index 0000000..48f376b
--- /dev/null
+++ b/dockerfiles/Dockerfile.php-cli+ic
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+ARG NDIST=12
+ARG REGISTRY=pubdocker.univ-reunion.fr/dist
+
+FROM $REGISTRY/src/base AS base
+FROM $REGISTRY/src/legacytools AS legacytools
+FROM $REGISTRY/src/instantclient AS instantclient
+FROM $REGISTRY/src/php AS php
+
+################################################################################
+FROM debian:${NDIST}-slim AS builder
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=base /src/ /src/
+RUN /g/build core lite _builder
+RUN /g/build _su-exec_builder
+
+COPY --from=instantclient /g/ /g/
+COPY --from=instantclient /src/ /src/
+RUN /g/build _instantclient_builder
+
+################################################################################
+FROM debian:${NDIST}-slim
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=builder /src/su-exec/su-exec /g/
+RUN /g/build
+
+COPY --from=legacytools /g/ /g/
+RUN /g/build nutools
+
+COPY --from=php /g/ /g/
+RUN /g/build @php-cli php-utils
+
+COPY --from=instantclient /g/ /g/
+COPY --from=builder /opt/oracle/ /opt/oracle/
+RUN /g/build instantclient
+
+ENTRYPOINT ["/g/entrypoint"]
diff --git a/dockerfiles/Dockerfile.postgres15 b/dockerfiles/Dockerfile.postgres15
new file mode 100644
index 0000000..4edb5c5
--- /dev/null
+++ b/dockerfiles/Dockerfile.postgres15
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+ARG REGISTRY=pubdocker.univ-reunion.fr/dist
+
+FROM $REGISTRY/src/base AS base
+FROM $REGISTRY/src/postgres AS postgres
+
+FROM postgres:15-bookworm
+ARG APT_PROXY TIMEZONE
+ENV APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=postgres /g/ /g/
+RUN /g/build -a @base @postgres
+RUN /g/pkg i @ssl @git
+
+EXPOSE 5432
+ENTRYPOINT ["/g/entrypoint"]
diff --git a/lib/completion.d/pman b/lib/completion.d/pman
new file mode 100644
index 0000000..e3dcb73
--- /dev/null
+++ b/lib/completion.d/pman
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+function __pman_pdev_branches() {
+ local toplevel="$(git rev-parse --show-toplevel 2>/dev/null)"
+ [ -n "$toplevel" ] || return 0
+
+ (
+ # cf pman.conf.sh
+ UPSTREAM=
+ DEVELOP=develop
+ FEATURE=wip/
+ RELEASE=release-
+ MAIN=master
+ TAG_PREFIX=
+ TAG_SUFFIX=
+ HOTFIX=hotfix-
+ DIST=
+ [ -f "$toplevel/.pman.conf" ] && source "$toplevel/.pman.conf"
+ # lister les branches
+ branches="$DEVELOP|$FEATURE.*"
+ [ -n "$UPSTREAM" ] && branches="$branches|$UPSTREAM"
+ remote=origin/
+ {
+ git for-each-ref refs/heads/ --format='%(refname:short)' 2>/dev/null
+ git for-each-ref "refs/remotes/$remote" --format='%(refname:short)' 2>/dev/null |
+ grep -F "$remote" |
+ cut -c $((${#remote} + 1))-
+ } | LANG=C sort -u | grep -E "^($branches)\$"
+ )
+}
+
+function __pdev_completion() {
+ local cur
+ _get_comp_words_by_ref cur
+ COMPREPLY=($(compgen -W "$(__pman_pdev_branches)" "$cur"))
+}
+complete -F __pdev_completion pdev
diff --git a/lib/profile.d/nulib b/lib/profile.d/nulib
new file mode 100644
index 0000000..542e828
--- /dev/null
+++ b/lib/profile.d/nulib
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+__uaddpath "@@dest@@/bin" PATH
diff --git a/lib/setup.sh b/lib/setup.sh
new file mode 100755
index 0000000..64bc396
--- /dev/null
+++ b/lib/setup.sh
@@ -0,0 +1,11 @@
+#!/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
+
+[ "$(id -u)" -eq 0 ] || die "Ce script doit être lancé avec les droits root"
+
+cd "$MYDIR/.."
+[ -n "$1" ] && dest="$1" || dest="$(pwd)"
+
+estep "Maj /etc/nulib.sh"
+sed "s|@@""dest""@@|$dest|g" load.sh >/etc/nulib.sh
diff --git a/lib/uinst/conf b/lib/uinst/conf
new file mode 100644
index 0000000..6f4d212
--- /dev/null
+++ b/lib/uinst/conf
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+source "$@" || exit 1
+
+# supprimer les fichiers de VCS
+rm -rf "$srcdir/.git"
diff --git a/lib/uinst/rootconf b/lib/uinst/rootconf
new file mode 100644
index 0000000..bdc0ed3
--- /dev/null
+++ b/lib/uinst/rootconf
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+source "$@" || exit 1
+
+"$srcdir/lib/setup.sh" "$dest"
diff --git a/load.sh b/load.sh
new file mode 100644
index 0000000..fa544c9
--- /dev/null
+++ b/load.sh
@@ -0,0 +1,182 @@
+##@cooked comments # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+## Charger nulib et rendre disponible les modules bash, awk, php et python
+##@cooked nocomments
+# Ce fichier doit être sourcé en premier. Si ce fichier n'est pas sourcé, alors
+# le répertoire nulib doit être disponible dans le répertoire du script qui
+# inclue ce fichier.
+# Une fois ce fichier sourcé, les autres modules peuvent être importés avec
+# require:() e.g.
+# source /etc/nulib.sh || exit 1
+# require: other_modules
+# ou pour une copie locale de nulib:
+# source "$(dirname "$0")/nulib/load.sh" || exit 1
+# require: other_modules
+
+# vérifier version minimum de bash
+if [ "x$BASH" = "x" ]; then
+ echo "ERROR: nulib: this script requires bash"
+ exit 1
+fi
+
+function eerror() { echo "ERROR: $*" 1>&2; }
+function die() { [ $# -gt 0 ] && eerror "$*"; exit 1; }
+function edie() { [ $# -gt 0 ] && eerror "$*"; return 1; }
+function delpath() { local _qdir="${1//\//\\/}"; eval "export ${2:-PATH}; ${2:-PATH}"'="${'"${2:-PATH}"'#$1:}"; '"${2:-PATH}"'="${'"${2:-PATH}"'%:$1}"; '"${2:-PATH}"'="${'"${2:-PATH}"'//:$_qdir:/:}"; [ "$'"${2:-PATH}"'" == "$1" ] && '"${2:-PATH}"'='; }
+function addpath() { local _qdir="${1//\//\\/}"; eval "export ${2:-PATH}; "'[ "${'"${2:-PATH}"'#$1:}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'%:$1}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'//:$_qdir:/:}" == "$'"${2:-PATH}"'" -a "$'"${2:-PATH}"'" != "$1" ] && '"${2:-PATH}"'="${'"${2:-PATH}"':+$'"${2:-PATH}"':}$1"'; }
+function inspathm() { local _qdir="${1//\//\\/}"; eval "export ${2:-PATH}; "'[ "${'"${2:-PATH}"'#$1:}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'%:$1}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'//:$_qdir:/:}" == "$'"${2:-PATH}"'" -a "$'"${2:-PATH}"'" != "$1" ] && '"${2:-PATH}"'="$1${'"${2:-PATH}"':+:$'"${2:-PATH}"'}"'; }
+function inspath() { delpath "$@"; inspathm "$@"; }
+
+if [ ${BASH_VERSINFO[0]} -ge 5 -o \( ${BASH_VERSINFO[0]} -eq 4 -a ${BASH_VERSINFO[1]} -ge 1 \) ]; then :
+elif [ -n "$NULIB_IGNORE_BASH_VERSION" ]; then :
+else die "nulib: bash 4.1+ is required"
+fi
+
+# Calculer emplacement de nulib
+NULIBDIR="@@dest@@"
+if [ "$NULIBDIR" = "@@""dest""@@" ]; then
+ # La valeur "@@"dest"@@" n'est remplacée que dans la copie de ce script
+ # faite dans /etc. Sinon, il faut toujours faire le calcul. Cela permet de
+ # déplacer la librairie n'importe où sur le disque, ce qui est
+ # particulièrement intéressant quand on fait du déploiement.
+ NULIBDIR="${BASH_SOURCE[0]}"
+ if [ -f "$NULIBDIR" -a "$(basename -- "$NULIBDIR")" == load.sh ]; then
+ # Fichier sourcé depuis nulib/
+ NULIB_SOURCED=1
+ NULIBDIR="$(dirname -- "$NULIBDIR")"
+ elif [ -f "$NULIBDIR" -a "$(basename -- "$NULIBDIR")" == nulib.sh ]; then
+ # Fichier sourcé depuis nulib/bash/src
+ NULIB_SOURCED=1
+ NULIBDIR="$(dirname -- "$NULIBDIR")/../.."
+ else
+ # Fichier non sourcé. Tout exprimer par rapport au script courant
+ NULIB_SOURCED=
+ NULIBDIR="$(dirname -- "$0")"
+ if [ -d "$NULIBDIR/nulib" ]; then
+ NULIBDIR="$NULIBDIR/nulib"
+ elif [ -d "$NULIBDIR/lib/nulib" ]; then
+ NULIBDIR="$NULIBDIR/lib/nulib"
+ fi
+ fi
+elif [ "${BASH_SOURCE[0]}" = /etc/nulib.sh ]; then
+ # Fichier chargé depuis /etc/nulib.sh
+ NULIB_SOURCED=1
+fi
+NULIBDIR="$(cd "$NULIBDIR" 2>/dev/null; pwd)"
+NULIBDIRS=("$NULIBDIR/bash/src")
+
+# marqueur pour vérifier que nulib a réellement été chargé. il faut avoir $NULIBINIT == $NULIBDIR
+# utilisé par le module base qui doit pouvoir être inclus indépendamment
+NULIBINIT="$NULIBDIR"
+
+## Modules bash
+NULIB_LOADED_MODULES=(nulib)
+NULIB_DEFAULT_MODULES=(base pretty sysinfos)
+
+# Si cette variable est non vide, require: recharge toujours le module, même
+# s'il a déjà été chargé. Cette valeur n'est pas transitive: il faut toujours
+# recharger explicitement tous les modules désirés
+NULIB_FORCE_RELOAD=
+
+function nulib__define_functions() {
+ function nulib_check_loaded() {
+ local module
+ for module in "${NULIB_LOADED_MODULES[@]}"; do
+ [ "$module" == "$1" ] && return 0
+ done
+ return 1
+ }
+ function module:() {
+ NULIB_MODULE="$1"
+ if ! nulib_check_loaded "$1"; then
+ NULIB_LOADED_MODULES+=("$1")
+ fi
+ }
+ function function:() {
+ :
+ }
+}
+
+function nulib__load:() {
+ local nl__module nl__nulibdir nl__found
+ [ $# -gt 0 ] || set DEFAULTS
+
+ for nl__module in "$@"; do
+ nl__found=
+ for nl__nulibdir in "${NULIBDIRS[@]}"; do
+ if [ -f "$nl__nulibdir/$nl__module.sh" ]; then
+ source "$nl__nulibdir/$nl__module.sh" || die
+ nl__found=1
+ break
+ fi
+ done
+ [ -n "$nl__found" ] || die "nulib: unable to find module $nl__module in (${NULIBDIRS[*]})"
+ done
+}
+function nulib__require:() {
+ local nr__module nr__nulibdir nr__found
+ [ $# -gt 0 ] || set DEFAULTS
+
+ # sauvegarder valeurs globales
+ local nr__orig_module="$NULIB_MODULE"
+ NULIB_MODULE=
+
+ # garder une copie de la valeur originale et casser la transitivité
+ local nr__force_reload="$NULIB_FORCE_RELOAD"
+ local NULIB_FORCE_RELOAD
+
+ for nr__module in "$@"; do
+ nr__found=
+ for nr__nulibdir in "${NULIBDIRS[@]}"; do
+ if [ -f "$nr__nulibdir/$nr__module.sh" ]; then
+ nr__found=1
+ if [ -n "$nr__force_reload" ] || ! nulib_check_loaded "$nr__module"; then
+ NULIB_LOADED_MODULES+=("$nr__module")
+ source "$nr__nulibdir/$nr__module.sh" || die
+ fi
+ break
+ fi
+ done
+ if [ -z "$nr__found" -a "$nr__module" == DEFAULTS ]; then
+ for nr__module in "${NULIB_DEFAULT_MODULES[@]}"; do
+ if [ -f "$nr__nulibdir/$nr__module.sh" ]; then
+ nr__found=1
+ if [ -n "$nr__force_reload" ] || ! nulib_check_loaded "$nr__module"; then
+ NULIB_LOADED_MODULES+=("$nr__module")
+ source "$nr__nulibdir/$nr__module.sh" || die
+ fi
+ else
+ break
+ fi
+ done
+ fi
+ [ -n "$nr__found" ] || die "nulib: unable to find module $nr__module in (${NULIBDIRS[*]})"
+ done
+
+ # restaurer valeurs globales
+ NULIB_MODULE="$nr__orig_module"
+}
+
+# désactiver set -x
+NULIB__DISABLE_SET_X='local NULIB__SET_X; [ -z "$NULIB_NO_DISABLE_SET_X" ] && [[ $- == *x* ]] && { set +x; NULIB__SET_X=1; }'
+NULIB__ENABLE_SET_X='[ -n "$NULIB__SET_X" ] && set -x'
+# désactiver set -x de manière réentrante
+NULIB__RDISABLE_SET_X='[ -z "$NULIB_NO_DISABLE_SET_X" ] && [[ $- == *x* ]] && { set +x; local NULIB_REQUIRE_SET_X=1; }; if [ -n "$NULIB_REQUIRE_SET_X" ]; then [ -n "$NULIB_REQUIRE_SET_X_RL1" ] || local NULIB_REQUIRE_SET_X_RL1; local NULIB_REQUIRE_SET_X_RL2=$RANDOM; [ -n "$NULIB_REQUIRE_SET_X_RL1" ] || NULIB_REQUIRE_SET_X_RL1=$NULIB_REQUIRE_SET_X_RL2; fi'
+NULIB__RENABLE_SET_X='[ -n "$NULIB_REQUIRE_SET_X" -a "$NULIB_REQUIRE_SET_X_RL1" == "$NULIB_REQUIRE_SET_X_RL2" ] && set -x'
+
+function require:() {
+ eval "$NULIB__RDISABLE_SET_X"
+ nulib__define_functions
+ nulib__require: "$@"
+ eval "$NULIB__RENABLE_SET_X"
+ return 0
+}
+
+## Autres modules
+[ -d "$NULIBDIR/awk/src" ] && inspath "$NULIBDIR/awk/src" AWKPATH; export AWKPATH
+[ -d "$NULIBDIR/python3/src" ] && inspath "$NULIBDIR/python3/src" PYTHONPATH; export PYTHONPATH
+
+## Auto import DEFAULTS
+nulib__define_functions
+if [ -n "$NULIB_SOURCED" -a -z "$NULIB_NO_IMPORT_DEFAULTS" ]; then
+ require: DEFAULTS
+fi
diff --git a/php/run-tests b/php/run-tests
new file mode 100755
index 0000000..1b0c5a1
--- /dev/null
+++ b/php/run-tests
@@ -0,0 +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/bin/phpunit" --bootstrap "$VENDOR/autoload.php" "$@" "$MYDIR/tests"
diff --git a/php/src/A.php b/php/src/A.php
new file mode 100644
index 0000000..c7d982b
--- /dev/null
+++ b/php/src/A.php
@@ -0,0 +1,235 @@
+wrappedArray();
+ if ($array === null || $array === false) $array = [];
+ elseif ($array instanceof Traversable) $array = cl::all($array);
+ else $array = [$array];
+ return false;
+ }
+
+ /**
+ * s'assurer que $array est un array s'il est non null. retourner true si
+ * $array n'a pas été modifié (s'il était déjà un array ou s'il valait null).
+ */
+ static final function ensure_narray(&$array): bool {
+ if ($array instanceof IArrayWrapper) $array = $array->wrappedArray();
+ if ($array === null || is_array($array)) return true;
+ if ($array === false) $array = [];
+ elseif ($array instanceof Traversable) $array = cl::all($array);
+ else $array = [$array];
+ return false;
+ }
+
+ /**
+ * s'assurer que $array est un tableau de $size éléments, en complétant avec
+ * des occurrences de $default si nécessaire
+ *
+ * @return bool true si le tableau a été modifié, false sinon
+ */
+ static final function ensure_size(?array &$array, int $size, $default=null): bool {
+ $modified = false;
+ if ($array === null) {
+ $array = [];
+ $modified = true;
+ }
+ if ($size < 0) return $modified;
+ $count = count($array);
+ if ($count == $size) return $modified;
+ if ($count < $size) {
+ # agrandir le tableau
+ while ($count++ < $size) {
+ $array[] = $default;
+ }
+ return true;
+ }
+ # rétrécir le tableau
+ $tmparray = [];
+ foreach ($array as $key => $value) {
+ if ($size-- == 0) break;
+ $tmparray[$key] = $value;
+ }
+ $array = $tmparray;
+ return true;
+ }
+
+ static function merge(&$dest, ...$merges): void {
+ self::ensure_narray($dest);
+ $dest = cl::merge($dest, ...$merges);
+ }
+
+ static function merge2(&$dest, ...$merges): void {
+ self::ensure_narray($dest);
+ $dest = cl::merge2($dest, ...$merges);
+ }
+
+ static final function select(&$dest, ?array $mappings, bool $inverse=false): void {
+ self::ensure_narray($dest);
+ $dest = cl::select($dest, $mappings, $inverse);
+ }
+
+ static final function selectm(&$dest, ?array $mappings, ?array $merge=null): void {
+ self::ensure_narray($dest);
+ $dest = cl::selectm($dest, $mappings, $merge);
+ }
+
+ static final function mselect(&$dest, ?array $merge, ?array $mappings): void {
+ self::ensure_narray($dest);
+ $dest = cl::mselect($dest, $merge, $mappings);
+ }
+
+ static final function pselect(&$dest, ?array $pkeys): void {
+ self::ensure_narray($dest);
+ $dest = cl::pselect($dest, $pkeys);
+ }
+
+ static final function pselectm(&$dest, ?array $pkeys, ?array $merge=null): void {
+ self::ensure_narray($dest);
+ $dest = cl::pselectm($dest, $pkeys, $merge);
+ }
+
+ static final function mpselect(&$dest, ?array $merge, ?array $pkeys): void {
+ self::ensure_narray($dest);
+ $dest = cl::mpselect($dest, $merge, $pkeys);
+ }
+
+ static final function set_nn(&$dest, $key, $value) {
+ self::ensure_narray($dest);
+ if ($value !== null) {
+ if ($key === null) $dest[] = $value;
+ else $dest[$key] = $value;
+ }
+ return $value;
+ }
+
+ static final function append_nn(&$dest, $value) {
+ return self::set_nn($dest, null, $value);
+ }
+
+ static final function set_nz(&$dest, $key, $value) {
+ self::ensure_narray($dest);
+ if ($value !== null && $value !== false) {
+ if ($key === null) $dest[] = $value;
+ else $dest[$key] = $value;
+ }
+ return $value;
+ }
+
+ static final function append_nz(&$dest, $value) {
+ self::ensure_narray($dest);
+ return self::set_nz($dest, null, $value);
+ }
+
+ static final function prepend_nn(&$dest, $value) {
+ self::ensure_narray($dest);
+ if ($value !== null) {
+ if ($dest === null) $dest = [];
+ array_unshift($dest, $value);
+ }
+ return $value;
+ }
+
+ static final function prepend_nz(&$dest, $value) {
+ self::ensure_narray($dest);
+ if ($value !== null && $value !== false) {
+ if ($dest === null) $dest = [];
+ array_unshift($dest, $value);
+ }
+ return $value;
+ }
+
+ static final function replace_nx(&$dest, $key, $value) {
+ self::ensure_narray($dest);
+ if ($dest !== null && !array_key_exists($key, $dest)) {
+ return $dest[$key] = $value;
+ } else {
+ return $dest[$key] ?? null;
+ }
+ }
+
+ static final function replace_n(&$dest, $key, $value) {
+ self::ensure_narray($dest);
+ $pvalue = $dest[$key] ?? null;
+ if ($pvalue === null) $dest[$key] = $value;
+ return $pvalue;
+ }
+
+ static final function replace_z(&$dest, $key, $value) {
+ self::ensure_narray($dest);
+ $pvalue = $dest[$key] ?? null;
+ if ($pvalue === null || $pvalue === false) $dest[$key] = $value;
+ return $pvalue;
+ }
+
+ static final function pop(&$dest, $key, $default=null) {
+ if ($dest === null) return $default;
+ self::ensure_narray($dest);
+ if ($key === null) return array_pop($dest);
+ $value = $dest[$key] ?? $default;
+ unset($dest[$key]);
+ return $value;
+ }
+
+ static final function popx(&$dest, ?array $keys): array {
+ $values = [];
+ if ($dest === null) return $values;
+ self::ensure_narray($dest);
+ if ($keys === null) return $values;
+ foreach ($keys as $key) {
+ $values[$key] = self::pop($dest, $key);
+ }
+ return $values;
+ }
+
+ static final function filter_if(&$dest, callable $cond): void {
+ self::ensure_narray($dest);
+ $dest = cl::filter_if($dest, $cond);
+ }
+
+ static final function filter_z($dest): void { self::filter_if($dest, [cv::class, "z"]);}
+ static final function filter_nz($dest): void { self::filter_if($dest, [cv::class, "nz"]);}
+ static final function filter_n($dest): void { self::filter_if($dest, [cv::class, "n"]);}
+ static final function filter_nn($dest): void { self::filter_if($dest, [cv::class, "nn"]);}
+ static final function filter_t($dest): void { self::filter_if($dest, [cv::class, "t"]);}
+ static final function filter_f($dest): void { self::filter_if($dest, [cv::class, "f"]);}
+ static final function filter_pt($dest): void { self::filter_if($dest, [cv::class, "pt"]);}
+ static final function filter_pf($dest): void { self::filter_if($dest, [cv::class, "pf"]);}
+ static final function filter_equals($dest, $value): void { self::filter_if($dest, cv::equals($value)); }
+ static final function filter_not_equals($dest, $value): void { self::filter_if($dest, cv::not_equals($value)); }
+ static final function filter_same($dest, $value): void { self::filter_if($dest, cv::same($value)); }
+ static final function filter_not_same($dest, $value): void { self::filter_if($dest, cv::not_same($value)); }
+
+ #############################################################################
+
+ static final function sort(?array &$array, int $flags=SORT_REGULAR, bool $assoc=false): void {
+ if ($array === null) return;
+ if ($assoc) asort($array, $flags);
+ else sort($array, $flags);
+ }
+
+ static final function ksort(?array &$array, int $flags=SORT_REGULAR): void {
+ if ($array === null) return;
+ ksort($array, $flags);
+ }
+
+ static final function usort(?array &$array, array $keys, bool $assoc=false): void {
+ if ($array === null) return;
+ if ($assoc) uasort($array, cl::compare($keys));
+ else usort($array, cl::compare($keys));
+ }
+}
diff --git a/php/src/AccessException.php b/php/src/AccessException.php
new file mode 100644
index 0000000..6996667
--- /dev/null
+++ b/php/src/AccessException.php
@@ -0,0 +1,36 @@
+ $file,
+ "line" => $line,
+ "class" => $class,
+ "object" => null,
+ "type" => $type,
+ "function" => $function,
+ "args" => [],
+ ];
+ }
+ return $frames;
+ }
+
+ function __construct(Throwable $exception) {
+ $this->class = get_class($exception);
+ $this->message = $exception->getMessage();
+ $this->code = $exception->getCode();
+ $this->file = $exception->getFile();
+ $this->line = $exception->getLine();
+ $this->trace = self::extract_trace($exception->getTrace());
+ $previous = $exception->getPrevious();
+ if ($previous !== null) $this->previous = new static($previous);
+ }
+
+ /** @var string */
+ protected $class;
+
+ function getClass(): string {
+ return $this->class;
+ }
+
+ /** @var string */
+ protected $message;
+
+ function getMessage(): string {
+ return $this->message;
+ }
+
+ /** @var mixed */
+ protected $code;
+
+ function getCode() {
+ return $this->code;
+ }
+
+ /** @var string */
+ protected $file;
+
+ function getFile(): string {
+ return $this->file;
+ }
+
+ /** @var int */
+ protected $line;
+
+ function getLine(): int {
+ return $this->line;
+ }
+
+ /** @var array */
+ protected $trace;
+
+ function getTrace(): array {
+ return $this->trace;
+ }
+
+ function getTraceAsString(): string {
+ $lines = [];
+ foreach ($this->trace as $index => $frame) {
+ $lines[] = "#$index $frame[file]($frame[line]): $frame[class]$frame[type]$frame[function]()";
+ }
+ $index++;
+ $lines[] = "#$index {main}";
+ return implode("\n", $lines);
+ }
+
+ /** @var ExceptionShadow */
+ protected $previous;
+
+ function getPrevious(): ?ExceptionShadow {
+ return $this->previous;
+ }
+}
diff --git a/php/src/ExitError.php b/php/src/ExitError.php
new file mode 100644
index 0000000..a14c3a8
--- /dev/null
+++ b/php/src/ExitError.php
@@ -0,0 +1,31 @@
+userMessage = $userMessage;
+ }
+
+ function isError(): bool {
+ return $this->getCode() !== 0;
+ }
+
+ /** @var ?string */
+ protected $userMessage;
+
+ function haveUserMessage(): bool {
+ return $this->userMessage !== null;
+ }
+
+ function getUserMessage(): ?string {
+ return $this->userMessage;
+ }
+}
diff --git a/php/src/IArrayWrapper.php b/php/src/IArrayWrapper.php
new file mode 100644
index 0000000..1509129
--- /dev/null
+++ b/php/src/IArrayWrapper.php
@@ -0,0 +1,11 @@
+getUserMessage();
+ else return null;
+ }
+
+ /** @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);
+ }
+
+ /** @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 {
+ return $this->userMessage;
+ }
+}
diff --git a/php/src/ValueException.php b/php/src/ValueException.php
new file mode 100644
index 0000000..12813d2
--- /dev/null
+++ b/php/src/ValueException.php
@@ -0,0 +1,76 @@
+";
+ } 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"));
+ }
+}
diff --git a/php/src/app/LockFile.php b/php/src/app/LockFile.php
new file mode 100644
index 0000000..d32bd2d
--- /dev/null
+++ b/php/src/app/LockFile.php
@@ -0,0 +1,89 @@
+file = new SharedFile($file);
+ $this->name = $name ?? static::NAME;
+ $this->title = $title ?? static::TITLE;
+ }
+
+ /** @var SharedFile */
+ protected $file;
+
+ /** @var ?string */
+ protected $name;
+
+ /** @var ?string */
+ protected $title;
+
+ protected function initData(): array {
+ return [
+ "name" => $this->name,
+ "title" => $this->title,
+ "locked" => false,
+ "date_lock" => null,
+ "date_release" => null,
+ ];
+ }
+
+ function read(bool $close=true): array {
+ $data = $this->file->unserialize(null, $close);
+ if (!is_array($data)) $data = $this->initData();
+ return $data;
+ }
+
+ function isLocked(?array &$data=null): bool {
+ $data = $this->read();
+ return $data["locked"];
+ }
+
+ function warnIfLocked(?array $data=null): bool {
+ if ($data === null) $data = $this->read();
+ if ($data["locked"]) {
+ msg::warning("$data[name]: possède le verrou depuis $data[date_lock] -- $data[title]");
+ return true;
+ }
+ return false;
+ }
+
+ function lock(?array &$data=null): bool {
+ $file = $this->file;
+ $data = $this->read(false);
+ if ($data["locked"]) {
+ $file->close();
+ return false;
+ } else {
+ $file->ftruncate();
+ $file->serialize(cl::merge($data, [
+ "locked" => true,
+ "date_lock" => new DateTime(),
+ "date_release" => null,
+ ]));
+ return true;
+ }
+ }
+
+ function release(?array &$data=null): void {
+ $file = $this->file;
+ $data = $this->read(false);
+ $file->ftruncate();
+ $file->serialize(cl::merge($data, [
+ "locked" => false,
+ "date_release" => new DateTime(),
+ ]));
+ }
+}
diff --git a/php/src/app/RunFile.php b/php/src/app/RunFile.php
new file mode 100644
index 0000000..d76beb9
--- /dev/null
+++ b/php/src/app/RunFile.php
@@ -0,0 +1,496 @@
+name = $name ?? static::NAME;
+ $this->file = new SharedFile($file);
+ $this->outfile = $outfile;
+ }
+
+ protected ?string $name;
+
+ protected SharedFile $file;
+
+ protected ?string $outfile;
+
+ function getOutfile(): ?string {
+ return $this->outfile;
+ }
+
+ protected static function merge(array $data, array $merge): array {
+ return cl::merge($data, [
+ "serial" => $data["serial"] + 1,
+ ], $merge);
+ }
+
+ protected function initData(): array {
+ return [
+ "name" => $this->name,
+ "pgid" => null,
+ "pid" => null,
+ "serial" => 0,
+ # lock
+ "locked" => false,
+ "date_lock" => null,
+ "date_release" => null,
+ # run
+ "logfile" => $this->outfile,
+ "date_start" => null,
+ "date_stop" => null,
+ "exitcode" => null,
+ "is_reaped" => null,
+ "is_ack_done" => null,
+ # action
+ "action" => null,
+ "action_date_start" => null,
+ "action_current_step" => null,
+ "action_max_step" => null,
+ "action_date_step" => null,
+ ];
+ }
+
+ function reset(bool $delete=false) {
+ $file = $this->file;
+ if ($delete) {
+ $file->close();
+ unlink($file->getFile());
+ } else {
+ $file->ftruncate();
+ }
+ }
+
+ function read(): array {
+ $data = $this->file->unserialize();
+ if (!is_array($data)) $data = $this->initData();
+ return $data;
+ }
+
+ protected function willWrite(): array {
+ $file = $this->file;
+ $file->lockWrite();
+ $data = $file->unserialize(null, false, true);
+ if (!is_array($data)) {
+ $data = $this->initData();
+ $file->ftruncate();
+ $file->serialize($data, false, true);
+ }
+ return [$file, $data];
+ }
+
+ protected function serialize(SharedFile $file, array $data, ?array $merge=null): void {
+ $file->ftruncate();
+ $file->serialize(self::merge($data, $merge), true, true);
+ }
+
+ protected function update(callable $func): void {
+ /** @var SharedFile$file */
+ [$file, $data] = $this->willWrite();
+ $merge = call_user_func($func, $data);
+ if ($merge !== null && $merge !== false) {
+ $this->serialize($file, $data, $merge);
+ } else {
+ $file->cancelWrite();
+ }
+ }
+
+ function haveWorked(int $serial, ?int &$currentSerial=null, ?array $data=null): bool {
+ $data ??= $this->read();
+ $currentSerial = $data["serial"];
+ return $serial !== $currentSerial;
+ }
+
+ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # verrouillage par défaut
+
+ function isLocked(?array &$data=null): bool {
+ $data = $this->read();
+ return $data["locked"];
+ }
+
+ function warnIfLocked(?array $data=null): bool {
+ $data ??= $this->read();
+ if ($data["locked"]) {
+ msg::warning("$data[name]: possède le verrou depuis $data[date_lock]");
+ return true;
+ }
+ return false;
+ }
+
+ function lock(): bool {
+ $this->update(function ($data) use (&$locked) {
+ if ($data["locked"]) {
+ $locked = false;
+ return null;
+ } else {
+ $locked = true;
+ return [
+ "locked" => true,
+ "date_lock" => new DateTime(),
+ "date_release" => null,
+ ];
+ }
+ });
+ return $locked;
+ }
+
+ function release(): void {
+ $this->update(function ($data) {
+ return [
+ "locked" => false,
+ "date_release" => new DateTime(),
+ ];
+ });
+ }
+
+ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # cycle de vie de l'application
+
+ /**
+ * Préparer le démarrage de l'application. Cette méhode est appelée par un
+ * script externe qui doit préparer le démarrage du script
+ *
+ * - démarrer un groupe de process dont le process courant est le leader
+ */
+ function wfPrepare(?int &$pgid=null): void {
+ $this->update(function (array $data) use (&$pgid) {
+ posix_setsid();
+ $pgid = posix_getpid();
+ return cl::merge($this->initData(), [
+ "pgid" => $pgid,
+ "pid" => null,
+ "date_start" => new DateTime(),
+ ]);
+ });
+ }
+
+ /** indiquer que l'application démarre. */
+ function wfStart(): void {
+ $this->update(function (array $data) {
+ $pid = posix_getpid();
+ if ($data["pgid"] !== null) {
+ A::merge($data, [
+ "pid" => $pid,
+ ]);
+ } else {
+ $data = cl::merge($this->initData(), [
+ "pid" => $pid,
+ "date_start" => new DateTime(),
+ ]);
+ }
+ return $data;
+ });
+ }
+
+ /** tester si l'application a déjà été démarrée au moins une fois */
+ function wasStarted(?array $data=null): bool {
+ $data ??= $this->read();
+ return $data["date_start"] !== null;
+ }
+
+ /** tester si l'application est démarrée et non arrêtée */
+ function isStarted(?array $data=null): bool {
+ $data ??= $this->read();
+ return $data["date_start"] !== null && $data["date_stop"] === null;
+ }
+
+ function _getCid(array $data=null): int {
+ if ($data["pgid"] !== null) return -$data["pgid"];
+ else return $data["pid"];
+ }
+
+ function _isRunning(array $data=null): bool {
+ if (!posix_kill($data["pid"], 0)) {
+ switch (posix_get_last_error()) {
+ case 1: #PCNTL_EPERM:
+ # process auquel on n'a pas accès?! est-ce un autre process qui a
+ # réutilisé le PID?
+ return false;
+ case 3: #PCNTL_ESRCH:
+ # process inexistant
+ return false;
+ case 22: #PCNTL_EINVAL:
+ # ne devrait pas se produire
+ return false;
+ }
+ }
+ # process existant auquel on a accès
+ return true;
+ }
+
+ /**
+ * vérifier si l'application marquée comme démarrée tourne réellement
+ */
+ function isRunning(?array $data=null): bool {
+ $data ??= $this->read();
+ if ($data["date_start"] === null) return false;
+ if ($data["date_stop"] !== null) return false;
+ return $this->_isRunning($data);
+ }
+
+ /** indiquer que l'application s'arrête */
+ function wfStop(): void {
+ $this->update(function (array $data) {
+ return [
+ "date_stop" => new DateTime(),
+ ];
+ });
+ }
+
+ /** tester si l'application est déjà été stoppée au moins une fois */
+ function wasStopped(?array $data=null): bool {
+ $data ??= $this->read();
+ return $data["date_stop"] !== null;
+ }
+
+ /** tester si l'application a été démarrée puis arrêtée */
+ function isStopped(?array $data=null): bool {
+ $data ??= $this->read();
+ return $data["date_start"] !== null && $data["date_stop"] !== null;
+ }
+
+ /** après l'arrêt de l'application, mettre à jour le code de retour */
+ function wfReaped(int $exitcode): void {
+ $this->update(function (array $data) use ($exitcode) {
+ return [
+ "pgid" => null,
+ "date_stop" => $data["date_stop"] ?? new DateTime(),
+ "exitcode" => $exitcode,
+ "is_reaped" => true,
+ ];
+ });
+ }
+
+ private static function kill(int $pid, int $signal, ?string &$reason=null): bool {
+ if (!posix_kill($pid, $signal)) {
+ switch (posix_get_last_error()) {
+ case PCNTL_ESRCH:
+ $reason = "process inexistant";
+ break;
+ case PCNTL_EPERM:
+ $reason = "process non accessible";
+ break;
+ case PCNTL_EINVAL:
+ $reason = "signal invalide";
+ break;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ function wfKill(?string &$reason=null): bool {
+ $data = $this->read();
+ $pid = $this->_getCid($data);
+ $stopped = false;
+ $timeout = 10;
+ $delay = 300000;
+ while (--$timeout >= 0) {
+ if (!self::kill($pid, SIGTERM, $reason)) return false;
+ usleep($delay);
+ $delay = 1000000; // attendre 1 seconde à partir de la deuxième fois
+ if (!$this->_isRunning($data)) {
+ $stopped = true;
+ break;
+ }
+ }
+ if (!$stopped) {
+ $timeout = 3;
+ $delay = 300000;
+ while (--$timeout >= 0) {
+ if (!self::kill($pid, SIGKILL, $reason)) return false;
+ usleep($delay);
+ $delay = 1000000; // attendre 1 seconde à partir de la deuxième fois
+ if (!$this->_isRunning($data)) {
+ $stopped = true;
+ break;
+ }
+ }
+ }
+ if ($stopped) {
+ sh::_waitpid($pid, $exitcode);
+ $this->wfReaped($exitcode);
+ }
+ return $stopped;
+ }
+
+ /**
+ * vérifier si on est dans le cas où la tâche devrait tourner mais en réalité
+ * ce n'est pas le cas
+ */
+ function _isUndead(?int $pid=null): bool {
+ $data = $this->read();
+ if ($data["date_start"] === null) return false;
+ if ($data["date_stop"] !== null) return false;
+ $pid ??= $data["pid"];
+ if (!posix_kill($pid, 0)) {
+ switch (posix_get_last_error()) {
+ case 1: #PCNTL_EPERM:
+ # process auquel on n'a pas accès?! est-ce un autre process qui a
+ # réutilisé le PID?
+ return false;
+ case 3: #PCNTL_ESRCH:
+ # process inexistant
+ return true;
+ case 22: #PCNTL_EINVAL:
+ # ne devrait pas se produire
+ return false;
+ }
+ }
+ # process existant auquel on a accès
+ return false;
+ }
+
+ /**
+ * comme {@link self::isStopped()} mais ne renvoie true qu'une seule fois si
+ * $updateDone==true
+ */
+ function isDone(?array &$data=null, bool $updateDone=true): bool {
+ $done = false;
+ $this->update(function (array $ldata) use (&$done, &$data, $updateDone) {
+ $data = $ldata;
+ if ($data["date_start"] === null || $data["date_stop"] === null || $data["is_ack_done"]) {
+ return false;
+ }
+ $done = true;
+ if ($updateDone) return ["is_ack_done" => $done];
+ else return null;
+ });
+ return $done;
+ }
+
+ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # gestion des actions
+
+ /** indiquer le début d'une action */
+ function action(?string $title, ?int $maxSteps=null): void {
+ $this->update(function (array $data) use ($title, $maxSteps) {
+ return [
+ "action" => $title,
+ "action_date_start" => new DateTime(),
+ "action_max_step" => $maxSteps,
+ "action_current_step" => 0,
+ ];
+ });
+ app::_dispatch_signals();
+ }
+
+ /** indiquer qu'une étape est franchie dans l'action en cours */
+ function step(int $nbSteps=1): void {
+ $this->update(function (array $data) use ($nbSteps) {
+ return [
+ "action_date_step" => new DateTime(),
+ "action_current_step" => $data["action_current_step"] + $nbSteps,
+ ];
+ });
+ app::_dispatch_signals();
+ }
+
+ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ # Divers
+
+ function getLockFile(?string $name=null, ?string $title=null): LockFile {
+ $ext = self::LOCK_EXT;
+ if ($name !== null) $ext = ".$name$ext";
+ $file = path::ensure_ext($this->file->getFile(), $ext, self::RUN_EXT);
+ $name = str::join("/", [$this->name, $name]);
+ return new LockFile($file, $name, $title);
+ }
+
+ function getDesc(?array $data=null, bool $withAction=true): ?array {
+ $data ??= $this->read();
+ $desc = $data["name"];
+ $dateStart = $data["date_start"];
+ $action = $withAction? $data["action"]: null;
+ $dateStop = $data["date_stop"];
+ $exitcode = $data["exitcode"];
+ if ($action !== null) {
+ $date ??= $data["action_date_step"];
+ $date ??= $data["action_date_start"];
+ if ($date !== null) $action = "$date $action";
+ $action = "Etape en cours: $action";
+ $current = $data["action_current_step"];
+ $max = $data["action_max_step"];
+ if ($current !== null && $max !== null) {
+ $action .= " ($current / $max)";
+ } elseif ($current !== null) {
+ $action .= " ($current)";
+ }
+ }
+ if ($exitcode !== null) {
+ $result = ["Code de retour $exitcode"];
+ if ($data["is_reaped"]) $result[] = "reaped";
+ if ($data["is_ack_done"]) $result[] = "acknowledged";
+ $result = join(", ", $result);
+ } else {
+ $result = null;
+ }
+ if (!$this->wasStarted($data)) {
+ $type = "neutral";
+ $haveLog = false;
+ $exitcode = null;
+ $message = [
+ "status" => "$desc: pas encore démarré",
+ ];
+ } elseif ($this->isRunning($data)) {
+ $sinceStart = Elapsed::format_since($dateStart);
+ $type = "info";
+ $haveLog = true;
+ $exitcode = null;
+ $message = [
+ "status" => "$desc: EN COURS pid $data[pid]",
+ "started" => "Démarrée depuis $dateStart ($sinceStart)",
+ "action" => $action,
+ ];
+ } elseif ($this->isStopped($data)) {
+ $duration = "\nDurée ".Elapsed::format_delay($dateStart, $dateStop);
+ $sinceStop = Elapsed::format_since($dateStop);
+ $haveLog = true;
+ if ($exitcode === null) $type = "warning";
+ elseif ($exitcode === 0) $type = "success";
+ else $type = "danger";
+ $message = [
+ "status" => "$desc: TERMINEE$duration",
+ "stopped" => "Arrêtée $sinceStop le $dateStop",
+ "result" => $result,
+ ];
+ } else {
+ $type = "warning";
+ $haveLog = true;
+ $exitcode = null;
+ $message = [
+ "status" => "$desc: ETAT INCONNU",
+ "started" => "Commencée le $dateStart",
+ "stopped" => $dateStop? "Arrêtée le $dateStop": null,
+ "exitcode" => $result !== null? "Code de retour $result": null,
+ ];
+ }
+ return [
+ "type" => $type,
+ "have_log" => $haveLog,
+ "exitcode" => $exitcode,
+ "message" => array_filter($message),
+ ];
+ }
+}
diff --git a/php/src/app/args.php b/php/src/app/args.php
new file mode 100644
index 0000000..90e24c7
--- /dev/null
+++ b/php/src/app/args.php
@@ -0,0 +1,39 @@
+ $value] devient ["--my-arg", "$value"]
+ * - ["myOpt" => true] devient ["--my-opt"]
+ * - ["myOpt" => false] est omis
+ * - les autres valeurs sont prises telles quelles
+ */
+ static function from_array(?array $array): array {
+ $args = [];
+ if ($array === null) return $args;
+ $index = 0;
+ foreach ($array as $arg => $value) {
+ if ($value === false) continue;
+ if ($arg === $index) {
+ $index++;
+ } else {
+ $arg = str::us2camel($arg);
+ $arg = str::camel2us($arg, false, "-");
+ $arg = str_replace("_", "-", $arg);
+ $args[] = "--$arg";
+ if (is_array($value)) $value[] = "--";
+ elseif ($value === true) $value = null;
+ }
+ if (is_array($value)) {
+ A::merge($args, array_map("strval", $value));
+ } elseif ($value !== null) {
+ $args[] = "$value";
+ }
+ }
+ return $args;
+ }
+}
diff --git a/php/src/app/cli/include-launcher.php b/php/src/app/cli/include-launcher.php
new file mode 100644
index 0000000..0958cb7
--- /dev/null
+++ b/php/src/app/cli/include-launcher.php
@@ -0,0 +1,29 @@
+ $name,
+ ]);
+ require $app;
+}
diff --git a/php/src/cl.php b/php/src/cl.php
new file mode 100644
index 0000000..8bb3b37
--- /dev/null
+++ b/php/src/cl.php
@@ -0,0 +1,769 @@
+ $value) {
+ if (is_int($key)) $array[] = $value;
+ else $array[$key] = $value;
+ }
+ return $array;
+ }
+
+ /**
+ * construire un tableau avec le résultat de $row[$key] pour chaque élément
+ * de $rows
+ */
+ static function all_get($key, ?iterable $rows): array {
+ $array = [];
+ if ($rows !== null) {
+ foreach ($rows as $row) {
+ $array[] = self::get($row, $key);
+ }
+ }
+ return $array;
+ }
+
+ /**
+ * retourner la première valeur de $array ou $default si le tableau est null
+ * ou vide
+ */
+ static final function first(?iterable $iterable, $default=null) {
+ if (is_array($iterable)) {
+ $key = array_key_first($iterable);
+ if ($key === null) return $default;
+ return $iterable[$key];
+ }
+ if (is_iterable($iterable)) {
+ foreach ($iterable as $value) {
+ return $value;
+ }
+ }
+ return $default;
+ }
+
+ /**
+ * retourner la dernière valeur de $array ou $default si le tableau est null
+ * ou vide
+ */
+ static final function last(?iterable $iterable, $default=null) {
+ if (is_array($iterable)) {
+ $key = array_key_last($iterable);
+ if ($key === null) return $default;
+ return $iterable[$key];
+ }
+ $value = $default;
+ if (is_iterable($iterable)) {
+ foreach ($iterable as $value) {
+ # parcourir tout l'iterateur pour avoir le dernier élément
+ }
+ }
+ return $value;
+ }
+
+ /** retourner un array non null à partir de $array */
+ static final function with($array): array {
+ if ($array instanceof IArrayWrapper) $array = $array->wrappedArray();
+ if (is_array($array)) return $array;
+ elseif ($array === null || $array === false) return [];
+ elseif ($array instanceof Traversable) return self::all($array);
+ else return [$array];
+ }
+
+ /** retourner un array à partir de $array, ou null */
+ static final function withn($array): ?array {
+ if ($array instanceof IArrayWrapper) $array = $array->wrappedArray();
+ if (is_array($array)) return $array;
+ elseif ($array === null || $array === false) return null;
+ elseif ($array instanceof Traversable) return self::all($array);
+ else return [$array];
+ }
+
+ /** tester si $array a au moins une clé numérique */
+ static final function have_num_keys(?array $array): bool {
+ if ($array === null) return false;
+ foreach ($array as $key => $value) {
+ if (is_int($key)) return true;
+ }
+ return false;
+ }
+
+ /**
+ * tester si $array est une liste, c'est à dire un tableau non null avec
+ * uniquement des clés numériques séquentielles commençant à zéro
+ *
+ * NB: is_list(null) === false
+ * et is_list([]) === true
+ */
+ static final function is_list(?array $array): bool {
+ if ($array === null) return false;
+ $index = -1;
+ foreach ($array as $key => $value) {
+ ++$index;
+ if ($key !== $index) return false;
+ }
+ return true;
+ }
+
+ /**
+ * tester si $array contient la clé $key
+ *
+ * @param array|ArrayAccess $array
+ */
+ static final function has($array, $key): bool {
+ if (is_array($array)) {
+ return array_key_exists($key, $array);
+ } elseif ($array instanceof ArrayAccess) {
+ return $array->offsetExists($key);
+ }
+ return false;
+ }
+
+ /**
+ * retourner $array[$key] ou $default si la clé n'existe pas
+ *
+ * @param array|ArrayAccess $array
+ */
+ static final function get($array, $key, $default=null) {
+ if (is_array($array)) {
+ if (array_key_exists($key, $array)) return $array[$key];
+ } elseif ($array instanceof ArrayAccess) {
+ if ($array->offsetExists($key)) return $array->offsetGet($key);
+ }
+ return $default;
+ }
+
+ /**
+ * retourner un tableau construit à partir des clés de $keys
+ * - [$to => $from] --> $dest[$to] = self::get($array, $from)
+ * - [$to => null] --> $dest[$to] = null
+ * - [$to => false] --> NOP
+ * - [$to] --> $dest[$to] = self::get($array, $to)
+ * - [null] --> $dest[] = null
+ * - [false] --> NOP
+ *
+ * Si $inverse===true, le mapping est inversé:
+ * - [$to => $from] --> $dest[$from] = self::get($array, $to)
+ * - [$to => null] --> $dest[$to] = self::get($array, $to)
+ * - [$to => false] --> NOP
+ * - [$to] --> $dest[$to] = self::get($array, $to)
+ * - [null] --> NOP (XXX que faire dans ce cas?)
+ * - [false] --> NOP
+ *
+ * notez que l'ordre est inversé par rapport à {@link self::rekey()} qui
+ * attend des mappings [$from => $to], alors que cette méthode attend des
+ * mappings [$to => $from]
+ */
+ static final function select($array, ?array $mappings, bool $inverse=false): array {
+ $dest = [];
+ $index = 0;
+ if (!$inverse) {
+ foreach ($mappings as $to => $from) {
+ if ($to === $index) {
+ $index++;
+ $to = $from;
+ if ($to === false) continue;
+ elseif ($to === null) $dest[] = null;
+ else $dest[$to] = self::get($array, $to);
+ } elseif ($from === false) {
+ continue;
+ } elseif ($from === null) {
+ $dest[$to] = null;
+ } else {
+ $dest[$to] = self::get($array, $from);
+ }
+ }
+ } else {
+ foreach ($mappings as $to => $from) {
+ if ($to === $index) {
+ $index++;
+ $to = $from;
+ if ($to === false) continue;
+ elseif ($to === null) continue;
+ else $dest[$to] = self::get($array, $to);
+ } elseif ($from === false) {
+ continue;
+ } elseif ($from === null) {
+ $dest[$to] = self::get($array, $to);
+ } else {
+ $dest[$from] = self::get($array, $to);
+ }
+ }
+ }
+ return $dest;
+ }
+
+ /**
+ * obtenir la liste des clés finalement obtenues après l'appel à
+ * {@link self::select()} avec le mapping spécifié
+ */
+ static final function selected_keys(?array $mappings): array {
+ if ($mappings === null) return [];
+ $keys = [];
+ $index = 0;
+ foreach ($mappings as $to => $from) {
+ if ($to === $index) {
+ if ($from === false) continue;
+ elseif ($from === null) $keys[] = $index;
+ else $keys[] = $from;
+ $index++;
+ } elseif ($from === false) {
+ continue;
+ } else {
+ $keys[] = $to;
+ }
+ }
+ return $keys;
+ }
+
+ /**
+ * méthode de convenance qui sélectionne certaines clés de $array avec
+ * {@link self::select()} puis merge le tableau $merge au résultat.
+ */
+ static final function selectm($array, ?array $mappings, ?array $merge=null): array {
+ return cl::merge(self::select($array, $mappings), $merge);
+ }
+
+ /**
+ * méthode de convenance qui merge $merge dans $array puis sélectionne
+ * certaines clés avec {@link self::select()}
+ */
+ static final function mselect($array, ?array $merge, ?array $mappings): array {
+ return self::select(cl::merge($array, $merge), $mappings);
+ }
+
+ /**
+ * construire un sous-ensemble du tableau $array en sélectionnant les clés de
+ * $includes qui ne sont pas mentionnées dans $excludes.
+ *
+ * - si $includes===null && $excludes===null, retourner le tableau inchangé
+ * - si $includes vaut null, prendre toutes les clés
+ *
+ */
+ static final function xselect($array, ?array $includes, ?array $excludes=null): ?array {
+ if ($array === null) return null;
+ $array = self::withn($array);
+ if ($includes === null && $excludes === null) return $array;
+ if ($includes === null) $includes = array_keys($array);
+ if ($excludes === null) $excludes = [];
+ $result = [];
+ foreach ($array as $key => $value) {
+ if (!in_array($key, $includes)) continue;
+ if (in_array($key, $excludes)) continue;
+ $result[$key] = $value;
+ }
+ return $result;
+ }
+
+ /**
+ * si $array est un array ou une instance de ArrayAccess, créer ou modifier
+ * l'élément dont la clé est $key
+ *
+ * @param array|ArrayAccess $array
+ */
+ static final function set(&$array, $key, $value): void {
+ if (is_array($array) || $array === null) {
+ if ($key === null) $array[] = $value;
+ else $array[$key] = $value;
+ } elseif ($array instanceof ArrayAccess) {
+ $array->offsetSet($key, $value);
+ }
+ }
+
+ /**
+ * si $array est un array ou une instance de ArrayAccess, supprimer l'élément
+ * dont la clé est $key
+ *
+ * @param array|ArrayAccess $array
+ */
+ static final function del(&$array, $key): void {
+ if (is_array($array)) {
+ unset($array[$key]);
+ } elseif ($array instanceof ArrayAccess) {
+ $array->offsetUnset($key);
+ }
+ }
+
+ /** retourner le nombre d'éléments de $array */
+ static final function count(?array $array): int {
+ return $array !== null? count($array): 0;
+ }
+
+ /** retourner la liste des clés de $array */
+ static final function keys(?array $array): array {
+ return $array !== null? array_keys($array): [];
+ }
+
+ #############################################################################
+
+ /**
+ * Fusionner tous les tableaux spécifiés. Les valeurs null sont ignorées.
+ * IMPORTANT: les clés numériques sont réordonnées.
+ * Retourner null si aucun tableau n'est fourni ou s'ils étaient tous null.
+ */
+ static final function merge(...$arrays): ?array {
+ $merges = [];
+ foreach ($arrays as $array) {
+ A::ensure_narray($array);
+ if ($array !== null) $merges[] = $array;
+ }
+ return $merges? array_merge(...$merges): null;
+ }
+
+ /**
+ * Fusionner tous les tableaux spécifiés. Les valeurs null sont ignorées.
+ * IMPORTANT: les clés numériques NE SONT PAS réordonnées.
+ * Retourner null si aucun tableau n'est fourni ou s'ils étaient tous null.
+ */
+ static final function merge2(...$arrays): ?array {
+ $merged = null;
+ foreach ($arrays as $array) {
+ $array = self::withn($array);
+ if ($array === null) continue;
+ $merged ??= [];
+ foreach ($array as $key => $value) {
+ $merged[$key] = $value;
+ }
+ }
+ return $merged;
+ }
+
+ #############################################################################
+
+ static final function map(callable $callback, ?iterable $array): array {
+ $result = [];
+ if ($array !== null) {
+ $ctx = nur_func::_prepare($callback);
+ foreach ($array as $key => $value) {
+ $result[$key] = nur_func::_call($ctx, [$value, $key]);
+ }
+ }
+ return $result;
+ }
+
+ #############################################################################
+
+ /**
+ * vérifier que le chemin $keys existe dans le tableau $array
+ *
+ * si $pkey est vide ou null, retourner true
+ */
+ static final function phas($array, $pkey): bool {
+ # optimisations
+ if ($pkey === null || $pkey === []) {
+ return true;
+ } elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) {
+ return self::has($array, $pkey);
+ } elseif (!is_array($pkey)) {
+ $pkey = explode(".", strval($pkey));
+ }
+ # phas
+ $first = true;
+ foreach($pkey as $key) {
+ if ($key === "" && $first) {
+ # une chaine vide en première position est ignorée
+ continue;
+ } elseif (is_array($array)) {
+ if (!array_key_exists($key, $array)) return false;
+ $array = $array[$key];
+ } elseif ($array instanceof ArrayAccess) {
+ if (!$array->offsetExists($key)) return false;
+ $array = $array->offsetGet($key);
+ } else {
+ return false;
+ }
+ $first = false;
+ }
+ return true;
+ }
+
+ static final function each_phas($array, ?array $pkeys): array {
+ $result = [];
+ if ($pkeys !== null) {
+ foreach ($pkeys as $pkey) {
+ $result[] = self::phas($array, $pkey);
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * obtenir la valeur correspondant au chemin $keys dans $array
+ *
+ * si $pkey est vide ou null, retourner $default
+ */
+ static final function pget($array, $pkey, $default=null) {
+ # optimisations
+ if ($pkey === null || $pkey === []) return $default;
+ elseif ($pkey === "") return $array;
+ elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) {
+ return self::get($array, $pkey, $default);
+ } elseif (!is_array($pkey)) {
+ $pkey = explode(".", strval($pkey));
+ }
+ # pget
+ $value = $array;
+ $first = true;
+ foreach($pkey as $key) {
+ if ($key === "" && $first) {
+ # une chaine vide en première position est ignorée
+ continue;
+ } elseif (is_array($value)) {
+ if (!array_key_exists($key, $value)) return $default;
+ $value = $value[$key];
+ } elseif ($value instanceof ArrayAccess) {
+ if (!$value->offsetExists($key)) return $default;
+ $value = $value->offsetGet($key);
+ } else {
+ return $default;
+ }
+ $first = false;
+ }
+ return $value;
+ }
+
+ static final function each_pget($array, ?array $pkeys): array {
+ $result = [];
+ if ($pkeys !== null) {
+ foreach ($pkeys as $key => $pkey) {
+ $result[$key] = self::pget($array, $pkey);
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * retourner un tableau construit à partir des chemins de clé de $pkeys
+ * ces chemins peuvent être exprimés de plusieurs façon:
+ * - [$key => $pkey] --> $dest[$key] = self::pget($array, $pkey)
+ * - [$key => null] --> $dest[$key] = null
+ * - [$pkey] --> $dest[$key] = self::pget($array, $pkey)
+ * avec $key = implode("__", $pkey))
+ * - [null] --> $dest[] = null
+ * - [false] --> NOP
+ */
+ static final function pselect($array, ?array $pkeys): array {
+ $dest = [];
+ $index = 0;
+ foreach ($pkeys as $key => $pkey) {
+ if ($key === $index) {
+ $index++;
+ if ($pkey === null) continue;
+ $value = self::pget($array, $pkey);
+ if (!is_array($pkey)) $pkey = explode(".", strval($pkey));
+ $key = implode("__", $pkey);
+ } elseif ($pkey === null) {
+ $value = null;
+ } else {
+ $value = self::pget($array, $pkey);
+ }
+ $dest[$key] = $value;
+ }
+ return $dest;
+ }
+
+ /**
+ * méthode de convenance qui sélectionne certaines clés de $array avec
+ * {@link self::pselect()} puis merge le tableau $merge au résultat.
+ */
+ static final function pselectm($array, ?array $pkeys, ?array $merge=null): array {
+ return cl::merge(self::pselect($array, $pkeys), $merge);
+ }
+
+ /**
+ * méthode de convenance qui merge $merge dans $array puis sélectionne
+ * certaines clés avec {@link self::pselect()}
+ */
+ static final function mpselect($array, ?array $merge, ?array $mappings): array {
+ return self::pselect(cl::merge($array, $merge), $mappings);
+ }
+
+ /**
+ * modifier la valeur au chemin de clé $keys dans le tableau $array
+ *
+ * utiliser la clé "" (chaine vide) en dernière position pour rajouter à la fin, e.g
+ * - pset($array, [""], $value) est équivalent à $array[] = $value
+ * - pset($array, ["a", "b", ""], $value) est équivalent à $array["a"]["b"][] = $value
+ * la clé "" n'a pas de propriété particulière quand elle n'est pas en dernière position
+ *
+ * si $pkey est vide ou null, $array est remplacé par $value
+ */
+ static final function pset(&$array, $pkey, $value): void {
+ # optimisations
+ if ($pkey === null || $pkey === []) {
+ $array = $value;
+ return;
+ } elseif ($pkey === "") {
+ $array[] = $value;
+ return;
+ } elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) {
+ self::set($array, $pkey, $value);
+ return;
+ } elseif (!is_array($pkey)) {
+ $pkey = explode(".", strval($pkey));
+ }
+ # pset
+ A::ensure_array($array);
+ $current =& $array;
+ $key = null;
+ $last = count($pkey) - 1;
+ $i = 0;
+ foreach ($pkey as $key) {
+ if ($i == $last) break;
+ if ($current instanceof ArrayAccess) {
+ if (!$current->offsetExists($key)) $current->offsetSet($key, []);
+ $current =& $current->offsetGet($key);
+ if ($current === null) {
+ $current = [];
+ } elseif (!is_array($current) && !($current instanceof ArrayAccess)) {
+ $current = [$current];
+ }
+ } else {
+ A::ensure_array($current[$key]);
+ $current =& $current[$key];
+ }
+ $i++;
+ }
+ if ($key === "") $current[] = $value;
+ else $current[$key] = $value;
+ }
+
+ static final function each_pset(&$array, ?array $values): void {
+ if ($values !== null) {
+ foreach ($values as $pkey => $value) {
+ self::pset($array, $pkey, $value);
+ }
+ }
+ }
+
+ /**
+ * supprimer la valeur au chemin de clé $keys dans $array
+ *
+ * si $array vaut null ou false, sa valeur est inchangée.
+ * si $pkey est vide ou null, $array devient null
+ */
+ static final function pdel(&$array, $pkey): void {
+ # optimisations
+ if ($array === null || $array === false) {
+ return;
+ } elseif ($pkey === null || $pkey === []) {
+ $array = null;
+ return;
+ } elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) {
+ self::del($array, $pkey);
+ return;
+ } elseif (!is_array($pkey)) {
+ $pkey = explode(".", strval($pkey));
+ }
+ # pdel
+ A::ensure_array($array);
+ $current =& $array;
+ $key = null;
+ $last = count($pkey) - 1;
+ $i = 0;
+ foreach ($pkey as $key) {
+ if ($i == $last) break;
+ if ($current instanceof ArrayAccess) {
+ if (!$current->offsetExists($key)) break;
+ } elseif (is_array($current)) {
+ if (!array_key_exists($key, $current)) break;
+ } else {
+ break;
+ }
+ $current =& $current[$key];
+ $i++;
+ }
+ if ($i == $last) {
+ if ($current instanceof ArrayAccess) {
+ $current->offsetUnset($key);
+ } elseif (is_array($current)) {
+ unset($current[$key]);
+ }
+ }
+ }
+
+ static final function each_pdel(&$array, ?array $pkeys): void {
+ if ($pkeys !== null) {
+ foreach ($pkeys as $pkey) {
+ self::pdel($array, $pkey);
+ }
+ }
+ }
+
+ #############################################################################
+
+ /**
+ * retourner le tableau $array en "renommant" les clés selon le tableau
+ * $mappings qui contient des associations de la forme [$from => $to]
+ *
+ * Si $inverse===true, renommer dans le sens $to => $from
+ */
+ static function rekey(?array $array, ?array $mappings, bool $inverse=false): ?array {
+ if ($array === null || $mappings === null) return $array;
+ if ($inverse) $mappings = array_flip($mappings);
+ $mapped = [];
+ foreach ($array as $key => $value) {
+ if (array_key_exists($key, $mappings)) $key = $mappings[$key];
+ $mapped[$key] = $value;
+ }
+ return $mapped;
+ }
+
+ /**
+ * indiquer si {@link self::rekey()} modifierai le tableau indiqué (s'il y a
+ * des modifications à faire)
+ */
+ static function would_rekey(?array $array, ?array $mappings, bool $inverse=false): bool {
+ if ($array === null || $mappings === null) return false;
+ if ($inverse) $mappings = array_flip($mappings);
+ foreach ($array as $key => $value) {
+ if (array_key_exists($key, $mappings)) return true;
+ }
+ return false;
+ }
+
+ #############################################################################
+
+ /** tester si tous les éléments du tableau satisfont la condition */
+ static final function all_if(?array $array, callable $cond): bool {
+ if ($array !== null) {
+ foreach ($array as $value) {
+ if (!$cond($value)) return false;
+ }
+ }
+ return true;
+ }
+
+ static final function all_z(?array $array): bool { return self::all_if($array, [cv::class, "z"]);}
+ static final function all_nz(?array $array): bool { return self::all_if($array, [cv::class, "nz"]);}
+ static final function all_n(?array $array): bool { return self::all_if($array, [cv::class, "n"]);}
+ static final function all_nn(?array $array): bool { return self::all_if($array, [cv::class, "nn"]);}
+ static final function all_t(?array $array): bool { return self::all_if($array, [cv::class, "t"]);}
+ static final function all_f(?array $array): bool { return self::all_if($array, [cv::class, "f"]);}
+ static final function all_pt(?array $array): bool { return self::all_if($array, [cv::class, "pt"]);}
+ static final function all_pf(?array $array): bool { return self::all_if($array, [cv::class, "pf"]);}
+ static final function all_equals(?array $array, $value): bool { return self::all_if($array, cv::equals($value)); }
+ static final function all_not_equals(?array $array, $value): bool { return self::all_if($array, cv::not_equals($value)); }
+ static final function all_same(?array $array, $value): bool { return self::all_if($array, cv::same($value)); }
+ static final function all_not_same(?array $array, $value): bool { return self::all_if($array, cv::not_same($value)); }
+
+ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ /** tester si au moins un élément du tableau satisfait la condition */
+ static final function any_if(?array $array, callable $cond): bool {
+ if ($array !== null) {
+ foreach ($array as $value) {
+ if ($cond($value)) return true;
+ }
+ }
+ return false;
+ }
+
+ static final function any_z(?array $array): bool { return self::any_if($array, [cv::class, "z"]);}
+ static final function any_nz(?array $array): bool { return self::any_if($array, [cv::class, "nz"]);}
+ static final function any_n(?array $array): bool { return self::any_if($array, [cv::class, "n"]);}
+ static final function any_nn(?array $array): bool { return self::any_if($array, [cv::class, "nn"]);}
+ static final function any_t(?array $array): bool { return self::any_if($array, [cv::class, "t"]);}
+ static final function any_f(?array $array): bool { return self::any_if($array, [cv::class, "f"]);}
+ static final function any_pt(?array $array): bool { return self::any_if($array, [cv::class, "pt"]);}
+ static final function any_pf(?array $array): bool { return self::any_if($array, [cv::class, "pf"]);}
+ static final function any_equals(?array $array, $value): bool { return self::any_if($array, cv::equals($value)); }
+ static final function any_not_equals(?array $array, $value): bool { return self::any_if($array, cv::not_equals($value)); }
+ static final function any_same(?array $array, $value): bool { return self::any_if($array, cv::same($value)); }
+ static final function any_not_same(?array $array, $value): bool { return self::any_if($array, cv::not_same($value)); }
+
+ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ static final function filter_if(?array $array, callable $cond): ?array {
+ if ($array === null) return null;
+ $filtered = [];
+ $index = 0;
+ foreach ($array as $key => $value) {
+ if (!$cond($value)) {
+ if ($key === $index) {
+ $index++;
+ $filtered[] = $value;
+ } else {
+ $filtered[$key] = $value;
+ }
+ } elseif ($key === $index) {
+ $index++;
+ }
+ }
+ return $filtered;
+ }
+
+ static final function filter_z(?array $array): ?array { return self::filter_if($array, [cv::class, "z"]);}
+ static final function filter_nz(?array $array): ?array { return self::filter_if($array, [cv::class, "nz"]);}
+ static final function filter_n(?array $array): ?array { return self::filter_if($array, [cv::class, "n"]);}
+ static final function filter_nn(?array $array): ?array { return self::filter_if($array, [cv::class, "nn"]);}
+ static final function filter_t(?array $array): ?array { return self::filter_if($array, [cv::class, "t"]);}
+ static final function filter_f(?array $array): ?array { return self::filter_if($array, [cv::class, "f"]);}
+ static final function filter_pt(?array $array): ?array { return self::filter_if($array, [cv::class, "pt"]);}
+ static final function filter_pf(?array $array): ?array { return self::filter_if($array, [cv::class, "pf"]);}
+ static final function filter_equals(?array $array, $value): ?array { return self::filter_if($array, cv::equals($value)); }
+ static final function filter_not_equals(?array $array, $value): ?array { return self::filter_if($array, cv::not_equals($value)); }
+ static final function filter_same(?array $array, $value): ?array { return self::filter_if($array, cv::same($value)); }
+ static final function filter_not_same(?array $array, $value): ?array { return self::filter_if($array, cv::not_same($value)); }
+
+ #############################################################################
+
+ static final function sorted(?array $array, int $flags=SORT_REGULAR, bool $assoc=false): ?array {
+ A::sort($array, $flags, $assoc);
+ return $array;
+ }
+
+ static final function ksorted(?array $array, int $flags=SORT_REGULAR): ?array {
+ A::ksort($array, $flags);
+ return $array;
+ }
+
+ /**
+ * retourner une fonction permettant de trier un tableau sur les clés
+ * spécifiées.
+ *
+ * - les clés ayant le préfixe '+' ou le suffixe '|asc' indiquent un tri
+ * ascendant
+ * - les clés ayant le préfixe '-' ou le suffixe '|desc' indiquent un tri
+ * descendant
+ * - sinon, par défaut, le tri est ascendant
+ */
+ static final function compare(array $keys): callable {
+ return function ($a, $b) use ($keys) {
+ foreach ($keys as $key) {
+ if (str::del_prefix($key, "+")) $w = 1;
+ elseif (str::del_prefix($key, "-")) $w = -1;
+ elseif (str::del_suffix($key, "|asc")) $w = 1;
+ elseif (str::del_suffix($key, "|desc")) $w = -1;
+ else $w = 1;
+ if ($c = $w * cv::compare(cl::get($a, $key), cl::get($b, $key))) {
+ return $c;
+ }
+ }
+ return 0;
+ };
+ }
+
+ static final function usorted(?array $array, array $keys, bool $assoc=false): ?array {
+ A::usort($array, $keys, $assoc);
+ return $array;
+ }
+}
diff --git a/php/src/cv.php b/php/src/cv.php
new file mode 100644
index 0000000..3bd8773
--- /dev/null
+++ b/php/src/cv.php
@@ -0,0 +1,234 @@
+ $b) return 1;
+ return 0;
+ }
+
+ /** comparer la longueur de $a et $b */
+ static final function complen($a, $b): int {
+ if (is_array($a)) $la = count($a);
+ else $la = strlen(strval($a));
+ if (is_array($b)) $lb = count($b);
+ else $lb = strlen(strval($b));
+ return self::compare($la, $lb);
+ }
+}
diff --git a/php/src/db/Capacitor.php b/php/src/db/Capacitor.php
new file mode 100644
index 0000000..90c3c9b
--- /dev/null
+++ b/php/src/db/Capacitor.php
@@ -0,0 +1,173 @@
+storage = $storage;
+ $this->channel = $channel;
+ $this->channel->setCapacitor($this);
+ if ($ensureExists) $this->ensureExists();
+ }
+
+ /** @var CapacitorStorage */
+ protected $storage;
+
+ function getStorage(): CapacitorStorage {
+ return $this->storage;
+ }
+
+ function db(): IDatabase {
+ return $this->getStorage()->db();
+ }
+
+ /** @var CapacitorChannel */
+ protected $channel;
+
+ function getChannel(): CapacitorChannel {
+ return $this->channel;
+ }
+
+ function getTableName(): string {
+ return $this->getChannel()->getTableName();
+ }
+
+ /** @var CapacitorChannel[] */
+ protected ?array $subChannels = null;
+
+ protected ?array $subManageTransactions = null;
+
+ function willUpdate(...$channels): self {
+ if ($this->subChannels === null) {
+ # désactiver la gestion des transaction sur le channel local aussi
+ $this->subChannels[] = $this->channel;
+ }
+ if ($channels) {
+ foreach ($channels as $channel) {
+ if ($channel instanceof Capacitor) $channel = $channel->getChannel();
+ if ($channel instanceof CapacitorChannel) {
+ $this->subChannels[] = $channel;
+ } else {
+ throw ValueException::invalid_type($channel, CapacitorChannel::class);
+ }
+ }
+ }
+ return $this;
+ }
+
+ function inTransaction(): bool {
+ return $this->db()->inTransaction();
+ }
+
+ function beginTransaction(?callable $func=null, bool $commit=true): void {
+ $db = $this->db();
+ if ($this->subChannels !== null) {
+ # on gère des subchannels: ne débuter la transaction que si ce n'est déjà fait
+ if ($this->subManageTransactions === null) {
+ foreach ($this->subChannels as $channel) {
+ $name = $channel->getName();
+ $this->subManageTransactions ??= [];
+ if (!array_key_exists($name, $this->subManageTransactions)) {
+ $this->subManageTransactions[$name] = $channel->isManageTransactions();
+ }
+ $channel->setManageTransactions(false);
+ }
+ if (!$db->inTransaction()) $db->beginTransaction();
+ }
+ } elseif (!$db->inTransaction()) {
+ $db->beginTransaction();
+ }
+ if ($func !== null) {
+ $commited = false;
+ try {
+ nur_func::call($func, $this);
+ if ($commit) {
+ $this->commit();
+ $commited = true;
+ }
+ } finally {
+ if ($commit && !$commited) $this->rollback();
+ }
+ }
+ }
+
+ protected function beforeEndTransaction(): void {
+ if ($this->subManageTransactions !== null) {
+ foreach ($this->subChannels as $channel) {
+ $name = $channel->getName();
+ $channel->setManageTransactions($this->subManageTransactions[$name]);
+ }
+ $this->subManageTransactions = null;
+ }
+ }
+
+ function commit(): void {
+ $this->beforeEndTransaction();
+ $db = $this->db();
+ if ($db->inTransaction()) $db->commit();
+ }
+
+ function rollback(): void {
+ $this->beforeEndTransaction();
+ $db = $this->db();
+ if ($db->inTransaction()) $db->rollback();
+ }
+
+ function getCreateSql(): string {
+ return $this->storage->_getCreateSql($this->channel);
+ }
+
+ function exists(): bool {
+ return $this->storage->_exists($this->channel);
+ }
+
+ function ensureExists(): void {
+ $this->storage->_ensureExists($this->channel);
+ }
+
+ function reset(bool $recreate=false): void {
+ $this->storage->_reset($this->channel, $recreate);
+ }
+
+ function charge($item, $func=null, ?array $args=null, ?array &$values=null): int {
+ if ($this->subChannels !== null) $this->beginTransaction();
+ return $this->storage->_charge($this->channel, $item, $func, $args, $values);
+ }
+
+ function discharge(bool $reset=true): Traversable {
+ return $this->storage->_discharge($this->channel, $reset);
+ }
+
+ function count($filter=null): int {
+ return $this->storage->_count($this->channel, $filter);
+ }
+
+ function one($filter, ?array $mergeQuery=null): ?array {
+ return $this->storage->_one($this->channel, $filter, $mergeQuery);
+ }
+
+ function all($filter, ?array $mergeQuery=null): Traversable {
+ return $this->storage->_all($this->channel, $filter, $mergeQuery);
+ }
+
+ function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
+ if ($this->subChannels !== null) $this->beginTransaction();
+ return $this->storage->_each($this->channel, $filter, $func, $args, $mergeQuery, $nbUpdated);
+ }
+
+ function delete($filter, $func=null, ?array $args=null): int {
+ if ($this->subChannels !== null) $this->beginTransaction();
+ return $this->storage->_delete($this->channel, $filter, $func, $args);
+ }
+
+ function close(): void {
+ $this->storage->close();
+ }
+}
diff --git a/php/src/db/CapacitorChannel.php b/php/src/db/CapacitorChannel.php
new file mode 100644
index 0000000..4495074
--- /dev/null
+++ b/php/src/db/CapacitorChannel.php
@@ -0,0 +1,407 @@
+name = $name;
+ $this->tableName = $tableName;
+ $this->manageTransactions = $manageTransactions ?? static::MANAGE_TRANSACTIONS;
+ $this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
+ $this->useCache = static::USE_CACHE;
+ $this->setup = false;
+ $this->created = false;
+ $columnDefinitions = cl::withn(static::COLUMN_DEFINITIONS);
+ $primaryKeys = cl::withn(static::PRIMARY_KEYS);
+ if ($primaryKeys === null && $columnDefinitions !== null) {
+ $index = 0;
+ foreach ($columnDefinitions as $col => $def) {
+ if ($col === $index) {
+ $index++;
+ if (preg_match('/\bprimary\s+key\s+\((.+)\)/i', $def, $ms)) {
+ $primaryKeys = preg_split('/\s*,\s*/', trim($ms[1]));
+ }
+ } else {
+ if (preg_match('/\bprimary\s+key\b/i', $def)) {
+ $primaryKeys[] = $col;
+ }
+ }
+ }
+ }
+ $this->columnDefinitions = $columnDefinitions;
+ $this->primaryKeys = $primaryKeys;
+ }
+
+ protected string $name;
+
+ function getName(): string {
+ return $this->name;
+ }
+
+ protected string $tableName;
+
+ function getTableName(): string {
+ return $this->tableName;
+ }
+
+ /**
+ * @var bool indiquer si les modifications de each doivent être gérées dans
+ * une transaction. si false, l'utilisateur doit lui même gérer la
+ * transaction.
+ */
+ protected bool $manageTransactions;
+
+ function isManageTransactions(): bool {
+ return $this->manageTransactions;
+ }
+
+ function setManageTransactions(bool $manageTransactions=true): self {
+ $this->manageTransactions = $manageTransactions;
+ return $this;
+ }
+
+ /**
+ * @var ?int nombre maximum de modifications dans une transaction avant un
+ * commit automatique dans {@link Capacitor::each()}. Utiliser null pour
+ * désactiver la fonctionnalité.
+ *
+ * ce paramètre n'a d'effet que si $manageTransactions==true
+ */
+ protected ?int $eachCommitThreshold;
+
+ function getEachCommitThreshold(): ?int {
+ return $this->eachCommitThreshold;
+ }
+
+ function setEachCommitThreshold(?int $eachCommitThreshold=null): self {
+ $this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
+ return $this;
+ }
+
+ /**
+ * @var bool faut-il passer par le cache pour les requêtes de all(), each()
+ * et delete()?
+ * ça peut être nécessaire avec MySQL/MariaDB si on utilise les requêtes non
+ * bufférisées, et que la fonction manipule la base de données
+ */
+ protected bool $useCache;
+
+ function isUseCache(): bool {
+ return $this->useCache;
+ }
+
+ function setUseCache(bool $useCache=true): self {
+ $this->useCache = $useCache;
+ return $this;
+ }
+
+ /**
+ * initialiser ce channel avant sa première utilisation.
+ */
+ protected function setup(): void {
+ }
+
+ protected bool $setup;
+
+ function ensureSetup() {
+ if (!$this->setup) {
+ $this->setup();
+ $this->setup = true;
+ }
+ }
+
+ protected bool $created;
+
+ function isCreated(): bool {
+ return $this->created;
+ }
+
+ function setCreated(bool $created=true): void {
+ $this->created = $created;
+ }
+
+ protected ?array $columnDefinitions;
+
+ /**
+ * retourner un ensemble de définitions pour des colonnes supplémentaires à
+ * insérer lors du chargement d'une valeur
+ *
+ * la clé primaire "id_" a pour définition "integer primary key autoincrement".
+ * elle peut être redéfinie, et dans ce cas la valeur à utiliser doit être
+ * retournée par {@link getItemValues()}
+ *
+ * la colonne "item__" contient la valeur sérialisée de l'élément chargé. bien
+ * que ce soit possible techniquement, cette colonne n'a pas à être redéfinie
+ *
+ * les colonnes dont le nom se termine par "_" sont réservées.
+ * les colonnes dont le nom se termine par "__" sont automatiquement sérialisées
+ * lors de l'insertion dans la base de données, et automatiquement désérialisées
+ * avant d'être retournées à l'utilisateur (sans le suffixe "__")
+ */
+ function getColumnDefinitions(): ?array {
+ return $this->columnDefinitions;
+ }
+
+ protected ?array $primaryKeys;
+
+ function getPrimaryKeys(): ?array {
+ return $this->primaryKeys;
+ }
+
+ /**
+ * calculer les valeurs des colonnes supplémentaires à insérer pour le
+ * chargement de $item. pour une même valeur de $item, la valeur de retour
+ * doit toujours être la même. pour rajouter des valeurs supplémentaires qui
+ * dépendent de l'environnement, il faut plutôt les retournner dans
+ * {@link self::onCreate()} ou {@link self::onUpdate()}
+ *
+ * Cette méthode est utilisée par {@link Capacitor::charge()}. Si la clé
+ * primaire est incluse (il s'agit généralement de "id_"), la ligne
+ * correspondate est mise à jour si elle existe.
+ * Retourner la clé primaire par cette méthode est l'unique moyen de
+ * déclencher une mise à jour plutôt qu'une nouvelle création.
+ *
+ * Retourner [false] pour annuler le chargement
+ */
+ function getItemValues($item): ?array {
+ return null;
+ }
+
+ /**
+ * Avant d'utiliser un id pour rechercher dans la base de donnée, corriger sa
+ * valeur le cas échéant.
+ *
+ * Cette fonction assume que la clé primaire n'est pas multiple. Elle n'est
+ * pas utilisée si une clé primaire multiple est définie.
+ */
+ function verifixId(string &$id): void {
+ }
+
+ /**
+ * retourne true si un nouvel élément ou un élément mis à jour a été chargé.
+ * false si l'élément chargé est identique au précédent.
+ *
+ * cette méthode doit être utilisée dans {@link self::onUpdate()}
+ */
+ function wasRowModified(array $values, array $pvalues): bool {
+ return $values["item__sum_"] !== $pvalues["item__sum_"];
+ }
+
+ final function serialize($item): ?string {
+ return $item !== null? serialize($item): null;
+ }
+
+ final function unserialize(?string $serial) {
+ return $serial !== null? unserialize($serial): null;
+ }
+
+ const SERIAL_DEFINITION = "mediumtext";
+ const SUM_DEFINITION = "varchar(40)";
+
+ final function sum(?string $serial, $value=null): ?string {
+ if ($serial === null) $serial = $this->serialize($value);
+ return $serial !== null? sha1($serial): null;
+ }
+
+ final function isSerialCol(string &$key): bool {
+ return str::del_suffix($key, "__");
+ }
+
+ final function getSumCols(string $key): array {
+ return ["${key}__", "${key}__sum_"];
+ }
+
+ function getSum(string $key, $value): array {
+ $sumCols = $this->getSumCols($key);
+ $serial = $this->serialize($value);
+ $sum = $this->sum($serial, $value);
+ return array_combine($sumCols, [$serial, $sum]);
+ }
+
+ function wasSumModified(string $key, $value, array $pvalues): bool {
+ $sumCol = $this->getSumCols($key)[1];
+ $sum = $this->sum(null, $value);
+ $psum = $pvalues[$sumCol] ?? $this->sum(null, $pvalues[$key] ?? null);
+ return $sum !== $psum;
+ }
+
+ function _wasSumModified(string $key, array $row, array $prow): bool {
+ $sumCol = $this->getSumCols($key)[1];
+ $sum = $row[$sumCol] ?? null;
+ $psum = $prow[$sumCol] ?? null;
+ return $sum !== $psum;
+ }
+
+ /**
+ * méthode appelée lors du chargement avec {@link Capacitor::charge()} pour
+ * créer un nouvel élément
+ *
+ * @param mixed $item l'élément à charger
+ * @param array $values la ligne à créer, calculée à partir de $item et des
+ * valeurs retournées par {@link getItemValues()}
+ * @return ?array le cas échéant, un tableau non null à merger dans $values et
+ * utilisé pour provisionner la ligne nouvellement créée.
+ * Retourner [false] pour annuler le chargement (la ligne n'est pas créée)
+ *
+ * Si $item est modifié dans cette méthode, il est possible de le retourner
+ * avec la clé "item" pour mettre à jour la ligne correspondante.
+ *
+ * la création ou la mise à jour est uniquement décidée en fonction des
+ * valeurs calculées par {@link self::getItemValues()}. Bien que cette méthode
+ * peut techniquement retourner de nouvelles valeurs pour la clé primaire, ça
+ * risque de créer des doublons
+ */
+ function onCreate($item, array $values, ?array $alwaysNull): ?array {
+ return null;
+ }
+
+ /**
+ * méthode appelée lors du chargement avec {@link Capacitor::charge()} pour
+ * mettre à jour un élément existant
+ *
+ * @param mixed $item l'élément à charger
+ * @param array $values la nouvelle ligne, calculée à partir de $item et
+ * des valeurs retournées par {@link getItemValues()}
+ * @param array $pvalues la précédente ligne, chargée depuis la base de
+ * données
+ * @return ?array null s'il ne faut pas mettre à jour la ligne. sinon, ce
+ * tableau est mergé dans $values puis utilisé pour mettre à jour la ligne
+ * existante
+ * Retourner [false] pour annuler le chargement (la ligne n'est pas mise à
+ * jour)
+ *
+ * - Il est possible de mettre à jour $item en le retourant avec la clé "item"
+ * - La clé primaire (il s'agit généralement de "id_") ne peut pas être
+ * modifiée. si elle est retournée, elle est ignorée
+ */
+ function onUpdate($item, array $values, array $pvalues): ?array {
+ return null;
+ }
+
+ /**
+ * méthode appelée lors du parcours des éléments avec
+ * {@link Capacitor::each()}
+ *
+ * @param mixed $item l'élément courant
+ * @param ?array $values la ligne courante
+ * @return ?array le cas échéant, un tableau non null utilisé pour mettre à
+ * jour la ligne courante
+ *
+ * - Il est possible de mettre à jour $item en le retourant avec la clé "item"
+ * - La clé primaire (il s'agit généralement de "id_") ne peut pas être
+ * modifiée. si elle est retournée, elle est ignorée
+ */
+ function onEach($item, array $values): ?array {
+ return null;
+ }
+ const onEach = "->".[self::class, "onEach"][1];
+
+ /**
+ * méthode appelée lors du parcours des éléments avec
+ * {@link Capacitor::delete()}
+ *
+ * @param mixed $item l'élément courant
+ * @param ?array $values la ligne courante
+ * @return bool true s'il faut supprimer la ligne, false sinon
+ */
+ function onDelete($item, array $values): bool {
+ return true;
+ }
+ const onDelete = "->".[self::class, "onDelete"][1];
+
+ #############################################################################
+ # Méthodes déléguées pour des workflows centrés sur le channel
+
+ /**
+ * @var Capacitor|null instance de Capacitor par laquelle cette instance est
+ * utilisée
+ */
+ protected ?Capacitor $capacitor;
+
+ function getCapacitor(): ?Capacitor {
+ return $this->capacitor;
+ }
+
+ function setCapacitor(Capacitor $capacitor): self {
+ $this->capacitor = $capacitor;
+ return $this;
+ }
+
+ function charge($item, $func=null, ?array $args=null, ?array &$values=null): int {
+ return $this->capacitor->charge($item, $func, $args, $values);
+ }
+
+ function discharge(bool $reset=true): Traversable {
+ return $this->capacitor->discharge($reset);
+ }
+
+ function count($filter=null): int {
+ return $this->capacitor->count($filter);
+ }
+
+ function one($filter, ?array $mergeQuery=null): ?array {
+ return $this->capacitor->one($filter, $mergeQuery);
+ }
+
+ function all($filter, ?array $mergeQuery=null): Traversable {
+ return $this->capacitor->all($filter, $mergeQuery);
+ }
+
+ function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
+ return $this->capacitor->each($filter, $func, $args, $mergeQuery, $nbUpdated);
+ }
+
+ function delete($filter, $func=null, ?array $args=null): int {
+ return $this->capacitor->delete($filter, $func, $args);
+ }
+}
diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php
new file mode 100644
index 0000000..ec27fae
--- /dev/null
+++ b/php/src/db/CapacitorStorage.php
@@ -0,0 +1,632 @@
+_create($channel);
+ $this->channels[$channel->getName()] = $channel;
+ return $channel;
+ }
+
+ protected function getChannel(?string $name): CapacitorChannel {
+ CapacitorChannel::verifix_name($name);
+ $channel = $this->channels[$name] ?? null;
+ if ($channel === null) {
+ $channel = $this->addChannel(new CapacitorChannel($name));
+ }
+ return $channel;
+ }
+
+ /** DOIT être défini dans les classes dérivées */
+ const PRIMARY_KEY_DEFINITION = null;
+
+ const COLUMN_DEFINITIONS = [
+ "item__" => CapacitorChannel::SERIAL_DEFINITION,
+ "item__sum_" => CapacitorChannel::SUM_DEFINITION,
+ "created_" => "datetime",
+ "modified_" => "datetime",
+ ];
+
+ protected function ColumnDefinitions(CapacitorChannel $channel): array {
+ $definitions = [];
+ if ($channel->getPrimaryKeys() === null) {
+ $definitions[] = static::PRIMARY_KEY_DEFINITION;
+ }
+ $definitions[] = $channel->getColumnDefinitions();
+ $definitions[] = static::COLUMN_DEFINITIONS;
+ # forcer les définitions sans clé à la fin (sqlite requière par exemple que
+ # primary key (columns) soit à la fin)
+ $tmp = cl::merge(...$definitions);
+ $definitions = [];
+ $constraints = [];
+ $index = 0;
+ foreach ($tmp as $col => $def) {
+ if ($col === $index) {
+ $index++;
+ $constraints[] = $def;
+ } else {
+ $definitions[$col] = $def;
+ }
+ }
+ return cl::merge($definitions, $constraints);
+ }
+
+ /** sérialiser les valeurs qui doivent l'être dans $values */
+ protected function serialize(CapacitorChannel $channel, ?array $values): ?array {
+ if ($values === null) return null;
+ $cols = $this->ColumnDefinitions($channel);
+ $index = 0;
+ $row = [];
+ foreach (array_keys($cols) as $col) {
+ $key = $col;
+ if ($key === $index) {
+ $index++;
+ } elseif ($channel->isSerialCol($key)) {
+ [$serialCol, $sumCol] = $channel->getSumCols($key);
+ if (array_key_exists($key, $values)) {
+ $sum = $channel->getSum($key, $values[$key]);
+ $row[$serialCol] = $sum[$serialCol];
+ if (array_key_exists($sumCol, $cols)) {
+ $row[$sumCol] = $sum[$sumCol];
+ }
+ }
+ } elseif (array_key_exists($key, $values)) {
+ $row[$col] = $values[$key];
+ }
+ }
+ return $row;
+ }
+
+ /** désérialiser les valeurs qui doivent l'être dans $values */
+ protected function unserialize(CapacitorChannel $channel, ?array $row): ?array {
+ if ($row === null) return null;
+ $cols = $this->ColumnDefinitions($channel);
+ $index = 0;
+ $values = [];
+ foreach (array_keys($cols) as $col) {
+ $key = $col;
+ if ($key === $index) {
+ $index++;
+ } elseif (!array_key_exists($col, $row)) {
+ } elseif ($channel->isSerialCol($key)) {
+ $value = $row[$col];
+ if ($value !== null) $value = $channel->unserialize($value);
+ $values[$key] = $value;
+ } else {
+ $values[$key] = $row[$col];
+ }
+ }
+ return $values;
+ }
+
+ function getPrimaryKeys(CapacitorChannel $channel): array {
+ $primaryKeys = $channel->getPrimaryKeys();
+ if ($primaryKeys === null) $primaryKeys = ["id_"];
+ return $primaryKeys;
+ }
+
+ function getRowIds(CapacitorChannel $channel, ?array $row, ?array &$primaryKeys=null): ?array {
+ $primaryKeys = $this->getPrimaryKeys($channel);
+ $rowIds = cl::select($row, $primaryKeys);
+ if (cl::all_n($rowIds)) return null;
+ else return $rowIds;
+ }
+
+ protected function _createSql(CapacitorChannel $channel): array {
+ $cols = $this->ColumnDefinitions($channel);
+ return [
+ "create table if not exists",
+ "table" => $channel->getTableName(),
+ "cols" => $cols,
+ ];
+ }
+
+ protected static function format_sql(CapacitorChannel $channel, string $sql): string {
+ $class = get_class($channel);
+ return <<_getCreateSql($this->getChannel($channel));
+ }
+
+ protected function _afterCreate(CapacitorChannel $channel): void {
+ }
+
+ protected function _create(CapacitorChannel $channel): void {
+ $channel->ensureSetup();
+ if (!$channel->isCreated()) {
+ $this->db->exec($this->_createSql($channel));
+ $this->_afterCreate($channel);
+ $channel->setCreated();
+ }
+ }
+
+ /** tester si le canal spécifié existe */
+ abstract function _exists(CapacitorChannel $channel): bool;
+
+ function exists(?string $channel): bool {
+ return $this->_exists($this->getChannel($channel));
+ }
+
+ /** s'assurer que le canal spécifié existe */
+ function _ensureExists(CapacitorChannel $channel): void {
+ $this->_create($channel);
+ }
+
+ function ensureExists(?string $channel): void {
+ $this->_ensureExists($this->getChannel($channel));
+ }
+
+ protected function _beforeReset(CapacitorChannel $channel): void {
+ }
+
+ /** supprimer le canal spécifié */
+ function _reset(CapacitorChannel $channel, bool $recreate=false): void {
+ $this->_beforeReset($channel);
+ $this->db->exec([
+ "drop table if exists",
+ $channel->getTableName(),
+ ]);
+ $channel->setCreated(false);
+ if ($recreate) $this->_ensureExists($channel);
+ }
+
+ function reset(?string $channel, bool $recreate=false): void {
+ $this->_reset($this->getChannel($channel), $recreate);
+ }
+
+ /**
+ * charger une valeur dans le canal
+ *
+ * Après avoir calculé les valeurs des clés supplémentaires
+ * avec {@link CapacitorChannel::getItemValues()}, l'une des deux fonctions
+ * {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
+ * est appelée en fonction du type d'opération: création ou mise à jour
+ *
+ * Ensuite, si $func !== null, la fonction est appelée avec la signature de
+ * {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
+ * en fonction du type d'opération: création ou mise à jour
+ *
+ * Dans les deux cas, si la fonction retourne un tableau, il est utilisé pour
+ * modifier les valeurs insérées/mises à jour. De plus, $values obtient la
+ * valeur finale des données insérées/mises à jour
+ *
+ * Si $args est renseigné, il est ajouté aux arguments utilisés pour appeler
+ * les méthodes {@link CapacitorChannel::getItemValues()},
+ * {@link CapacitorChannel::onCreate()} et/ou
+ * {@link CapacitorChannel::onUpdate()}
+ *
+ * @return int 1 si l'objet a été chargé ou mis à jour, 0 s'il existait
+ * déjà à l'identique dans le canal
+ */
+ function _charge(CapacitorChannel $channel, $item, $func, ?array $args, ?array &$values=null): int {
+ $this->_create($channel);
+ $tableName = $channel->getTableName();
+ $db = $this->db();
+ $args ??= [];
+
+ $initFunc = [$channel, "getItemValues"];
+ $initArgs = $args;
+ nur_func::ensure_func($initFunc, null, $initArgs);
+ $values = nur_func::call($initFunc, $item, ...$initArgs);
+ if ($values === [false]) return 0;
+
+ $row = cl::merge(
+ $channel->getSum("item", $item),
+ $this->serialize($channel, $values));
+ $prow = null;
+ $rowIds = $this->getRowIds($channel, $row, $primaryKeys);
+ if ($rowIds !== null) {
+ # modification
+ $prow = $db->one([
+ "select",
+ "from" => $tableName,
+ "where" => $rowIds,
+ ]);
+ }
+
+ $now = date("Y-m-d H:i:s");
+ $insert = null;
+ if ($prow === null) {
+ # création
+ $row = cl::merge($row, [
+ "created_" => $now,
+ "modified_" => $now,
+ ]);
+ $insert = true;
+ $initFunc = [$channel, "onCreate"];
+ $initArgs = $args;
+ nur_func::ensure_func($initFunc, null, $initArgs);
+ $values = $this->unserialize($channel, $row);
+ $pvalues = null;
+ } else {
+ # modification
+ # intégrer autant que possible les valeurs de prow dans row, de façon que
+ # l'utilisateur puisse voir clairement ce qui a été modifié
+ if ($channel->_wasSumModified("item", $row, $prow)) {
+ $insert = false;
+ $row = cl::merge($prow, $row, [
+ "modified_" => $now,
+ ]);
+ } else {
+ $row = cl::merge($prow, $row);
+ }
+ $initFunc = [$channel, "onUpdate"];
+ $initArgs = $args;
+ nur_func::ensure_func($initFunc, null, $initArgs);
+ $values = $this->unserialize($channel, $row);
+ $pvalues = $this->unserialize($channel, $prow);
+ }
+
+ $updates = nur_func::call($initFunc, $item, $values, $pvalues, ...$initArgs);
+ if ($updates === [false]) return 0;
+ if (is_array($updates) && $updates) {
+ if ($insert === null) $insert = false;
+ if (!array_key_exists("modified_", $updates)) {
+ $updates["modified_"] = $now;
+ }
+ $values = cl::merge($values, $updates);
+ $row = cl::merge($row, $this->serialize($channel, $updates));
+ }
+
+ if ($func !== null) {
+ nur_func::ensure_func($func, $channel, $args);
+ $updates = nur_func::call($func, $item, $values, $pvalues, ...$args);
+ if ($updates === [false]) return 0;
+ if (is_array($updates) && $updates) {
+ if ($insert === null) $insert = false;
+ if (!array_key_exists("modified_", $updates)) {
+ $updates["modified_"] = $now;
+ }
+ $values = cl::merge($values, $updates);
+ $row = cl::merge($row, $this->serialize($channel, $updates));
+ }
+ }
+
+ # aucune modification
+ if ($insert === null) return 0;
+
+ # si on est déjà dans une transaction, désactiver la gestion des transactions
+ $manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
+ if ($manageTransactions) {
+ $commited = false;
+ $db->beginTransaction();
+ }
+ $nbModified = 0;
+ try {
+ if ($insert) {
+ $id = $db->exec([
+ "insert",
+ "into" => $tableName,
+ "values" => $row,
+ ]);
+ if (count($primaryKeys) == 1 && $rowIds === null) {
+ # mettre à jour avec l'id généré
+ $values[$primaryKeys[0]] = $id;
+ }
+ $nbModified = 1;
+ } else {
+ # calculer ce qui a changé pour ne mettre à jour que le nécessaire
+ $updates = [];
+ foreach ($row as $col => $value) {
+ if (array_key_exists($col, $rowIds)) {
+ # ne jamais mettre à jour la clé primaire
+ continue;
+ }
+ $pvalue = $prow[$col] ?? null;
+ if ($value !== ($pvalue)) {
+ $updates[$col] = $value;
+ }
+ }
+ if (count($updates) == 1 && array_key_first($updates) == "modified_") {
+ # si l'unique modification porte sur la date de modification, alors
+ # la ligne n'est pas modifiée. ce cas se présente quand on altère la
+ # valeur de $item
+ $updates = null;
+ }
+ if ($updates) {
+ $db->exec([
+ "update",
+ "table" => $tableName,
+ "values" => $updates,
+ "where" => $rowIds,
+ ]);
+ $nbModified = 1;
+ }
+ }
+ if ($manageTransactions) {
+ $db->commit();
+ $commited = true;
+ }
+ return $nbModified;
+ } finally {
+ if ($manageTransactions && !$commited) $db->rollback();
+ }
+ }
+
+ function charge(?string $channel, $item, $func=null, ?array $args=null, ?array &$values=null): int {
+ return $this->_charge($this->getChannel($channel), $item, $func, $args, $values);
+ }
+
+ /** décharger les données du canal spécifié */
+ function _discharge(CapacitorChannel $channel, bool $reset=true): Traversable {
+ $this->_create($channel);
+ $rows = $this->db()->all([
+ "select item__",
+ "from" => $channel->getTableName(),
+ ]);
+ foreach ($rows as $row) {
+ yield unserialize($row['item__']);
+ }
+ if ($reset) $this->_reset($channel);
+ }
+
+ function discharge(?string $channel, bool $reset=true): Traversable {
+ return $this->_discharge($this->getChannel($channel), $reset);
+ }
+
+ protected function _convertValue2row(CapacitorChannel $channel, array $filter, array $cols): array {
+ $index = 0;
+ $fixed = [];
+ foreach ($filter as $key => $value) {
+ if ($key === $index) {
+ $index++;
+ if (is_array($value)) {
+ $value = $this->_convertValue2row($channel, $value, $cols);
+ }
+ $fixed[] = $value;
+ } else {
+ $col = "${key}__";
+ if (array_key_exists($col, $cols)) {
+ # colonne sérialisée
+ $fixed[$col] = $channel->serialize($value);
+ } else {
+ $fixed[$key] = $value;
+ }
+ }
+ }
+ return $fixed;
+ }
+
+ protected function verifixFilter(CapacitorChannel $channel, &$filter): void {
+ if ($filter !== null && !is_array($filter)) {
+ $primaryKeys = $this->getPrimaryKeys($channel);
+ $id = $filter;
+ $channel->verifixId($id);
+ $filter = [$primaryKeys[0] => $id];
+ }
+ $cols = $this->ColumnDefinitions($channel);
+ if ($filter !== null) {
+ $filter = $this->_convertValue2row($channel, $filter, $cols);
+ }
+ }
+
+ /** indiquer le nombre d'éléments du canal spécifié */
+ function _count(CapacitorChannel $channel, $filter): int {
+ $this->_create($channel);
+ $this->verifixFilter($channel, $filter);
+ return $this->db()->get([
+ "select count(*)",
+ "from" => $channel->getTableName(),
+ "where" => $filter,
+ ]);
+ }
+
+ function count(?string $channel, $filter=null): int {
+ return $this->_count($this->getChannel($channel), $filter);
+ }
+
+ /**
+ * obtenir la ligne correspondant au filtre sur le canal spécifié
+ *
+ * si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
+ */
+ function _one(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): ?array {
+ if ($filter === null) throw ValueException::null("filter");
+ $this->_create($channel);
+ $this->verifixFilter($channel, $filter);
+ $row = $this->db()->one(cl::merge([
+ "select",
+ "from" => $channel->getTableName(),
+ "where" => $filter,
+ ], $mergeQuery));
+ return $this->unserialize($channel, $row);
+ }
+
+ function one(?string $channel, $filter, ?array $mergeQuery=null): ?array {
+ return $this->_one($this->getChannel($channel), $filter, $mergeQuery);
+ }
+
+ private function _allCached(string $id, CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable {
+ $this->_create($channel);
+ $this->verifixFilter($channel, $filter);
+ $rows = $this->db()->all(cl::merge([
+ "select",
+ "from" => $channel->getTableName(),
+ "where" => $filter,
+ ], $mergeQuery), null, $this->getPrimaryKeys($channel));
+ if ($channel->isUseCache()) {
+ $cacheIds = [$id, get_class($channel)];
+ cache::get()->resetCached($cacheIds);
+ $rows = cache::new(null, $cacheIds, function() use ($rows) {
+ yield from $rows;
+ });
+ }
+ foreach ($rows as $key => $row) {
+ yield $key => $this->unserialize($channel, $row);
+ }
+ }
+
+ /**
+ * obtenir les lignes correspondant au filtre sur le canal spécifié
+ *
+ * si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
+ */
+ function _all(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable {
+ return $this->_allCached("all", $channel, $filter, $mergeQuery);
+ }
+
+ function all(?string $channel, $filter, $mergeQuery=null): Traversable {
+ return $this->_all($this->getChannel($channel), $filter, $mergeQuery);
+ }
+
+ /**
+ * appeler une fonction pour chaque élément du canal spécifié.
+ *
+ * $filter permet de filtrer parmi les élements chargés
+ *
+ * $func est appelé avec la signature de {@link CapacitorChannel::onEach()}
+ * si la fonction retourne un tableau, il est utilisé pour mettre à jour la
+ * ligne
+ *
+ * @param int $nbUpdated reçoit le nombre de lignes mises à jour
+ * @return int le nombre de lignes parcourues
+ */
+ function _each(CapacitorChannel $channel, $filter, $func, ?array $args, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
+ $this->_create($channel);
+ if ($func === null) $func = CapacitorChannel::onEach;
+ nur_func::ensure_func($func, $channel, $args);
+ $onEach = nur_func::_prepare($func);
+ $db = $this->db();
+ # si on est déjà dans une transaction, désactiver la gestion des transactions
+ $manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
+ if ($manageTransactions) {
+ $commited = false;
+ $db->beginTransaction();
+ $commitThreshold = $channel->getEachCommitThreshold();
+ }
+ $count = 0;
+ $nbUpdated = 0;
+ $tableName = $channel->getTableName();
+ try {
+ $args ??= [];
+ $all = $this->_allCached("each", $channel, $filter, $mergeQuery);
+ foreach ($all as $values) {
+ $rowIds = $this->getRowIds($channel, $values);
+ $updates = nur_func::_call($onEach, [$values["item"], $values, ...$args]);
+ if (is_array($updates) && $updates) {
+ if (!array_key_exists("modified_", $updates)) {
+ $updates["modified_"] = date("Y-m-d H:i:s");
+ }
+ $nbUpdated += $db->exec([
+ "update",
+ "table" => $tableName,
+ "values" => $this->serialize($channel, $updates),
+ "where" => $rowIds,
+ ]);
+ if ($manageTransactions && $commitThreshold !== null) {
+ $commitThreshold--;
+ if ($commitThreshold <= 0) {
+ $db->commit();
+ $db->beginTransaction();
+ $commitThreshold = $channel->getEachCommitThreshold();
+ }
+ }
+ }
+ $count++;
+ }
+ if ($manageTransactions) {
+ $db->commit();
+ $commited = true;
+ }
+ return $count;
+ } finally {
+ if ($manageTransactions && !$commited) $db->rollback();
+ }
+ }
+
+ function each(?string $channel, $filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
+ return $this->_each($this->getChannel($channel), $filter, $func, $args, $mergeQuery, $nbUpdated);
+ }
+
+ /**
+ * supprimer tous les éléments correspondant au filtre et pour lesquels la
+ * fonction retourne une valeur vraie si elle est spécifiée
+ *
+ * $filter permet de filtrer parmi les élements chargés
+ *
+ * $func est appelé avec la signature de {@link CapacitorChannel::onDelete()}
+ * si la fonction retourne un tableau, il est utilisé pour mettre à jour la
+ * ligne
+ *
+ * @return int le nombre de lignes parcourues
+ */
+ function _delete(CapacitorChannel $channel, $filter, $func, ?array $args): int {
+ $this->_create($channel);
+ if ($func === null) $func = CapacitorChannel::onDelete;
+ nur_func::ensure_func($func, $channel, $args);
+ $onEach = nur_func::_prepare($func);
+ $db = $this->db();
+ # si on est déjà dans une transaction, désactiver la gestion des transactions
+ $manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
+ if ($manageTransactions) {
+ $commited = false;
+ $db->beginTransaction();
+ $commitThreshold = $channel->getEachCommitThreshold();
+ }
+ $count = 0;
+ $tableName = $channel->getTableName();
+ try {
+ $args ??= [];
+ $all = $this->_allCached("delete", $channel, $filter);
+ foreach ($all as $values) {
+ $rowIds = $this->getRowIds($channel, $values);
+ $delete = boolval(nur_func::_call($onEach, [$values["item"], $values, ...$args]));
+ if ($delete) {
+ $db->exec([
+ "delete",
+ "from" => $tableName,
+ "where" => $rowIds,
+ ]);
+ if ($manageTransactions && $commitThreshold !== null) {
+ $commitThreshold--;
+ if ($commitThreshold <= 0) {
+ $db->commit();
+ $db->beginTransaction();
+ $commitThreshold = $channel->getEachCommitThreshold();
+ }
+ }
+ }
+ $count++;
+ }
+ if ($manageTransactions) {
+ $db->commit();
+ $commited = true;
+ }
+ return $count;
+ } finally {
+ if ($manageTransactions && !$commited) $db->rollback();
+ }
+ }
+
+ function delete(?string $channel, $filter, $func=null, ?array $args=null): int {
+ return $this->_delete($this->getChannel($channel), $filter, $func, $args);
+ }
+
+ abstract function close(): void;
+}
diff --git a/php/src/db/IDatabase.php b/php/src/db/IDatabase.php
new file mode 100644
index 0000000..497e5be
--- /dev/null
+++ b/php/src/db/IDatabase.php
@@ -0,0 +1,19 @@
+format('Y-m-d');
+ } elseif ($value instanceof DateTime) {
+ $value = $value->format('Y-m-d H:i:s');
+ } elseif ($value instanceof DateTimeInterface) {
+ $value = $value->format('Y-m-d H:i:s');
+ str::del_suffix($value, " 00:00:00");
+ } elseif (is_string($value)) {
+ if (self::is_sqldate($value)) {
+ # déjà dans le bon format
+ } elseif (Date::isa_date($value, true)) {
+ $value = new Date($value);
+ $value = $value->format('Y-m-d');
+ } elseif (DateTime::isa_datetime($value, true)) {
+ $value = new DateTime($value);
+ $value = $value->format('Y-m-d H:i:s');
+ }
+ } elseif (is_bool($value)) {
+ $value = $value? 1: 0;
+ }
+ }
+}
diff --git a/php/src/db/_private/Tcreate.php b/php/src/db/_private/Tcreate.php
new file mode 100644
index 0000000..a85d118
--- /dev/null
+++ b/php/src/db/_private/Tcreate.php
@@ -0,0 +1,39 @@
+ &$definition) {
+ if ($col === $index) {
+ $index++;
+ } else {
+ $definition = "$col $definition";
+ }
+ }; unset($definition);
+ $sql[] = "(\n ".implode("\n, ", $cols)."\n)";
+
+ ## suffixe
+ if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
+
+ ## fin de la requête
+ return implode(" ", $sql);
+ }
+}
diff --git a/php/src/db/_private/Tdelete.php b/php/src/db/_private/Tdelete.php
new file mode 100644
index 0000000..0eeb91a
--- /dev/null
+++ b/php/src/db/_private/Tdelete.php
@@ -0,0 +1,38 @@
+ $col) {
+ if ($key === $index) {
+ $index++;
+ $cols[] = $col;
+ $usercols[] = self::add_prefix($col, $colPrefix);
+ } else {
+ $cols[] = $key;
+ $usercols[] = self::add_prefix($col, $colPrefix)." as $key";
+ }
+ }
+ } else {
+ $cols = null;
+ if ($schema && is_array($schema) && !in_array("*", $usercols)) {
+ $cols = array_keys($schema);
+ foreach ($cols as $col) {
+ $usercols[] = self::add_prefix($col, $colPrefix);
+ }
+ }
+ }
+ if (!$usercols && !$cols) $usercols = [self::add_prefix("*", $colPrefix)];
+ $sql[] = implode(", ", $usercols);
+
+ ## from
+ $from = $query["from"] ?? null;
+ if (self::consume('from\s+([a-z_][a-z0-9_]*)\s*(?=;?\s*$|\bwhere\b)', $tmpsql, $ms)) {
+ if ($from === null) $from = $ms[1];
+ $sql[] = "from";
+ $sql[] = $from;
+ } elseif ($from !== null) {
+ $sql[] = "from";
+ $sql[] = $from;
+ } else {
+ throw new ValueException("expected table name: $usersql");
+ }
+
+ ## where
+ $userwhere = [];
+ if (self::consume('where\b\s*(.*?)(?=;?\s*$|\border\s+by\b)', $tmpsql, $ms)) {
+ if ($ms[1]) $userwhere[] = $ms[1];
+ }
+ $where = cl::withn($query["where"] ?? null);
+ if ($where !== null) self::parse_conds($where, $userwhere, $bindings);
+ if ($userwhere) {
+ $sql[] = "where";
+ $sql[] = implode(" and ", $userwhere);
+ }
+
+ ## order by
+ $userorderby = [];
+ if (self::consume('order\s+by\b\s*(.*?)(?=;?\s*$|\bgroup\s+by\b)', $tmpsql, $ms)) {
+ if ($ms[1]) $userorderby[] = $ms[1];
+ }
+ $orderby = cl::withn($query["order by"] ?? null);
+ if ($orderby !== null) {
+ $index = 0;
+ foreach ($orderby as $key => $value) {
+ if ($key === $index) {
+ $userorderby[] = $value;
+ $index++;
+ } else {
+ if ($value === null) $value = false;
+ if (!is_bool($value)) {
+ $userorderby[] = "$key $value";
+ } elseif ($value) {
+ $userorderby[] = $key;
+ }
+ }
+ }
+ }
+ if ($userorderby) {
+ $sql[] = "order by";
+ $sql[] = implode(", ", $userorderby);
+ }
+ ## group by
+ $usergroupby = [];
+ if (self::consume('group\s+by\b\s*(.*?)(?=;?\s*$|\bhaving\b)', $tmpsql, $ms)) {
+ if ($ms[1]) $usergroupby[] = $ms[1];
+ }
+ $groupby = cl::withn($query["group by"] ?? null);
+ if ($groupby !== null) {
+ $index = 0;
+ foreach ($groupby as $key => $value) {
+ if ($key === $index) {
+ $usergroupby[] = $value;
+ $index++;
+ } else {
+ if ($value === null) $value = false;
+ if (!is_bool($value)) {
+ $usergroupby[] = "$key $value";
+ } elseif ($value) {
+ $usergroupby[] = $key;
+ }
+ }
+ }
+ }
+ if ($usergroupby) {
+ $sql[] = "group by";
+ $sql[] = implode(", ", $usergroupby);
+ }
+
+ ## having
+ $userhaving = [];
+ if (self::consume('having\b\s*(.*?)(?=;?\s*$)', $tmpsql, $ms)) {
+ if ($ms[1]) $userhaving[] = $ms[1];
+ }
+ $having = cl::withn($query["having"] ?? null);
+ if ($having !== null) self::parse_conds($having, $userhaving, $bindings);
+ if ($userhaving) {
+ $sql[] = "having";
+ $sql[] = implode(" and ", $userhaving);
+ }
+
+ ## suffixe
+ if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix;
+
+ ## fin de la requête
+ self::check_eof($tmpsql, $usersql);
+ return implode(" ", $sql);
+ }
+}
diff --git a/php/src/db/_private/Tupdate.php b/php/src/db/_private/Tupdate.php
new file mode 100644
index 0000000..4e1de5b
--- /dev/null
+++ b/php/src/db/_private/Tupdate.php
@@ -0,0 +1,40 @@
+ $value) {
+ if ($key === $index) {
+ $index++;
+ if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) {
+ $sql .= " ";
+ }
+ $sql .= $value;
+ }
+ }
+ return $sql;
+ }
+
+ protected static function is_sep(&$cond): bool {
+ if (!is_string($cond)) return false;
+ if (!preg_match('/^\s*(and|or|not)\s*$/i', $cond, $ms)) return false;
+ $cond = $ms[1];
+ return true;
+ }
+
+ static function parse_conds(?array $conds, ?array &$sql, ?array &$bindings): void {
+ if (!$conds) return;
+ $sep = null;
+ $index = 0;
+ $condsql = [];
+ foreach ($conds as $key => $cond) {
+ if ($key === $index) {
+ ## séquentiel
+ if ($index === 0 && self::is_sep($cond)) {
+ $sep = $cond;
+ } elseif (is_bool($cond)) {
+ # ignorer les valeurs true et false
+ } elseif (is_array($cond)) {
+ # condition récursive
+ self::parse_conds($cond, $condsql, $bindings);
+ } else {
+ # condition litérale
+ $condsql[] = strval($cond);
+ }
+ $index++;
+ } elseif ($cond === false) {
+ ## associatif
+ # condition litérale ignorée car condition false
+ } elseif ($cond === true) {
+ # condition litérale sélectionnée car condition true
+ $condsql[] = strval($key);
+ } else {
+ ## associatif
+ # paramètre
+ $param0 = preg_replace('/^.+\./', "", $key);
+ $i = false;
+ if ($bindings !== null && array_key_exists($param0, $bindings)) {
+ $i = 2;
+ while (array_key_exists("$param0$i", $bindings)) {
+ $i++;
+ }
+ }
+ # value ou [operator, value]
+ $condprefix = $condsep = $condsuffix = null;
+ if (is_array($cond)) {
+ $condkey = 0;
+ $condkeys = array_keys($cond);
+ $op = null;
+ if (array_key_exists("op", $cond)) {
+ $op = $cond["op"];
+ } elseif (array_key_exists($condkey, $condkeys)) {
+ $op = $cond[$condkeys[$condkey]];
+ $condkey++;
+ }
+ $op = strtolower($op);
+ $condvalues = null;
+ switch ($op) {
+ case "between":
+ # ["between", $upper, $lower]
+ $condsep = " and ";
+ if (array_key_exists("lower", $cond)) {
+ $condvalues[] = $cond["lower"];
+ } elseif (array_key_exists($condkey, $condkeys)) {
+ $condvalues[] = $cond[$condkeys[$condkey]];
+ $condkey++;
+ }
+ if (array_key_exists("upper", $cond)) {
+ $condvalues[] = $cond["upper"];
+ } elseif (array_key_exists($condkey, $condkeys)) {
+ $condvalues[] = $cond[$condkeys[$condkey]];
+ $condkey++;
+ }
+ break;
+ case "any":
+ case "all":
+ case "not any":
+ case "not all":
+ # ["list", $values]
+ if ($op === "any" || $op === "all") {
+ $condprefix = $op;
+ $op = "=";
+ } elseif ($op === "not any" || $op === "not all") {
+ $condprefix = substr($op, strlen("not "));
+ $op = "<>";
+ }
+ $condprefix .= "(array[";
+ $condsep = ", ";
+ $condsuffix = "])";
+ $condvalues = null;
+ if (array_key_exists("values", $cond)) {
+ $condvalues = cl::with($cond["values"]);
+ } elseif (array_key_exists($condkey, $condkeys)) {
+ $condvalues = cl::with($cond[$condkeys[$condkey]]);
+ $condkey++;
+ }
+ break;
+ case "in":
+ # ["in", $values]
+ $condprefix = "(";
+ $condsep = ", ";
+ $condsuffix = ")";
+ $condvalues = null;
+ if (array_key_exists("values", $cond)) {
+ $condvalues = cl::with($cond["values"]);
+ } elseif (array_key_exists($condkey, $condkeys)) {
+ $condvalues = cl::with($cond[$condkeys[$condkey]]);
+ $condkey++;
+ }
+ break;
+ case "null":
+ case "is null":
+ $op = "is null";
+ break;
+ case "not null":
+ case "is not null":
+ $op = "is not null";
+ break;
+ default:
+ if (array_key_exists("value", $cond)) {
+ $condvalues = [$cond["value"]];
+ } elseif (array_key_exists($condkey, $condkeys)) {
+ $condvalues = [$cond[$condkeys[$condkey]]];
+ $condkey++;
+ }
+ }
+ } elseif ($cond !== null) {
+ $op = "=";
+ $condvalues = [$cond];
+ } else {
+ $op = "is null";
+ $condvalues = null;
+ }
+ $cond = [$key, $op];
+ if ($condvalues !== null) {
+ $parts = [];
+ foreach ($condvalues as $condvalue) {
+ if (is_array($condvalue)) {
+ $first = true;
+ foreach ($condvalue as $value) {
+ if ($first) {
+ $first = false;
+ } else {
+ if ($sep === null) $sep = "and";
+ $parts[] = " $sep ";
+ $parts[] = $key;
+ $parts[] = " $op ";
+ }
+ $param = "$param0$i";
+ $parts[] = ":$param";
+ $bindings[$param] = $value;
+ if ($i === false) $i = 2;
+ else $i++;
+ }
+ } else {
+ $param = "$param0$i";
+ $parts[] = ":$param";
+ $bindings[$param] = $condvalue;
+ if ($i === false) $i = 2;
+ else $i++;
+ }
+ }
+ $cond[] = $condprefix.implode($condsep, $parts).$condsuffix;
+ }
+ $condsql[] = implode(" ", $cond);
+ }
+ }
+ if ($sep === null) $sep = "and";
+ $count = count($condsql);
+ if ($count > 1) {
+ $sql[] = "(" . implode(" $sep ", $condsql) . ")";
+ } elseif ($count == 1) {
+ $sql[] = $condsql[0];
+ }
+ }
+
+ static function parse_set_values(?array $values, ?array &$sql, ?array &$bindings): void {
+ if (!$values) return;
+ $index = 0;
+ $parts = [];
+ foreach ($values as $key => $part) {
+ if ($key === $index) {
+ ## séquentiel
+ if (is_array($part)) {
+ # paramètres récursifs
+ self::parse_set_values($part, $parts, $bindings);
+ } else {
+ # paramètre litéral
+ $parts[] = strval($part);
+ }
+ $index++;
+ } else {
+ ## associatif
+ # paramètre
+ $param = $param0 = preg_replace('/^.+\./', "", $key);
+ if ($bindings !== null && array_key_exists($param0, $bindings)) {
+ $i = 2;
+ while (array_key_exists("$param0$i", $bindings)) {
+ $i++;
+ }
+ $param = "$param0$i";
+ }
+ # value
+ $value = $part;
+ $part = [$key, "="];
+ if ($value === null) {
+ $part[] = "null";
+ } else {
+ $part[] = ":$param";
+ $bindings[$param] = $value;
+ }
+ $parts[] = implode(" ", $part);
+ }
+ }
+ $sql = cl::merge($sql, $parts);
+ }
+
+ protected static function check_eof(string $tmpsql, string $usersql): void {
+ self::consume(';\s*', $tmpsql);
+ if ($tmpsql) {
+ throw new ValueException("unexpected value at end: $usersql");
+ }
+ }
+
+ abstract protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void;
+
+ function __construct($sql, ?array $bindings=null) {
+ static::verifix($sql, $bindings, $meta);
+ $this->sql = $sql;
+ $this->bindings = $bindings;
+ $this->meta = $meta;
+ }
+
+ /** @var string */
+ protected $sql;
+
+ function getSql(): string {
+ return $this->sql;
+ }
+
+ /** @var ?array */
+ protected $bindings;
+
+ function getBindings(): ?array {
+ return $this->bindings;
+ }
+
+ /** @var ?array */
+ protected $meta;
+
+ function isInsert(): bool {
+ return ($this->meta["isa"] ?? null) === "insert";
+ }
+}
diff --git a/php/src/db/_private/_create.php b/php/src/db/_private/_create.php
new file mode 100644
index 0000000..64c29a5
--- /dev/null
+++ b/php/src/db/_private/_create.php
@@ -0,0 +1,12 @@
+ "?string",
+ "table" => "string",
+ "schema" => "?array",
+ "cols" => "?array",
+ "suffix" => "?string",
+ ];
+}
diff --git a/php/src/db/_private/_delete.php b/php/src/db/_private/_delete.php
new file mode 100644
index 0000000..e79ec34
--- /dev/null
+++ b/php/src/db/_private/_delete.php
@@ -0,0 +1,11 @@
+ "?string",
+ "from" => "?string",
+ "where" => "?array",
+ "suffix" => "?string",
+ ];
+}
diff --git a/php/src/db/_private/_generic.php b/php/src/db/_private/_generic.php
new file mode 100644
index 0000000..97d4b51
--- /dev/null
+++ b/php/src/db/_private/_generic.php
@@ -0,0 +1,7 @@
+ "?string",
+ "into" => "?string",
+ "schema" => "?array",
+ "cols" => "?array",
+ "values" => "?array",
+ "suffix" => "?string",
+ ];
+}
diff --git a/php/src/db/_private/_select.php b/php/src/db/_private/_select.php
new file mode 100644
index 0000000..ee2bdbc
--- /dev/null
+++ b/php/src/db/_private/_select.php
@@ -0,0 +1,17 @@
+ "?string",
+ "schema" => "?array",
+ "cols" => "?array",
+ "col_prefix" => "?string",
+ "from" => "?string",
+ "where" => "?array",
+ "order by" => "?array",
+ "group by" => "?array",
+ "having" => "?array",
+ "suffix" => "?string",
+ ];
+}
diff --git a/php/src/db/_private/_update.php b/php/src/db/_private/_update.php
new file mode 100644
index 0000000..b5b2dc6
--- /dev/null
+++ b/php/src/db/_private/_update.php
@@ -0,0 +1,14 @@
+ "?string",
+ "table" => "?string",
+ "schema" => "?array",
+ "cols" => "?array",
+ "values" => "?array",
+ "where" => "?array",
+ "suffix" => "?string",
+ ];
+}
diff --git a/php/src/db/cache/CacheChannel.php b/php/src/db/cache/CacheChannel.php
new file mode 100644
index 0000000..b1f8619
--- /dev/null
+++ b/php/src/db/cache/CacheChannel.php
@@ -0,0 +1,116 @@
+ "varchar(64) not null",
+ "id" => "varchar(64) not null",
+ "date_start" => "datetime",
+ "duration_" => "text",
+ "primary key (group_id, id)",
+ ];
+
+ static function get_cache_ids($id): array {
+ if (is_array($id)) {
+ $keys = array_keys($id);
+ if (array_key_exists("group_id", $id)) $groupIdKey = "group_id";
+ else $groupIdKey = $keys[1] ?? null;
+ $groupId = $id[$groupIdKey] ?? "";
+ if (array_key_exists("id", $id)) $idKey = "id";
+ else $idKey = $keys[0] ?? null;
+ $id = $id[$idKey] ?? "";
+ } else {
+ $groupId = "";
+ }
+ if (preg_match('/^(.*\\\\)?([^\\\\]+)$/', $groupId, $ms)) {
+ # si le groupe est une classe, faire un hash du package pour limiter la
+ # longueur du groupe
+ [$package, $groupId] = [$ms[1], $ms[2]];
+ $package = substr(md5($package), 0, 4);
+ $groupId = "${groupId}_$package";
+ }
+ return ["group_id" => $groupId, "id" => $id];
+ }
+
+ function __construct(?string $duration=null, ?string $name=null) {
+ parent::__construct($name);
+ $this->duration = $duration ?? static::DURATION;
+ $this->includes = static::INCLUDES;
+ $this->excludes = static::EXCLUDES;
+ }
+
+ protected string $duration;
+
+ protected ?array $includes;
+
+ protected ?array $excludes;
+
+ function getItemValues($item): ?array {
+ return cl::merge(self::get_cache_ids($item), [
+ "item" => null,
+ ]);
+ }
+
+ function onCreate($item, array $values, ?array $alwaysNull, ?string $duration=null): ?array {
+ $now = new DateTime();
+ $duration ??= $this->duration;
+ return [
+ "date_start" => $now,
+ "duration" => new Delay($duration, $now),
+ ];
+ }
+
+ function onUpdate($item, array $values, array $pvalues, ?string $duration=null): ?array {
+ $now = new DateTime();
+ $duration ??= $this->duration;
+ return [
+ "date_start" => $now,
+ "duration" => new Delay($duration, $now),
+ ];
+ }
+
+ function shouldUpdate($id, bool $noCache=false): bool {
+ if ($noCache) return true;
+
+ $cacheIds = self::get_cache_ids($id);
+ $groupId = $cacheIds["group_id"];
+ if ($groupId) {
+ $includes = $this->includes;
+ $shouldInclude = $includes !== null && in_array($groupId, $includes);
+ $excludes = $this->excludes;
+ $shouldExclude = $excludes !== null && in_array($groupId, $excludes);
+ if (!$shouldInclude || $shouldExclude) return true;
+ }
+
+ $found = false;
+ $expired = false;
+ $this->each($cacheIds,
+ function($item, $values) use (&$found, &$expired) {
+ $found = true;
+ $expired = $values["duration"]->isElapsed();
+ });
+ return !$found || $expired;
+ }
+
+ function setCached($id, ?string $duration=null): void {
+ $cacheIds = self::get_cache_ids($id);
+ $this->charge($cacheIds, null, [$duration]);
+ }
+
+ function resetCached($id) {
+ $cacheIds = self::get_cache_ids($id);
+ $this->delete($cacheIds);
+ }
+}
diff --git a/php/src/db/cache/RowsChannel.php b/php/src/db/cache/RowsChannel.php
new file mode 100644
index 0000000..a3f7055
--- /dev/null
+++ b/php/src/db/cache/RowsChannel.php
@@ -0,0 +1,51 @@
+ "varchar(128) primary key not null",
+ "all_values" => "mediumtext",
+ ];
+
+ function __construct($id, callable $builder, ?string $duration=null) {
+ $this->cacheIds = $cacheIds = CacheChannel::get_cache_ids($id);
+ $this->builder = Closure::fromCallable($builder);
+ $this->duration = $duration;
+ $name = "{$cacheIds["group_id"]}-{$cacheIds["id"]}";
+ parent::__construct($name);
+ }
+
+ protected array $cacheIds;
+
+ protected Closure $builder;
+
+ protected ?string $duration = null;
+
+ function getItemValues($item): ?array {
+ $key = array_keys($item)[0];
+ $row = $item[$key];
+ return [
+ "key" => $key,
+ "item" => $row,
+ "all_values" => implode(" ", cl::filter_n(cl::with($row))),
+ ];
+ }
+
+ function getIterator(): Traversable {
+ $cm = cache::get();
+ if ($cm->shouldUpdate($this->cacheIds)) {
+ $this->capacitor->reset();
+ foreach (($this->builder)() as $key => $row) {
+ $this->charge([$key => $row]);
+ }
+ $cm->setCached($this->cacheIds, $this->duration);
+ }
+ return $this->discharge(false);
+ }
+}
diff --git a/php/src/db/cache/cache.php b/php/src/db/cache/cache.php
new file mode 100644
index 0000000..401fb19
--- /dev/null
+++ b/php/src/db/cache/cache.php
@@ -0,0 +1,37 @@
+dbconn["name"] ?? null;
+ if ($url !== null && preg_match('/^mysql(?::|.*;)dbname=([^;]+)/i', $url, $ms)) {
+ return $ms[1];
+ }
+ return null;
+ }
+}
diff --git a/php/src/db/mysql/MysqlStorage.php b/php/src/db/mysql/MysqlStorage.php
new file mode 100644
index 0000000..50b09d2
--- /dev/null
+++ b/php/src/db/mysql/MysqlStorage.php
@@ -0,0 +1,46 @@
+db = Mysql::with($mysql);
+ }
+
+ /** @var Mysql */
+ protected $db;
+
+ function db(): Mysql {
+ return $this->db;
+ }
+
+ const PRIMARY_KEY_DEFINITION = [
+ "id_" => "integer primary key auto_increment",
+ ];
+
+ function _getCreateSql(CapacitorChannel $channel): string {
+ $query = new _query_base($this->_createSql($channel));
+ return self::format_sql($channel, $query->getSql());
+ }
+
+ function _exists(CapacitorChannel $channel): bool {
+ $db = $this->db;
+ $tableName = $db->get([
+ "select table_name from information_schema.tables",
+ "where" => [
+ "table_schema" => $db->getDbname(),
+ "table_name" => $channel->getTableName(),
+ ],
+ ]);
+ return $tableName !== null;
+ }
+
+ function close(): void {
+ $this->db->close();
+ }
+}
diff --git a/php/src/db/mysql/_query_base.php b/php/src/db/mysql/_query_base.php
new file mode 100644
index 0000000..614ec06
--- /dev/null
+++ b/php/src/db/mysql/_query_base.php
@@ -0,0 +1,52 @@
+ "create", "type" => "ddl"];
+ } elseif (_query_select::isa($prefix)) {
+ $sql = _query_select::parse($sql, $bindinds);
+ $meta = ["isa" => "select", "type" => "dql"];
+ } elseif (_query_insert::isa($prefix)) {
+ $sql = _query_insert::parse($sql, $bindinds);
+ $meta = ["isa" => "insert", "type" => "dml"];
+ } elseif (_query_update::isa($prefix)) {
+ $sql = _query_update::parse($sql, $bindinds);
+ $meta = ["isa" => "update", "type" => "dml"];
+ } elseif (_query_delete::isa($prefix)) {
+ $sql = _query_delete::parse($sql, $bindinds);
+ $meta = ["isa" => "delete", "type" => "dml"];
+ } elseif (_query_generic::isa($prefix)) {
+ $sql = _query_generic::parse($sql, $bindinds);
+ $meta = ["isa" => "generic", "type" => null];
+ } else {
+ throw ValueException::invalid_kind($sql, "query");
+ }
+ } else {
+ if (!is_string($sql)) $sql = strval($sql);
+ if (_query_create::isa($sql)) {
+ $meta = ["isa" => "create", "type" => "ddl"];
+ } elseif (_query_select::isa($sql)) {
+ $meta = ["isa" => "select", "type" => "dql"];
+ } elseif (_query_insert::isa($sql)) {
+ $meta = ["isa" => "insert", "type" => "dml"];
+ } elseif (_query_update::isa($sql)) {
+ $meta = ["isa" => "update", "type" => "dml"];
+ } elseif (_query_delete::isa($sql)) {
+ $meta = ["isa" => "delete", "type" => "dml"];
+ } elseif (_query_generic::isa($sql)) {
+ $meta = ["isa" => "generic", "type" => null];
+ } else {
+ $meta = ["isa" => "generic", "type" => null];
+ }
+ }
+ }
+}
diff --git a/php/src/db/mysql/_query_create.php b/php/src/db/mysql/_query_create.php
new file mode 100644
index 0000000..11f6602
--- /dev/null
+++ b/php/src/db/mysql/_query_create.php
@@ -0,0 +1,10 @@
+ $pdo->dbconn,
+ "options" => $pdo->options,
+ "config" => $pdo->config,
+ "migrate" => $pdo->migration,
+ ], $params));
+ } else {
+ return new static($pdo, $params);
+ }
+ }
+
+ static function config_errmodeException_lowerCase(self $pdo): void {
+ $pdo->db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+ $pdo->db->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_LOWER);
+ }
+ const CONFIG_errmodeException_lowerCase = [self::class, "config_errmodeException_lowerCase"];
+
+ static function config_unbufferedQueries(self $pdo): void {
+ $pdo->db->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
+ }
+ const CONFIG_unbufferedQueries = [self::class, "config_unbufferedQueries"];
+
+ protected const OPTIONS = [
+ \PDO::ATTR_PERSISTENT => true,
+ ];
+
+ protected const DEFAULT_CONFIG = [
+ self::CONFIG_errmodeException_lowerCase,
+ ];
+
+ protected const CONFIG = null;
+
+ protected const MIGRATE = null;
+
+ const dbconn_SCHEMA = [
+ "name" => "string",
+ "user" => "?string",
+ "pass" => "?string",
+ ];
+
+ const params_SCHEMA = [
+ "dbconn" => ["array"],
+ "options" => ["?array|callable"],
+ "replace_config" => ["?array|callable"],
+ "config" => ["?array|callable"],
+ "migrate" => ["?array|string|callable"],
+ "auto_open" => ["bool", true],
+ ];
+
+ function __construct($dbconn=null, ?array $params=null) {
+ if ($dbconn !== null) {
+ if (!is_array($dbconn)) {
+ $dbconn = ["name" => $dbconn];
+ #XXX à terme, il faudra interroger config
+ #$tmp = config::db($dbconn);
+ #if ($tmp !== null) $dbconn = $tmp;
+ #else $dbconn = ["name" => $dbconn];
+ }
+ $params["dbconn"] = $dbconn;
+ }
+ # dbconn
+ $this->dbconn = $params["dbconn"] ?? null;
+ $this->dbconn["name"] ??= null;
+ $this->dbconn["user"] ??= null;
+ $this->dbconn["pass"] ??= null;
+ # options
+ $this->options = $params["options"] ?? static::OPTIONS;
+ # configuration
+ $config = $params["replace_config"] ?? null;
+ if ($config === null) {
+ $config = $params["config"] ?? static::CONFIG;
+ if (is_callable($config)) $config = [$config];
+ $config = cl::merge(static::DEFAULT_CONFIG, $config);
+ }
+ $this->config = $config;
+ # migrations
+ $this->migration = $params["migrate"] ?? static::MIGRATE;
+ #
+ $defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
+ if ($params["auto_open"] ?? $defaultAutoOpen) {
+ $this->open();
+ }
+ }
+
+ protected ?array $dbconn;
+
+ /** @var array|callable */
+ protected array $options;
+
+ /** @var array|string|callable */
+ protected $config;
+
+ /** @var array|string|callable */
+ protected $migration;
+
+ protected ?\PDO $db = null;
+
+ function open(): self {
+ if ($this->db === null) {
+ $dbconn = $this->dbconn;
+ $options = $this->options;
+ if (is_callable($options)) {
+ nur_func::ensure_func($options, $this, $args);
+ $options = nur_func::call($options, ...$args);
+ }
+ $this->db = new \PDO($dbconn["name"], $dbconn["user"], $dbconn["pass"], $options);
+ _config::with($this->config)->configure($this);
+ //_migration::with($this->migration)->migrate($this);
+ }
+ return $this;
+ }
+
+ function close(): void {
+ $this->db = null;
+ }
+
+ protected function db(): \PDO {
+ $this->open();
+ return $this->db;
+ }
+
+ /** @return int|false */
+ function _exec(string $query) {
+ return $this->db()->exec($query);
+ }
+
+ private static function is_insert(?string $sql): bool {
+ if ($sql === null) return false;
+ return preg_match('/^\s*insert\b/i', $sql);
+ }
+
+ function exec($query, ?array $params=null) {
+ $db = $this->db();
+ $query = new _query_base($query, $params);
+ if ($query->useStmt($db, $stmt, $sql)) {
+ if ($stmt->execute() === false) return false;
+ if ($query->isInsert()) return $db->lastInsertId();
+ else return $stmt->rowCount();
+ } else {
+ $rowCount = $db->exec($sql);
+ if (self::is_insert($sql)) return $db->lastInsertId();
+ else return $rowCount;
+ }
+ }
+
+ /** @var ITransactor[] */
+ protected ?array $transactors = null;
+
+ function willUpdate(...$transactors): self {
+ foreach ($transactors as $transactor) {
+ if ($transactor instanceof ITransactor) {
+ $this->transactors[] = $transactor;
+ $transactor->willUpdate();
+ } else {
+ throw ValueException::invalid_type($transactor, ITransactor::class);
+ }
+ }
+ return $this;
+ }
+
+ function inTransaction(): bool {
+ return $this->db()->inTransaction();
+ }
+
+ function beginTransaction(?callable $func=null, bool $commit=true): void {
+ $this->db()->beginTransaction();
+ if ($this->transactors !== null) {
+ foreach ($this->transactors as $transactor) {
+ $transactor->beginTransaction();
+ }
+ }
+ if ($func !== null) {
+ $commited = false;
+ try {
+ nur_func::call($func, $this);
+ if ($commit) {
+ $this->commit();
+ $commited = true;
+ }
+ } finally {
+ if ($commit && !$commited) $this->rollback();
+ }
+ }
+ }
+
+ function commit(): void {
+ $this->db()->commit();
+ if ($this->transactors !== null) {
+ foreach ($this->transactors as $transactor) {
+ $transactor->commit();
+ }
+ }
+ }
+
+ function rollback(): void {
+ $this->db()->rollBack();
+ if ($this->transactors !== null) {
+ foreach ($this->transactors as $transactor) {
+ $transactor->rollback();
+ }
+ }
+ }
+
+ function get($query, ?array $params=null, bool $entireRow=false) {
+ $db = $this->db();
+ $query = new _query_base($query, $params);
+ $stmt = null;
+ try {
+ /** @var \PDOStatement $stmt */
+ if ($query->useStmt($db, $stmt, $sql)) {
+ if ($stmt->execute() === false) return null;
+ } else {
+ $stmt = $db->query($sql);
+ }
+ $row = $stmt->fetch(\PDO::FETCH_ASSOC);
+ if ($row === false) return null;
+ $this->verifixRow($row);
+ if ($entireRow) return $row;
+ else return cl::first($row);
+ } finally {
+ if ($stmt instanceof \PDOStatement) $stmt->closeCursor();
+ }
+ }
+
+ function one($query, ?array $params=null): ?array {
+ return $this->get($query, $params, true);
+ }
+
+ /**
+ * si $primaryKeys est fourni, le résultat est indexé sur la(es) colonne(s)
+ * spécifiée(s)
+ */
+ function all($query, ?array $params=null, $primaryKeys=null): Generator {
+ $db = $this->db();
+ $query = new _query_base($query, $params);
+ $stmt = null;
+ try {
+ /** @var \PDOStatement $stmt */
+ if ($query->useStmt($db, $stmt, $sql)) {
+ if ($stmt->execute() === false) return;
+ } else {
+ $stmt = $db->query($sql);
+ }
+ if ($primaryKeys !== null) $primaryKeys = cl::with($primaryKeys);
+ while (($row = $stmt->fetch(\PDO::FETCH_ASSOC)) !== false) {
+ $this->verifixRow($row);
+ if ($primaryKeys !== null) {
+ $key = implode("-", cl::select($row, $primaryKeys));
+ yield $key => $row;
+ } else {
+ yield $row;
+ }
+ }
+ } finally {
+ if ($stmt instanceof \PDOStatement) $stmt->closeCursor();
+ }
+ }
+}
diff --git a/php/src/db/pdo/_config.php b/php/src/db/pdo/_config.php
new file mode 100644
index 0000000..5055d6f
--- /dev/null
+++ b/php/src/db/pdo/_config.php
@@ -0,0 +1,36 @@
+configs = $configs;
+ }
+
+ /** @var array */
+ protected $configs;
+
+ function configure(Pdo $pdo): void {
+ foreach ($this->configs as $key => $config) {
+ if (is_string($config) && !nur_func::is_method($config)) {
+ $pdo->exec($config);
+ } else {
+ nur_func::ensure_func($config, $this, $args);
+ nur_func::call($config, $pdo, $key, ...$args);
+ }
+ }
+ }
+}
diff --git a/php/src/db/pdo/_query_base.php b/php/src/db/pdo/_query_base.php
new file mode 100644
index 0000000..921704d
--- /dev/null
+++ b/php/src/db/pdo/_query_base.php
@@ -0,0 +1,76 @@
+ "create", "type" => "ddl"];
+ } elseif (_query_select::isa($prefix)) {
+ $sql = _query_select::parse($sql, $bindinds);
+ $meta = ["isa" => "select", "type" => "dql"];
+ } elseif (_query_insert::isa($prefix)) {
+ $sql = _query_insert::parse($sql, $bindinds);
+ $meta = ["isa" => "insert", "type" => "dml"];
+ } elseif (_query_update::isa($prefix)) {
+ $sql = _query_update::parse($sql, $bindinds);
+ $meta = ["isa" => "update", "type" => "dml"];
+ } elseif (_query_delete::isa($prefix)) {
+ $sql = _query_delete::parse($sql, $bindinds);
+ $meta = ["isa" => "delete", "type" => "dml"];
+ } elseif (_query_generic::isa($prefix)) {
+ $sql = _query_generic::parse($sql, $bindinds);
+ $meta = ["isa" => "generic", "type" => null];
+ } else {
+ throw ValueException::invalid_kind($sql, "query");
+ }
+ } else {
+ if (!is_string($sql)) $sql = strval($sql);
+ if (_query_create::isa($sql)) {
+ $meta = ["isa" => "create", "type" => "ddl"];
+ } elseif (_query_select::isa($sql)) {
+ $meta = ["isa" => "select", "type" => "dql"];
+ } elseif (_query_insert::isa($sql)) {
+ $meta = ["isa" => "insert", "type" => "dml"];
+ } elseif (_query_update::isa($sql)) {
+ $meta = ["isa" => "update", "type" => "dml"];
+ } elseif (_query_delete::isa($sql)) {
+ $meta = ["isa" => "delete", "type" => "dml"];
+ } elseif (_query_generic::isa($sql)) {
+ $meta = ["isa" => "generic", "type" => null];
+ } else {
+ $meta = ["isa" => "generic", "type" => null];
+ }
+ }
+ }
+
+ const DEBUG_QUERIES = false;
+
+ function useStmt(\PDO $db, ?\PDOStatement &$stmt=null, ?string &$sql=null): bool {
+ if (static::DEBUG_QUERIES) { #XXX
+ error_log($this->sql);
+ //error_log(var_export($this->bindings, true));
+ }
+ if ($this->bindings !== null) {
+ $stmt = $db->prepare($this->sql);
+ foreach ($this->bindings as $name => $value) {
+ $this->verifixBindings($value);
+ $stmt->bindValue($name, $value);
+ }
+ return true;
+ } else {
+ $sql = $this->sql;
+ return false;
+ }
+ }
+}
diff --git a/php/src/db/pdo/_query_create.php b/php/src/db/pdo/_query_create.php
new file mode 100644
index 0000000..997349a
--- /dev/null
+++ b/php/src/db/pdo/_query_create.php
@@ -0,0 +1,10 @@
+ $sqlite->file,
+ "flags" => $sqlite->flags,
+ "encryption_key" => $sqlite->encryptionKey,
+ "allow_wal" => $sqlite->allowWal,
+ "config" => $sqlite->config,
+ "migrate" => $sqlite->migration,
+ ], $params));
+ } elseif (is_array($sqlite)) {
+ return new static(null, cl::merge($sqlite, $params));
+ } else {
+ return new static($sqlite, $params);
+ }
+ }
+
+ static function config_enableExceptions(self $sqlite): void {
+ $sqlite->db->enableExceptions(true);
+ }
+ const CONFIG_enableExceptions = [self::class, "config_enableExceptions"];
+
+ /**
+ * @var int temps maximum à attendre que la base soit accessible si elle est
+ * verrouillée
+ */
+ protected const BUSY_TIMEOUT = 30 * 1000;
+
+ static function config_busyTimeout(self $sqlite): void {
+ $sqlite->db->busyTimeout(static::BUSY_TIMEOUT);
+ }
+ const CONFIG_busyTimeout = [self::class, "config_busyTimeout"];
+
+ static function config_enableWalIfAllowed(self $sqlite): void {
+ if ($sqlite->isWalAllowed()) {
+ $sqlite->db->exec("PRAGMA journal_mode=WAL");
+ }
+ }
+ const CONFIG_enableWalIfAllowed = [self::class, "config_enableWalIfAllowed"];
+
+ const ALLOW_WAL = null;
+
+ const DEFAULT_CONFIG = [
+ self::CONFIG_enableExceptions,
+ self::CONFIG_busyTimeout,
+ self::CONFIG_enableWalIfAllowed,
+ ];
+
+ const CONFIG = null;
+
+ const MIGRATE = null;
+
+ const params_SCHEMA = [
+ "file" => ["string", ""],
+ "flags" => ["int", SQLITE3_OPEN_READWRITE + SQLITE3_OPEN_CREATE],
+ "encryption_key" => ["string", ""],
+ "allow_wal" => ["?bool"],
+ "replace_config" => ["?array|callable"],
+ "config" => ["?array|callable"],
+ "migrate" => ["?array|string|callable"],
+ "auto_open" => ["bool", true],
+ ];
+
+ function __construct(?string $file=null, ?array $params=null) {
+ if ($file !== null) $params["file"] = $file;
+ ##schéma
+ $defaultFile = self::params_SCHEMA["file"][1];
+ $this->file = $file = strval($params["file"] ?? $defaultFile);
+ $inMemory = $file === ":memory:" || $file === "";
+ #
+ $defaultFlags = self::params_SCHEMA["flags"][1];
+ $this->flags = intval($params["flags"] ?? $defaultFlags);
+ #
+ $defaultEncryptionKey = self::params_SCHEMA["encryption_key"][1];
+ $this->encryptionKey = strval($params["encryption_key"] ?? $defaultEncryptionKey);
+ #
+ $defaultAllowWal = static::ALLOW_WAL ?? !$inMemory;
+ $this->allowWal = $params["allow_wal"] ?? $defaultAllowWal;
+ # configuration
+ $config = $params["replace_config"] ?? null;
+ if ($config === null) {
+ $config = $params["config"] ?? static::CONFIG;
+ if (is_callable($config)) $config = [$config];
+ $config = cl::merge(static::DEFAULT_CONFIG, $config);
+ }
+ $this->config = $config;
+ # migrations
+ $this->migration = $params["migrate"] ?? static::MIGRATE;
+ #
+ $defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
+ $this->inTransaction = false;
+ if ($params["auto_open"] ?? $defaultAutoOpen) {
+ $this->open();
+ }
+ }
+
+ /** @var string */
+ protected $file;
+
+ /** @var int */
+ protected $flags;
+
+ /** @var string */
+ protected $encryptionKey;
+
+ /** @var bool */
+ protected $allowWal;
+
+ /** vérifier s'il est autorisé de configurer le mode WAL */
+ function isWalAllowed(): bool {
+ return $this->allowWal;
+ }
+
+ /** @var array|string|callable */
+ protected $config;
+
+ /** @var array|string|callable */
+ protected $migration;
+
+ /** @var SQLite3 */
+ protected $db;
+
+ protected bool $inTransaction;
+
+ function open(): self {
+ if ($this->db === null) {
+ $this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey);
+ _config::with($this->config)->configure($this);
+ _migration::with($this->migration)->migrate($this);
+ $this->inTransaction = false;
+ }
+ return $this;
+ }
+
+ function close(): void {
+ if ($this->db !== null) {
+ $this->db->close();
+ $this->db = null;
+ $this->inTransaction = false;
+ }
+ }
+
+ protected function checkStmt($stmt): SQLite3Stmt {
+ return SqliteException::check($this->db, $stmt);
+ }
+
+ protected function checkResult($result): SQLite3Result {
+ return SqliteException::check($this->db, $result);
+ }
+
+ protected function db(): SQLite3 {
+ $this->open();
+ return $this->db;
+ }
+
+ function _exec(string $query): bool {
+ return $this->db()->exec($query);
+ }
+
+ private static function is_insert(?string $sql): bool {
+ if ($sql === null) return false;
+ return preg_match('/^\s*insert\b/i', $sql);
+ }
+
+ function exec($query, ?array $params=null) {
+ $db = $this->db();
+ $query = new _query_base($query, $params);
+ if ($query->useStmt($db, $stmt, $sql)) {
+ try {
+ $result = $stmt->execute();
+ if ($result === false) return false;
+ $result->finalize();
+ if ($query->isInsert()) return $db->lastInsertRowID();
+ else return $db->changes();
+ } finally {
+ $stmt->close();
+ }
+ } else {
+ $result = $db->exec($sql);
+ if ($result === false) return false;
+ if (self::is_insert($sql)) return $db->lastInsertRowID();
+ else return $db->changes();
+ }
+ }
+
+ /** @var ITransactor[] */
+ protected ?array $transactors = null;
+
+ function willUpdate(...$transactors): self {
+ foreach ($transactors as $transactor) {
+ if ($transactor instanceof ITransactor) {
+ $this->transactors[] = $transactor;
+ $transactor->willUpdate();
+ } else {
+ throw ValueException::invalid_type($transactor, ITransactor::class);
+ }
+ }
+ return $this;
+ }
+
+ function inTransaction(): bool {
+ #XXX très imparfait, mais y'a rien de mieux pour le moment :-(
+ return $this->inTransaction;
+ }
+
+ function beginTransaction(?callable $func=null, bool $commit=true): void {
+ $this->db()->exec("begin");
+ $this->inTransaction = true;
+ if ($this->transactors !== null) {
+ foreach ($this->transactors as $transactor) {
+ $transactor->beginTransaction();
+ }
+ }
+ if ($func !== null) {
+ $commited = false;
+ try {
+ nur_func::call($func, $this);
+ if ($commit) {
+ $this->commit();
+ $commited = true;
+ }
+ } finally {
+ if ($commit && !$commited) $this->rollback();
+ }
+ }
+ }
+
+ function commit(): void {
+ $this->inTransaction = false;
+ $this->db()->exec("commit");
+ if ($this->transactors !== null) {
+ foreach ($this->transactors as $transactor) {
+ $transactor->commit();
+ }
+ }
+ }
+
+ function rollback(): void {
+ $this->inTransaction = false;
+ $this->db()->exec("rollback");
+ if ($this->transactors !== null) {
+ foreach ($this->transactors as $transactor) {
+ $transactor->rollback();
+ }
+ }
+ }
+
+ function _get(string $query, bool $entireRow=false) {
+ return $this->db()->querySingle($query, $entireRow);
+ }
+
+ function get($query, ?array $params=null, bool $entireRow=false) {
+ $db = $this->db();
+ $query = new _query_base($query, $params);
+ if ($query->useStmt($db, $stmt, $sql)) {
+ try {
+ $result = $this->checkResult($stmt->execute());
+ try {
+ $row = $result->fetchArray(SQLITE3_ASSOC);
+ if ($row === false) return null;
+ $this->verifixRow($row);
+ if ($entireRow) return $row;
+ else return cl::first($row);
+ } finally {
+ $result->finalize();
+ }
+ } finally {
+ $stmt->close();
+ }
+ } else {
+ return $db->querySingle($sql, $entireRow);
+ }
+ }
+
+ function one($query, ?array $params=null): ?array {
+ return $this->get($query, $params, true);
+ }
+
+ protected function _fetchResult(SQLite3Result $result, ?SQLite3Stmt $stmt=null, $primaryKeys=null): Generator {
+ if ($primaryKeys !== null) $primaryKeys = cl::with($primaryKeys);
+ try {
+ while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) {
+ $this->verifixRow($row);
+ if ($primaryKeys !== null) {
+ $key = implode("-", cl::select($row, $primaryKeys));
+ yield $key => $row;
+ } else {
+ yield $row;
+ }
+ }
+ } finally {
+ $result->finalize();
+ if ($stmt !== null) $stmt->close();
+ }
+ }
+
+ /**
+ * si $primaryKeys est fourni, le résultat est indexé sur la(es) colonne(s)
+ * spécifiée(s)
+ */
+ function all($query, ?array $params=null, $primaryKeys=null): iterable {
+ $db = $this->db();
+ $query = new _query_base($query, $params);
+ if ($query->useStmt($db, $stmt, $sql)) {
+ $result = $this->checkResult($stmt->execute());
+ return $this->_fetchResult($result, $stmt, $primaryKeys);
+ } else {
+ $result = $this->checkResult($db->query($sql));
+ return $this->_fetchResult($result, null, $primaryKeys);
+ }
+ }
+}
diff --git a/php/src/db/sqlite/SqliteException.php b/php/src/db/sqlite/SqliteException.php
new file mode 100644
index 0000000..b71fc38
--- /dev/null
+++ b/php/src/db/sqlite/SqliteException.php
@@ -0,0 +1,18 @@
+lastErrorMsg(), $db->lastErrorCode());
+ }
+
+ static final function wrap(Exception $e): self{
+ return new static($e->getMessage(), $e->getCode(), $e);
+ }
+}
diff --git a/php/src/db/sqlite/SqliteStorage.php b/php/src/db/sqlite/SqliteStorage.php
new file mode 100644
index 0000000..287a9f7
--- /dev/null
+++ b/php/src/db/sqlite/SqliteStorage.php
@@ -0,0 +1,97 @@
+db = Sqlite::with($sqlite);
+ }
+
+ /** @var Sqlite */
+ protected $db;
+
+ function db(): Sqlite {
+ return $this->db;
+ }
+
+ const PRIMARY_KEY_DEFINITION = [
+ "id_" => "integer primary key autoincrement",
+ ];
+
+ function _getCreateSql(CapacitorChannel $channel): string {
+ $query = new _query_base($this->_createSql($channel));
+ return self::format_sql($channel, $query->getSql());
+ }
+
+ function tableExists(string $tableName): bool {
+ $name = $this->db->get([
+ # depuis la version 3.33.0 le nom officiel de la table est sqlite_schema,
+ # mais le nom sqlite_master est toujours valable pour le moment
+ "select name from sqlite_master ",
+ "where" => ["name" => $tableName],
+ ]);
+ return $name !== null;
+ }
+
+ function channelExists(string $name): bool {
+ $name = $this->db->get([
+ "select name from _channels",
+ "where" => ["name" => $name],
+ ]);
+ return $name !== null;
+ }
+
+ protected function _afterCreate(CapacitorChannel $channel): void {
+ $db = $this->db;
+ if (!$this->tableExists("_channels")) {
+ # ne pas créer si la table existe déjà, pour éviter d'avoir besoin d'un
+ # verrou en écriture
+ $db->exec([
+ "create table if not exists",
+ "table" => "_channels",
+ "cols" => [
+ "name" => "varchar primary key",
+ "table_name" => "varchar",
+ "class" => "varchar",
+ ],
+ ]);
+ }
+ if (!$this->channelExists($channel->getName())) {
+ # ne pas insérer si la ligne existe déjà, pour éviter d'avoir besoin d'un
+ # verrou en écriture
+ $db->exec([
+ "insert",
+ "into" => "_channels",
+ "values" => [
+ "name" => $channel->getName(),
+ "table_name" => $channel->getTableName(),
+ "class" => get_class($channel),
+ ],
+ "suffix" => "on conflict do nothing",
+ ]);
+ }
+ }
+
+ protected function _beforeReset(CapacitorChannel $channel): void {
+ $this->db->exec([
+ "delete",
+ "from" => "_channels",
+ "where" => [
+ "name" => $channel->getName(),
+ ],
+ ]);
+ }
+
+ function _exists(CapacitorChannel $channel): bool {
+ return $this->tableExists($channel->getTableName());
+ }
+
+ function close(): void {
+ $this->db->close();
+ }
+}
diff --git a/php/src/db/sqlite/_config.php b/php/src/db/sqlite/_config.php
new file mode 100644
index 0000000..ea7553a
--- /dev/null
+++ b/php/src/db/sqlite/_config.php
@@ -0,0 +1,36 @@
+configs = $configs;
+ }
+
+ /** @var array */
+ protected $configs;
+
+ function configure(Sqlite $sqlite): void {
+ foreach ($this->configs as $key => $config) {
+ if (is_string($config) && !nur_func::is_method($config)) {
+ $sqlite->exec($config);
+ } else {
+ nur_func::ensure_func($config, $this, $args);
+ nur_func::call($config, $sqlite, $key, ...$args);
+ }
+ }
+ }
+}
diff --git a/php/src/db/sqlite/_migration.php b/php/src/db/sqlite/_migration.php
new file mode 100644
index 0000000..d2adf93
--- /dev/null
+++ b/php/src/db/sqlite/_migration.php
@@ -0,0 +1,55 @@
+migrations);
+ } else {
+ return new static($migrations);
+ }
+ }
+
+ const MIGRATE = null;
+
+ function __construct($migrations) {
+ if ($migrations === null) $migrations = static::MIGRATE;
+ if ($migrations === null) $migrations = [];
+ elseif (is_string($migrations)) $migrations = [$migrations];
+ elseif (is_callable($migrations)) $migrations = [$migrations];
+ elseif (!is_array($migrations)) $migrations = [strval($migrations)];
+ $this->migrations = $migrations;
+ }
+
+ /** @var callable[]|string[] */
+ protected $migrations;
+
+ function migrate(Sqlite $sqlite): void {
+ $sqlite->exec("create table if not exists _migration(key varchar primary key, value varchar not null, done integer default 0)");
+ foreach ($this->migrations as $key => $migration) {
+ $exists = $sqlite->get("select 1 from _migration where key = :key and done = 1", [
+ "key" => $key,
+ ]);
+ if (!$exists) {
+ $sqlite->exec("insert or replace into _migration(key, value, done) values(:key, :value, :done)", [
+ "key" => $key,
+ "value" => $migration,
+ "done" => 0,
+ ]);
+ if (is_string($migration) && !nur_func::is_method($migration)) {
+ $sqlite->exec($migration);
+ } else {
+ nur_func::ensure_func($migration, $this, $args);
+ nur_func::call($migration, $sqlite, $key, ...$args);
+ }
+ $sqlite->exec("update _migration set done = 1 where key = :key", [
+ "key" => $key,
+ ]);
+ }
+ }
+ }
+}
diff --git a/php/src/db/sqlite/_query_base.php b/php/src/db/sqlite/_query_base.php
new file mode 100644
index 0000000..b397e0d
--- /dev/null
+++ b/php/src/db/sqlite/_query_base.php
@@ -0,0 +1,62 @@
+sql); #XXX
+ if ($this->bindings !== null) {
+ /** @var SQLite3Stmt $stmt */
+ $stmt = SqliteException::check($db, $db->prepare($this->sql));
+ $close = true;
+ try {
+ foreach ($this->bindings as $param => $value) {
+ $this->verifixBindings($value);
+ SqliteException::check($db, $stmt->bindValue($param, $value));
+ }
+ $close = false;
+ return true;
+ } finally {
+ if ($close) $stmt->close();
+ }
+ } else {
+ $sql = $this->sql;
+ return false;
+ }
+ }
+}
diff --git a/php/src/db/sqlite/_query_create.php b/php/src/db/sqlite/_query_create.php
new file mode 100644
index 0000000..5aa7aa1
--- /dev/null
+++ b/php/src/db/sqlite/_query_create.php
@@ -0,0 +1,10 @@
+getContents();
+ return JsonException::ensure_json_value(self::decode($contents));
+ }
+
+ /** obtenir la valeur JSON correspondant au corps de la requête POST */
+ static final function post_data() {
+ $content = file_get_contents("php://input");
+ return JsonException::ensure_json_value(self::decode($content));
+ }
+
+ /** envoyer $data au format JSON */
+ static final function send($data, bool $exit=true): void {
+ header("Content-Type: application/json");
+ print self::encode($data);
+ if ($exit) exit;
+ }
+
+ const INDENT_TABS = "\t";
+
+ static final function with($data, ?string $indent=null): string {
+ $json = self::encode($data, JSON_PRETTY_PRINT);
+ if ($indent !== null) {
+ $json = preg_replace_callback('/^(?: {4})+/m', function (array $ms) use ($indent) {
+ return str_repeat($indent, strlen($ms[0]) / 4);
+ }, $json);
+ }
+ return $json;
+ }
+
+ static final function dump($data, $output=null): void {
+ file::writer($output)->putContents(self::with($data));
+ }
+}
diff --git a/php/src/ext/yaml.php b/php/src/ext/yaml.php
new file mode 100644
index 0000000..997d66a
--- /dev/null
+++ b/php/src/ext/yaml.php
@@ -0,0 +1,35 @@
+getContents();
+ try {
+ return cl::with(SymfonyYaml::parse($contents));
+ } catch (ParseException $e) {
+ $message = "parse error";
+ if (is_string($input)) $message .= " while loading $input";
+ throw new IOException($message, 0, $e);
+ }
+ }
+
+ static final function with($data, int $indent=2, int $flags=0): string {
+ return SymfonyYaml::dump($data, PHP_INT_MAX, $indent, $flags);
+ }
+
+ static final function dump($data, $output=null, int $indent=2, int $flags=0): void {
+ file::writer($output)->putContents(self::with($data, $indent, $flags));
+ }
+}
diff --git a/php/src/file.php b/php/src/file.php
new file mode 100644
index 0000000..feaa12c
--- /dev/null
+++ b/php/src/file.php
@@ -0,0 +1,91 @@
+close();
+ }
+ }
+ return $file;
+ }
+
+ static function writer($output, ?string $mode="w+b", ?callable $func=null): FileWriter {
+ $file = new FileWriter(self::fix_dash($output), $mode);
+ if ($func !== null) {
+ try {
+ $func($file);
+ } finally {
+ $file->close();
+ }
+ }
+ return $file;
+ }
+
+ static function shared($file, ?callable $func=null): SharedFile {
+ $file = new SharedFile($file);
+ if ($func !== null) {
+ try {
+ $func($file);
+ } finally {
+ $file ->close();
+ }
+ }
+ return $file;
+ }
+
+ static function tmpwriter($destdir=null, ?callable $func=null): TmpfileWriter {
+ $file = new TmpfileWriter($destdir);
+ if ($func !== null) {
+ try {
+ $func($file);
+ } finally {
+ $file->close();
+ }
+ }
+ return $file;
+ }
+
+ static function memory(?callable $func=null): MemoryStream {
+ $file = new MemoryStream();
+ if ($func !== null) {
+ try {
+ $func($file);
+ } finally {
+ $file->close();
+ }
+ }
+ return $file;
+ }
+
+ static function temp(?callable $func=null): TempStream {
+ $file = new TempStream();
+ if ($func !== null) {
+ try {
+ $func($file);
+ } finally {
+ $file->close();
+ }
+ }
+ return $file;
+ }
+}
diff --git a/php/src/file/FileReader.php b/php/src/file/FileReader.php
new file mode 100644
index 0000000..d663296
--- /dev/null
+++ b/php/src/file/FileReader.php
@@ -0,0 +1,51 @@
+ignoreBom = $ignoreBom;
+ if ($input === null) {
+ $fd = STDIN;
+ $close = false;
+ } elseif (is_resource($input)) {
+ $fd = $input;
+ $close = false;
+ } else {
+ $file = $input;
+ if ($mode === null) $mode = static::DEFAULT_MODE;
+ $this->file = $file;
+ $this->mode = $mode;
+ $fd = $this->open();
+ $close = true;
+ }
+ parent::__construct($fd, $close, $throwOnError, $allowLocking);
+ }
+
+ /** @return resource */
+ protected function open() {
+ $fd = parent::open();
+ $this->haveBom = false;
+ if ($this->ignoreBom) {
+ $bom = fread($fd, 3);
+ if ($bom === "\xEF\xBB\xBF") $this->seekOffset = 3;
+ else rewind($fd);
+ }
+ return $fd;
+ }
+
+ function haveBom(): bool {
+ return $this->seekOffset !== 0;
+ }
+}
diff --git a/php/src/file/FileWriter.php b/php/src/file/FileWriter.php
new file mode 100644
index 0000000..b3fdfc9
--- /dev/null
+++ b/php/src/file/FileWriter.php
@@ -0,0 +1,31 @@
+file = $file;
+ $this->mode = $mode;
+ $fd = $this->open();
+ $close = true;
+ }
+ parent::__construct($fd, $close, $throwOnError, $allowLocking);
+ }
+}
diff --git a/php/src/file/IReader.php b/php/src/file/IReader.php
new file mode 100644
index 0000000..36a351b
--- /dev/null
+++ b/php/src/file/IReader.php
@@ -0,0 +1,43 @@
+fd === null) $this->fd = self::memory_fd();
+ return parent::getResource();
+ }
+}
diff --git a/php/src/file/SharedFile.php b/php/src/file/SharedFile.php
new file mode 100644
index 0000000..c14001e
--- /dev/null
+++ b/php/src/file/SharedFile.php
@@ -0,0 +1,15 @@
+fd = $fd;
+ $this->close = $close;
+ $this->throwOnError = $throwOnError ?? static::THROW_ON_ERROR;
+ $this->useLocking = $useLocking ?? static::USE_LOCKING;
+ }
+
+ #############################################################################
+ # File
+
+ /**
+ * @return resource|null retourner la resource associée à ce fichier si cela
+ * a du sens
+ */
+ function getResource() {
+ IOException::ensure_open($this->fd === null);
+ $this->_streamAppendFilters($this->fd);
+ return $this->fd;
+ }
+
+ protected function lock(int $operation, ?int &$wouldBlock=null): bool {
+ $locked = flock($this->getResource(), $operation, $wouldBlock);
+ if ($locked) return true;
+ if ($operation & LOCK_NB) return false;
+ else throw IOException::error();
+ }
+
+ protected function unlock(bool $close=false): void {
+ if ($this->fd !== null) {
+ flock($this->fd, LOCK_UN);
+ if ($close) $this->close();
+ }
+ }
+
+ function isatty(): bool {
+ return stream_isatty($this->getResource());
+ }
+
+ /** obtenir des informations sur le fichier */
+ function fstat(bool $reload=false): array {
+ if ($this->stat === null || $reload) {
+ $fd = $this->getResource();
+ $this->stat = IOException::ensure_valid(fstat($fd), $this->throwOnError);
+ }
+ return $this->stat;
+ }
+
+ function getSize(?int $seekOffset=null): int {
+ if ($seekOffset === null) $seekOffset = $this->seekOffset;
+ return $this->fstat()["size"] - $seekOffset;
+ }
+
+ /** @throws IOException */
+ function ftell(?int $seekOffset=null): int {
+ $fd = $this->getResource();
+ if ($seekOffset === null) $seekOffset = $this->seekOffset;
+ return IOException::ensure_valid(ftell($fd), $this->throwOnError) - $seekOffset;
+ }
+
+ /**
+ * @return int la position après avoir déplacé le pointeur
+ * @throws IOException
+ */
+ function fseek(int $offset, int $whence=SEEK_SET, ?int $seekOffset=null): int {
+ $fd = $this->getResource();
+ if ($seekOffset === null) $seekOffset = $this->seekOffset;
+ if ($whence === SEEK_SET) $offset += $seekOffset;
+ IOException::ensure_valid(fseek($fd, $offset, $whence), $this->throwOnError, -1);
+ return $this->ftell($seekOffset);
+ }
+
+ function seek(int $offset, int $whence=SEEK_SET): self {
+ $this->fseek($offset, $whence);
+ return $this;
+ }
+
+ /** fermer le fichier si c'est nécessaire */
+ function close(bool $close=true, ?int $ifSerial=null): void {
+ AbstractIterator::rewind();
+ if ($this->fd !== null && $close && $this->close && ($ifSerial === null || $this->serial === $ifSerial)) {
+ fclose($this->fd);
+ $this->fd = null;
+ }
+ }
+
+ function copyTo(IWriter $dest, bool $closeWriter=false, bool $closeReader=true): void {
+ $srcr = $this->getResource();
+ $destr = $dest->getResource();
+ if ($srcr !== null && $destr !== null) {
+ while (!feof($srcr)) {
+ fwrite($destr, fread($srcr, 8192));
+ }
+ } else {
+ $dest->fwrite($this->getContents(false));
+ }
+ if ($closeWriter) $dest->close();
+ if ($closeReader) $this->close();
+ }
+
+ const DEFAULT_CSV_FLAVOUR = ref_csv::OO_FLAVOUR;
+
+ /** @var string paramètres pour la lecture et l'écriture de flux au format CSV */
+ protected $csvFlavour;
+
+ function setCsvFlavour(?string $flavour): void {
+ $this->csvFlavour = csv_flavours::verifix($flavour);
+ }
+
+ protected function getCsvParams($fd): array {
+ $flavour = $this->csvFlavour;
+ if ($flavour === null) {
+ self::probe_fd($fd, $seekable, $readable);
+ if (!$seekable || !$readable) $fd = null;
+ if ($fd === null) {
+ # utiliser la valeur par défaut
+ $flavour = static::DEFAULT_CSV_FLAVOUR;
+ } else {
+ # il faut déterminer le type de fichier CSV en lisant la première ligne
+ $pos = IOException::ensure_valid(ftell($fd));
+ $line = fgets($fd);
+ if ($line !== false) $line = strpbrk($line, ",;\t");
+ if ($line === false) {
+ # aucun séparateur trouvé, prender la valeur par défaut
+ $flavour = static::DEFAULT_CSV_FLAVOUR;
+ } else {
+ $flavour = substr($line, 0, 1);
+ $flavour = csv_flavours::verifix($flavour);
+ }
+ IOException::ensure_valid(fseek($fd, $pos), true, -1);
+ }
+ $this->csvFlavour = $flavour;
+ }
+ return csv_flavours::get_params($flavour);
+ }
+
+ #############################################################################
+ # Reader
+
+ /** @throws IOException */
+ function fread(int $length): string {
+ $fd = $this->getResource();
+ return IOException::ensure_valid(fread($fd, $length), $this->throwOnError);
+ }
+
+ /**
+ * lire la prochaine ligne. la ligne est retournée avec le caractère de fin
+ * de ligne[\r]\n
+ *
+ * @throws EOFException si plus aucune ligne n'est disponible
+ * @throws IOException si une erreur se produit
+ */
+ function fgets(?int $length=null): string {
+ $fd = $this->getResource();
+ if ($length === null) $r = fgets($fd);
+ else $r = fgets($fd, $length);
+ return EOFException::ensure_not_eof($r, $this->throwOnError);
+ }
+
+ /** @throws IOException */
+ function fpassthru(): int {
+ $fd = $this->getResource();
+ return IOException::ensure_valid(fpassthru($fd), $this->throwOnError);
+ }
+
+ /**
+ * retourner la prochaine ligne au format CSV ou null si le fichier est arrivé
+ * à sa fin
+ */
+ function fgetcsv(): ?array {
+ $fd = $this->getResource();
+ $params = $this->getCsvParams($fd);
+ $row = fgetcsv($fd, 0, $params[0], $params[1], $params[2]);
+ if ($row === false && feof($fd)) return null;
+ return IOException::ensure_valid($row, $this->throwOnError);
+ }
+
+ /**
+ * lire la prochaine ligne. la ligne est retournée *sans* le caractère de fin
+ * de ligne [\r]\n
+ *
+ * @throws EOFException si plus aucune ligne n'est disponible
+ * @throws IOException si une erreur se produit
+ */
+ function readLine(): ?string {
+ return str::strip_nl($this->fgets());
+ }
+
+ /** lire et retourner toutes les lignes */
+ function readLines(): array {
+ return iterator_to_array($this);
+ }
+
+ /**
+ * verrouiller le fichier en lecture de façon inconditionelle (ignorer la
+ * valeur de $useLocking). bloquer jusqu'à ce que le verrou soit disponible
+ */
+ function lockRead(): void {
+ $this->lock(LOCK_SH);
+ }
+
+ /**
+ * essayer de verrouiller le fichier en lecture. retourner true si l'opération
+ * réussit. dans ce cas, il faut appeler {@link getReader()} avec l'argument
+ * true
+ */
+ function canRead(): bool {
+ if ($this->useLocking) return $this->lock(LOCK_SH + LOCK_NB);
+ else return true;
+ }
+
+ /**
+ * verrouiller en mode partagé puis retourner un objet permettant de lire le
+ * fichier.
+ */
+ function getReader(bool $alreadyLocked=false): IReader {
+ if ($this->useLocking && !$alreadyLocked) $this->lock(LOCK_SH);
+ return new class($this->fd, ++$this->serial, $this) extends Stream {
+ function __construct($fd, int $serial, Stream $parent) {
+ $this->parent = $parent;
+ $this->serial = $serial;
+ parent::__construct($fd);
+ }
+
+ /** @var Stream */
+ private $parent;
+
+ function close(bool $close=true, ?int $ifSerial=null): void {
+ if ($this->parent !== null && $close) {
+ $this->parent->close(true, $this->serial);
+ $this->fd = null;
+ $this->parent = null;
+ }
+ }
+ };
+ }
+
+ /** retourner le contenu du fichier sous forme de chaine */
+ function getContents(bool $close=true, bool $alreadyLocked=false): string {
+ $useLocking = $this->useLocking;
+ if ($useLocking && !$alreadyLocked) $this->lock(LOCK_SH);
+ try {
+ return IOException::ensure_valid(stream_get_contents($this->fd), $this->throwOnError);
+ } finally {
+ if ($useLocking) $this->unlock($close);
+ elseif ($close) $this->close();
+ }
+ }
+
+ /** @var bool faut-il faire un mapping pour la compatibilité avec nur/sery */
+ const NURSERY_COMPAT_ENABLED = true;
+
+ /**
+ * @var string[] mappings pour la compatibilité avec des fichiers générés par
+ * nur/sery
+ */
+ const NURSERY_COMPAT_MAPPING = [
+ 'O:22:"nur\sery\php\time\Date":' => 'O:19:"nulib\php\time\Date":',
+ 'O:26:"nur\sery\php\time\DateTime":' => 'O:23:"nulib\php\time\DateTime":',
+ 'O:23:"nur\sery\php\time\Delay":' => 'O:20:"nulib\php\time\Delay":',
+ ];
+
+ static function nursery_compat_verifix(string $contents): string {
+ if (static::NURSERY_COMPAT_ENABLED) {
+ foreach (self::NURSERY_COMPAT_MAPPING as $from => $to) {
+ $contents = str_replace($from, $to, $contents);
+ }
+ }
+ return $contents;
+ }
+
+ function unserialize(?array $options=null, bool $close=true, bool $alreadyLocked=false) {
+ $contents = $this->getContents($close, $alreadyLocked);
+ $contents = self::nursery_compat_verifix($contents);
+ $args = [$contents];
+ if ($options !== null) $args[] = $options;
+ return unserialize(...$args);
+ }
+
+ function decodeJson(bool $close=true, bool $alreadyLocked=false) {
+ $contents = $this->getContents($close, $alreadyLocked);
+ return json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
+ }
+
+ #############################################################################
+ # Iterator
+
+ protected function iter_setup(): void {
+ }
+
+ protected function iter_next(&$key) {
+ try {
+ return $this->fgets();
+ } catch (EOFException $e) {
+ throw new NoMoreDataException();
+ }
+ }
+
+ private function _rewindFd(): void {
+ self::probe_fd($this->fd, $seekable);
+ if ($seekable) $this->fseek(0);
+ }
+
+ protected function iter_teardown(): void {
+ $this->_rewindFd();
+ }
+
+ function rewind(): void {
+ # il faut toujours faire un rewind sur la resource, que l'itérateur aie été
+ # initialisé ou non
+ if ($this->_hasIteratorBeenSetup()) parent::rewind();
+ else $this->_rewindFd();
+ }
+
+ #############################################################################
+ # Writer
+
+ /** @throws IOException */
+ function ftruncate(int $size=0, bool $rewind=true): self {
+ $fd = $this->getResource();
+ IOException::ensure_valid(ftruncate($fd, $size), $this->throwOnError);
+ if ($rewind) rewind($fd);
+ return $this;
+ }
+
+ /** @throws IOException */
+ function fwrite(string $data, ?int $length=null): int {
+ $fd = $this->getResource();
+ if ($length === null) $r = fwrite($fd, $data);
+ else $r = fwrite($fd, $data, $length);
+ return IOException::ensure_valid($r, $this->throwOnError);
+ }
+
+ /** @throws IOException */
+ function fputcsv(array $row): void {
+ $fd = $this->getResource();
+ $params = $this->getCsvParams($fd);
+ if (csv_flavours::is_dumb($this->csvFlavour, $sep)) {
+ $line = [];
+ foreach ($row as $col) {
+ $line[] = strval($col);
+ }
+ $line = implode($sep, $line);
+ IOException::ensure_valid(fwrite($fd, "$line\n"), $this->throwOnError);
+ } else {
+ IOException::ensure_valid(fputcsv($fd, $row, $params[0], $params[1], $params[2]), $this->throwOnError);
+ }
+ }
+
+ /** @throws IOException */
+ function fflush(): self {
+ $fd = $this->getResource();
+ IOException::ensure_valid(fflush($fd), $this->throwOnError);
+ return $this;
+ }
+
+ function writeLines(?iterable $lines): IWriter {
+ if ($lines !== null) {
+ foreach ($lines as $line) {
+ $this->fwrite($line);
+ $this->fwrite("\n");
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * verrouiller le fichier en écriture de façon inconditionelle (ignorer la
+ * valeur de $useLocking). bloquer jusqu'à ce que le verrou soit disponible
+ */
+ function lockWrite(): void {
+ $this->lock(LOCK_EX);
+ }
+
+ /**
+ * essayer de verrouiller le fichier en écriture. retourner true si l'opération
+ * réussit. dans ce cas, il faut appeler {@link getWriter()} avec l'argument
+ * true
+ */
+ function canWrite(): bool {
+ if ($this->useLocking) return $this->lock(LOCK_EX + LOCK_NB);
+ else return true;
+ }
+
+ /**
+ * verrouiller en mode exclusif puis retourner un objet permettant d'écrire
+ * dans le fichier
+ */
+ function getWriter(bool $alreadyLocked=false): IWriter {
+ if ($this->useLocking && !$alreadyLocked) $this->lock(LOCK_EX);
+ return new class($this->fd, ++$this->serial, $this) extends Stream {
+ function __construct($fd, int $serial, Stream $parent) {
+ $this->parent = $parent;
+ $this->serial = $serial;
+ parent::__construct($fd);
+ }
+
+ /** @var Stream */
+ private $parent;
+
+ function close(bool $close=true, ?int $ifSerial=null): void {
+ if ($this->parent !== null && $close) {
+ $this->parent->close(true, $this->serial);
+ $this->fd = null;
+ $this->parent = null;
+ }
+ }
+ };
+ }
+
+ function putContents(string $contents, bool $close=true, bool $alreadyLocked=false): void {
+ $useLocking = $this->useLocking;
+ if ($useLocking && !$alreadyLocked) $this->lock(LOCK_EX);
+ try {
+ $this->fwrite($contents);
+ } finally {
+ if ($useLocking) $this->unlock($close);
+ elseif ($close) $this->close();
+ }
+ }
+
+ function serialize($object, bool $close=true, bool $alreadyLocked=false): void {
+ $this->putContents(serialize($object), $close, $alreadyLocked);
+ }
+
+ function encodeJson($data, bool $close=true, bool $alreadyLocked=false): void {
+ $contents = json_encode($data, JSON_UNESCAPED_SLASHES + JSON_UNESCAPED_UNICODE);
+ $this->putContents($contents, $close, $alreadyLocked);
+ }
+
+ /**
+ * annuler une tentative d'écriture commencée avec {@link self::canWrite()}
+ */
+ function cancelWrite(bool $close=true): void {
+ if ($this->useLocking) $this->unlock($close);
+ elseif ($close) $this->close();
+ }
+}
diff --git a/php/src/file/TStreamFilter.php b/php/src/file/TStreamFilter.php
new file mode 100644
index 0000000..93063ae
--- /dev/null
+++ b/php/src/file/TStreamFilter.php
@@ -0,0 +1,49 @@
+filters[] = [$filterName, $readWrite, $params];
+ }
+
+ function prependFilter(string $filterName, ?int $readWrite=null, $params=null): void {
+ if ($this->filters === null) $this->filters = [];
+ array_unshift($this->filters, [$filterName, $readWrite, $params]);
+ }
+
+ function setEncodingFilter(string $from, string $to): void {
+ if ($to !== $from) {
+ $this->appendFilter("convert.iconv.$from.$to");
+ }
+ }
+
+ /**
+ * @param $fd resource
+ * @throws IOException
+ */
+ protected function _streamAppendFilters($fd): void {
+ if ($this->filters !== null) {
+ foreach ($this->filters as [$filterName, $readWrite, $params]) {
+ if (stream_filter_append($fd, $filterName, $readWrite, $params) === false) {
+ throw new IOException("unable to add filter $filterName");
+ }
+ }
+ $this->filters = null;
+ }
+ }
+
+ /**
+ * @param $file _IFile
+ */
+ protected function _appendFilters($file): void {
+ if ($this->filters !== null) {
+ foreach ($this->filters as [$filterName, $readWrite, $params]) {
+ $file->appendFilter($filterName, $readWrite, $params);
+ }
+ }
+ }
+}
diff --git a/php/src/file/TempStream.php b/php/src/file/TempStream.php
new file mode 100644
index 0000000..28961c5
--- /dev/null
+++ b/php/src/file/TempStream.php
@@ -0,0 +1,28 @@
+maxMemory = $maxMemory ?? static::MAX_MEMORY;
+ parent::__construct($this->tempFd(), true, $throwOnError);
+ }
+
+ /** @var int */
+ protected $maxMemory;
+
+ protected function tempFd() {
+ return fopen("php://temp/maxmemory:$this->maxMemory", "w+b");
+ }
+
+ function getResource() {
+ if ($this->fd === null) $this->fd = $this->tempFd();
+ return parent::getResource();
+ }
+}
diff --git a/php/src/file/TmpfileWriter.php b/php/src/file/TmpfileWriter.php
new file mode 100644
index 0000000..2292235
--- /dev/null
+++ b/php/src/file/TmpfileWriter.php
@@ -0,0 +1,99 @@
+delete = true;
+ } elseif (is_file($destdir)) {
+ # si on spécifie un fichier qui existe le prendre comme "fichier
+ # temporaire" mais ne pas le supprimer automatiquement
+ $file = $destdir;
+ if (!path::is_qualified($file)) $file = path::join($tmpDir, $file);
+ $this->delete = false;
+ } else {
+ # un chemin qui n'existe pas: ne le sélectionner que si le répertoire
+ # existe. dans ce cas, le fichier sera créé automatiquement, mais pas
+ # supprimé
+ if (!is_dir(dirname($destdir))) {
+ throw new IOException("$destdir: no such file or directory");
+ }
+ $file = $destdir;
+ $this->delete = false;
+ }
+ parent::__construct($file, $mode, $throwOnError, $allowLocking);
+ }
+
+ /** @var bool */
+ protected $delete;
+
+ /** désactiver la suppression automatique du fichier temporaire */
+ function keep(): self {
+ $this->delete = false;
+ return $this;
+ }
+
+ function __destruct() {
+ $this->close();
+ if ($this->delete) $this->delete();
+ }
+
+ /** supprimer le fichier. NB: le flux **n'est pas** fermé au préalable */
+ function delete(): self {
+ if (file_exists($this->file)) unlink($this->file);
+ return $this;
+ }
+
+ /**
+ * renommer le fichier. le flux est fermé d'abord
+ *
+ * @param int|null $defaultMode mode par défaut si le fichier destination
+ * n'existe pas. sinon, changer le mode du fichier temporaire à la valeur du
+ * fichier destination après renommage
+ * @param bool $setOwner si le propriétaire et/ou le groupe du fichier
+ * temporaire ne sont pas les mêmes que le fichier destination, tenter de
+ * changer le propriétaire et le groupe du fichier temporaire à la valeur
+ * du fichier destination après le renommage (nécessite les droits de root)
+ * @throws IOException
+ */
+ function rename(string $dest, ?int $defaultMode=0644, bool $setOwner=true): void {
+ $this->close();
+ $file = $this->file;
+ if (file_exists($dest)) {
+ $mode = fileperms($dest);
+ if ($setOwner) {
+ $tmpowner = fileowner($file);
+ $owner = fileowner($dest);
+ $tmpgroup = filegroup($file);
+ $group = filegroup($dest);
+ } else {
+ $owner = $group = null;
+ }
+ } else {
+ $mode = $defaultMode;
+ $owner = $group = null;
+ }
+ if (!rename($file, $dest)) {
+ throw new IOException("$file: unable to rename to $dest");
+ }
+ $this->file = $dest;
+ if ($mode !== null) chmod($dest, $mode);
+ if ($owner !== null) {
+ if ($owner !== $tmpowner) chown($dest, $owner);
+ if ($group !== $tmpgroup) chgrp($dest, $group);
+ }
+ if ($mode !== null || $owner !== null) clearstatcache(true, $file);
+ }
+}
diff --git a/php/src/file/_File.php b/php/src/file/_File.php
new file mode 100644
index 0000000..80a50f2
--- /dev/null
+++ b/php/src/file/_File.php
@@ -0,0 +1,39 @@
+file;
+ }
+
+ /** @var string */
+ protected $mode;
+
+ function getMode(): string {
+ return $this->mode;
+ }
+
+ /** @return resource */
+ protected function open() {
+ return IOException::ensure_valid(@fopen($this->file, $this->mode));
+ }
+
+ /** @return resource */
+ function getResource() {
+ if ($this->fd === null && $this->file !== null) $this->fd = $this->open();
+ return parent::getResource();
+ }
+
+ /** streamer le contenu du fichier en sortie */
+ function readfile(?string $contentType=null, ?string $charset=null, ?string $filename=null, string $disposition=null): bool {
+ if ($contentType !== null) http::content_type($contentType, $charset);
+ if ($filename !== null) http::download_as($filename, $disposition);
+ return readfile($this->file) !== false;
+ }
+}
diff --git a/php/src/file/_IFile.php b/php/src/file/_IFile.php
new file mode 100644
index 0000000..f62560e
--- /dev/null
+++ b/php/src/file/_IFile.php
@@ -0,0 +1,57 @@
+csvFlavour = csv_flavours::verifix($csvFlavour);
+ parent::__construct($output, $params);
+ }
+
+ protected function _write(array $row, ?array $colStyles =null, ?array $rowStyle=null): void {
+ $this->fputcsv($row);
+ }
+
+ function _sendContentType(): void {
+ http::content_type("text/csv");
+ }
+
+ protected function _checkOk(): bool {
+ $size = $this->ftell();
+ if ($size === 0) return false;
+ $this->rewind();
+ return true;
+ }
+}
diff --git a/php/src/file/csv/CsvReader.php b/php/src/file/csv/CsvReader.php
new file mode 100644
index 0000000..d923976
--- /dev/null
+++ b/php/src/file/csv/CsvReader.php
@@ -0,0 +1,41 @@
+csvFlavour = $params["csv_flavour"] ?? null;
+ $this->inputEncoding = $params["input_encoding"] ?? null;
+ }
+
+ protected ?string $csvFlavour;
+
+ protected ?string $inputEncoding;
+
+ function getIterator() {
+ $reader = new FileReader(file::fix_dash($this->input));
+ $inputEncoding = $this->inputEncoding;
+ if ($inputEncoding !== null) {
+ $reader->appendFilter("convert.iconv.$inputEncoding.utf-8");
+ }
+ $reader->setCsvFlavour($this->csvFlavour);
+ while (($row = $reader->fgetcsv()) !== null) {
+ foreach ($row as &$col) {
+ $this->verifixCol($col);
+ }; unset($col);
+ if ($this->cookRow($row)) {
+ yield $row;
+ $this->idest++;
+ }
+ $this->isrc++;
+ }
+ $reader->close();
+ }
+}
diff --git a/php/src/file/csv/csv_flavours.php b/php/src/file/csv/csv_flavours.php
new file mode 100644
index 0000000..4bc7bd9
--- /dev/null
+++ b/php/src/file/csv/csv_flavours.php
@@ -0,0 +1,59 @@
+ ref_csv::OO_FLAVOUR,
+ "ooffice" => ref_csv::OO_FLAVOUR,
+ ref_csv::OOCALC => ref_csv::OO_FLAVOUR,
+ "xl" => ref_csv::XL_FLAVOUR,
+ "excel" => ref_csv::XL_FLAVOUR,
+ ref_csv::MSEXCEL => ref_csv::XL_FLAVOUR,
+ "dumb;" => ref_csv::DUMB_XL_FLAVOUR,
+ "dumb," => ref_csv::DUMB_OO_FLAVOUR,
+ "dumb" => ref_csv::DUMB_FLAVOUR,
+ ];
+
+ const ENCODINGS = [
+ ref_csv::OO_FLAVOUR => ref_csv::OO_ENCODING,
+ ref_csv::XL_FLAVOUR => ref_csv::XL_ENCODING,
+ ref_csv::DUMB_FLAVOUR => ref_csv::DUMB_ENCODING,
+ ];
+
+ static final function verifix(?string $flavour): ?string {
+ if ($flavour === null) return null;
+ $lflavour = strtolower($flavour);
+ if (array_key_exists($lflavour, self::MAP)) {
+ $flavour = self::MAP[$lflavour];
+ }
+ if (strlen($flavour) < 1) $flavour .= ",";
+ if (strlen($flavour) < 2) $flavour .= "\"";
+ if (strlen($flavour) < 3) $flavour .= "\\";
+ return $flavour;
+ }
+
+ static final function get_name(string $flavour): string {
+ if ($flavour == ref_csv::OO_FLAVOUR) return ref_csv::OOCALC;
+ elseif ($flavour == ref_csv::XL_FLAVOUR) return ref_csv::MSEXCEL;
+ else return $flavour;
+ }
+
+ static final function get_params(string $flavour): array {
+ return [$flavour[0], $flavour[1], $flavour[2]];
+ }
+
+ static final function get_encoding(string $flavour): ?string {
+ return cl::get(self::ENCODINGS, $flavour);
+ }
+
+ static final function is_dumb(string $flavour, ?string &$sep): bool {
+ if (!str::del_prefix($flavour, "xxx")) return false;
+ $sep = $flavour;
+ if (!$sep) $sep = ";";
+ return true;
+ }
+}
diff --git a/php/src/file/tab/AbstractBuilder.php b/php/src/file/tab/AbstractBuilder.php
new file mode 100644
index 0000000..f1ec869
--- /dev/null
+++ b/php/src/file/tab/AbstractBuilder.php
@@ -0,0 +1,177 @@
+schema = $params["schema"] ?? static::SCHEMA;
+ $this->headers = $params["headers"] ?? static::HEADERS;
+ $this->useHeaders = $params["use_headers"] ?? static::USE_HEADERS;
+ $rows = $params["rows"] ?? null;
+ if (is_callable($rows)) $rows = $rows();
+ $this->rows = $rows;
+ $this->index = 0;
+ $cookFunc = $params["cook_func"] ?? null;
+ $cookCtx = $cookArgs = null;
+ if ($cookFunc !== null) {
+ nur_func::ensure_func($cookFunc, $this, $cookArgs);
+ $cookCtx = nur_func::_prepare($cookFunc);
+ }
+ $this->cookCtx = $cookCtx;
+ $this->cookArgs = $cookArgs;
+ $this->output = $params["output"] ?? static::OUTPUT;
+ $maxMemory = $params["max_memory"] ?? null;
+ $throwOnError = $params["throw_on_error"] ?? null;
+ parent::__construct($maxMemory, $throwOnError);
+ }
+
+ protected ?array $schema;
+
+ protected ?array $headers;
+
+ protected bool $useHeaders;
+
+ protected ?iterable $rows;
+
+ protected int $index;
+
+ protected ?string $output;
+
+ protected ?array $cookCtx;
+
+ protected ?array $cookArgs;
+
+ protected function ensureHeaders(?array $row=null): void {
+ if ($this->headers !== null || !$this->useHeaders) return;
+ if ($this->schema === null) $headers = null;
+ else $headers = array_keys($this->schema);
+ if ($headers === null && $row !== null) $headers = array_keys($row);
+ $this->headers = $headers;
+ }
+
+ protected abstract function _write(array $row, ?array $colStyles =null, ?array $rowStyle=null): void;
+
+ protected bool $wroteHeaders = false;
+
+ function writeHeaders(?array $headers=null): void {
+ if ($this->wroteHeaders) return;
+ if ($this->useHeaders) {
+ if ($headers !== null) $this->headers = $headers;
+ else $this->ensureHeaders();
+ if ($this->headers !== null) $this->_write($this->headers);
+ }
+ $this->wroteHeaders = true;
+ }
+
+ protected function cookRow(?array $row): ?array {
+ if ($this->cookCtx !== null) {
+ $args = cl::merge([$row], $this->cookArgs);
+ $row = nur_func::_call($this->cookCtx, $args);
+ }
+ if ($row !== null) {
+ foreach ($row as &$col) {
+ # formatter les dates
+ if ($col instanceof DateTime) {
+ $col = $col->format();
+ } elseif ($col instanceof DateTimeInterface) {
+ $col = DateTime::with($col)->format();
+ }
+ }; unset($col);
+ }
+ return $row;
+ }
+
+ function write(?array $row, ?array $colsStyle=null, ?array $rowStyle=null): void {
+ $row = $this->cookRow($row);
+ if ($row === null) return;
+ $this->writeHeaders(array_keys($row));
+ $this->_write($row, $colsStyle, $rowStyle);
+ $this->index++;
+ }
+
+ function writeAll(?iterable $rows=null, ?array $rowStyle=null): void {
+ $unsetRows = false;
+ if ($rows === null) {
+ $rows = $this->rows;
+ $unsetRows = true;
+ }
+ if ($rows !== null) {
+ foreach ($rows as $row) {
+ $this->write(cl::with($row), null, $rowStyle);
+ }
+ }
+ if ($unsetRows) $this->rows = null;
+ }
+
+ abstract protected function _sendContentType(): void;
+
+ protected bool $sentHeaders = false;
+
+ function sendHeaders(): void {
+ if ($this->sentHeaders) return;
+ $this->_sendContentType();
+ $output = $this->output;
+ if ($output !== null) {
+ http::download_as(path::filename($output));
+ }
+ $this->sentHeaders = true;
+ }
+
+ protected function _build(?iterable $rows=null): void {
+ $this->writeAll($rows);
+ $this->writeHeaders();
+ }
+
+ abstract protected function _checkOk(): bool;
+
+ protected bool $built = false, $closed = false;
+
+ function build(?iterable $rows=null, bool $close=true): bool {
+ $ok = true;
+ if (!$this->built) {
+ $this->_build($rows);
+ $this->built = true;
+ }
+ if ($close && !$this->closed) {
+ $ok = $this->_checkOk();
+ $this->closed = true;
+ }
+ return $ok;
+ }
+
+ function sendFile(?iterable $rows=null): int {
+ if (!$this->built) {
+ $this->_build($rows);
+ $this->built = true;
+ }
+ if (!$this->closed) {
+ if (!$this->_checkOk()) return 0;
+ $this->closed = true;
+ }
+ $this->sendHeaders();
+ return $this->fpassthru();
+ }
+}
diff --git a/php/src/file/tab/AbstractReader.php b/php/src/file/tab/AbstractReader.php
new file mode 100644
index 0000000..6418235
--- /dev/null
+++ b/php/src/file/tab/AbstractReader.php
@@ -0,0 +1,129 @@
+schema = $params["schema"] ?? static::SCHEMA;
+ $this->headers = $params["headers"] ?? static::HEADERS;
+ $this->useHeaders = $params["use_headers"] ?? static::USE_HEADERS;
+ $this->input = $params["input"] ?? static::INPUT;
+ $this->trim = boolval($params["trim"] ?? static::TRIM);
+ $this->emptyAsNull = boolval($params["empty_as_null"] ?? static::EMPTY_AS_NULL);
+ $this->parseNone = boolval($params["parse_none"] ?? static::PARSE_NONE);
+ $this->parseNumeric = boolval($params["parse_numeric"] ?? static::PARSE_NUMERIC);
+ $this->parseDate = boolval($params["parse_date"] ?? static::PARSE_DATE);
+ }
+
+ protected ?array $schema;
+
+ protected ?array $headers;
+
+ protected bool $useHeaders;
+
+ protected $input;
+
+ protected bool $trim;
+
+ protected bool $emptyAsNull;
+
+ protected bool $parseNone;
+
+ protected bool $parseNumeric;
+
+ protected bool $parseDate;
+
+ protected int $isrc = 0, $idest = 0;
+
+ protected function cookRow(array &$row): bool {
+ if (!$this->useHeaders) return true;
+ if ($this->isrc == 0) {
+ # ligne d'en-tête
+ $headers = $this->headers;
+ if ($headers === null) {
+ if ($this->schema === null) $headers = null;
+ else $headers = array_keys($this->schema);
+ if ($headers === null) $headers = $row;
+ $this->headers = $headers;
+ }
+ return false;
+ }
+ A::ensure_size($row, count($this->headers));
+ $row = array_combine($this->headers, $row);
+ return true;
+ }
+
+ protected function verifixCol(&$col): void {
+ if ($this->trim && is_string($col)) {
+ $col = trim($col);
+ }
+ if ($this->emptyAsNull && $col === "") {
+ # valeur vide --> null
+ $col = null;
+ }
+ if (!is_string($col) || $this->parseNone) return;
+ if ($this->parseDate) {
+ if (DateTime::isa_datetime($col, true)) {
+ # datetime
+ $col = new DateTime($col);
+ } elseif (DateTime::isa_date($col, true)) {
+ # date
+ $col = new Date($col);
+ }
+ if (!is_string($col)) return;
+ }
+ $parseNumeric = $this->parseNumeric || substr($col, 0, 1) !== "0";
+ if ($parseNumeric) {
+ $tmp = str_replace(",", ".", $col);
+ $float = strpos($tmp, ".") !== false;
+ if (is_numeric($tmp)) {
+ if ($float) $col = floatval($tmp);
+ else $col = intval($tmp);
+ }
+ }
+ }
+}
diff --git a/php/src/file/tab/IBuilder.php b/php/src/file/tab/IBuilder.php
new file mode 100644
index 0000000..9cc794e
--- /dev/null
+++ b/php/src/file/tab/IBuilder.php
@@ -0,0 +1,14 @@
+isExt(".csv")) {
+ $class = CsvBuilder::class;
+ } else {
+ $class = static::class;
+ if ($builder->isExt(".ods")) {
+ $params["ss_type"] = "ods";
+ } else {
+ $params["ss_type"] = "xlsx";
+ }
+ }
+ return new $class($builder->name, $params);
+ }
+
+ if (is_string($builder)) {
+ $params["output"] = $builder;
+ } elseif (is_array($builder)) {
+ $params = cl::merge($builder, $params);
+ } elseif ($builder !== null) {
+ throw ValueException::invalid_type($builder, self::class);
+ }
+
+ $output = $params["output"] ?? null;
+ $ssType = null;
+ if (is_string($output)) {
+ $ext = path::ext($output);
+ if ($output === "-" || $ext === ".csv") {
+ $class = CsvBuilder::class;
+ } elseif ($ext === ".ods") {
+ $ssType = "ods";
+ } elseif ($ext === ".xlsx") {
+ $ssType = "xlsx";
+ } else {
+ $ssType = "xlsx";
+ }
+ }
+ $params["ss_type"] = $ssType;
+ if ($class === null) $class = static::class;
+ return new $class(null, $params);
+ }
+}
diff --git a/php/src/file/tab/TAbstractReader.php b/php/src/file/tab/TAbstractReader.php
new file mode 100644
index 0000000..f6037c7
--- /dev/null
+++ b/php/src/file/tab/TAbstractReader.php
@@ -0,0 +1,55 @@
+isExt(".csv")) {
+ $class = CsvReader::class;
+ } else {
+ $class = static::class;
+ if ($reader->isExt(".ods")) {
+ $params["ss_type"] = "ods";
+ } else {
+ $params["ss_type"] = "xlsx";
+ }
+ }
+ return new $class($reader->getFile(), $params);
+ }
+
+ if (is_string($reader)) {
+ $params["input"] = $reader;
+ } elseif (is_array($reader)) {
+ $params = cl::merge($reader, $params);
+ } elseif ($reader !== null) {
+ throw ValueException::invalid_type($reader, self::class);
+ }
+
+ $input = $params["input"] ?? null;
+ $ssType = null;
+ if (is_string($input)) {
+ $ext = path::ext($input);
+ if ($input === "-" || $ext === ".csv") {
+ $class = CsvReader::class;
+ } elseif ($ext === ".ods") {
+ $ssType = "ods";
+ } elseif ($ext === ".xlsx") {
+ $ssType = "xlsx";
+ } else {
+ $ssType = "xlsx";
+ }
+ }
+ $params["ss_type"] = $ssType;
+ if ($class === null) $class = static::class;
+ return new $class(null, $params);
+ }
+}
diff --git a/php/src/file/web/Upload.php b/php/src/file/web/Upload.php
new file mode 100644
index 0000000..ce4d19b
--- /dev/null
+++ b/php/src/file/web/Upload.php
@@ -0,0 +1,123 @@
+ "Ceci n'est pas un fichier téléversé",
+ "nofile" => "Aucun fichier n'a été fourni",
+ "toobig" => "Le fichier que vous avez fourni est trop volumineux.",
+ "unknown" => "Une erreur s'est produite pendant le transfert du fichier. Veuillez réessayer.",
+ ];
+
+ protected static function error(string $message) {
+ return new ValueException(static::MESSAGES[$message]);
+ }
+
+ function __construct(?array $file, bool $required=true, bool $check=true) {
+ parent::__construct($file);
+ if ($check) $this->check($required);
+ }
+
+ function check(bool $required=true, bool $throw=true): bool {
+ $file = $this->data;
+ if ($file) {
+ $name = $file["name"] ?? null;
+ $type = $file["type"] ?? null;
+ $error = $file["error"] ?? null;
+ if (!is_scalar($name) || !is_scalar($type) || !is_scalar($error)) {
+ if ($throw) throw static::error("invalid");
+ else return false;
+ }
+ switch ($error) {
+ case UPLOAD_ERR_OK:
+ break;
+ case UPLOAD_ERR_NO_FILE:
+ if ($required) {
+ if ($throw) throw self::error("nofile");
+ else return false;
+ }
+ break;
+ case UPLOAD_ERR_INI_SIZE:
+ case UPLOAD_ERR_FORM_SIZE:
+ if ($throw) throw self::error("toobig");
+ else return false;
+ default:
+ if ($throw) self::error("unknown");
+ else return false;
+ }
+ } elseif ($required) {
+ if ($throw) throw static::error("nofile");
+ else return false;
+ }
+ return true;
+ }
+
+ const _AUTO_PROPERTIES = [
+ "tmpName" => "tmp_name",
+ "fullPath" => "full_path",
+ ];
+ function &__get($name) {
+ $name = static::_AUTO_PROPERTIES[$name] ?? $name;
+ return parent::__get($name);
+ }
+
+ function isError(): bool {
+ $error = $this->error;
+ return $error !== UPLOAD_ERR_OK && $error !== UPLOAD_ERR_NO_FILE;
+ }
+
+ function isValid(): bool {
+ return $this->error === UPLOAD_ERR_OK;
+ }
+
+ /**
+ * retourner true si le nom du fichier a une des extensions de $exts
+ *
+ * @param string|array $exts une ou plusieurs extensions qui sont vérifiées
+ */
+ function isExt($exts): bool {
+ if ($exts === null) return false;
+ $ext = path::ext($this->name);
+ $exts = cl::with($exts);
+ return in_array($ext, $exts);
+ }
+
+ /** @var ?string chemin du fichier, s'il a été déplacé */
+ protected $file;
+
+ function moveTo(string $dest): bool {
+ if ($this->file === null) {
+ sh::mkdirof($dest);
+ $moved = move_uploaded_file($this->tmpName, $dest);
+ if ($moved) $this->file = $dest;
+ } else {
+ $moved = false;
+ }
+ return $moved;
+ }
+
+ function getFile(): string {
+ return $this->file ?? $this->tmpName;
+ }
+
+ function getReader(): FileReader {
+ return new FileReader($this->getFile(), "r+b");
+ }
+}
diff --git a/php/src/os/EOFException.php b/php/src/os/EOFException.php
new file mode 100644
index 0000000..c7af63d
--- /dev/null
+++ b/php/src/os/EOFException.php
@@ -0,0 +1,14 @@
+needsStdin = true;
+ $this->needsTty = true;
+ $this->sources = null;
+ $this->vars = null;
+ $this->cmds = [];
+ }
+
+ function then($cmd, ?string $input=null, ?string $output=null): Cmd {
+ if ($this instanceof Cmd) {
+ $this->add($cmd, $input, $output);
+ return $this;
+ } else {
+ return (new Cmd($this))->add($cmd, $input, $output);
+ }
+ }
+
+ function or($cmd, ?string $input=null, ?string $output=null): CmdOr {
+ if ($this instanceof CmdOr) {
+ $this->add($cmd, $input, $output);
+ return $this;
+ } else {
+ return (new CmdOr($this))->add($cmd, $input, $output);
+ }
+ }
+
+ function and($cmd, ?string $input=null, ?string $output=null): CmdAnd {
+ if ($this instanceof CmdAnd) {
+ $this->add($cmd, $input, $output);
+ return $this;
+ } else {
+ return (new CmdAnd($this))->add($cmd, $input, $output);
+ }
+ }
+
+ function pipe($cmd): CmdPipe {
+ if ($this instanceof CmdPipe) {
+ $this->add($cmd);
+ return $this;
+ } else {
+ return new CmdPipe([$this, $cmd]);
+ }
+ }
+
+ function isNeedsStdin(): bool {
+ return $this->needsStdin;
+ }
+
+ function setNeedsStdin(bool $needsStdin): void {
+ $this->needsStdin = $needsStdin;
+ }
+
+ function isNeedsTty(): bool {
+ return $this->needsTty;
+ }
+
+ function setNeedsTty(bool $needsTty): void {
+ $this->needsTty = $needsTty;
+ }
+
+ function addSource(?string $source, bool $onlyIfExists=true): void {
+ if ($source === null) return;
+ if (!$onlyIfExists || file_exists($source)) {
+ $source = implode(" ", [".", sh::quote($source)]);
+ $this->sources[] = $source;
+ }
+ }
+
+ function getSources(?string $sep=null): ?string {
+ if ($this->sources === null) return null;
+ if ($sep === null) $sep = "\n";
+ return implode($sep, $this->sources);
+ }
+
+ function addLiteralVars($vars, ?string $sep=null): void {
+ if (cv::z($vars)) return;
+ if (is_array($vars)) {
+ if ($sep === null) $sep = "\n";
+ $vars = implode($sep, $vars);
+ }
+ $this->vars[] = strval($vars);
+ }
+
+ function addVars(?array $vars): void {
+ if ($vars === null) return;
+ foreach ($vars as $name => $value) {
+ $var = [];
+ if (!is_array($value)) $var[] = "export ";
+ A::merge($var, [$name, "=", sh::quote($value)]);
+ $this->vars[] = implode("", $var);
+ }
+ }
+
+ function getVars(?string $sep=null): ?string {
+ if ($this->vars === null) return null;
+ if ($sep === null) $sep = "\n";
+ return implode($sep, $this->vars);
+ }
+
+ function addPrefix($prefix): void {
+ $count = count($this->cmds);
+ if ($count == 0) return;
+ $cmd =& $this->cmds[$count - 1];
+ if ($cmd instanceof ICmd) {
+ $cmd->addPrefix($prefix);
+ } elseif (is_array($prefix)) {
+ $prefix = sh::join($prefix);
+ $cmd = "$prefix $cmd";
+ } else {
+ $cmd = "$prefix $cmd";
+ }
+ }
+
+ function addRedir(?string $redir, $output=null, bool $append=false, $input=null): void {
+ $count = count($this->cmds);
+ if ($count == 0) return;
+
+ if ($output !== null) $output = escapeshellarg($output);
+ if ($input !== null) $input = escapeshellarg($input);
+ if ($redir === "default") $redir = null;
+ $gt = $append? ">>": ">";
+ if ($redir === null) {
+ $redirs = [];
+ if ($input !== null) $redirs[] = "<$input";
+ if ($output !== null) $redirs[] = "$gt$output";
+ if ($redirs) $redir = implode(" ", $redir);
+ } else {
+ switch ($redir) {
+ case "outonly":
+ case "noerr":
+ if ($output !== null) $redir = "$gt$output 2>/dev/null";
+ else $redir = "2>/dev/null";
+ break;
+ case "erronly":
+ case "noout":
+ if ($output !== null) $redir = "2$gt$output >/dev/null";
+ else $redir = "2>&1 >/dev/null";
+ break;
+ case "both":
+ case "err2out":
+ if ($output !== null) $redir = "$gt$output 2>&1";
+ else $redir = "2>&1";
+ break;
+ case "none":
+ case "null":
+ $redir = ">/dev/null 2>&1";
+ break;
+ }
+ }
+ if ($redir !== null) {
+ $cmd =& $this->cmds[$count - 1];
+ if ($cmd instanceof ICmd) {
+ $cmd->addRedir($redir);
+ } else {
+ $cmd = "$cmd $redir";
+ }
+ }
+ }
+
+ abstract function getCmd(?string $sep=null, bool $exec=false): string;
+
+ function passthru(int &$retcode=null): bool {
+ passthru($this->getCmd(), $retcode);
+ return $retcode == 0;
+ }
+
+ function system(string &$output=null, int &$retcode=null): bool {
+ $lastLine = system($this->getCmd(), $retcode);
+ if ($lastLine !== false) $output = $lastLine;
+ return $retcode == 0;
+ }
+
+ function exec(array &$output=null, int &$retcode=null): bool {
+ exec($this->getCmd(), $output, $retcode);
+ return $retcode == 0;
+ }
+
+ function fork_exec(?int &$retcode=null, bool $wait=true): bool {
+ $cmd = $this->getCmd(null, true);
+ sh::_fork_exec($cmd, $retcode, $wait);
+ return $retcode == 0;
+ }
+}
diff --git a/php/src/os/proc/AbstractCmdList.php b/php/src/os/proc/AbstractCmdList.php
new file mode 100644
index 0000000..25de171
--- /dev/null
+++ b/php/src/os/proc/AbstractCmdList.php
@@ -0,0 +1,53 @@
+sep = $sep;
+ $this->add($cmd, $input, $output);
+ }
+
+ function addLiteral($cmd): self {
+ A::append_nn($this->cmds, $cmd);
+ return $this;
+ }
+
+ function add($cmd, ?string $input=null, ?string $output=null): self {
+ if ($cmd !== null) {
+ if (!($cmd instanceof ICmd)) {
+ sh::verifix_cmd($cmd, null, $input, $output);
+ }
+ $this->cmds[] = $cmd;
+ }
+ return $this;
+ }
+
+ function getCmd(?string $sep=null, bool $exec=false): string {
+ if ($sep === null) $sep = "\n";
+
+ $actualCmd = [];
+ A::append_nn($actualCmd, $this->getSources($sep));
+ A::append_nn($actualCmd, $this->getVars($sep));
+
+ $parts = [];
+ foreach ($this->cmds as $cmd) {
+ if ($cmd instanceof ICmd) {
+ $cmd = "(".$cmd->getCmd($sep).")";
+ }
+ $parts[] = $cmd;
+ }
+ if (count($parts) == 1 && $exec) $parts[0] = "exec $parts[0]";
+ $actualCmd[] = implode($this->sep ?? $sep, $parts);
+
+ return implode($sep, $actualCmd);
+ }
+}
diff --git a/php/src/os/proc/Cmd.php b/php/src/os/proc/Cmd.php
new file mode 100644
index 0000000..c5d6634
--- /dev/null
+++ b/php/src/os/proc/Cmd.php
@@ -0,0 +1,19 @@
+add($command);
+ }
+ }
+ $this->input = $input;
+ $this->output = $output;
+ }
+
+ function addLiteral($cmd): self {
+ A::append_nn($this->cmds, $cmd);
+ return $this;
+ }
+
+ function add($cmd): self {
+ if ($cmd !== null) {
+ if (!($cmd instanceof ICmd)) {
+ sh::verifix_cmd($cmd);
+ }
+ $this->cmds[] = $cmd;
+ }
+ return $this;
+ }
+
+ function setInput(?string $input=null): self {
+ $this->input = $input;
+ return $this;
+ }
+
+ function setOutput(?string $output=null): self {
+ $this->output = $output;
+ return $this;
+ }
+
+ function getCmd(?string $sep=null, bool $exec=false): string {
+ if ($sep === null) $sep = "\n";
+
+ $actualCmd = [];
+ A::append_nn($actualCmd, $this->getSources($sep));
+ A::append_nn($actualCmd, $this->getVars($sep));
+
+ $parts = [];
+ foreach ($this->cmds as $cmd) {
+ if ($cmd instanceof ICmd) {
+ $cmd = "(".$cmd->getCmd($sep).")";
+ }
+ $parts[] = $cmd;
+ }
+ $cmd = implode(" | ", $parts);
+
+ $input = $this->input;
+ $output = $this->output;
+ if ($input !== null || $output !== null) {
+ $parts = [];
+ if ($input !== null) $parts[] = "<".escapeshellarg($input);
+ $parts[] = $cmd;
+ if ($output !== null) $parts[] = ">".escapeshellarg($output);
+ $cmd = implode(" ", $parts);
+ }
+ $actualCmd[] = $cmd;
+
+ return implode($sep, $actualCmd);
+ }
+}
diff --git a/php/src/os/proc/ICmd.php b/php/src/os/proc/ICmd.php
new file mode 100644
index 0000000..2659f66
--- /dev/null
+++ b/php/src/os/proc/ICmd.php
@@ -0,0 +1,82 @@
+ $part) {
+ $key = self::_quote($key);
+ $val = self::_quote($part);
+ $parts[] = "[$key]=$val";
+ }
+ }
+ return "(".implode(" ", $parts).")";
+ } else {
+ return self::_quote(strval($value));
+ }
+ }
+
+ /**
+ * obtenir une commande shell à partir du tableau des arguments.
+ * à utiliser avec exec()
+ */
+ static final function join(array $parts): string {
+ $count = count($parts);
+ for($i = 0; $i < $count; $i++) {
+ $parts[$i] = self::_quote(strval($parts[$i]));
+ }
+ return implode(" ", $parts);
+ }
+
+ private static function add_redir(string &$cmd, ?string $redir, ?string $input, ?string $output): void {
+ if ($redir !== null) {
+ switch ($redir) {
+ case "outonly":
+ case "noerr":
+ $redir = "2>/dev/null";
+ break;
+ case "erronly":
+ case "noout":
+ $redir = "2>&1 >/dev/null";
+ break;
+ case "both":
+ case "err2out":
+ $redir = "2>&1";
+ break;
+ case "none":
+ case "null":
+ $redir = ">/dev/null 2>&1";
+ break;
+ case "default":
+ $redir = null;
+ break;
+ }
+ }
+ if ($input !== null) {
+ $redir = $redir !== null? "$redir ": "";
+ $redir .= "<".escapeshellarg($input);
+ }
+ if ($output !== null) {
+ $redir = $redir !== null? "$redir ": "";
+ $redir .= ">".escapeshellarg($output);
+ }
+ if ($redir !== null) $cmd .= " $redir";
+ }
+
+ /**
+ * Corriger la commande $cmd:
+ * - si c'est tableau, joindre les arguments avec {@link join()}
+ * - sinon, mettre les caractères en échappement avec {@link escapeshellarg()}
+ */
+ static final function verifix_cmd(&$cmd, ?string $redir=null, ?string $input=null, ?string $output=null): void {
+ if (is_array($cmd)) $cmd = self::join($cmd);
+ else $cmd = escapeshellcmd(strval($cmd));
+ self::add_redir($cmd, $redir, $input, $output);
+ }
+
+ /**
+ * Lancer la commande spécifiée avec passthru() et retourner le code de retour
+ * dans la variable $retcode. $cmd doit déjà être formaté comme il convient
+ *
+ * voici la différence entre system(), passthru() et exec()
+ * +----------------+-----------------+----------------+----------------+
+ * | Command | Displays Output | Can Get Output | Gets Exit Code |
+ * +----------------+-----------------+----------------+----------------+
+ * | passthru() | Yes (raw) | No | Yes |
+ * | system() | Yes (as text) | Last line only | Yes |
+ * | exec() | No | Yes (array) | Yes |
+ * +----------------+-----------------+----------------+----------------+
+ *
+ * @return bool true si la commande s'est lancée sans erreur, false sinon
+ */
+ static final function _passthru(string $cmd, int &$retcode=null): bool {
+ passthru($cmd, $retcode);
+ return $retcode == 0;
+ }
+
+ /**
+ * Comme {@link _passthru()} mais lancer la commande spécifiée avec system().
+ * cf la doc de {@link _passthru()} pour les autres détails
+ */
+ static final function _system(string $cmd, string &$output=null, int &$retcode=null): bool {
+ $last_line = system($cmd, $retcode);
+ if ($last_line !== false) $output = $last_line;
+ return $retcode == 0;
+ }
+
+ /**
+ * Comme {@link _passthru()} mais lancer la commande spécifiée avec exec().
+ * cf la doc de {@link _passthru()} pour les autres détails
+ */
+ static final function _exec(string $cmd, array &$output=null, int &$retcode=null): bool {
+ exec($cmd, $output, $retcode);
+ return $retcode == 0;
+ }
+
+ static function _waitpid(int $pid, ?int &$retcode=null): bool {
+ pcntl_waitpid($pid, $status);
+ if (pcntl_wifexited($status)) $retcode = pcntl_wexitstatus($status);
+ elseif (pcntl_wifsignaled($status)) $retcode = -pcntl_wtermsig($status);
+ else $retcode = app::EC_FORK_CHILD;
+ return $retcode == 0;
+ }
+
+ /**
+ * Lancer la commande $cmd dans un processus fils via un shell et attendre la
+ * fin de son exécution.
+ *
+ * $cmd doit déjà être formaté comme il convient
+ */
+ static final function _fork_exec(string $cmd, ?int &$retcode=null, bool $wait=true): bool {
+ $pid = pcntl_fork();
+ if ($pid == -1) {
+ // parent, impossible de forker
+ throw new ExitError(app::EC_FORK_PARENT, "unable to fork");
+ } elseif ($pid) {
+ // parent, fork ok
+ if ($wait) return self::_waitpid($pid, $retcode);
+ $retcode = null;
+ return true;
+ }
+ // child, fork ok
+ pcntl_exec("/bin/sh", ["-c", $cmd]);
+ throw StateException::unexpected_state();
+ }
+
+ /**
+ * Corriger la commande spécifiée avec {@link verifix_cmd()} puis la lancer
+ * avec passthru() et retourner le code de retour dans la variable $retcode
+ *
+ * $redir spécifie le type de redirection demandée:
+ * - "default" | null: $output reçoit STDOUT et STDERR n'est pas redirigé
+ * - "outonly" | "noerr": $output ne reçoit que STDOUT et STDERR est perdu
+ * - "erronly" | "noout": $output ne reçoit que STDERR et STDOUT est perdu
+ * - "both" | "err2out": $output reçoit STDOUT et STDERR
+ * - "none" | "null": STDOUT et STDERR sont perdus
+ * - sinon c'est une redirection spécifique, et la valeur est rajoutée telle
+ * quelle à la ligne de commande
+ *
+ * @return bool true si la commande s'est lancée sans erreur, false sinon
+ */
+ static final function passthru($cmd, int &$retcode=null, ?string $redir=null): bool {
+ self::verifix_cmd($cmd, $redir);
+ return self::_passthru($cmd, $retcode);
+ }
+
+ /**
+ * Comme {@link passthru()} mais lancer la commande spécifiée avec system().
+ * Cf la doc de {@link passthru()} pour les autres détails
+ */
+ static final function system($cmd, string &$output=null, int &$retcode=null, ?string $redir=null): bool {
+ self::verifix_cmd($cmd, $redir);
+ return self::_system($cmd, $output, $retcode);
+ }
+
+ /**
+ * Comme {@link passthru()} mais lancer la commande spécifiée avec exec().
+ * Cf la doc de {@link passthru()} pour les autres détails
+ */
+ static final function exec($cmd, array &$output=null, int &$retcode=null, ?string $redir=null): bool {
+ self::verifix_cmd($cmd, $redir);
+ return self::_exec($cmd, $output, $retcode);
+ }
+
+ /**
+ * Corriger la commande spécifiée avec {@link verifix_cmd()}, la préfixer de
+ * "exec" puis la lancer avec {@link _fork_exec()}
+ */
+ static final function fork_exec($cmd, int &$retcode=null, ?string $redir=null): bool {
+ self::verifix_cmd($cmd, $redir);
+ return self::_fork_exec("exec $cmd", $retcode);
+ }
+
+ #############################################################################
+
+ /** retourner le répertoire $HOME */
+ static final function homedir(): string {
+ $homedir = getenv("HOME");
+ if ($homedir === false) {
+ $homedir = posix_getpwuid(posix_getuid())["dir"];
+ }
+ return path::abspath($homedir);
+ }
+
+ /** s'assurer que le répertoire $dir existe */
+ static final function mkdirp(string $dir): bool {
+ if (is_dir($dir)) return true;
+ return mkdir($dir, 0777, true);
+ }
+
+ /** créer le répertoire qui va contenir le fichier $file */
+ static final function mkdirof(string $file): bool {
+ if (file_exists($file)) return true;
+ $dir = path::dirname($file);
+ if (file_exists($dir)) return true;
+ return mkdir($dir, 0777, true);
+ }
+
+ /**
+ * créer un répertoire avec un nom unique. ce répertoire doit être supprimé
+ * manuellement quand il n'est plus utilisé.
+ *
+ * @return string le chemin du répertoire
+ * @throws IOException si une erreur se produit (impossible de créer un
+ * répertoire unique après 2560 essais)
+ */
+ static final function mktempdir(?string $prefix=null, ?string $basedir=null): string {
+ if ($basedir === null) $basedir = sys_get_temp_dir();
+ if ($prefix !== null) $prefix .= "-";
+ $max = 2560;
+ do {
+ $dir = "$basedir/$prefix".uniqid();
+ $r = @mkdir($dir);
+ $max--;
+ } while ($r === false && $max > 0);
+ if ($r === false) {
+ throw IOException::last_error("$dir: unable to create directory");
+ }
+ return $dir;
+ }
+
+ /**
+ * Supprimer un répertoire créé avec mktempdir
+ *
+ * un minimum de vérification est effectué qu'il s'agit bien d'un répertoire
+ * généré par mktempdir
+ */
+ static final function rmtempdir(string $tmpdir, ?string $prefix=null, ?string $basedir=null): void {
+ if ($basedir === null) $basedir = sys_get_temp_dir();
+ if ($prefix !== null) $prefix .= "-";
+ // 13 '?' parce que c'est la taille d'une chaine générée par uniqid()
+ if (fnmatch("$basedir/$prefix?????????????", $tmpdir)) {
+ self::exec(["rm", "-rf", $tmpdir]);
+ } else {
+ throw new IOException("$tmpdir: n'est pas un répertoire temporaire");
+ }
+ }
+
+ /**
+ * supprimer tous les répertoires temporaires qui ont été créés avec le
+ * suffixe spécifié dans le répertoire $basedir
+ */
+ static final function cleantempdirs(string $prefix, ?string $basedir=null): void {
+ if ($basedir === null) $basedir = sys_get_temp_dir();
+ $prefix .= "-";
+ // 13 '?' parce que c'est la taille d'une chaine générée par uniqid()
+ $tmpdirs = glob("$basedir/$prefix?????????????", GLOB_ONLYDIR);
+ if ($tmpdirs) {
+ self::exec(["rm", "-rf", ...$tmpdirs]);
+ }
+ }
+
+ static final function is_diff_file(string $f, string $g): bool {
+ if (!is_file($f) || !is_file($g)) return true;
+ self::exec(array("diff", "-q", $f, $g), $output, $retcode);
+ return $retcode !== 0;
+ }
+
+ static final function is_same_file(string $f, string $g): bool {
+ if (!is_file($f) || !is_file($g)) return false;
+ self::exec(array("diff", "-q", $f, $g), $output, $retcode);
+ return $retcode === 0;
+ }
+
+ static final function is_diff_link(string $f, string $g): bool {
+ if (!is_link($f) || !is_link($g)) return true;
+ return @readlink($f) !== @readlink($g);
+ }
+
+ static final function is_same_link(string $f, string $g): bool {
+ if (!is_link($f) || !is_link($g)) return false;
+ return @readlink($f) === @readlink($g);
+ }
+
+ #############################################################################
+
+ static function ls_all(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array {
+ $all = scandir($dir, $sorting_order);
+ if ($all === false) return [];
+ return array_values(array_filter($all,
+ function ($file) use ($pattern) {
+ if ($file === "." || $file === "..") return false;
+ return $pattern === null || fnmatch($pattern, $file);
+ }
+ ));
+ }
+
+ static function ls_dirs(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array {
+ return array_values(array_filter(self::ls_all($dir, $pattern, $sorting_order),
+ function ($file) use ($dir) {
+ return path::is_dir(path::join($dir, $file));
+ }
+ ));
+ }
+
+ static function ls_files(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array {
+ return array_values(array_filter(self::ls_all($dir, $pattern, $sorting_order),
+ function ($file) use ($dir) {
+ return path::is_file(path::join($dir, $file));
+ }
+ ));
+ }
+
+ static function ls_pall(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array {
+ return array_map(function(string $name) use ($dir) {
+ return path::join($dir, $name);
+ }, self::ls_all($dir, $pattern, $sorting_order));
+ }
+
+ static function ls_pdirs(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array {
+ return array_map(function(string $name) use ($dir) {
+ return path::join($dir, $name);
+ }, self::ls_dirs($dir, $pattern, $sorting_order));
+ }
+
+ static function ls_pfiles(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array {
+ return array_map(function(string $name) use ($dir) {
+ return path::join($dir, $name);
+ }, self::ls_files($dir, $pattern, $sorting_order));
+ }
+}
diff --git a/php/src/output/IMessenger.php b/php/src/output/IMessenger.php
new file mode 100644
index 0000000..7b8fdec
--- /dev/null
+++ b/php/src/output/IMessenger.php
@@ -0,0 +1,107 @@
+ ou renommer `say` en `console`, et `ui` en `say`
+
+-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary
\ No newline at end of file
diff --git a/php/src/output/_messenger.php b/php/src/output/_messenger.php
new file mode 100644
index 0000000..1226c24
--- /dev/null
+++ b/php/src/output/_messenger.php
@@ -0,0 +1,74 @@
+clone($params);
+ }
+
+ static final function __callStatic($name, $args) {
+ $name = str::us2camel($name);
+ call_user_func_array([static::get(), $name], $args);
+ }
+
+ #############################################################################
+
+ const DEBUG = IMessenger::DEBUG;
+ const MINOR = IMessenger::MINOR;
+ const NORMAL = IMessenger::NORMAL;
+ const MAJOR = IMessenger::MAJOR;
+ const NONE = IMessenger::NONE;
+
+ static function reset_params(?array $params=null): void { static::get()->resetParams($params); }
+ static function section($content, ?callable $func=null, ?int $level=null): void { static::get()->section($content, $func, $level); }
+ static function title($content, ?callable $func=null, ?int $level=null): void { static::get()->title($content, $func, $level); }
+ static function desc($content, ?int $level=null): void { static::get()->desc($content, $level); }
+ static function action($content, ?callable $func=null, ?int $level=null): void { static::get()->action($content, $func, $level); }
+ static function step($content, ?int $level=null): void { static::get()->step($content, $level); }
+ static function asuccess($content=null, ?int $override_level=null): void { static::get()->asuccess($content, $override_level); }
+ static function afailure($content=null, ?int $override_level=null): void { static::get()->afailure($content, $override_level); }
+ static function adone($content=null, ?int $override_level=null): void { static::get()->adone($content, $override_level); }
+ static function aresult($result=null, ?int $override_level=null): void { static::get()->aresult($result, $override_level); }
+ static function print($content, ?int $level=null): void { static::get()->print($content, $level); }
+ static function info($content, ?int $level=null): void { static::get()->info($content, $level); }
+ static function note($content, ?int $level=null): void { static::get()->note($content, $level); }
+ static function warning($content, ?int $level=null): void { static::get()->warning($content, $level); }
+ static function error($content, ?int $level=null): void { static::get()->error($content, $level); }
+ static function end(bool $all=false): void { static::get()->end($all); }
+
+ static function debug($content): void { self::info($content, self::DEBUG);}
+ static function normal($content): void { self::info($content, self::NORMAL);}
+ static function minor($content): void { self::info($content, self::MINOR);}
+ static function important($content): void { self::info($content, self::MAJOR);}
+ static function attention($content): void { self::note($content, self::MAJOR);}
+ static function critwarning($content): void { self::warning($content, self::MAJOR);}
+ static function criterror($content): void { self::error($content, self::MAJOR);}
+}
diff --git a/php/src/output/console.php b/php/src/output/console.php
new file mode 100644
index 0000000..0ef8c81
--- /dev/null
+++ b/php/src/output/console.php
@@ -0,0 +1,28 @@
+resetParams($params);
+ return self::$out;
+ }
+
+ static function write(...$values): void { self::$out->write(...$values); }
+ static function print(...$values): void { self::$out->print(...$values); }
+
+ static function iwrite(int $indentLevel, ...$values): void { self::$out->iwrite($indentLevel, ...$values); }
+ static function iprint(int $indentLevel, ...$values): void { self::$out->iprint($indentLevel, ...$values); }
+}
+out::reset();
diff --git a/php/src/output/say.php b/php/src/output/say.php
new file mode 100644
index 0000000..9d8b6d0
--- /dev/null
+++ b/php/src/output/say.php
@@ -0,0 +1,28 @@
+msgs = [];
+ foreach ($msgs as $msg) {
+ if ($msg !== null) $this->msgs[] = $msg;
+ }
+ }
+
+ /** @var IMessenger[] */
+ protected $msgs;
+
+ function resetParams(?array $params=null): void { foreach ($this->msgs as $msg) { $msg->resetParams($params); } }
+ function clone(?array $params=null): self {
+ $clone = clone $this;
+ foreach ($clone->msgs as &$msg) {
+ $msg = $msg->clone($params);
+ }; unset($msg);
+ return $clone;
+ }
+ function section($content, ?callable $func=null, ?int $level=null): void {
+ $useFunc = false;
+ foreach ($this->msgs as $msg) {
+ $msg->section($content, null, $level);
+ if ($msg instanceof _IMessenger) $useFunc = true;
+ }
+ if ($useFunc && $func !== null) {
+ try {
+ $func($this);
+ } finally {
+ /** @var _IMessenger $msg */
+ foreach ($this->msgs as $msg) {
+ $msg->_endSection();
+ }
+ }
+ }
+ }
+ function title($content, ?callable $func=null, ?int $level=null): void {
+ $useFunc = false;
+ $untils = [];
+ foreach ($this->msgs as $msg) {
+ if ($msg instanceof _IMessenger) {
+ $useFunc = true;
+ $untils[] = $msg->_getTitleMark();
+ }
+ $msg->title($content, null, $level);
+ }
+ if ($useFunc && $func !== null) {
+ try {
+ $func($this);
+ } finally {
+ /** @var _IMessenger $msg */
+ $index = 0;
+ foreach ($this->msgs as $msg) {
+ if ($msg instanceof _IMessenger) {
+ $msg->_endTitle($untils[$index++]);
+ }
+ }
+ }
+ }
+ }
+ function desc($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->desc($content, $level); } }
+ function action($content, ?callable $func=null, ?int $level=null): void {
+ $useFunc = false;
+ $untils = [];
+ foreach ($this->msgs as $msg) {
+ if ($msg instanceof _IMessenger) {
+ $useFunc = true;
+ $untils[] = $msg->_getActionMark();
+ }
+ $msg->action($content, null, $level);
+ }
+ if ($useFunc && $func !== null) {
+ try {
+ $result = $func($this);
+ /** @var _IMessenger $msg */
+ $index = 0;
+ foreach ($this->msgs as $msg) {
+ if ($msg->_getActionMark() > $untils[$index++]) {
+ $msg->aresult($result);
+ }
+ }
+ } catch (Exception $e) {
+ /** @var _IMessenger $msg */
+ foreach ($this->msgs as $msg) {
+ $msg->afailure($e);
+ }
+ throw $e;
+ } finally {
+ /** @var _IMessenger $msg */
+ $index = 0;
+ foreach ($this->msgs as $msg) {
+ $msg->_endAction($untils[$index++]);
+ }
+ }
+ }
+ }
+ function step($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->step($content, $level); } }
+ function asuccess($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->asuccess($content, $overrideLevel); } }
+ function afailure($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->afailure($content, $overrideLevel); } }
+ function adone($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->adone($content, $overrideLevel); } }
+ function aresult($result=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->aresult($result, $overrideLevel); } }
+ function print($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->print($content, $level); } }
+ function info($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->info($content, $level); } }
+ function note($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->note($content, $level); } }
+ function warning($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->warning($content, $level); } }
+ function error($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->error($content, $level); } }
+ function end(bool $all=false): void { foreach ($this->msgs as $msg) { $msg->end($all); } }
+}
diff --git a/php/src/output/std/StdMessenger.php b/php/src/output/std/StdMessenger.php
new file mode 100644
index 0000000..3892a0d
--- /dev/null
+++ b/php/src/output/std/StdMessenger.php
@@ -0,0 +1,722 @@
+ self::DEBUG,
+ "minor" => self::MINOR, "verbose" => self::MINOR,
+ "normal" => self::NORMAL,
+ "major" => self::MAJOR, "quiet" => self::MAJOR,
+ "none" => self::NONE, "silent" => self::NONE,
+ ];
+
+ protected static function verifix_level($level, int $max_level=self::MAX_LEVEL): int {
+ if (!in_array($level, self::VALID_LEVELS, true)) {
+ $level = cl::get(self::LEVEL_MAP, $level, $level);
+ }
+ if (!in_array($level, self::VALID_LEVELS, true)) {
+ throw new Exception("$level: invalid level");
+ }
+ if ($level > $max_level) {
+ throw new Exception("$level: level not allowed here");
+ }
+ return $level;
+ }
+
+ const GENERIC_PREFIXES = [
+ self::MAJOR => [
+ "section" => [true, "SECTION!", "===", "=", "=", "==="],
+ "title" => [false, "TITLE!", null, "T", "", "==="],
+ "desc" => ["DESC!", ">", ""],
+ "error" => ["CRIT.ERROR!", "E!", ""],
+ "warning" => ["CRIT.WARNING!", "W!", ""],
+ "note" => ["ATTENTION!", "N!", ""],
+ "info" => ["IMPORTANT!", "N!", ""],
+ "step" => ["*", ".", ""],
+ "print" => [null, null, null],
+ ],
+ self::NORMAL => [
+ "section" => [true, "SECTION:", "---", "-", "-", "---"],
+ "title" => [false, "TITLE:", null, "T", "", "---"],
+ "desc" => ["DESC:", ">", ""],
+ "error" => ["ERROR:", "E", ""],
+ "warning" => ["WARNING:", "W", ""],
+ "note" => ["NOTE:", "N", ""],
+ "info" => ["INFO:", "I", ""],
+ "step" => ["*", ".", ""],
+ "print" => [null, null, null],
+ ],
+ self::MINOR => [
+ "section" => [true, "section", null, ">>", "<<", null],
+ "title" => [false, "title", null, "t", "", null],
+ "desc" => ["desc", ">", ""],
+ "error" => ["error", "E", ""],
+ "warning" => ["warning", "W", ""],
+ "note" => ["note", "N", ""],
+ "info" => ["info", "I", ""],
+ "step" => ["*", ".", ""],
+ "print" => [null, null, null],
+ ],
+ self::DEBUG => [
+ "section" => [true, "section", null, ">>", "<<", null],
+ "title" => [false, "title", null, "t", "", null],
+ "desc" => ["desc", ">", ""],
+ "error" => ["debugE", "e", ""],
+ "warning" => ["debugW", "w", ""],
+ "note" => ["debugN", "i", ""],
+ "info" => ["debug", "D", ""],
+ "step" => ["*", ".", ""],
+ "print" => [null, null, null],
+ ],
+ ];
+
+ const RESULT_PREFIXES = [
+ "failure" => ["(FAILURE)", "✘"],
+ "success" => ["(SUCCESS)", "✔"],
+ "done" => [null, null],
+ ];
+
+ function __construct(?array $params=null) {
+ $output = cl::get($params, "output");
+ $color = cl::get($params, "color");
+ $indent = cl::get($params, "indent", static::INDENT);
+
+ $defaultLevel = cl::get($params, "default_level");
+ if ($defaultLevel === null) $defaultLevel = self::NORMAL;
+ $defaultLevel = self::verifix_level($defaultLevel);
+
+ $debug = boolval(cl::get($params, "debug"));
+ $minLevel = cl::get($params, "min_level");
+ if ($minLevel === null && $debug) $minLevel = self::DEBUG;
+ if ($minLevel === null) $minLevel = cl::get($params, "verbosity"); # alias
+ if ($minLevel === null) $minLevel = self::NORMAL;
+ $minLevel = self::verifix_level($minLevel, self::NONE);
+
+ $addDate = boolval(cl::get($params, "add_date"));
+ $dateFormat = cl::get($params, "date_format", static::DATE_FORMAT);
+ $id = cl::get($params, "id");
+
+ $params = [
+ "color" => $color,
+ "indent" => $indent,
+ ];
+ if ($output !== null) {
+ $this->err = $this->out = new StdOutput($output, $params);
+ } else {
+ $this->out = new StdOutput(STDOUT, $params);
+ $this->err = new StdOutput(STDERR, $params);
+ }
+ $this->defaultLevel = $defaultLevel;
+ $this->minLevel = $minLevel;
+ $this->addDate = $addDate;
+ $this->dateFormat = $dateFormat;
+ $this->id = $id;
+ $this->inSection = false;
+ $this->titles = [];
+ $this->actions = [];
+ }
+
+ function resetParams(?array $params=null): void {
+ $output = cl::get($params, "output");
+ $color = cl::get($params, "color");
+ $indent = cl::get($params, "indent");
+
+ $defaultLevel = cl::get($params, "default_level");
+ if ($defaultLevel !== null) $defaultLevel = self::verifix_level($defaultLevel);
+
+ $debug = cl::get($params, "debug");
+ $minLevel = cl::get($params, "min_level");
+ if ($minLevel === null && $debug !== null) $minLevel = $debug? self::DEBUG: self::NORMAL;
+ if ($minLevel === null) $minLevel = cl::get($params, "verbosity"); # alias
+ if ($minLevel !== null) $minLevel = self::verifix_level($minLevel, self::NONE);
+
+ $addDate = cl::get($params, "add_date");
+ $dateFormat = cl::get($params, "date_format");
+ $id = cl::get($params, "id");
+
+ $params = [
+ "output" => $output,
+ "color" => $color,
+ "indent" => $indent,
+ ];
+ if ($this->out === $this->err) {
+ $this->out->resetParams($params);
+ } else {
+ # NB: si initialement [output] était null, et qu'on spécifie une valeur
+ # [output], alors les deux instances $out et $err sont mis à jour
+ # séparément avec la même valeur de output
+ # de plus, on ne peut plus revenir à la situation initiale avec une
+ # destination différente pour $out et $err
+ $this->out->resetParams($params);
+ $this->err->resetParams($params);
+ }
+ if ($defaultLevel !== null) $this->defaultLevel = $defaultLevel;
+ if ($minLevel !== null) $this->minLevel = $minLevel;
+ if ($addDate !== null) $this->addDate = boolval($addDate);
+ if ($dateFormat !== null) $this->dateFormat = $dateFormat;
+ if ($id !== null) $this->id = $id;
+ }
+
+ function clone(?array $params=null): IMessenger {
+ $clone = clone $this;
+ if ($params !== null) $clone->resetParams($params);
+ #XXX faut-il marquer la section et les titres du clone à "print" => false?
+ # ou en faire des références au parent?
+ # dans tous les cas, on considère qu'il n'y a pas d'actions en cours, et on
+ # ne doit pas dépiler avec end() plus que l'état que l'on a eu lors du clone
+ return $clone;
+ }
+
+ /** @var StdOutput la sortie standard */
+ protected $out;
+
+ /** @var StdOutput la sortie d'erreur */
+ protected $err;
+
+ /** @var int level par défaut dans lequel les messages sont affichés */
+ protected $defaultLevel;
+
+ /** @var int level minimum que doivent avoir les messages pour être affichés */
+ protected $minLevel;
+
+ /** @var bool faut-il ajouter la date à chaque ligne? */
+ protected $addDate;
+
+ /** @var string format de la date */
+ protected $dateFormat;
+
+ /** @var ?string identifiant de ce messenger, à ajouter à chaque ligne */
+ protected $id;
+
+ protected function getLinePrefix(): ?string {
+ $linePrefix = null;
+ if ($this->addDate) {
+ $date = date_create()->format($this->dateFormat);
+ $linePrefix .= "$date ";
+ }
+ if ($this->id !== null) {
+ $linePrefix .= "$this->id ";
+ }
+ return $linePrefix;
+ }
+
+ protected function decrLevel(int $level, int $amount=-1): int {
+ $level += $amount;
+ if ($level < self::MIN_LEVEL) $level = self::MIN_LEVEL;
+ return $level;
+ }
+
+ protected function checkLevel(?int &$level): bool {
+ if ($level === null) $level = $this->defaultLevel;
+ elseif ($level < 0) $level = $this->decrLevel($this->defaultLevel, $level);
+ return $level >= $this->minLevel;
+ }
+
+ protected function getIndentLevel(bool $withActions=true): int {
+ $indentLevel = count($this->titles) - 1;
+ if ($indentLevel < 0) $indentLevel = 0;
+ if ($withActions) {
+ foreach ($this->actions as $action) {
+ if ($action["level"] < $this->minLevel) continue;
+ $indentLevel++;
+ }
+ }
+ return $indentLevel;
+ }
+
+ protected function _printTitle(?string $linePrefix, int $level,
+ string $type, $content,
+ int $indentLevel, StdOutput $out): void {
+ $prefixes = self::GENERIC_PREFIXES[$level][$type];
+ if ($prefixes[0]) $out->print();
+ $content = cl::with($content);
+ if ($out->isColor()) {
+ $before = $prefixes[2];
+ $prefix = $prefixes[3];
+ $prefix2 = $prefix !== null? "$prefix ": null;
+ $suffix = $prefixes[4];
+ $suffix2 = $suffix !== null? " $suffix": null;
+ $after = $prefixes[5];
+
+ $lines = $out->getLines(false, ...$content);
+ $maxlen = 0;
+ foreach ($lines as &$content) {
+ $line = $out->filterColors($content);
+ $len = mb_strlen($line);
+ if ($len > $maxlen) $maxlen = $len;
+ $content = [$content, $len];
+ }; unset($content);
+ if ($before !== null) {
+ if ($linePrefix !== null) $out->write($linePrefix);
+ $out->iprint($indentLevel, $prefix, substr($before, 1), str_repeat($before[0], $maxlen), $suffix);
+ }
+ foreach ($lines as [$content, $len]) {
+ if ($linePrefix !== null) $out->write($linePrefix);
+ $padding = $len < $maxlen? str_repeat(" ", $maxlen - $len): null;
+ $out->iprint($indentLevel, $prefix2, $content, $padding, $suffix2);
+ }
+ if ($after !== null) {
+ if ($linePrefix !== null) $out->write($linePrefix);
+ $out->iprint($indentLevel, $prefix, substr($after, 1), str_repeat($after[0], $maxlen), $suffix);
+ }
+ } else {
+ $prefix = $prefixes[1];
+ if ($prefix !== null) $prefix .= " ";
+ $prefix2 = str_repeat(" ", mb_strlen($prefix));
+ $lines = $out->getLines(false, ...$content);
+ foreach ($lines as $content) {
+ if ($linePrefix !== null) $out->write($linePrefix);
+ $out->iprint($indentLevel, $prefix, $content);
+ $prefix = $prefix2;
+ }
+ }
+ }
+
+ protected function _printAction(?string $linePrefix, int $level,
+ bool $printContent, $content,
+ bool $printResult, ?bool $rsuccess, $rcontent,
+ int $indentLevel, StdOutput $out): void {
+ $color = $out->isColor();
+ if ($rsuccess === true) $type = "success";
+ elseif ($rsuccess === false) $type = "failure";
+ else $type = "done";
+ $rprefixes = self::RESULT_PREFIXES[$type];
+ if ($color) {
+ $rprefix = $rprefixes[1];
+ $rprefix2 = null;
+ if ($rprefix !== null) {
+ $rprefix .= " ";
+ $rprefix2 = $out->filterColors($out->filterContent($rprefix));
+ $rprefix2 = str_repeat(" ", mb_strlen($rprefix2));
+ }
+ } else {
+ $rprefix = $rprefixes[0];
+ if ($rprefix !== null) $rprefix .= " ";
+ $rprefix2 = str_repeat(" ", mb_strlen($rprefix));
+ }
+ if ($printContent && $printResult) {
+ A::ensure_array($content);
+ if ($rcontent) {
+ $content[] = ": ";
+ $content[] = $rcontent;
+ }
+ $lines = $out->getLines(false, ...$content);
+ foreach ($lines as $content) {
+ if ($linePrefix !== null) $out->write($linePrefix);
+ $out->iprint($indentLevel, $rprefix, $content);
+ $rprefix = $rprefix2;
+ }
+ } elseif ($printContent) {
+ $prefixes = self::GENERIC_PREFIXES[$level]["step"];
+ if ($color) {
+ $prefix = $prefixes[1];
+ if ($prefix !== null) $prefix .= " ";
+ $prefix2 = $out->filterColors($out->filterContent($prefix));
+ $prefix2 = str_repeat(" ", mb_strlen($prefix2));
+ $suffix = $prefixes[2];
+ } else {
+ $prefix = $prefixes[0];
+ if ($prefix !== null) $prefix .= " ";
+ $prefix2 = str_repeat(" ", mb_strlen($prefix));
+ $suffix = null;
+ }
+ A::ensure_array($content);
+ $content[] = ":";
+ $lines = $out->getLines(false, ...$content);
+ foreach ($lines as $content) {
+ if ($linePrefix !== null) $out->write($linePrefix);
+ $out->iprint($indentLevel, $prefix, $content, $suffix);
+ $prefix = $prefix2;
+ }
+ } elseif ($printResult) {
+ if (!$rcontent) {
+ if ($type === "success") $rcontent = $color? "succès": "";
+ elseif ($type === "failure") $rcontent = $color? "échec": "";
+ elseif ($type === "done") $rcontent = "fait";
+ }
+ $rprefix = " $rprefix";
+ $rprefix2 = " $rprefix2";
+ $lines = $out->getLines(false, $rcontent);
+ foreach ($lines as $rcontent) {
+ if ($linePrefix !== null) $out->write($linePrefix);
+ $out->iprint($indentLevel, $rprefix, $rcontent);
+ $rprefix = $rprefix2;
+ }
+ }
+ }
+
+ protected function _printGeneric(?string $linePrefix, int $level,
+ string $type, $content,
+ int $indentLevel, StdOutput $out): void {
+ $prefixes = self::GENERIC_PREFIXES[$level][$type];
+ $content = cl::with($content);
+ if ($out->isColor()) {
+ $prefix = $prefixes[1];
+ $prefix2 = null;
+ if ($prefix !== null) {
+ $prefix .= " ";
+ $prefix2 = $out->filterColors($out->filterContent($prefix));
+ $prefix2 = str_repeat(" ", mb_strlen($prefix2));
+ }
+ $suffix = $prefixes[2];
+ $lines = $out->getLines(false, ...$content);
+ foreach ($lines as $content) {
+ if ($linePrefix !== null) $out->write($linePrefix);
+ $out->iprint($indentLevel, $prefix, $content, $suffix);
+ $prefix = $prefix2;
+ }
+ } else {
+ $prefix = $prefixes[0];
+ if ($prefix !== null) $prefix .= " ";
+ $prefix2 = str_repeat(" ", mb_strlen($prefix));
+ $lines = $out->getLines(false, ...$content);
+ foreach ($lines as $content) {
+ if ($linePrefix !== null) $out->write($linePrefix);
+ $out->iprint($indentLevel, $prefix, $content);
+ $prefix = $prefix2;
+ }
+ }
+ }
+
+ protected function _printGenericOrException(?int $level, string $type, $content, int $indentLevel, StdOutput $out): void {
+ $linePrefix = $this->getLinePrefix();
+ # si $content contient des exceptions, les afficher avec un level moindre
+ $exceptions = null;
+ if (is_array($content)) {
+ $valueContent = null;
+ foreach ($content as $value) {
+ if ($value instanceof Throwable || $value instanceof ExceptionShadow) {
+ $exceptions[] = $value;
+ } else {
+ $valueContent[] = $value;
+ }
+ }
+ if ($valueContent === null) $content = null;
+ elseif (count($valueContent) == 1) $content = $valueContent[0];
+ else $content = $valueContent;
+ } elseif ($content instanceof Throwable || $content instanceof ExceptionShadow) {
+ $exceptions[] = $content;
+ $content = null;
+ }
+
+ $printActions = true;
+ $showContent = $this->checkLevel($level);
+ if ($content !== null && $showContent) {
+ $this->printActions(); $printActions = false;
+ $this->_printGeneric($linePrefix, $level, $type, $content, $indentLevel, $out);
+ }
+ if ($exceptions !== null) {
+ $level1 = $this->decrLevel($level);
+ $showTraceback = $this->checkLevel($level1);
+ foreach ($exceptions as $exception) {
+ # tout d'abord userMessage
+ if ($exception instanceof UserException) {
+ $userMessage = UserException::get_user_message($exception);
+ $showSummary = true;
+ } else {
+ $userMessage = UserException::get_summary($exception);
+ $showSummary = false;
+ }
+ if ($userMessage !== null && $showContent) {
+ if ($printActions) { $this->printActions(); $printActions = false; }
+ $this->_printGeneric($linePrefix, $level, $type, $userMessage, $indentLevel, $out);
+ }
+ # puis summary et traceback
+ if ($showTraceback) {
+ if ($printActions) { $this->printActions(); $printActions = false; }
+ if ($showSummary) {
+ $summary = UserException::get_summary($exception);
+ $this->_printGeneric($linePrefix, $level1, $type, $summary, $indentLevel, $out);
+ }
+ $traceback = UserException::get_traceback($exception);
+ $this->_printGeneric($linePrefix, $level1, $type, $traceback, $indentLevel, $out);
+ }
+ }
+ }
+ }
+
+ /** @var bool est-on dans une section? */
+ protected $inSection;
+
+ /** @var array section qui est en attente d'affichage */
+ protected $section;
+
+ function section($content, ?callable $func=null, ?int $level=null): void {
+ $this->_endSection();
+ $this->inSection = true;
+ if (!$this->checkLevel($level)) return;
+ $this->section = [
+ "line_prefix" => $this->getLinePrefix(),
+ "level" => $level,
+ "content" => $content,
+ "print_content" => true,
+ ];
+ if ($func !== null) {
+ try {
+ $func($this);
+ } finally {
+ $this->_endSection();
+ }
+ }
+ }
+
+ protected function printSection() {
+ $section =& $this->section;
+ if ($section !== null && $section["print_content"]) {
+ $this->_printTitle(
+ $section["line_prefix"], $section["level"],
+ "section", $section["content"],
+ 0, $this->err);
+ $section["print_content"] = false;
+ }
+ }
+
+ function _endSection(): void {
+ $this->inSection = false;
+ $this->section = null;
+ }
+
+ /** @var array */
+ protected $titles;
+
+ /** @var array */
+ protected $title;
+
+ function _getTitleMark(): int {
+ return count($this->titles);
+ }
+
+ function title($content, ?callable $func=null, ?int $level=null): void {
+ if (!$this->checkLevel($level)) return;
+ $until = $this->_getTitleMark();
+ $this->titles[] = [
+ "line_prefix" => $this->getLinePrefix(),
+ "level" => $level,
+ "content" => $content,
+ "print_content" => true,
+ "descs" => [],
+ "print_descs" => false,
+ ];
+ $this->title =& $this->titles[$until];
+ if ($func !== null) {
+ try {
+ $func($this);
+ } finally {
+ $this->_endTitle($until);
+ }
+ }
+ }
+
+ function desc($content, ?int $level=null): void {
+ if (!$this->checkLevel($level)) return;
+ $title =& $this->title;
+ $title["descs"][] = [
+ "line_prefix" => $this->getLinePrefix(),
+ "level" => $level,
+ "content" => $content,
+ ];
+ $title["print_descs"] = true;
+ }
+
+ protected function printTitles(): void {
+ $this->printSection();
+ $err = $this->err;
+ $indentLevel = 0;
+ foreach ($this->titles as &$title) {
+ if ($title["print_content"]) {
+ $this->_printTitle(
+ $title["line_prefix"], $title["level"],
+ "title", $title["content"],
+ $indentLevel, $err);
+ $title["print_content"] = false;
+ }
+ if ($title["print_descs"]) {
+ foreach ($title["descs"] as $desc) {
+ $this->_printGeneric(
+ $desc["line_prefix"], $desc["level"],
+ "desc", $desc["content"],
+ $indentLevel, $err);
+ }
+ $title["descs"] = [];
+ $title["print_descs"] = false;
+ }
+ $indentLevel++;
+ }; unset($title);
+ }
+
+ function _endTitle(?int $until=null): void {
+ if ($until === null) $until = $this->_getTitleMark() - 1;
+ while (count($this->titles) > $until) {
+ array_pop($this->titles);
+ }
+ if ($this->titles) {
+ $this->title =& $this->titles[count($this->titles) - 1];
+ } else {
+ $this->titles = [];
+ unset($this->title);
+ }
+ }
+
+ /** @var array */
+ protected $actions;
+
+ /** @var array */
+ protected $action;
+
+ function _getActionMark(): int {
+ return count($this->actions);
+ }
+
+ function action($content, ?callable $func=null, ?int $level=null): void {
+ $this->checkLevel($level);
+ $until = $this->_getActionMark();
+ $this->actions[] = [
+ "line_prefix" => $this->getLinePrefix(),
+ "level" => $level,
+ "content" => $content,
+ "print_content" => true,
+ "result_success" => null,
+ "result_content" => null,
+ ];
+ $this->action =& $this->actions[$until];
+ if ($func !== null) {
+ try {
+ $result = $func($this);
+ if ($this->_getActionMark() > $until) {
+ $this->aresult($result);
+ }
+ } catch (Exception $e) {
+ $this->afailure($e);
+ throw $e;
+ } finally {
+ $this->_endAction($until);
+ }
+ }
+ }
+
+ function printActions(bool $endAction=false, ?int $overrideLevel=null): void {
+ $this->printTitles();
+ $err = $this->err;
+ $indentLevel = $this->getIndentLevel(false);
+ $lastIndex = count($this->actions) - 1;
+ $index = 0;
+ foreach ($this->actions as &$action) {
+ $mergeResult = $index++ == $lastIndex && $endAction;
+ $linePrefix = $action["line_prefix"];
+ $level = $overrideLevel?? $action["level"];
+ $content = $action["content"];
+ $printContent = $action["print_content"];
+ $rsuccess = $action["result_success"];
+ $rcontent = $action["result_content"];
+ if ($level < $this->minLevel) continue;
+ if ($mergeResult) {
+ $this->_printAction(
+ $linePrefix, $level,
+ $printContent, $content,
+ true, $rsuccess, $rcontent,
+ $indentLevel, $err);
+ } elseif ($printContent) {
+ $this->_printAction(
+ $linePrefix, $level,
+ $printContent, $content,
+ false, $rsuccess, $rcontent,
+ $indentLevel, $err);
+ $action["print_content"] = false;
+ }
+ $indentLevel++;
+ }; unset($action);
+ if ($endAction) $this->_endAction();
+ }
+
+ function step($content, ?int $level=null): void {
+ $this->_printGenericOrException($level, "step", $content, $this->getIndentLevel(), $this->err);
+ }
+
+ function asuccess($content=null, ?int $overrideLevel=null): void {
+ if (!$this->actions) $this->action(null);
+ $this->action["result_success"] = true;
+ $this->action["result_content"] = $content;
+ $this->printActions(true, $overrideLevel);
+ }
+
+ function afailure($content=null, ?int $overrideLevel=null): void {
+ if (!$this->actions) $this->action(null);
+ $this->action["result_success"] = false;
+ $this->action["result_content"] = $content;
+ $this->printActions(true, $overrideLevel);
+ }
+
+ function adone($content=null, ?int $overrideLevel=null): void {
+ if (!$this->actions) $this->action(null);
+ $this->action["result_success"] = null;
+ $this->action["result_content"] = $content;
+ $this->printActions(true, $overrideLevel);
+ }
+
+ function aresult($result=null, ?int $overrideLevel=null): void {
+ if (!$this->actions) $this->action(null);
+ if ($result === true) $this->asuccess(null, $overrideLevel);
+ elseif ($result === false) $this->afailure(null, $overrideLevel);
+ elseif ($result instanceof Exception) $this->afailure($result, $overrideLevel);
+ else $this->adone($result, $overrideLevel);
+ }
+
+ function _endAction(?int $until=null): void {
+ if ($until === null) $until = $this->_getActionMark() - 1;
+ while (count($this->actions) > $until) {
+ array_pop($this->actions);
+ }
+ if ($this->actions) {
+ $this->action =& $this->actions[count($this->actions) - 1];
+ } else {
+ $this->actions = [];
+ unset($this->action);
+ }
+ }
+
+ function print($content, ?int $level=null): void {
+ $this->_printGenericOrException($level, "print", $content, $this->getIndentLevel(), $this->out);
+ }
+
+ function info($content, ?int $level=null): void {
+ $this->_printGenericOrException($level, "info", $content, $this->getIndentLevel(), $this->err);
+ }
+
+ function note($content, ?int $level=null): void {
+ $this->_printGenericOrException($level, "note", $content, $this->getIndentLevel(), $this->err);
+ }
+
+ function warning($content, ?int $level=null): void {
+ $this->_printGenericOrException($level, "warning", $content, $this->getIndentLevel(), $this->err);
+ }
+
+ function error($content, ?int $level=null): void {
+ $this->_printGenericOrException($level, "error", $content, $this->getIndentLevel(), $this->err);
+ }
+
+ function end(bool $all=false): void {
+ if ($all) {
+ while ($this->actions) $this->adone();
+ while ($this->titles) $this->_endTitle();
+ $this->_endSection();
+ } elseif ($this->actions) {
+ $this->_endAction();
+ } elseif ($this->titles) {
+ $this->_endTitle();
+ } else {
+ $this->_endSection();
+ }
+ }
+}
diff --git a/php/src/output/std/StdOutput.php b/php/src/output/std/StdOutput.php
new file mode 100644
index 0000000..c30c466
--- /dev/null
+++ b/php/src/output/std/StdOutput.php
@@ -0,0 +1,248 @@
+ "0",
+ "bold" => "1",
+ "faint" => "2",
+ "underlined" => "4",
+ "reverse" => "7",
+ "normal" => "22",
+ "black" => "30",
+ "red" => "31",
+ "green" => "32",
+ "yellow" => "33",
+ "blue" => "34",
+ "magenta" => "35",
+ "cyan" => "36",
+ "white" => "37",
+ "default" => "39",
+ "black-bg" => "40",
+ "red-bg" => "41",
+ "green-bg" => "42",
+ "yellow-bg" => "43",
+ "blue-bg" => "44",
+ "magenta-bg" => "45",
+ "cyan-bg" => "46",
+ "white-bg" => "47",
+ "default-bg" => "49",
+ ];
+
+ const COLOR_MAP = [
+ "z" => "reset",
+ "o" => "black",
+ "r" => "red",
+ "g" => "green",
+ "y" => "yellow",
+ "b" => "blue",
+ "m" => "magenta",
+ "c" => "cyan",
+ "w" => "white",
+ "O" => "black_bg",
+ "R" => "red_bg",
+ "G" => "green_bg",
+ "Y" => "yellow_bg",
+ "B" => "blue_bg",
+ "M" => "magenta_bg",
+ "C" => "cyan_bg",
+ "W" => "white_bg",
+ "@" => "bold",
+ "-" => "faint",
+ "_" => "underlined",
+ "~" => "reverse",
+ "n" => "normal",
+ ];
+
+ /**
+ * @param resource|null $outf
+ * @throws Exception si la destination est un fichier et que son ouverture a
+ * échoué
+ */
+ function __construct($output=null, ?array $params=null) {
+ if ($output !== null) $params["output"] = $output;
+ elseif (!isset($params["output"])) $params["output"] = STDOUT;
+ if (!isset($params["filter_tags"])) $params["filter_tags"] = true;
+ if (!isset($params["indent"])) $params["indent"] = " ";
+ $this->resetParams($params);
+ }
+
+ function resetParams(?array $params=null): void {
+ $output = cl::get($params, "output");
+ $maskErrors = null;
+ $color = cl::get($params, "color");
+ $filterTags = cl::get($params, "filter_tags");
+ $indent = cl::get($params, "indent");
+ $flush = cl::get($params, "flush");
+
+ if ($output instanceof Stream) $output = $output->getResource();
+ if ($output !== null) {
+ if ($output === "php://stdout") {
+ $outf = STDOUT;
+ } elseif ($output === "php://stderr") {
+ $outf = STDERR;
+ } elseif (!is_resource($output)) {
+ # si $outf est un nom de fichier, vérifier que l'ouverture se fait sans
+ # erreur. à partir de là, plus aucune gestion d'erreur n'est faite, à
+ # part afficher les erreurs d'écriture la première fois qu'elles se
+ # produisent
+ $maskErrors = false;
+ $outf = @fopen($output, "ab");
+ if ($outf === false) {
+ $error = error_get_last();
+ if ($error !== null) $message = $error["message"];
+ else $message = "$output: open error";
+ throw new Exception($message);
+ }
+ if ($flush === null) $flush = true;
+ } else {
+ $outf = $output;
+ }
+ $this->outf = $outf;
+ $this->maskErrors = $maskErrors;
+ if ($color === null) $color = stream_isatty($outf);
+ if ($flush === null) $flush = false;
+ }
+ if ($color !== null) $this->color = boolval($color);
+ if ($filterTags !== null) $this->filterTags = boolval($filterTags);
+ if ($indent !== null) $this->indent = strval($indent);
+ if ($flush !== null) $this->flush = boolval($flush);
+ }
+
+ /** @var resource */
+ protected $outf;
+
+ /** @var bool faut-il masquer les erreurs d'écriture? */
+ protected $maskErrors;
+
+ /** @var bool faut-il autoriser la sortie en couleur? */
+ protected $color;
+
+ function isColor(): bool {
+ return $this->color;
+ }
+
+ /** @var bool faut-il enlever les tags dans la sortie? */
+ protected $filterTags;
+
+ /** @var string indentation unitaire */
+ protected $indent;
+
+ /** @var bool faut-il flush le fichier après l'écriture de chaque ligne */
+ protected $flush;
+
+ function isatty(): bool {
+ return stream_isatty($this->outf);
+ }
+
+ private static function replace_colors(array $ms): string {
+ $colors = [];
+ foreach (preg_split('/\s+/', $ms[1]) as $color) {
+ while ($color && !cl::has(self::COLORS, $color)) {
+ $alias = substr($color, 0, 1);
+ $colors[] = self::COLOR_MAP[$alias];
+ $color = substr($color, 1);
+ }
+ if ($color) $colors[] = $color;
+ }
+ $text = "\x1B[";
+ $first = true;
+ foreach ($colors as $color) {
+ if (!$color) continue;
+ if ($first) $first = false;
+ else $text .= ";";
+ $text .= self::COLORS[$color];
+ }
+ $text .= "m";
+ return $text;
+ }
+ function filterContent(string $text): string {
+ # couleur au début
+ $text = preg_replace_callback('/]*)>/', [self::class, "replace_colors"], $text);
+ # reset à la fin
+ $text = preg_replace('/<\/color>/', "\x1B[0m", $text);
+ # enlever les tags classiques
+ if ($this->filterTags) {
+ $text = preg_replace('/<[^>]*>/', "", $text);
+ }
+ return $text;
+ }
+ function filterColors(string $text): string {
+ return preg_replace('/\x1B\[.*?m/', "", $text);
+ }
+
+ function getIndent(int $indentLevel): string {
+ return str_repeat($this->indent, $indentLevel);
+ }
+
+ function getLines(bool $withNl, ...$values): array {
+ $values = c::resolve($values, null, false);
+ if (!$values) return [];
+ $text = c::to_string($values, false);
+ if ($text === "") return [""];
+ $text = $this->filterContent($text);
+ if (!$this->color) $text = $this->filterColors($text);
+ $lines = explode("\n", $text);
+ $max = count($lines) - 1;
+ if ($withNl) {
+ for ($i = 0; $i < $max; $i++) {
+ $lines[$i] .= "\n";
+ }
+ }
+ if ($lines[$max] === "") unset($lines[$max]);
+ return $lines;
+ }
+
+ private function _fwrite($outf, string $data): void {
+ if ($this->maskErrors === null) {
+ # masquer les erreurs d'écriture en permanence
+ @fwrite($outf, $data);
+ return;
+ }
+ # masquer uniquement la première erreur, jusqu'à ce que l'erreur disparaisse
+ if ($this->maskErrors) $r = @fwrite($outf, $data);
+ else $r = fwrite($outf, $data);
+ $this->maskErrors = $r === false;
+ }
+
+ function writeLines($indent, array $lines, bool $addNl=false): void {
+ $outf = $this->outf;
+ foreach ($lines as $line) {
+ if ($indent !== null) $this->_fwrite($outf, $indent);
+ $this->_fwrite($outf, $line);
+ if ($addNl) $this->_fwrite($outf, "\n");
+ }
+ if ($this->flush) @fflush($outf);
+ }
+
+ function write(...$values): void {
+ $this->writeLines(null, $this->getLines(true, ...$values));
+ }
+
+ function print(...$values): void {
+ $values[] = "\n";
+ $this->writeLines(null, $this->getLines(true, ...$values));
+ }
+
+ function iwrite(int $indentLevel, ...$values): void {
+ $indent = $this->getIndent($indentLevel);
+ $this->writeLines($indent, $this->getLines(true, ...$values));
+ }
+
+ function iprint(int $indentLevel, ...$values): void {
+ $values[] = "\n";
+ $indent = $this->getIndent($indentLevel);
+ $this->writeLines($indent, $this->getLines(true, ...$values));
+ }
+}
diff --git a/php/src/output/std/_IMessenger.php b/php/src/output/std/_IMessenger.php
new file mode 100644
index 0000000..9b54b59
--- /dev/null
+++ b/php/src/output/std/_IMessenger.php
@@ -0,0 +1,19 @@
+offsetExists($key)) return $array->offsetGet($key);
+ else return $default;
+ } else {
+ if (!is_array($array)) $array = cl::with($array);
+ return cl::get($array, $key, $default);
+ }
+ }
+
+ /** spécifier la valeur d'une clé */
+ static final function set(&$array, $key, $value) {
+ if ($array instanceof ArrayAccess) {
+ $array->offsetSet($key, $value);
+ } else {
+ cl::set($array, $key, $value);
+ }
+ return $value;
+ }
+
+ /** initialiser $dest avec les valeurs de $values */
+ static final function set_values(&$array, ?array $values): void {
+ if ($values === null) return;
+ foreach ($values as $key => $value) {
+ self::set($array, $key, $value);
+ }
+ }
+
+ /** incrémenter la valeur de la clé */
+ static final function inc(&$array, $key): int {
+ if ($array instanceof ArrayAccess) {
+ $value = (int)$array->offsetGet($key);
+ $array->offsetSet($key, ++$value);
+ return $value;
+ } else {
+ A::ensure_array($array);
+ $value = (int)cl::get($array, $key);
+ return $array[$key] = ++$value;
+ }
+ }
+
+ /** décrémenter la valeur de la clé */
+ static final function dec(&$array, $key, bool $allow_negative=false): int {
+ if ($array instanceof ArrayAccess) {
+ $value = (int)$array->offsetGet($key);
+ if ($allow_negative || $value > 0) $array->offsetSet($key, --$value);
+ return $value;
+ } else {
+ A::ensure_array($array);
+ $value = (int)cl::get($array, $key);
+ if ($allow_negative || $value > 0) $array[$key] = --$value;
+ return $value;
+ }
+ }
+
+ /**
+ * fusionner $merge dans la valeur de la clé, qui est d'abord transformé en
+ * tableau si nécessaire
+ */
+ static final function merge(&$array, $key, $merge): void {
+ if ($array instanceof ArrayAccess) {
+ $value = $array->offsetGet($key);
+ $value = cl::merge($value, $merge);
+ $array->offsetSet($key, $value);
+ } else {
+ A::ensure_array($array);
+ $array[$key] = cl::merge($array[$key], $merge);
+ }
+ }
+
+ /**
+ * ajouter $value à la valeur de la clé, qui est d'abord transformé en
+ * tableau si nécessaire
+ */
+ static final function append(&$array, $key, $value): void {
+ if ($array instanceof ArrayAccess) {
+ $value = $array->offsetGet($key);
+ cl::set($value, null, $value);
+ $array->offsetSet($key, $value);
+ } else {
+ A::ensure_array($array);
+ cl::set($array[$key], null, $value);
+ }
+ }
+}
diff --git a/php/src/php/coll/AutoArray.php b/php/src/php/coll/AutoArray.php
new file mode 100644
index 0000000..82d1904
--- /dev/null
+++ b/php/src/php/coll/AutoArray.php
@@ -0,0 +1,44 @@
+has($name)) return true;
+ $properties = self::_AUTO_PROPERTIES();
+ if ($properties === null) return false;
+ return array_key_exists($name, $properties);
+ }
+ function __get($name) {
+ $properties = self::_AUTO_PROPERTIES();
+ if ($this->has($name)) return $this->get($name);
+ $pkey = cl::get($properties, $name, $name);
+ return cl::pget($this->data, $pkey);
+ }
+ function __set($name, $value) {
+ $properties = self::_AUTO_PROPERTIES();
+ if ($this->has($name)) {
+ $this->set($name, $value);
+ } else {
+ $pkey = cl::get($properties, $name, $name);
+ cl::pset($this->data, $pkey, $value);
+ }
+ }
+ function __unset($name) {
+ $properties = self::_AUTO_PROPERTIES();
+ if ($this->has($name)) {
+ $this->del($name);
+ } else {
+ $pkey = cl::get($properties, $name, $name);
+ cl::pdel($this->data, $pkey);
+ }
+ }
+}
diff --git a/php/src/php/coll/BaseArray.php b/php/src/php/coll/BaseArray.php
new file mode 100644
index 0000000..ff809fc
--- /dev/null
+++ b/php/src/php/coll/BaseArray.php
@@ -0,0 +1,117 @@
+reset($data);
+ }
+
+ /** @var array */
+ protected $data;
+
+ function &wrappedArray(): ?array { return $this->data; }
+
+ function __toString(): string { return var_export($this->data, true); }
+ #function __debugInfo() { return $this->data; }
+ function reset(?array &$data): void { $this->data =& $data; }
+ function count(): int { return $this->data !== null? count($this->data): 0; }
+ function keys(): array { return $this->data !== null? array_keys($this->data): []; }
+
+ #############################################################################
+ # base
+
+ function has($key): bool {
+ return $this->data !== null && array_key_exists($key, $this->data);
+ }
+ function &get($key, $default=null) {
+ if ($this->data !== null && array_key_exists($key, $this->data)) {
+ return $this->data[$key];
+ } else return $default;
+ }
+ function set($key, $value): void {
+ if ($key === null) $this->data[] = $value;
+ else $this->data[$key] = $value;
+ }
+ function del($key): void {
+ unset($this->data[$key]);
+ }
+
+ function offsetExists($offset): bool { return $this->has($offset); }
+ function &offsetGet($offset) { return $this->get($offset); }
+ function offsetSet($offset, $value) { $this->set($offset, $value); }
+ function offsetUnset($offset) { $this->del($offset); }
+
+ function __isset($name) { return $this->has($name); }
+ function &__get($name) { return $this->get($name); }
+ function __set($name, $value) { $this->set($name, $value); }
+ function __unset($name) { $this->del($name); }
+
+ #############################################################################
+ # iterator
+
+ /** @var bool */
+ private $valid = false;
+
+ function rewind() {
+ if ($this->data !== null) {
+ $first = reset($this->data);
+ $this->valid = $first !== false || key($this->data) !== null;
+ } else {
+ $this->valid = false;
+ }
+ }
+ function valid(): bool { return $this->valid; }
+ function key() { return key($this->data); }
+ function current() { return current($this->data); }
+ function next() {
+ $next = next($this->data);
+ $this->valid = $next !== false || key($this->data) !== null;
+ }
+
+ #############################################################################
+ # divers
+
+ function phas($pkey): bool { return cl::phas($this->data, $pkey); }
+ function pget($pkey, $default=null): bool { return cl::pget($this->data, $pkey, $default); }
+ function pset($pkey, $value): void { cl::pset($this->data, $pkey, $value); }
+ function pdel($pkey): void { cl::pdel($this->data, $pkey); }
+
+ function contains($value, bool $strict=false): bool {
+ if ($value === null || $this->data === null) return false;
+ return in_array($value, $this->data, $strict);
+ }
+
+ function add($value, bool $unique=true, bool $strict=false): bool {
+ if ($unique && $this->contains($value, $strict)) return false;
+ $this->set(null, $value);
+ return true;
+ }
+
+ function addAll(?array $values, bool $unique=true, bool $strict=false): void {
+ if ($values === null) return;
+ $index = 0;
+ foreach ($values as $key => $value) {
+ if ($key === $index) {
+ $this->add($value, $unique, $strict);
+ $index++;
+ } else {
+ $this->set($key, $value);
+ }
+ }
+ }
+
+ function resetAll(?array $values): void {
+ $this->data = null;
+ $this->addAll($values);
+ }
+}
diff --git a/php/src/php/content/IContent.php b/php/src/php/content/IContent.php
new file mode 100644
index 0000000..7fd4386
--- /dev/null
+++ b/php/src/php/content/IContent.php
@@ -0,0 +1,11 @@
+content = $content;
+ }
+
+ protected IContent $content;
+
+ function print(): void {
+ $content = $this->content->getContent();
+ c::write($content);
+ }
+
+ function __call($name, $args) {
+ $content = func::call([$this->content, $name], ...$args);
+ c::write($content);
+ }
+}
diff --git a/php/src/php/content/README.md b/php/src/php/content/README.md
new file mode 100644
index 0000000..0e7eb5a
--- /dev/null
+++ b/php/src/php/content/README.md
@@ -0,0 +1,77 @@
+# nulib\php\content
+
+un contenu (ou "content") est une liste de valeurs, avec une syntaxe pour que
+certains éléments soient dynamiquement calculés.
+
+le contenu final est résolu selon les règles suivantes:
+- Si le contenu n'est pas un tableau:
+ - une chaine est quotée avec `htmlspecialchars()`
+ - un scalaire ou une instance d'objet sont pris tels quels
+- Sinon, le contenu doit être un tableau, séquentiel ou associatif, ça n'a pas
+ d'incidence
+ - les éléments scalaires ou instance d'objets sont pris tels quels
+ - les Closure sont appelés dès la résolution, et leur valeur de retour est
+ considéré comme un contenu *statique* inséré tel quel dans le flux i.e dans
+ l'exemple suivant $c1 et $c2 sont globalement équivalents:
+ ~~~php
+ $closure = function() { ... }
+ $c1 = [...$before, $closure, ...$after];
+ $c2 = [...$before, ...c::q($closure()), ...$after];
+ # $c1 == $c2, sauf si $closure() retourne des valeurs qui peuvent être
+ # considérées comme du contenu dynamique
+ ~~~
+ - les tableaux représentent un traitement dynamique: appel de fonction,
+ instanciation, etc. le contenu effectif n'est évalué que lors de l'affichage
+
+Les syntaxes possibles sont:
+
+`[[], $args...]`
+: contenu statique: les valeurs $args... sont insérées dans le flux du contenu
+ sans modification. c'est la seule façon d'insérer un tableau dans la liste des
+ valeurs (on peut aussi utiliser une Closure, mais ce n'est pas toujours
+ possible, notamment si le contenu est une constante)
+
+`["class_or_function", $args...]`
+`[["class_or_function"], $args...]`
+`[["function", $args0...], $args1...]`
+`[["class", null, $args0...], $args1...]`
+: instantiation ou appel de fonction
+
+`["->method", $args...]`
+`[["->method"], $args...]`
+`[[null, "method"], $args...]`
+`[[null, "method", $args0...], $args1...]`
+: appel de méthode sur l'objet contexte spécifié lors de la résolution du contenu
+
+`[[$object, "method"], $args...]`
+`[[$object, "method", $args0...], $args1...]`
+: appel de méthode sur l'objet spécifié
+
+`[["class", "method"], $args...]`
+`[["class", "method", $args0...], $args1...]`
+: appel de méthode statique de la classe spécifiée
+
+Le fait de transformer un contenu en une liste de valeurs statiques s'appelle
+la résolution. la résolution se fait par rapport à un objet contexte, qui est
+utilisé lors des appels de méthodes.
+
+Lors des appels de fonctions ou des instanciations, les $arguments sont tous des
+contenus:
+- une valeur scalaire ou une instance est passée inchangée
+- un tableau est traité comme un contenu avec les règles ci-dessus
+
+## Affichage d'un contenu
+
+Deux interfaces sont utilisées pour modéliser un élément de contenu à afficher:
+- IContent: objet capable de produire du contenu
+- IPrintable: objet capable d'afficher un contenu
+
+Tous les autres éléments de contenus sont transformés en string avant affichage.
+Un système de formatters permet de définir des fonctions ou méthodes à utiliser
+pour formatter des objets de certains types.
+
+Lors de l'affichage du contenu, deux éléments contigûs $a et $b sont affichés
+séparés par un espace si $a se termine par un mot (éventuellement terminé par
+un point '.') et $b commence par un mot.
+
+-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary
\ No newline at end of file
diff --git a/php/src/php/content/c.php b/php/src/php/content/c.php
new file mode 100644
index 0000000..9506835
--- /dev/null
+++ b/php/src/php/content/c.php
@@ -0,0 +1,178 @@
+ $svalue) {
+ if ($skey === $sindex) {
+ $sindex++;
+ if ($seq) {
+ $dest[] = $svalue;
+ } else {
+ # la première sous-clé séquentielle est ajoutée avec la clé du
+ # merge statique
+ $dest[$key] = $svalue;
+ $seq = true;
+ }
+ } else {
+ $dest[$skey] = $svalue;
+ }
+ }
+
+ }
+
+ /** résoudre le contenu, et retourner la liste des valeurs */
+ static final function resolve($content, $object_or_class=null, bool $quote=true, ?array &$dest=null): array {
+ if ($dest === null) $dest = [];
+ $content = $quote? self::q($content): self::nq($content);
+ $index = 0;
+ foreach ($content as $key => $value) {
+ if ($key === $index) {
+ $index++;
+ $seq = true;
+ } else {
+ $seq = false;
+ }
+ if ($value instanceof Closure) {
+ # contenu dynamique: le contenu est la valeur de retour de la fonction
+ # ce contenu est rajouté à la suite après avoir été quoté avec self::q()
+ $func = $value;
+ nur_func::ensure_func($func, $object_or_class, $args);
+ $values = self::q(nur_func::call($func, ...$args));
+ self::add_static_content($dest, $values, $key, $seq);
+ continue;
+ }
+ if (is_array($value)) {
+ # contenu dynamique
+ if (count($value) == 0) continue;
+ $func = cl::first($value);
+ $args = array_slice($value, 1);
+ if ($func === []) {
+ # merge statique
+ self::add_static_content($dest, $args, $key, $seq);
+ continue;
+ } else {
+ # chaque argument de la fonction à appeler est aussi un contenu
+ foreach ($args as &$arg) {
+ $array = is_array($arg);
+ $arg = self::resolve($arg, $object_or_class, false);
+ if (!$array) $arg = $arg[0];
+ }; unset($arg);
+ if (nur_func::is_static($func)) {
+ nur_func::ensure_func($func, $object_or_class, $args);
+ $value = nur_func::call($func, ...$args);
+ } elseif (nur_func::is_class($func)) {
+ nur_func::fix_class_args($func, $args);
+ $value = nur_func::cons($func, ...$args);
+ } else {
+ nur_func::ensure_func($func, $object_or_class, $args);
+ $value = nur_func::call($func, ...$args);
+ }
+ }
+ }
+ if ($seq) $dest[] = $value;
+ else $dest[$key] = $value;
+ }
+ return $dest;
+ }
+ const resolve = [self::class, "resolve"];
+
+ private static function wend(?string $value): bool {
+ return $value !== null && preg_match('/(\w|\w\.)$/', $value);
+ }
+ private static function startw(?string $value): bool {
+ return $value !== null && preg_match('/^\w/', $value);
+ }
+
+ private static function to_values($content, ?array &$values=null): void {
+ $pvalue = cl::last($values);
+ $wend = self::wend($pvalue);
+ foreach ($content as $value) {
+ if ($value === null || $value === false) {
+ continue;
+ } elseif ($value instanceof IContent) {
+ self::to_values($value->getContent(), $values);
+ continue;
+ } elseif ($value instanceof IPrintable) {
+ ob_start(null, 0, PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE);
+ $value->print();
+ $value = ob_get_clean();
+ } else {
+ $value = strval($value);
+ #XXX rendre paramétrable le formatage de $value
+ }
+ if ($value !== "") {
+ $startw = self::startw($value);
+ if ($wend && $startw) $values[] = " ";
+ $values[] = $value;
+ $wend = self::wend($value);
+ }
+ }
+ }
+
+ /** écrire le contenu sur la resource spécifiée, qui vaut STDOUT par défaut */
+ static final function write($content, $fd=null, bool $resolve=true): void {
+ if ($resolve) $content = self::resolve($content);
+ $wend = false;
+ foreach ($content as $value) {
+ if ($value === null || $value === false) {
+ continue;
+ } elseif ($value instanceof IPrintable) {
+ if ($fd === null) {
+ $value->print();
+ } else {
+ ob_start(null, 0, PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE);
+ $value->print();
+ fwrite($fd, ob_get_clean());
+ }
+ $wend = false;
+ continue;
+ } elseif ($value instanceof IContent) {
+ $values = [];
+ self::to_values($content, $values);
+ $value = implode("", $values);
+ } else {
+ $value = strval($value);
+ #XXX rendre paramétrable le formattage de $value
+ }
+ $startw = self::startw($value);
+ if (!$wend && !$startw) $value = " $value";
+ if ($fd === null) echo $value;
+ else fwrite($fd, $value);
+ $wend = self::wend($value);
+ }
+ }
+
+ /** retourner le contenu sous forme de chaine */
+ static final function to_string($content, bool $resolve=true): string {
+ if ($resolve) $content = self::resolve($content);
+ $values = [];
+ self::to_values($content, $values);
+ return implode("", $values);
+ }
+}
diff --git a/php/src/php/func.php b/php/src/php/func.php
new file mode 100644
index 0000000..63ea334
--- /dev/null
+++ b/php/src/php/func.php
@@ -0,0 +1,646 @@
+";
+ }
+
+ private static function _is_nfunction(?string $f): bool {
+ return strpos($f, "\\") !== false;
+ }
+
+ private static function _parse_static(?string &$m): bool {
+ $pos = strpos($m, "::");
+ if ($pos === false) return false;
+ $m = substr($m, $pos + 2);
+ return true;
+ }
+
+ private static function _parse_method(?string &$m): bool {
+ $pos = strpos($m, "->");
+ if ($pos === false) return false;
+ $m = substr($m, $pos + 2);
+ return true;
+ }
+
+ #############################################################################
+ # Fonctions
+
+ /**
+ * vérifier que $func est une fonction et la normaliser le cas échéant.
+ * retourner true si c'est une fonction, false sinon
+ *
+ * les formes suivantes sont supportées:
+ * - "function" si une classe du même nom n'existe pas déjà
+ * - [false, "function", ...$args] c'est la forme normalisée
+ *
+ * @param bool $strict vérifier l'inexistence de la classe et l'existence de
+ * la fonction (ne pas uniquement faire une vérification syntaxique)
+ */
+ static function verifix_function(&$func, bool $strict=true, ?string &$reason=null): bool {
+ if ($strict) {
+ $msg = var_export($func, true);
+ $reason = null;
+ }
+ if ($func instanceof ReflectionFunction) return true;
+ if (is_string($func)) {
+ $c = false;
+ $f = $func;
+ } elseif (is_array($func)) {
+ if (!array_key_exists(0, $func)) return false;
+ $c = $func[0];
+ if (!array_key_exists(1, $func)) return false;
+ $f = $func[1];
+ } else {
+ return false;
+ }
+ if ($c !== false) return false;
+ if (!is_string($f)) return false;
+ if (self::_is_invalid($f)) return false;
+ if (self::_parse_static($f)) return false;
+ if (self::_parse_method($f)) return false;
+ if ($strict) {
+ $reason = null;
+ if (class_exists($f)) {
+ $reason = "$msg: is a class";
+ return false;
+ }
+ if (!function_exists($f)) {
+ $reason = "$msg: function not found";
+ return false;
+ }
+ }
+ $func = [false, $f];
+ return true;
+ }
+
+ /**
+ * vérifier que $func est une fonction avec les règles de
+ * {@link self::verifix_function()}
+ */
+ static function is_function($func, bool $strict=true, ?string &$reason=null): bool {
+ return self::verifix_function($func, $strict, $reason);
+ }
+
+ #############################################################################
+ # Classes
+
+ /**
+ * vérifier que $func est une classe et la normaliser le cas échéant.
+ * retourner true si c'est une classe, false sinon
+ *
+ * les formes suivantes sont supportées:
+ * - "class"
+ * - ["class", false, ...$args] c'est la forme normalisée
+ *
+ * @param bool $strict vérifier l'existence de la classe (ne pas uniquement
+ * faire une vérification syntaxique)
+ */
+ static function verifix_class(&$func, bool $strict=true, ?string &$reason=null): bool {
+ if ($strict) {
+ $msg = var_export($func, true);
+ $reason = null;
+ }
+ if ($func instanceof ReflectionClass) return true;
+ if (is_string($func)) {
+ $c = $func;
+ $f = false;
+ } elseif (is_array($func)) {
+ if (!array_key_exists(0, $func)) return false;
+ $c = $func[0];
+ if (!array_key_exists(1, $func)) return false;
+ $f = $func[1];
+ } else {
+ return false;
+ }
+ if (!is_string($c)) return false;
+ if (self::_is_invalid($c)) return false;
+ if (self::_parse_static($c)) return false;
+ if (self::_parse_method($c)) return false;
+ if ($f !== false) return false;
+ if ($strict) {
+ if (!class_exists($c)) {
+ $reason = "$msg: class not found";
+ return false;
+ }
+ }
+ $func = [$c, false];
+ return true;
+ }
+
+ /**
+ * vérifier que $func est une classe avec les règles de
+ * {@link self::verifix_class()}
+ */
+ static function is_class($func, bool $strict=true, ?string &$reason=null): bool {
+ return self::verifix_class($func, $strict, $reason);
+ }
+
+ #############################################################################
+ # Méthodes statiques
+
+ private static function _parse_class_s(?string $cs, ?string &$c, ?string &$s): bool {
+ if (self::_is_invalid($cs) || self::_parse_method($cs)) return false;
+ $pos = strpos($cs, "::");
+ if ($pos === false) return false;
+ if ($pos === 0) return false;
+ $tmpc = substr($cs, 0, $pos);
+ $cs = substr($cs, $pos + 2);
+ if (self::_is_nfunction($cs)) return false;
+ [$c, $s] = [$tmpc, cv::vn($cs)];
+ return true;
+ }
+
+ private static function _parse_c_static(?string $cs, ?string &$c, ?string &$s, ?bool &$bound): bool {
+ if (self::_is_invalid($cs) || self::_parse_method($cs)) return false;
+ $pos = strpos($cs, "::");
+ if ($pos === false) return false;
+ if ($pos == strlen($cs) - 2) return false;
+ if ($pos > 0) {
+ $tmpc = substr($cs, 0, $pos);
+ $bound = true;
+ } else {
+ $tmpc = null;
+ $bound = false;
+ }
+ $cs = substr($cs, $pos + 2);
+ if (self::_is_nfunction($cs)) return false;
+ [$c, $s] = [$tmpc, cv::vn($cs)];
+ return true;
+ }
+
+ /**
+ * vérifier que $func est une méthode statique, et la normaliser le cas
+ * échéant. retourner true si c'est une méthode statique, false sinon
+ *
+ * les formes suivantes sont supportées (XXX étant null ou n'importe quelle
+ * valeur scalaire de n'importe quel type sauf false)
+ * - "XXX::function"
+ * - ["XXX::function", ...$args]
+ * - [XXX, "::function", ...$args]
+ * - [XXX, "function", ...$args] c'est la forme normalisée
+ *
+ * Si XXX est une classe, la méthode statique est liée. sinon, elle doit être
+ * liée à une classe avant d'être utilisée
+ *
+ * @param bool $strict vérifier l'existence de la classe et de la méthode si
+ * la méthode est liée (ne pas uniquement faire une vérification syntaxique)
+ */
+ static function verifix_static(&$func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool {
+ if ($strict) {
+ $msg = var_export($func, true);
+ $reason = null;
+ }
+ if ($func instanceof ReflectionMethod) {
+ $bound = false;
+ return true;
+ }
+ if (is_string($func)) {
+ if (!self::_parse_c_static($func, $c, $f, $bound)) return false;
+ $cf = [$c, $f];
+ } elseif (is_array($func)) {
+ $cf = $func;
+ if (!array_key_exists(0, $cf)) return false;
+ $c = $cf[0];
+ if ($c === false) return false;
+ if (is_object($c)) $c = get_class($c);
+ if (is_string($c)) {
+ if (self::_is_invalid($c)) return false;
+ if (self::_parse_class_s($c, $c, $f)) {
+ $cf[0] = $c;
+ if ($f !== null) {
+ # ["class::method"] --> ["class", "method"]
+ array_splice($cf, 1, 0, [$f]);
+ }
+ $bound = true;
+ } elseif (self::_parse_c_static($c, $c, $f, $bound)) {
+ # ["::method"] --> [null, "method"]
+ array_splice($cf, 0, 0, [null]);
+ $cf[1] = $f;
+ } else {
+ $cf[0] = $c;
+ $bound = is_string($c);
+ }
+ } else {
+ $cf[0] = null;
+ $bound = false;
+ }
+ #
+ if (!array_key_exists(1, $cf)) return false;
+ $f = $cf[1];
+ if (!is_string($f)) return false;
+ if (self::_parse_c_static($f, $rc, $f, $rbound)) {
+ if ($rc !== null && $c === null) {
+ $c = $rc;
+ $bound = $rbound;
+ }
+ } else {
+ if (self::_is_invalid($f)) return false;
+ if (self::_is_nfunction($f)) return false;
+ if (self::_parse_method($f)) return false;
+ self::_parse_static($f);
+ }
+ $cf[1] = $f;
+ } else {
+ return false;
+ }
+ if ($strict) {
+ $reason = null;
+ if ($bound) {
+ if (!class_exists($c)) {
+ $reason = "$msg: class not found";
+ return false;
+ }
+ if (!method_exists($c, $f)) {
+ $reason = "$msg: method not found";
+ return false;
+ }
+ } else {
+ $reason = "$msg: not bound";
+ }
+ }
+ $func = $cf;
+ return true;
+ }
+
+ /**
+ * vérifier que $func est une méthode statique avec les règles de
+ * {@link self::verifix_static()}
+ */
+ static function is_static($func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool {
+ return self::verifix_static($func, $strict, $bound, $reason);
+ }
+
+ #############################################################################
+ # Méthodes non statiques
+
+ private static function _parse_class_m(?string $cm, ?string &$c, ?string &$m): bool {
+ if (self::_is_invalid($cm) || self::_parse_static($cm)) return false;
+ $pos = strpos($cm, "->");
+ if ($pos === false) return false;
+ if ($pos === 0) return false;
+ $tmpc = substr($cm, 0, $pos);
+ $cm = substr($cm, $pos + 2);
+ if (self::_is_nfunction($cm)) return false;
+ [$c, $m] = [$tmpc, cv::vn($cm)];
+ return true;
+ }
+
+ private static function _parse_c_method(?string $cm, ?string &$c, ?string &$m, ?bool &$bound): bool {
+ if (self::_is_invalid($cm) || self::_parse_static($cm)) return false;
+ $pos = strpos($cm, "->");
+ if ($pos === false) return false;
+ if ($pos == strlen($cm) - 2) return false;
+ if ($pos > 0) {
+ $tmpc = substr($cm, 0, $pos);
+ $bound = true;
+ } else {
+ $tmpc = null;
+ $bound = false;
+ }
+ $cm = substr($cm, $pos + 2);
+ if (self::_is_nfunction($cm)) return false;
+ [$c, $m] = [$tmpc, cv::vn($cm)];
+ return true;
+ }
+
+ /**
+ * vérifier que $func est une méthode non statique, et la normaliser le cas
+ * échéant. retourner true si c'est une méthode non statique, false sinon
+ *
+ * les formes suivantes sont supportées (XXX étant null ou n'importe quelle
+ * valeur scalaire de n'importe quel type sauf false)
+ * - "XXX->function"
+ * - ["XXX->function", ...$args]
+ * - [XXX, "->function", ...$args]
+ * - [XXX, "function", ...$args] c'est la forme normalisée
+ *
+ * Si XXX est une classe ou un objet, la méthode est liée. dans tous les cas,
+ * elle doit être liée à un objet avant d'être utilisée
+ *
+ * @param bool $strict vérifier l'existence de la classe et de la méthode si
+ * la méthode est liée (ne pas uniquement faire une vérification syntaxique)
+ */
+ static function verifix_method(&$func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool {
+ if ($strict) {
+ $msg = var_export($func, true);
+ $reason = null;
+ }
+ if ($func instanceof ReflectionMethod) {
+ $bound = false;
+ return true;
+ }
+ if (is_string($func)) {
+ if (!self::_parse_c_method($func, $c, $f, $bound)) return false;
+ $cf = [$c, $f];
+ } elseif (is_array($func)) {
+ $cf = $func;
+ if (!array_key_exists(0, $cf)) return false;
+ $c = $cf[0];
+ if ($c === false) return false;
+ if (is_object($c)) {
+ $bound = true;
+ } elseif (is_string($c)) {
+ if (self::_is_invalid($c)) return false;
+ if (self::_parse_class_m($c, $c, $f)) {
+ $cf[0] = $c;
+ if ($f !== null) {
+ # ["class->method"] --> ["class", "method"]
+ array_splice($cf, 1, 0, [$f]);
+ }
+ $bound = true;
+ } elseif (self::_parse_c_method($c, $c, $f, $bound)) {
+ # ["->method"] --> [null, "method"]
+ array_splice($cf, 0, 0, [null]);
+ $cf[1] = $f;
+ } else {
+ $cf[0] = $c;
+ $bound = is_string($c);
+ }
+ } else {
+ $cf[0] = null;
+ $bound = false;
+ }
+ #
+ if (!array_key_exists(1, $cf)) return false;
+ $f = $cf[1];
+ if (!is_string($f)) return false;
+ if (self::_parse_c_method($f, $rc, $f, $rbound)) {
+ if ($rc !== null && $c === null) {
+ $c = $rc;
+ $bound = $rbound;
+ }
+ } else {
+ if (self::_is_invalid($f)) return false;
+ if (self::_is_nfunction($f)) return false;
+ if (self::_parse_static($f)) return false;
+ self::_parse_method($f);
+ }
+ $cf[1] = $f;
+ } else {
+ return false;
+ }
+ if ($strict) {
+ $reason = null;
+ if ($bound) {
+ if (!is_object($c) && !class_exists($c)) {
+ $reason = "$msg: class not found";
+ return false;
+ }
+ if (!method_exists($c, $f)) {
+ $reason = "$msg: method not found";
+ return false;
+ }
+ } else {
+ $reason = "$msg: not bound";
+ }
+ }
+ $func = $cf;
+ return true;
+ }
+
+ /**
+ * vérifier que $func est une méthode non statique avec les règles de
+ * {@link self::verifix_method()}
+ */
+ static function is_method($func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool {
+ return self::verifix_method($func, $strict, $bound, $reason);
+ }
+
+ #############################################################################
+ # func
+
+ const TYPE_MASK = 0b11;
+
+ const FLAG_STATIC = 0b100;
+
+ const TYPE_CLOSURE = 0, TYPE_FUNCTION = 1, TYPE_CLASS = 2, TYPE_METHOD = 3;
+
+ const TYPE_STATIC = self::TYPE_METHOD | self::FLAG_STATIC;
+
+ protected static function not_a_callable($func, ?string $reason) {
+ if ($reason === null) {
+ $msg = var_export($func, true);
+ $reason = "$msg: not a callable";
+ }
+ return new ValueException($reason);
+ }
+
+ static function with($func, ?array $args=null, bool $strict=true): self {
+ if (!is_array($func)) {
+ if ($func instanceof Closure) {
+ return new self(self::TYPE_CLOSURE, $func, $args);
+ } elseif ($func instanceof ReflectionFunction) {
+ return new self(self::TYPE_FUNCTION, $func, $args);
+ } elseif ($func instanceof ReflectionClass) {
+ return new self(self::TYPE_CLASS, $func, $args);
+ } elseif ($func instanceof ReflectionMethod) {
+ return new self(self::TYPE_METHOD, $func, $args, false);
+ }
+ }
+ if (self::verifix_function($func, $strict, $reason)) {
+ return new self(self::TYPE_FUNCTION, $func, $args, false, $reason);
+ } elseif (self::verifix_class($func, $strict, $reason)) {
+ return new self(self::TYPE_CLASS, $func, $args, false, $reason);
+ } elseif (self::verifix_method($func, $strict, $bound, $reason)) {
+ return new self(self::TYPE_METHOD, $func, $args, $bound, $reason);
+ } elseif (self::verifix_static($func, $strict, $bound, $reason)) {
+ return new self(self::TYPE_STATIC, $func, $args, $bound, $reason);
+ }
+ throw self::not_a_callable($func, $reason);
+ }
+
+ static function ensure($func, ?array $args=null, bool $strict=true): self {
+ $func = self::with($func, $args, $strict);
+ if (!$func->isBound()) {
+ throw self::not_a_callable($func->func, $func->reason);
+ }
+ return $func;
+ }
+
+ static function check($func, ?array $args=null, bool $strict=true): bool {
+ try {
+ self::ensure($func, $args, $strict);
+ return true;
+ } catch (Exception $e) {
+ return false;
+ }
+ }
+
+ static function call($func, ...$args) {
+ return self::with($func)->invoke($args);
+ }
+
+ #############################################################################
+
+ protected function __construct(int $type, $func, ?array $args=null, bool $bound=false, ?string $reason=null) {
+ $flags = $type & ~self::TYPE_MASK;
+ $type = $type & self::TYPE_MASK;
+ $object = null;
+ $prefixArgs = [];
+ if (!is_array($func)) {
+ $reflection = $func;
+ $func = null;
+ } else {
+ if (count($func) > 2) {
+ $prefixArgs = array_slice($func, 2);
+ $func = array_slice($func, 0, 2);
+ }
+ [$c, $f] = $func;
+ switch ($type) {
+ case self::TYPE_FUNCTION:
+ $reflection = new ReflectionFunction($f);
+ break;
+ case self::TYPE_CLASS:
+ $reflection = new ReflectionClass($c);
+ break;
+ case self::TYPE_METHOD:
+ if ($c === null) {
+ $reflection = null;
+ } else {
+ $reflection = new ReflectionMethod($c, $f);
+ if (is_object($c)) $object = $c;
+ }
+ break;
+ default:
+ throw StateException::unexpected_state();
+ }
+ }
+ A::merge($prefixArgs, $args);
+
+ $this->type = $type;
+ $this->flags = $flags;
+ $this->func = $func;
+ $this->bound = $bound;
+ $this->reason = $reason;
+ $this->object = $object;
+ $this->prefixArgs = $prefixArgs;
+ $this->updateReflection($reflection);
+ }
+
+ protected int $type;
+
+ protected int $flags;
+
+ protected ?array $func;
+
+ protected bool $bound;
+
+ protected ?string $reason;
+
+ protected ?object $object;
+
+ protected array $prefixArgs;
+
+ /** @var Closure|ReflectionFunction|ReflectionMethod|ReflectionClass */
+ protected $reflection;
+
+ protected bool $variadic;
+
+ protected int $minArgs;
+
+ protected int $maxArgs;
+
+ protected function updateReflection($reflection): void {
+ $variadic = false;
+ $minArgs = $maxArgs = 0;
+ if ($reflection instanceof Closure) {
+ $r = new ReflectionFunction($reflection);
+ $variadic = $r->isVariadic();
+ $minArgs = $r->getNumberOfRequiredParameters();
+ $maxArgs = $r->getNumberOfParameters();
+ } elseif ($reflection instanceof ReflectionClass) {
+ $r = $reflection->getConstructor();
+ if ($r === null) {
+ $variadic = false;
+ $minArgs = $maxArgs = 0;
+ } else {
+ $variadic = $r->isVariadic();
+ $minArgs = $r->getNumberOfRequiredParameters();
+ $maxArgs = $r->getNumberOfParameters();
+ }
+ } elseif ($reflection !== null) {
+ $variadic = $reflection->isVariadic();
+ $minArgs = $reflection->getNumberOfRequiredParameters();
+ $maxArgs = $reflection->getNumberOfParameters();
+ }
+ $this->reflection = $reflection;
+ $this->variadic = $variadic;
+ $this->minArgs = $minArgs;
+ $this->maxArgs = $maxArgs;
+ }
+
+ function isBound(): bool {
+ if ($this->type !== self::TYPE_METHOD) return true;
+ if ($this->flags & self::FLAG_STATIC) return $this->bound;
+ else return $this->bound && $this->object !== null;
+ }
+
+ function bind($object): self {
+ if ($this->type !== self::TYPE_METHOD) return $this;
+
+ [$c, $f] = $this->func;
+ if ($this->reflection === null) {
+ $this->func[0] = $c = $object;
+ $this->updateReflection(new ReflectionMethod($c, $f));
+ }
+ if (is_object($object) && !($this->flags & self::FLAG_STATIC)) {
+ if (is_object($c)) $c = get_class($c);
+ if (is_string($c) && !($object instanceof $c)) {
+ throw ValueException::invalid_type($object, $c);
+ }
+ $this->object = $object;
+ $this->bound = true;
+ }
+ return $this;
+ }
+
+ function invoke(?array $args=null) {
+ $args = array_merge($this->prefixArgs, $args ?? []);
+ if (!$this->variadic) $args = array_slice($args, 0, $this->maxArgs);
+ $minArgs = $this->minArgs;
+ while (count($args) < $minArgs) $args[] = null;
+
+ switch ($this->type) {
+ case self::TYPE_CLOSURE:
+ /** @var Closure $closure */
+ $closure = $this->reflection;
+ return $closure(...$args);
+ case self::TYPE_FUNCTION:
+ /** @var ReflectionFunction $function */
+ $function = $this->reflection;
+ return $function->invoke(...$args);
+ case self::TYPE_METHOD:
+ /** @var ReflectionMethod $method */
+ $method = $this->reflection;
+ if ($method === null) throw self::not_a_callable($this->func, $this->reason);
+ return $method->invoke($this->object, ...$args);
+ case self::TYPE_CLASS:
+ /** @var ReflectionClass $class */
+ $class = $this->reflection;
+ return $class->newInstance(...$args);
+ default:
+ throw StateException::unexpected_state();
+ }
+ }
+}
diff --git a/php/src/php/iter/AbstractIterator.php b/php/src/php/iter/AbstractIterator.php
new file mode 100644
index 0000000..1e9f991
--- /dev/null
+++ b/php/src/php/iter/AbstractIterator.php
@@ -0,0 +1,154 @@
+rewind();
+ }
+
+ #############################################################################
+ # Implémentation par défaut
+
+ private $setup = false;
+
+ protected function _hasIteratorBeenSetup(): bool {
+ return $this->setup;
+ }
+
+ private $valid = false;
+ private $toredown = true;
+
+ private $index = 0;
+ protected $key;
+ protected $item = null;
+
+ function key() {
+ return $this->key;
+ }
+
+ function current() {
+ return $this->item;
+ }
+
+ function next(): void {
+ if ($this->toredown) return;
+ $this->valid = false;
+ try {
+ $item = $this->iter_next($key);
+ } catch (NoMoreDataException $e) {
+ $this->iter_beforeClose();
+ try {
+ $this->iter_teardown();
+ } catch (Exception $e) {
+ }
+ $this->toredown = true;
+ return;
+ }
+ $this->iter_cook($item);
+ $this->item = $item;
+ if ($key !== null) {
+ $this->key = $key;
+ } else {
+ $this->index++;
+ $this->key = $this->index;
+ }
+ $this->valid = true;
+ }
+
+ function rewind(): void {
+ if ($this->setup) {
+ if (!$this->toredown) {
+ $this->iter_beforeClose();
+ try {
+ $this->iter_teardown();
+ } catch (Exception $e) {
+ }
+ }
+ $this->setup = false;
+ $this->valid = false;
+ $this->toredown = true;
+ $this->index = 0;
+ $this->key = null;
+ $this->item = null;
+ }
+ }
+
+ function valid(): bool {
+ if (!$this->setup) {
+ try {
+ $this->iter_setup();
+ } catch (Exception $e) {
+ }
+ $this->setup = true;
+ $this->toredown = false;
+ $this->iter_beforeStart();
+ $this->next();
+ }
+ return $this->valid;
+ }
+}
diff --git a/php/src/php/mprop.php b/php/src/php/mprop.php
new file mode 100644
index 0000000..a0bc28d
--- /dev/null
+++ b/php/src/php/mprop.php
@@ -0,0 +1,122 @@
+getMethod($method);
+ } catch (ReflectionException $e) {
+ return oprop::get($object, $property, $default);
+ }
+ return nur_func::call([$object, $m], $default);
+ }
+
+ /** spécifier la valeur d'une propriété */
+ static final function set(object $object, string $property, $value, ?string $method=null) {
+ $c = new ReflectionClass($object);
+ return self::_set($c, $object, $property, $value, $method);
+ }
+
+ private static function _set(ReflectionClass $c, object $object, string $property, $value, ?string $method) {
+ if ($method === null) $method = self::get_setter_name($property);
+ try {
+ $m = $c->getMethod($method);
+ } catch (ReflectionException $e) {
+ return oprop::_set($c, $object, $property, $value);
+ }
+ nur_func::call([$object, $m], $value);
+ return $value;
+ }
+
+ /**
+ * initialiser $dest avec les valeurs de $values
+ *
+ * les noms des clés de $values sont transformées en camelCase pour avoir les
+ * noms des propriétés correspondantes
+ */
+ static final function set_values(object $object, ?array $values, ?array $keys=null): void {
+ if ($values === null) return;
+ if ($keys === null) $keys = array_keys($values);
+ $c = new ReflectionClass($object);
+ foreach ($keys as $key) {
+ if (array_key_exists($key, $values)) {
+ $property = str::us2camel($key);
+ self::_set($c, $object, $property, $values[$key], null);
+ }
+ }
+ }
+
+ /** incrémenter la valeur d'une propriété */
+ static final function inc(object $object, string $property): int {
+ $value = intval(self::get($object, $property, 0));
+ $value++;
+ self::set($object, $property, $value);
+ return $value;
+ }
+
+ /** décrémenter la valeur d'une propriété */
+ static final function dec(object $object, string $property, bool $allow_negative=false): int {
+ $value = intval(self::get($object, $property, 0));
+ if ($allow_negative || $value > 0) {
+ $value--;
+ self::set($object, $property, $value);
+ }
+ return $value;
+ }
+
+ /**
+ * Fusionner la valeur à la propriété qui est transformée en tableau si
+ * nécessaire
+ */
+ static final function merge(object $object, string $property, $array): void {
+ $values = cl::with(self::get($object, $property));
+ $values = cl::merge($values, cl::with($array));
+ self::set($object, $property, $values);
+ }
+
+ /**
+ * Ajouter la valeur à la propriété qui est transformée en tableau si
+ * nécessaire
+ */
+ static final function append(object $object, string $property, $value): void {
+ $values = cl::with(self::get($object, $property));
+ $values[] = $value;
+ self::set($object, $property, $values);
+ }
+}
diff --git a/php/src/php/nur_func.php b/php/src/php/nur_func.php
new file mode 100644
index 0000000..ba4cc06
--- /dev/null
+++ b/php/src/php/nur_func.php
@@ -0,0 +1,453 @@
+ 1) {
+ if (!array_key_exists(1, $func)) return false;
+ if (!is_string($func[1]) || strlen($func[1]) == 0) return false;
+ if (strpos($func[1], "\\") !== false) return false;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * si $func est une chaine de la forme "::method" alors la remplacer par la
+ * chaine "$class::method"
+ *
+ * si $func est un tableau de la forme ["method"] ou [null, "method"], alors
+ * le remplacer par [$class, "method"]
+ *
+ * on assume que {@link is_static()}($func) retourne true
+ *
+ * @return bool true si la correction a été faite
+ */
+ static final function fix_static(&$func, $class): bool {
+ if (is_object($class)) $class = get_class($class);
+
+ if (is_string($func) && substr($func, 0, 2) == "::") {
+ $func = "$class$func";
+ return true;
+ } elseif (is_array($func) && array_key_exists(0, $func)) {
+ $count = count($func);
+ if ($count == 1) {
+ $func = [$class, $func[0]];
+ return true;
+ } elseif ($count > 1 && $func[0] === null) {
+ $func[0] = $class;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** tester si $method est une chaine de la forme "->method" */
+ private static function isam($method): bool {
+ return is_string($method)
+ && strlen($method) > 2
+ && substr($method, 0, 2) == "->";
+ }
+
+ /**
+ * tester si $func est une chaine de la forme "->method" ou un tableau de la
+ * forme ["->method", ...] ou [anything, "->method", ...]
+ */
+ static final function is_method($func): bool {
+ if (is_string($func)) {
+ return self::isam($func);
+ } elseif (is_array($func) && array_key_exists(0, $func)) {
+ if (self::isam($func[0])) {
+ # ["->method", ...]
+ return true;
+ }
+ if (array_key_exists(1, $func) && self::isam($func[1])) {
+ # [anything, "->method", ...]
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * si $func est une chaine de la forme "->method" alors la remplacer par le
+ * tableau [$object, "method"]
+ *
+ * si $func est un tableau de la forme ["->method"] ou [anything, "->method"],
+ * alors le remplacer par [$object, "method"]
+ *
+ * @return bool true si la correction a été faite
+ */
+ static final function fix_method(&$func, $object): bool {
+ if (!is_object($object)) return false;
+
+ if (is_string($func)) {
+ if (self::isam($func)) {
+ $func = [$object, substr($func, 2)];
+ return true;
+ }
+ } elseif (is_array($func) && array_key_exists(0, $func)) {
+ if (self::isam($func[0])) $func = array_merge([null], $func);
+ if (count($func) > 1 && array_key_exists(1, $func) && self::isam($func[1])) {
+ $func[0] = $object;
+ $func[1] = substr($func[1], 2);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * si $func est un tableau de plus de 2 éléments, alors déplacer les éléments
+ * supplémentaires au début de $args. par exemple:
+ * ~~~
+ * $func = ["class", "method", "arg1", "arg2"];
+ * $args = ["arg3"];
+ * func::fix_args($func, $args)
+ * # $func === ["class", "method"]
+ * # $args === ["arg1", "arg2", "arg3"]
+ * ~~~
+ *
+ * @return bool true si la correction a été faite
+ */
+ static final function fix_args(&$func, ?array &$args): bool {
+ if ($args === null) $args = [];
+ if (is_array($func) && count($func) > 2) {
+ $prefix_args = array_slice($func, 2);
+ $func = array_slice($func, 0, 2);
+ $args = array_merge($prefix_args, $args);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * s'assurer que $func est un appel de méthode ou d'une méthode statique;
+ * et renseigner le cas échéant les arguments. si $func ne fait pas mention
+ * de la classe ou de l'objet, le renseigner avec $class_or_object.
+ *
+ * @return bool true si c'est une fonction valide. il ne reste plus qu'à
+ * l'appeler avec {@link call()}
+ */
+ static final function check_func(&$func, $class_or_object, &$args=null): bool {
+ if ($func instanceof Closure) return true;
+ if (self::is_method($func)) {
+ # méthode
+ self::fix_method($func, $class_or_object);
+ self::fix_args($func, $args);
+ return true;
+ } elseif (self::is_static($func)) {
+ # méthode statique
+ self::fix_static($func, $class_or_object);
+ self::fix_args($func, $args);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Comme {@link check_func()} mais lance une exception si la fonction est
+ * invalide
+ *
+ * @throws ValueException si $func n'est pas une fonction ou une méthode valide
+ */
+ static final function ensure_func(&$func, $class_or_object, &$args=null): void {
+ if (!self::check_func($func, $class_or_object, $args)) {
+ throw ValueException::invalid_type($func, "callable");
+ }
+ }
+
+ static final function _prepare($func): array {
+ $object = null;
+ if (is_callable($func)) {
+ if (is_array($func)) {
+ $rf = new ReflectionMethod(...$func);
+ $object = $func[0];
+ if (is_string($object)) $object = null;
+ } elseif ($func instanceof Closure) {
+ $rf = new ReflectionFunction($func);
+ } elseif (is_string($func) && strpos($func, "::") === false) {
+ $rf = new ReflectionFunction($func);
+ } else {
+ $rf = new ReflectionMethod($func);
+ }
+ } elseif ($func instanceof ReflectionMethod) {
+ $rf = $func;
+ } elseif ($func instanceof ReflectionFunction) {
+ $rf = $func;
+ } elseif (is_array($func) && count($func) == 2 && isset($func[0]) && isset($func[1])
+ && ($func[1] instanceof ReflectionMethod || $func[1] instanceof ReflectionFunction)) {
+ $object = $func[0];
+ if (is_string($object)) $object = null;
+ $rf = $func[1];
+ } elseif (is_string($func) && strpos($func, "::") === false) {
+ $rf = new ReflectionFunction($func);
+ } else {
+ throw ValueException::invalid_type($func, "callable");
+ }
+ $minArgs = $rf->getNumberOfRequiredParameters();
+ $maxArgs = $rf->getNumberOfParameters();
+ $variadic = $rf->isVariadic();
+ return [$rf instanceof ReflectionMethod, $object, $rf, $minArgs, $maxArgs, $variadic];
+ }
+
+ static final function _fill(array $context, array &$args): void {
+ $minArgs = $context[3];
+ $maxArgs = $context[4];
+ $variadic = $context[5];
+ if (!$variadic) $args = array_slice($args, 0, $maxArgs);
+ while (count($args) < $minArgs) $args[] = null;
+ }
+
+ static final function _call($context, array $args) {
+ self::_fill($context, $args);
+ $use_object = $context[0];
+ $object = $context[1];
+ $method = $context[2];
+ if ($use_object) {
+ if (count($args) === 0) return $method->invoke($object);
+ else return $method->invokeArgs($object, $args);
+ } else {
+ if (count($args) === 0) return $method->invoke();
+ else return $method->invokeArgs($args);
+ }
+ }
+
+ /**
+ * Appeler la fonction spécifiée avec les arguments spécifiés.
+ * Adapter $args en fonction du nombre réel d'arguments de $func
+ *
+ * @param callable|ReflectionFunction|ReflectionMethod $func
+ */
+ static final function call($func, ...$args) {
+ return self::_call(self::_prepare($func), $args);
+ }
+
+ /** remplacer $value par $func($value, ...$args) */
+ static final function apply(&$value, $func, ...$args): void {
+ if ($func !== null) {
+ if ($args) $args = array_merge([$value], $args);
+ else $args = [$value];
+ $value = self::call($func, ...$args);
+ }
+ }
+
+ const MASK_PS = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC;
+ const MASK_P = ReflectionMethod::IS_PUBLIC;
+ const METHOD_PS = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC;
+ const METHOD_P = ReflectionMethod::IS_PUBLIC;
+
+ private static function matches(string $name, array $includes, array $excludes): bool {
+ if ($includes) {
+ $matches = false;
+ foreach ($includes as $include) {
+ if (substr($include, 0, 1) == "/") {
+ # expression régulière
+ if (preg_match($include, $name)) {
+ $matches = true;
+ break;
+ }
+ } else {
+ # tester la présence de la sous-chaine
+ if (strpos($name, $include) !== false) {
+ $matches = true;
+ break;
+ }
+ }
+ }
+ if (!$matches) return false;
+ }
+ foreach ($excludes as $exclude) {
+ if (substr($exclude, 0, 1) == "/") {
+ # expression régulière
+ if (preg_match($exclude, $name)) return false;
+ } else {
+ # tester la présence de la sous-chaine
+ if (strpos($name, $exclude) !== false) return false;
+ }
+ }
+ return true;
+ }
+
+ /** @var Schema */
+ private static $call_all_params_schema;
+
+ /**
+ * retourner la liste des méthodes de $class_or_object qui correspondent au
+ * filtre $options. le filtre doit respecter le schéme {@link CALL_ALL_PARAMS_SCHEMA}
+ */
+ static function get_all($class_or_object, $params=null): array {
+ Schema::nv($paramsv, $params, null
+ , self::$call_all_params_schema, ref_func::CALL_ALL_PARAMS_SCHEMA);
+ if (is_callable($class_or_object, true) && is_array($class_or_object)) {
+ # callable sous forme de tableau
+ $class_or_object = $class_or_object[0];
+ }
+ if (is_string($class_or_object)) {
+ # lister les méthodes publiques statiques de la classe
+ $mask = self::MASK_PS;
+ $expected = self::METHOD_PS;
+ $c = new ReflectionClass($class_or_object);
+ } elseif (is_object($class_or_object)) {
+ # lister les méthodes publiques de la classe
+ $c = new ReflectionClass($class_or_object);
+ $mask = $params["static_only"]? self::MASK_PS: self::MASK_P;
+ $expected = $params["static_only"]? self::METHOD_PS: self::METHOD_P;
+ } else {
+ throw new ValueException("$class_or_object: vous devez spécifier une classe ou un objet");
+ }
+ $prefix = $params["prefix"]; $prefixlen = strlen($prefix);
+ $args = $params["args"];
+ $includes = $params["include"];
+ $excludes = $params["exclude"];
+ $methods = [];
+ foreach ($c->getMethods() as $m) {
+ if (($m->getModifiers() & $mask) != $expected) continue;
+ $name = $m->getName();
+ if (substr($name, 0, $prefixlen) != $prefix) continue;
+ if (!self::matches($name, $includes, $excludes)) continue;
+ $methods[] = cl::merge([$class_or_object, $name], $args);
+ }
+ return $methods;
+ }
+
+ /**
+ * Appeler toutes les méthodes publiques de $object_or_class et retourner un
+ * tableau [$method_name => $return_value] des valeurs de retour.
+ */
+ static final function call_all($class_or_object, $params=null): array {
+ $methods = self::get_all($class_or_object, $params);
+ $values = [];
+ foreach ($methods as $method) {
+ self::fix_args($method, $args);
+ $values[$method[1]] = self::call($method, ...$args);
+ }
+ return $values;
+ }
+
+ /**
+ * tester si $func est une chaine de la forme "XXX" où XXX est une classe
+ * valide, ou un tableau de la forme ["XXX", ...]
+ *
+ * NB: il est possible d'avoir {@link is_static()} et {@link is_class()}
+ * vraies pour la même valeur. s'il faut supporter les deux cas, appeler
+ * {@link is_static()} d'abord, mais dans ce cas, on ne supporte que les
+ * classes qui sont dans un package
+ */
+ static final function is_class($class): bool {
+ if (is_string($class)) {
+ return class_exists($class);
+ } elseif (is_array($class) && array_key_exists(0, $class)) {
+ return class_exists($class[0]);
+ }
+ return false;
+ }
+
+ /**
+ * en assumant que {@link is_class()} est vrai, si $class est un tableau de
+ * plus de 1 éléments, alors déplacer les éléments supplémentaires au début de
+ * $args. par exemple:
+ * ~~~
+ * $class = ["class", "arg1", "arg2"];
+ * $args = ["arg3"];
+ * func::fix_class_args($class, $args)
+ * # $class === "class"
+ * # $args === ["arg1", "arg2", "arg3"]
+ * ~~~
+ *
+ * @return bool true si la correction a été faite
+ */
+ static final function fix_class_args(&$class, ?array &$args): bool {
+ if ($args === null) $args = [];
+ if (is_array($class)) {
+ if (count($class) > 1) {
+ $prefix_args = array_slice($class, 1);
+ $class = array_slice($class, 0, 1)[0];
+ $args = array_merge($prefix_args, $args);
+ } else {
+ $class = $class[0];
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * s'assurer que $class est une classe et renseigner le cas échéant les
+ * arguments.
+ *
+ * @return bool true si c'est une classe valide. il ne reste plus qu'à
+ * l'instancier avec {@link cons()}
+ */
+ static final function check_class(&$class, &$args=null): bool {
+ if (self::is_class($class)) {
+ self::fix_class_args($class, $args);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Comme {@link check_class()} mais lance une exception si la classe est
+ * invalide
+ *
+ * @throws ValueException si $class n'est pas une classe valide
+ */
+ static final function ensure_class(&$class, &$args=null): void {
+ if (!self::check_class($class, $args)) {
+ throw ValueException::invalid_type($class, "class");
+ }
+ }
+
+ /**
+ * Instancier la classe avec les arguments spécifiés.
+ * Adapter $args en fonction du nombre réel d'arguments du constructeur
+ */
+ static final function cons(string $class, ...$args) {
+ $c = new ReflectionClass($class);
+ $rf = $c->getConstructor();
+ if ($rf === null) {
+ return $c->newInstance();
+ } else {
+ if (!$rf->isVariadic()) {
+ $minArgs = $rf->getNumberOfRequiredParameters();
+ $maxArgs = $rf->getNumberOfParameters();
+ $args = array_slice($args, 0, $maxArgs);
+ while (count($args) < $minArgs) {
+ $args[] = null;
+ }
+ }
+ return $c->newInstanceArgs($args);
+ }
+ }
+}
diff --git a/php/src/php/oprop.php b/php/src/php/oprop.php
new file mode 100644
index 0000000..599645e
--- /dev/null
+++ b/php/src/php/oprop.php
@@ -0,0 +1,152 @@
+getProperty($property);
+ $p->setAccessible(true);
+ return $p->getValue($object);
+ } catch (ReflectionException $e) {
+ if (property_exists($object, $property)) return $object->$property;
+ else return $default;
+ }
+ }
+
+ static final function _set(ReflectionClass $c, object $object, string $property, $value) {
+ try {
+ $p = $c->getProperty($property);
+ $p->setAccessible(true);
+ $p->setValue($object, $value);
+ } catch (ReflectionException $e) {
+ $object->$property = $value;
+ }
+ return $value;
+ }
+
+ /** spécifier la valeur d'une propriété */
+ static final function set(object $object, string $property, $value) {
+ $c = new ReflectionClass($object);
+ return self::_set($c, $object, $property, $value);
+ }
+
+ /**
+ * initialiser $dest avec les valeurs de $values
+ *
+ * les noms des clés de $values sont transformées en camelCase pour avoir les
+ * noms des propriétés correspondantes
+ */
+ static final function set_values(object $object, ?array $values, ?array $keys=null): void {
+ if ($values === null) return;
+ if ($keys === null) $keys = array_keys($values);
+ $c = new ReflectionClass($object);
+ foreach ($keys as $key) {
+ if (array_key_exists($key, $values)) {
+ $property = str::us2camel($key);
+ self::_set($c, $object, $property, $values[$key]);
+ }
+ }
+ }
+
+ /** incrémenter la valeur d'une propriété */
+ static final function inc(object $object, string $property): int {
+ $c = new ReflectionClass($object);
+ try {
+ $p = $c->getProperty($property);
+ $p->setAccessible(true);
+ $value = (int)$p->getValue($object);
+ $value++;
+ $p->setValue($object, $value);
+ } catch (ReflectionException $e) {
+ if (property_exists($object, $property)) {
+ $value = (int)$object->$property;
+ $value++;
+ } else {
+ $value = 1;
+ }
+ $object->$property = $value;
+ }
+ return $value;
+ }
+
+ /** décrémenter la valeur d'une propriété */
+ static final function dec(object $object, string $property, bool $allow_negative=false): int {
+ $c = new ReflectionClass($object);
+ try {
+ $p = $c->getProperty($property);
+ $p->setAccessible(true);
+ $value = (int)$p->getValue($object);
+ if ($allow_negative || $value > 0) {
+ $value --;
+ $p->setValue($object, $value);
+ }
+ } catch (ReflectionException $e) {
+ if (property_exists($object, $property)) {
+ $value = (int)$object->$property;
+ } else {
+ $value = 0;
+ }
+ if ($allow_negative || $value > 0) $value--;
+ $object->$property = $value;
+ }
+ return $value;
+ }
+
+ /**
+ * Fusionner la valeur à la propriété qui est transformée en tableau si
+ * nécessaire
+ */
+ static final function merge(object $object, string $property, $array): void {
+ $c = new ReflectionClass($object);
+ try {
+ $p = $c->getProperty($property);
+ $p->setAccessible(true);
+ $values = cl::with($p->getValue($object));
+ $values = cl::merge($values, cl::with($array));
+ $p->setValue($object, $values);
+ } catch (ReflectionException $e) {
+ if (property_exists($object, $property)) {
+ $values = cl::with($object->$property);
+ } else {
+ $values = [];
+ }
+ $values = cl::merge($values, cl::with($array));
+ $object->$property = $values;
+ }
+ }
+
+ /**
+ * Ajouter la valeur à la propriété qui est transformée en tableau si
+ * nécessaire
+ */
+ static final function append(object $object, string $property, $value): void {
+ $c = new ReflectionClass($object);
+ try {
+ $p = $c->getProperty($property);
+ $p->setAccessible(true);
+ $values = cl::with($p->getValue($object));
+ $values[] = $value;
+ $p->setValue($object, $values);
+ } catch (ReflectionException $e) {
+ if (property_exists($object, $property)) {
+ $values = cl::with($object->$property);
+ } else {
+ $values = [];
+ }
+ $values[] = $value;
+ $object->$property = $values;
+ }
+ }
+}
diff --git a/php/src/php/time/Date.php b/php/src/php/time/Date.php
new file mode 100644
index 0000000..3556ed4
--- /dev/null
+++ b/php/src/php/time/Date.php
@@ -0,0 +1,20 @@
+setTime(0, 0);
+ }
+
+ function format($format=self::DEFAULT_FORMAT): string {
+ return \DateTime::format($format);
+ }
+}
diff --git a/php/src/php/time/DateInterval.php b/php/src/php/time/DateInterval.php
new file mode 100644
index 0000000..c9ca935
--- /dev/null
+++ b/php/src/php/time/DateInterval.php
@@ -0,0 +1,59 @@
+y;
+ $m = $interval->m;
+ $d = $interval->d;
+ if ($y > 0) $string .= "${y}Y";
+ if ($m > 0) $string .= "${m}M";
+ if ($d > 0) $string .= "${d}D";
+ $string .= "T";
+ $h = $interval->h;
+ $i = $interval->i;
+ $s = $interval->s;
+ if ($h > 0) $string .= "${h}H";
+ if ($i > 0) $string .= "${i}M";
+ if ($s > 0 || $string == "PT") $string .= "${s}S";
+ if ($interval->invert == 1) $string = "-$string";
+ return $string;
+ }
+
+ function __construct($duration) {
+ if (is_int($duration)) $duration = "PT${duration}S";
+ if ($duration instanceof \DateInterval) {
+ $this->y = $duration->y;
+ $this->m = $duration->m;
+ $this->d = $duration->d;
+ $this->h = $duration->h;
+ $this->i = $duration->i;
+ $this->s = $duration->s;
+ $this->invert = $duration->invert;
+ $this->days = $duration->days;
+ } elseif (!is_string($duration)) {
+ throw new InvalidArgumentException("duration must be a string");
+ } else {
+ if (substr($duration, 0, 1) == "-") {
+ $duration = substr($duration, 1);
+ $invert = true;
+ } else {
+ $invert = false;
+ }
+ parent::__construct($duration);
+ if ($invert) $this->invert = 1;
+ }
+ }
+
+ function __toString(): string {
+ return self::to_string($this);
+ }
+}
diff --git a/php/src/php/time/DateTime.php b/php/src/php/time/DateTime.php
new file mode 100644
index 0000000..1fcd632
--- /dev/null
+++ b/php/src/php/time/DateTime.php
@@ -0,0 +1,343 @@
+format("Ymd\\THis");
+ $Z = $datetime->format("P");
+ if ($Z === "+00:00") $Z = "Z";
+ return "$YmdHMS$Z";
+ }
+
+ const DEFAULT_FORMAT = "d/m/Y H:i:s";
+ const INT_FORMATS = [
+ "year" => "Y",
+ "month" => "m",
+ "day" => "d",
+ "hour" => "H",
+ "minute" => "i",
+ "second" => "s",
+ "wday" => "N",
+ "wnum" => "W",
+ ];
+ const STRING_FORMATS = [
+ "timezone" => "P",
+ "datetime" => "d/m/Y H:i:s",
+ "date" => "d/m/Y",
+ "Ymd" => "Ymd",
+ "YmdHMS" => "Ymd\\THis",
+ "YmdHMSZ" => [self::class, "_YmdHMSZ_format"],
+ ];
+
+ static function clone(DateTimeInterface $dateTime): self {
+ if ($dateTime instanceof static) return clone $dateTime;
+ $clone = new static();
+ $clone->setTimestamp($dateTime->getTimestamp());
+ $clone->setTimezone($dateTime->getTimezone());
+ return $clone;
+ }
+
+ /**
+ * corriger une année à deux chiffres qui est située dans le passé et
+ * retourner l'année à 4 chiffres.
+ *
+ * par exemple, si l'année courante est 2019, alors:
+ * - fix_past_year('18') === '2018'
+ * - fix_past_year('19') === '1919'
+ * - fix_past_year('20') === '1920'
+ */
+ static function fix_past_year(int $year): int {
+ if ($year < 100) {
+ $y = getdate(); $y = $y["year"];
+ $r = $y % 100;
+ $c = $y - $r;
+ if ($year >= $r) $year += $c - 100;
+ else $year += $c;
+ }
+ return $year;
+ }
+
+ /**
+ * corriger une année à deux chiffres et retourner l'année à 4 chiffres.
+ * l'année charnière entre année passée et année future est 70
+ *
+ * par exemple, si l'année courante est 2019, alors:
+ * - fix_past_year('18') === '2018'
+ * - fix_past_year('19') === '2019'
+ * - fix_past_year('20') === '2020'
+ * - fix_past_year('69') === '2069'
+ * - fix_past_year('70') === '1970'
+ * - fix_past_year('71') === '1971'
+ */
+ static function fix_any_year(int $year): int {
+ if ($year < 100) {
+ $y = intval(date("Y"));
+ $r = $y % 100;
+ $c = $y - $r;
+ if ($year >= 70) $year += $c - 100;
+ else $year += $c;
+ }
+ return $year;
+ }
+
+ function __construct($datetime="now", DateTimeZone $timezone=null) {
+ $datetime ??= "now";
+ if ($datetime instanceof \DateTimeInterface) {
+ if ($timezone === null) $timezone = $datetime->getTimezone();
+ parent::__construct();
+ $this->setTimestamp($datetime->getTimestamp());
+ $this->setTimezone($timezone);
+ } elseif (is_int($datetime)) {
+ parent::__construct("now", $timezone);
+ $this->setTimestamp($datetime);
+ } elseif (is_string($datetime)) {
+ $Y = $H = $Z = null;
+ if (preg_match(self::DMY_PATTERN, $datetime, $ms)) {
+ $Y = $ms[3] ?? null;
+ if ($Y !== null) $Y = self::fix_any_year(intval($Y));
+ else $Y = intval(date("Y"));
+ $m = intval($ms[2]);
+ $d = intval($ms[1]);
+ } elseif (preg_match(self::YMD_PATTERN, $datetime, $ms)) {
+ $Y = $ms[1];
+ if (strlen($Y) == 2) $Y = self::fix_any_year(intval($ms[1]));
+ else $Y = intval($Y);
+ $m = intval($ms[2]);
+ $d = intval($ms[3]);
+ } elseif (preg_match(self::DMYHIS_PATTERN, $datetime, $ms)) {
+ $Y = $ms[3];
+ if ($Y !== null) $Y = self::fix_any_year(intval($Y));
+ else $Y = intval(date("Y"));
+ $m = intval($ms[2]);
+ $d = intval($ms[1]);
+ $H = intval($ms[4]);
+ $M = intval($ms[5]);
+ $S = intval($ms[6] ?? 0);
+ } elseif (preg_match(self::YMDHISZ_PATTERN, $datetime, $ms)) {
+ $Y = $ms[1];
+ if (strlen($Y) == 2) $Y = self::fix_any_year(intval($ms[1]));
+ else $Y = intval($Y);
+ $m = intval($ms[2]);
+ $d = intval($ms[3]);
+ $H = intval($ms[4]);
+ $M = intval($ms[5]);
+ $S = intval($ms[6] ?? 0);
+ $Z = $ms[7] ?? null;
+ }
+ if ($Y !== null) {
+ if ($H === null) $datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d);
+ else $datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H, $M, $S);
+ if ($Z !== null) $timezone = new DateTimeZone("UTC");
+ }
+ parent::__construct($datetime, $timezone);
+ } elseif (is_array($datetime) && ($datetime = self::parse_array($datetime)) !== null) {
+ [$Y, $m, $d, $H, $M, $S, $tz] = $datetime;
+ if ($H === null && $M === null && $S === null) {
+ $datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d);
+ } else {
+ $datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H ?? 0, $M ?? 0, $S ?? 0);
+ }
+ if ($tz === "Z" || $tz === "z") $tz = "UTC";
+ $timezone = $tz !== null? new DateTimeZone($tz): null;
+ parent::__construct($datetime, $timezone);
+ } else {
+ throw new InvalidArgumentException("datetime must be a string or an instance of DateTimeInterface");
+ }
+ }
+
+ function diff($target, $absolute=false): DateInterval {
+ return new DateInterval(parent::diff($target, $absolute));
+ }
+
+ function format($format=self::DEFAULT_FORMAT): string {
+ return \DateTime::format($format);
+ }
+
+ /**
+ * modifier cet objet pour que l'heure soit à 00:00:00 ce qui le rend propice
+ * à l'utilisation comme borne inférieure d'une période
+ */
+ function wrapStartOfDay(): self {
+ $this->setTime(0, 0);
+ return $this;
+ }
+
+ /**
+ * modifier cet objet pour que l'heure soit à 23:59:59.999999 ce qui le rend
+ * propice à l'utilisation comme borne supérieure d'une période
+ */
+ function wrapEndOfDay(): self {
+ $this->setTime(23, 59, 59, 999999);
+ return $this;
+ }
+
+ function getPrevDay(int $nbDays=1, bool $skipWeekend=false): self {
+ if ($nbDays == 1 && $skipWeekend && $this->wday == 1) {
+ $nbdays = 3;
+ }
+ return static::with($this->sub(new \DateInterval("P${nbDays}D")));
+ }
+
+ function getNextDay(int $nbDays=1, bool $skipWeekend=false): self {
+ if ($nbDays == 1 && $skipWeekend) {
+ $wday = $this->wday;
+ if ($wday > 5) $nbDays = 8 - $this->wday;
+ }
+ return static::with($this->add(new \DateInterval("P${nbDays}D")));
+ }
+
+ function getElapsedAt(?DateTime $now=null, ?int $resolution=null): string {
+ return Elapsed::format_at($this, $now, $resolution);
+ }
+
+ function getElapsedSince(?DateTime $now=null, ?int $resolution=null): string {
+ return Elapsed::format_since($this, $now, $resolution);
+ }
+
+ function getElapsedDelay(?DateTime $now=null, ?int $resolution=null): string {
+ return Elapsed::format_delay($this, $now, $resolution);
+ }
+
+ function __toString(): string {
+ return $this->format();
+ }
+
+ function __get($name) {
+ if (array_key_exists($name, self::INT_FORMATS)) {
+ $format = self::INT_FORMATS[$name];
+ if (is_callable($format)) return $format($this);
+ else return intval($this->format($format));
+ } elseif (array_key_exists($name, self::STRING_FORMATS)) {
+ $format = self::STRING_FORMATS[$name];
+ if (is_callable($format)) return $format($this);
+ else return $this->format($format);
+ }
+ throw new InvalidArgumentException("Unknown property $name");
+ }
+}
diff --git a/php/src/php/time/Delay.php b/php/src/php/time/Delay.php
new file mode 100644
index 0000000..14a5307
--- /dev/null
+++ b/php/src/php/time/Delay.php
@@ -0,0 +1,174 @@
+ [0, 5],
+ "d" => [1, 5],
+ "h" => [1, 0],
+ "m" => [1, 0],
+ "s" => [1, 0],
+ ];
+
+ static function compute_dest(int $x, string $u, ?int $y, DateTime $from): array {
+ $dest = DateTime::clone($from);
+ $yu = null;
+ switch ($u) {
+ case "w":
+ if ($x > 0) {
+ $x *= 7;
+ $dest->add(new \DateInterval("P${x}D"));
+ }
+ $w = 7 - intval($dest->format("w"));
+ $dest->add(new \DateInterval("P${w}D"));
+ $yu = "h";
+ break;
+ case "d":
+ $dest->add(new \DateInterval("P${x}D"));
+ $yu = "h";
+ break;
+ case "h":
+ $dest->add(new \DateInterval("PT${x}H"));
+ $yu = "m";
+ break;
+ case "m":
+ $dest->add(new \DateInterval("PT${x}M"));
+ $yu = "s";
+ break;
+ case "s":
+ $dest->add(new \DateInterval("PT${x}S"));
+ break;
+ }
+ if ($y !== null && $yu !== null) {
+ $h = intval($dest->format("H"));
+ $m = intval($dest->format("i"));
+ switch ($yu) {
+ case "h":
+ $dest->setTime($y, 0, 0, 0);
+ break;
+ case "m":
+ $dest->setTime($h, $y, 0, 0);
+ break;
+ case "s":
+ $dest->setTime($h, $m, $y, 0);
+ break;
+ }
+ }
+ $u = strtoupper($u);
+ $repr = $y !== null? "$x$u$y": "$x";
+ return [$dest, $repr];
+ }
+
+ function __construct($delay, ?DateTimeInterface $from=null) {
+ if ($from === null) $from = new DateTime();
+ if ($delay === "INF") {
+ $dest = DateTime::clone($from);
+ $dest->add(new DateInterval("P9999Y"));
+ $repr = "INF";
+ } elseif (is_int($delay)) {
+ [$dest, $repr] = self::compute_dest($delay, "s", null, $from);
+ } elseif (is_string($delay) && preg_match('/^\d+$/', $delay)) {
+ $x = intval($delay);
+ [$dest, $repr] = self::compute_dest($x, "s", null, $from);
+ } elseif (is_string($delay) && preg_match('/^(\d*)([wdhms])(\d*)$/i', $delay, $ms)) {
+ [$x, $u, $y] = [$ms[1], $ms[2], $ms[3]];
+ $u = strtolower($u);
+ $default = self::DEFAULTS[$u];
+ if ($x === "") $x = $default[0];
+ else $x = intval($x);
+ if ($y === "") $y = $default[1];
+ else $y = intval($y);
+ [$dest, $repr] = self::compute_dest($x, $u, $y, $from);
+ } else {
+ throw new InvalidArgumentException("invalid delay");
+ }
+ $this->dest = $dest;
+ $this->repr = $repr;
+ }
+
+ function __serialize(): array {
+ return [$this->dest, $this->repr];
+ }
+ function __unserialize(array $data): void {
+ [$this->dest, $this->repr] = $data;
+ }
+
+ /** @var DateTime */
+ protected $dest;
+
+ function getDest(): DateTime {
+ return $this->dest;
+ }
+
+ function addDuration($duration) {
+ if (is_int($duration) && $duration < 0) {
+ $this->dest->sub(DateInterval::with(-$duration));
+ } else {
+ $this->dest->add(DateInterval::with($duration));
+ }
+ }
+
+ function subDuration($duration) {
+ if (is_int($duration) && $duration < 0) {
+ $this->dest->add(DateInterval::with(-$duration));
+ } else {
+ $this->dest->sub(DateInterval::with($duration));
+ }
+ }
+
+ /** @var string */
+ protected $repr;
+
+ function __toString(): string {
+ return $this->repr;
+ }
+
+ protected function _getDiff(?DateTimeInterface $now=null): \DateInterval {
+ if ($now === null) $now = new DateTime();
+ return $this->dest->diff($now);
+ }
+
+ /** retourner true si le délai imparti est écoulé */
+ function isElapsed(?DateTimeInterface $now=null): bool {
+ if ($this->repr === "INF") return false;
+ else return $this->_getDiff($now)->invert == 0;
+ }
+
+ /**
+ * retourner l'intervalle entre le moment courant et la destination.
+ *
+ * l'intervalle est négatif si le délai n'est pas écoulé, positif sinon
+ */
+ function getDiff(?DateTimeInterface $now=null): DateInterval {
+ return new DateInterval($this->_getDiff($now));
+ }
+}
diff --git a/php/src/php/time/Elapsed.php b/php/src/php/time/Elapsed.php
new file mode 100644
index 0000000..37f22c6
--- /dev/null
+++ b/php/src/php/time/Elapsed.php
@@ -0,0 +1,174 @@
+ 0) {
+ if ($text !== "") $text .= " ";
+ $text .= sprintf("%d jour", $d);
+ if ($d > 1) $text .= "s";
+ }
+ if ($h > 0) {
+ if ($text !== "") $text .= " ";
+ $text .= sprintf("%d heure", $h);
+ if ($h > 1) $text .= "s";
+ }
+ if ($m > 0) {
+ if ($text !== "") $text .= " ";
+ $text .= sprintf("%d minute", $m);
+ if ($m > 1) $text .= "s";
+ }
+ return $text;
+ }
+
+ private static function format_seconds(int $seconds, string $prefix, ?string $zero): string {
+ $seconds = abs($seconds);
+
+ if ($zero === null) $zero = "maintenant";
+ if ($seconds == 0) return $zero;
+
+ if ($seconds <= 3) {
+ if ($prefix !== "") $prefix .= " ";
+ return "${prefix}quelques secondes";
+ }
+
+ if ($seconds < 60) {
+ if ($prefix !== "") $prefix .= " ";
+ return sprintf("${prefix}%d secondes", $seconds);
+ }
+
+ $oneDay = 60 * 60 * 24;
+ $oneHour = 60 * 60;
+ $oneMinute = 60;
+ $rs = $seconds;
+ $d = intdiv($rs, $oneDay); $rs = $rs % $oneDay;
+ $h = intdiv($rs, $oneHour); $rs = $rs % $oneHour;
+ $m = intdiv($rs, $oneMinute);
+ return self::format_generic($prefix, $d, $h, $m);
+ }
+
+ private static function format_minutes(int $seconds, string $prefix, ?string $zero): string {
+ $minutes = intdiv(abs($seconds), 60);
+
+ if ($zero === null) $zero = "maintenant";
+ if ($minutes == 0) return $zero;
+
+ if ($minutes <= 3) {
+ if ($prefix !== "") $prefix .= " ";
+ return "${prefix}quelques minutes";
+ }
+
+ $oneDay = 60 * 24;
+ $oneHour = 60;
+ $rs = $minutes;
+ $d = intdiv($rs, $oneDay); $rs = $rs % $oneDay;
+ $h = intdiv($rs, $oneHour); $rs = $rs % $oneHour;
+ $m = $rs;
+ return self::format_generic($prefix, $d, $h, $m);
+ }
+
+ private static function format_hours(int $seconds, string $prefix, ?string $zero): string {
+ $minutes = intdiv(abs($seconds), 60);
+
+ if ($zero === null) $zero = "maintenant";
+ if ($minutes == 0) return $zero;
+
+ if ($minutes < 60) {
+ if ($prefix !== "") $prefix .= " ";
+ return "${prefix}moins d'une heure";
+ } elseif ($minutes < 120) {
+ if ($prefix !== "") $prefix .= " ";
+ return "${prefix}moins de deux heures";
+ }
+
+ $oneDay = 60 * 24;
+ $oneHour = 60;
+ $rs = $minutes;
+ $d = intdiv($rs, $oneDay); $rs = $rs % $oneDay;
+ $h = intdiv($rs, $oneHour);
+ return self::format_generic($prefix, $d, $h, 0);
+ }
+
+ private static function format_days(int $seconds, string $prefix, ?string $zero): string {
+ $hours = intdiv(abs($seconds), 60 * 60);
+
+ if ($zero === null) $zero = "aujourd'hui";
+ if ($hours < 24) return $zero;
+
+ $oneDay = 24;
+ $rs = $hours;
+ $d = intdiv($rs, $oneDay);
+ return self::format_generic($prefix, $d, 0, 0);
+ }
+
+ static function format_at(DateTime $start, ?DateTime $now=null, ?int $resolution=null): string {
+ $now ??= new DateTime();
+ $seconds = $now->getTimestamp() - $start->getTimestamp();
+ return (new self($seconds, $resolution))->formatAt();
+ }
+
+ static function format_since(DateTime $start, ?DateTime $now=null, ?int $resolution=null): string {
+ $now ??= new DateTime();
+ $seconds = $now->getTimestamp() - $start->getTimestamp();
+ return (new self($seconds, $resolution))->formatSince();
+ }
+
+ static function format_delay(DateTime $start, ?DateTime $now=null, ?int $resolution=null): string {
+ $now ??= new DateTime();
+ $seconds = $now->getTimestamp() - $start->getTimestamp();
+ return (new self($seconds, $resolution))->formatDelay();
+ }
+
+ function __construct(int $seconds, ?int $resolution=null) {
+ $resolution ??= static::DEFAULT_RESOLUTION;
+ if ($resolution < self::RESOLUTION_SECONDS) $resolution = self::RESOLUTION_SECONDS;
+ elseif ($resolution > self::RESOLUTION_DAYS) $resolution = self::RESOLUTION_DAYS;
+ $this->seconds = $seconds;
+ $this->resolution = $resolution;
+ }
+
+ /** @var int */
+ private $seconds;
+
+ /** @var int */
+ private $resolution;
+
+ function formatAt(): string {
+ $seconds = $this->seconds;
+ if ($seconds < 0) return self::format($seconds, $this->resolution, "dans");
+ else return self::format($seconds, $this->resolution, "il y a");
+ }
+
+ function formatSince(): string {
+ $seconds = $this->seconds;
+ if ($seconds < 0) return self::format(-$seconds, $this->resolution, "dans");
+ else return self::format($seconds, $this->resolution, "depuis");
+ }
+
+ function formatDelay(): string {
+ return self::format($this->seconds, $this->resolution, "", "immédiat");
+ }
+}
diff --git a/php/src/php/valm.php b/php/src/php/valm.php
new file mode 100644
index 0000000..99d5961
--- /dev/null
+++ b/php/src/php/valm.php
@@ -0,0 +1,84 @@
+ [null, null, "tableau contenant des paramètres et des options par défaut"],
+ "merge_arrays" => [null, null, "liste de tableaux à merger à celui-ci avant de calculer la liste effective des options"],
+ "merge" => [null, null, "tableau à merger à celui-ci avant de calculer la liste effective des options",
+ # si merge_arrays et merge sont spécifiés tous les deux, "merge" est mergé après "merge_arrays"
+ ],
+ "prefix" => [null, null, "texte à afficher avant l'aide générée automatiquement"],
+ "name" => [null, null, "nom du programme, utilisé pour l'affichage de l'aide"],
+ "purpose" => [null, null, "courte description de l'objet de ce programme"],
+ "usage" => [null, null, "exposé textuel des arguments valides du programme",
+ # ce peut être une chaine e.g '[options] SRC DESC'
+ # ou un tableau auquel cas autant de lignes que nécessaire sont affichées
+ ],
+ "description" => [null, null, "description longue de l'objet du programme, affiché après usage"],
+ "suffix" => [null, null, "texte à afficher après l'aide générée automatiquement"],
+ "dynamic_command" => [null, null, "fonction indiquant si une commande est valide",
+ # la signature de la fonction est function(string $command):?array
+ # elle doit retourner un tableau au format DEFS_SCHEMA qui définit la
+ # commande spécifiée, ou null si ce n'est pas une commande valide
+ ],
+ "sections" => [null, null, "liste de sections permettant de grouper les arguments"],
+ "commandname" => [null, null, "propriété ou clé qui obtient la commande courante",
+ # la valeur par défaut est "command" si ni commandproperty ni commandkey ne sont définis
+ ],
+ "commandproperty" => [null, null, "comme commandname mais force l'utilisation d'une propriété"],
+ "commandkey" => [null, null, "comme commandname mais force l'utilisation d'une clé"],
+ "argsname" => [null, null, "propriété ou clé qui obtient les arguments restants",
+ # la valeur par défaut est "args" si ni argsproperty ni argskey ne sont définis
+ ],
+ "argsproperty" => [null, null, "comme argsname mais force l'utilisation d'une propriété"],
+ "argskey" => [null, null, "comme argsname mais force l'utilisation d'une clé"],
+ "autohelp" => ["?bool", null, "faut-il ajouter automatiquement le support de l'option --help"],
+ "autoremains" => ["?bool", null, "faut-il ajouter automatiquement la prise en compte des arguments restants"],
+ ];
+
+ const SECTION_SCHEMA = [
+ "show" => ["bool", true, "faut-il afficher cette section?"],
+ "title" => [null, null, "titre de la section"],
+ "prefix" => [null, null, "texte à afficher avant l'aide générée automatiquement"],
+ "suffix" => [null, null, "texte à afficher après l'aide générée automatiquement"],
+
+ # ces valeurs sont calculées
+ "defs" => [null, null, "(interne) liste des définitions de cette section"],
+ ];
+
+ const DEF_SCHEMA = [
+ "set_defaults" => [null, null, "tableau contenant des paramètres par défaut"],
+ "merge_arrays" => [null, null, "liste de tableaux à merger à celui-ci"],
+ "merge" => [null, null, "tableau à merger à celui-ci",
+ # si merge_arrays et merge sont spécifiés tous les deux, "merge" est mergé après "merge_arrays"
+ ],
+ "kind" => [null, null, "type de définition: 'option' ou 'command'"],
+ "arg" => [null, null, "type de l'argument attendu par l'option"],
+ "args" => [null, null, "type des arguments attendus par l'option",
+ # si args est spécifié, arg est ignoré
+ ],
+ "argsdesc" => [null, null, "description textuelle des arguments, utilisé pour l'affichage de l'aide"],
+ "type" => [null, null, "types dans lesquels convertir les arguments avant de les fournir à l'utilisateur"],
+ "action" => [null, null, "fonction à appeler quand cette option est utilisée",
+ # la signature de la fonction est ($value, $name, $arg, $dest, $def)
+ ],
+ "name" => [null, null, "propriété ou clé à initialiser en réponse à l'utilisation de cette option",
+ # le nom à spécifier est au format under_score, qui est transformée en camelCase si la destination est un objet
+ ],
+ "property" => [null, null, "comme name mais force l'utilisation d'une propriété"],
+ "key" => [null, null, "comme name mais force l'utilisation d'une clé"],
+ "inverse" => ["bool", false, "décrémenter la destination au lieu de l'incrémenter pour une option sans argument"],
+ "value" => ["mixed", null, "valeur à forcer au lieu d'incrémenter la destination"],
+ "ensure_array" => [null, null, "forcer la destination à être un tableau"],
+ "help" => [null, null, "description de cette option, utilisé pour l'affichage de l'aide"],
+ "cmd_args" => [null, null, "définition des sous-options pour une commande"],
+
+ # ces valeurs sont calculées
+ "cmd_defs" => [null, null, "(interne) liste des définitions correspondant au paramètre options"],
+ ];
+
+ const ARGS_ALLOWED_VALUES = ["value", "path", "dir", "file", "host"];
+}
diff --git a/php/src/ref/file/csv/ref_csv.php b/php/src/ref/file/csv/ref_csv.php
new file mode 100644
index 0000000..22fcb9c
--- /dev/null
+++ b/php/src/ref/file/csv/ref_csv.php
@@ -0,0 +1,32 @@
+ ["string", null, "Ne sélectionner que les méthode dont le nom commence par ce préfixe"],
+ "args" => ["?array", null, "Arguments avec lesquels appeler les méthodes"],
+ "static_only" => ["bool", false, "N'appeler que les méthodes statiques si un objet est spécifié"],
+ "include" => ["?array", null, "N'inclure que les méthodes dont le nom correspond à ce motif, qui peut être une sous-chaine ou une expression régulière"],
+ "exclude" => ["?array", null, "Exclure les méthodes dont le nom correspond à ce motif, qui peut être une sous-chaine ou une expression régulière"],
+ ];
+}
diff --git a/php/src/ref/schema/ref_analyze.php b/php/src/ref/schema/ref_analyze.php
new file mode 100644
index 0000000..060d68b
--- /dev/null
+++ b/php/src/ref/schema/ref_analyze.php
@@ -0,0 +1,25 @@
+ ["string", null, "nature du schéma",
+ "pkey" => 0,
+ "allowed_values" => ["assoc", "list", "scalar"],
+ ],
+ "title" => ["?string", null, "libellé de la valeur"],
+ "required" => ["bool", false, "la valeur est-elle requise?"],
+ "nullable" => ["?bool", null, "la valeur peut-elle être nulle?"],
+ "desc" => ["?content", null, "description de la valeur"],
+ "name" => ["?key", null, "identifiant de la valeur"],
+ "schema" => ["?array", null, "définition du schéma"],
+ ];
+
+ /** @var array meta-schema d'un schéma de nature scalaire */
+ const SCALAR_METASCHEMA = [
+ "type" => ["array", null, "types possibles de la valeur", "required" => true],
+ "default" => [null, null, "valeur par défaut si la valeur n'existe pas"],
+ "title" => ["?string", null, "libellé de la valeur"],
+ "required" => ["bool", false, "la valeur est-elle requise?"],
+ "nullable" => ["?bool", null, "la valeur peut-elle être nulle?"],
+ "desc" => ["?content", null, "description de la valeur"],
+ "analyzer_func" => ["?callable", null, "fonction qui analyse une valeur entrante et indique comment la traiter"],
+ "extractor_func" => ["?callable", null, "fonction qui extrait la valeur à analyser dans une chaine de caractère"],
+ "parser_func" => ["?callable", null, "fonction qui analyse une chaine de caractères pour produire la valeur"],
+ "normalizer_func" => ["?callable", null, "fonction qui normalise la valeur"],
+ "messages" => ["?array", null, "messages à afficher en cas d'erreur d'analyse"],
+ "formatter_func" => ["?callable", null, "fonction qui formatte la valeur pour affichage"],
+ "format" => [null, null, "format à utiliser pour l'affichage"],
+ "" => ["array", "scalar", "nature du schéma",
+ "" => ["assoc", "schema" => self::NATURE_METASCHEMA],
+ ],
+ "name" => ["?string", null, "identifiant de la valeur"],
+ "pkey" => ["?pkey", null, "chemin de clé de la valeur dans un tableau associatif"],
+ "header" => ["?string", null, "nom de l'en-tête s'il faut présenter cette donnée dans un tableau"],
+ "composite" => ["?bool", null, "ce champ fait-il partie d'une valeur composite?"],
+ ];
+
+ const MESSAGES = [
+ "missing" => "{key}: Vous devez spécifier cette valeur",
+ "unavailable" => "{key}: Vous devez spécifier cette valeur",
+ "null" => "{key}: cette valeur ne doit pas être nulle",
+ "empty" => "{key}: cette valeur ne doit pas être vide",
+ "invalid" => "{key}: {orig}: cette valeur est invalide",
+ ];
+
+ /** @var array meta-schema d'un schéma de nature associative */
+ const ASSOC_METASCHEMA = [
+ ];
+
+ /** @var array meta-schema d'un schéma de nature liste */
+ const LIST_METASCHEMA = [
+ ];
+}
diff --git a/php/src/ref/schema/ref_types.php b/php/src/ref/schema/ref_types.php
new file mode 100644
index 0000000..24973d5
--- /dev/null
+++ b/php/src/ref/schema/ref_types.php
@@ -0,0 +1,10 @@
+ "bool",
+ "integer" => "int",
+ "flt" => "float", "double" => "float", "dbl" => "float",
+ ];
+}
diff --git a/php/src/ref/web/ref_mimetypes.php b/php/src/ref/web/ref_mimetypes.php
new file mode 100644
index 0000000..d896c13
--- /dev/null
+++ b/php/src/ref/web/ref_mimetypes.php
@@ -0,0 +1,12 @@
+ $length) {
+ if ($ellips && $length > 3) $s = substr($s, 0, $length - 3)."...";
+ else $s = substr($s, 0, $length);
+ }
+ if ($suffix !== null) $s .= $suffix;
+ return $s;
+ }
+
+ /** trimmer $s */
+ static final function trim(?string $s): ?string {
+ if ($s === null) return null;
+ else return trim($s);
+ }
+
+ /** trimmer $s à gauche */
+ static final function ltrim(?string $s): ?string {
+ if ($s === null) return null;
+ else return ltrim($s);
+ }
+
+ /** trimmer $s à droite */
+ static final function rtrim(?string $s): ?string {
+ if ($s === null) return null;
+ else return rtrim($s);
+ }
+
+ static final function left(?string $s, int $size): ?string {
+ if ($s === null) return null;
+ else return str_pad($s, $size);
+ }
+
+ static final function right(?string $s, int $size): ?string {
+ if ($s === null) return null;
+ else return str_pad($s, $size, " ", STR_PAD_LEFT);
+ }
+
+ static final function center(?string $s, int $size): ?string {
+ if ($s === null) return null;
+ else return str_pad($s, $size, " ", STR_PAD_BOTH);
+ }
+
+ static final function pad0(?string $s, int $size): ?string {
+ if ($s === null) return null;
+ else return str_pad($s, $size, "0", STR_PAD_LEFT);
+ }
+
+ static final function lower(?string $s): ?string {
+ if ($s === null) return null;
+ else return strtolower($s);
+ }
+
+ static final function lower1(?string $s): ?string {
+ if ($s === null) return null;
+ else return lcfirst($s);
+ }
+
+ static final function upper(?string $s): ?string {
+ if ($s === null) return null;
+ else return strtoupper($s);
+ }
+
+ static final function upper1(?string $s): ?string {
+ if ($s === null) return null;
+ else return ucfirst($s);
+ }
+
+ static final function upperw(?string $s, ?string $delimiters=null): ?string {
+ if ($s === null) return null;
+ if ($delimiters !== null) return ucwords($s, $delimiters);
+ else return ucwords($s, " _-\t\r\n\f\v");
+ }
+
+ protected static final function _starts_with(string $prefix, string $s, ?int $min_len=null): bool {
+ if ($prefix === $s) return true;
+ $len = strlen($prefix);
+ if ($min_len !== null && ($len < $min_len || $len > strlen($s))) return false;
+ return $len == 0 || $prefix === substr($s, 0, $len);
+ }
+
+ /**
+ * tester si $s commence par $prefix
+ * par exemple:
+ * - starts_with("", "whatever") est true
+ * - starts_with("fi", "first") est true
+ * - starts_with("no", "yes") est false
+ *
+ * si $min_len n'est pas null, c'est la longueur minimum requise de $prefix
+ * pour qu'on teste la correspondance. dans le cas contraire, la valeur de
+ * retour est toujours false, sauf s'il y a égalité. e.g
+ * - starts_with("a", "abc", 2) est false
+ * - starts_with("a", "a", 2) est true
+ */
+ static final function starts_with(?string $prefix, ?string $s, ?int $min_len=null): bool {
+ if ($s === null || $prefix === null) return false;
+ else return self::_starts_with($prefix, $s, $min_len);
+ }
+
+ /** Retourner $s sans le préfixe $prefix s'il existe */
+ static final function without_prefix(?string $prefix, ?string $s): ?string {
+ if ($s === null || $prefix === null) return $s;
+ if (self::_starts_with($prefix, $s)) $s = substr($s, strlen($prefix));
+ return $s;
+ }
+
+ /**
+ * modifier $s en place pour supprimer le préfixe $prefix s'il existe
+ *
+ * retourner true si le préfixe a été enlevé.
+ */
+ static final function del_prefix(?string &$s, ?string $prefix): bool {
+ if ($s === null || !self::_starts_with($prefix, $s)) return false;
+ $s = self::without_prefix($prefix, $s);
+ return true;
+ }
+
+ /**
+ * Retourner $s avec le préfixe $prefix
+ *
+ * Si $unless_exists, ne pas ajouter le préfixe s'il existe déjà
+ */
+ static final function with_prefix(?string $prefix, ?string $s, ?string $sep=null, bool $unless_exists=false): ?string {
+ if ($s === null || $prefix === null) return $s;
+ if (!self::_starts_with($prefix, $s) || !$unless_exists) $s = $prefix.$sep.$s;
+ return $s;
+ }
+
+ /**
+ * modifier $s en place pour ajouter le préfixe $prefix
+ *
+ * retourner true si le préfixe a été ajouté.
+ */
+ static final function add_prefix(?string &$s, ?string $prefix, bool $unless_exists=true): bool {
+ if (($s === null || self::_starts_with($prefix, $s)) && $unless_exists) return false;
+ $s = self::with_prefix($prefix, $s, null, $unless_exists);
+ return true;
+ }
+
+ protected static final function _ends_with(string $suffix, string $s, ?int $min_len=null): bool {
+ if ($suffix === $s) return true;
+ $len = strlen($suffix);
+ if ($min_len !== null && ($len < $min_len || $len > strlen($s))) return false;
+ return $len == 0 || $suffix === substr($s, -$len);
+ }
+
+ /**
+ * tester si $string se termine par $suffix
+ * par exemple:
+ * - ends_with("", "whatever") est true
+ * - ends_with("st", "first") est true
+ * - ends_with("no", "yes") est false
+ *
+ * si $min_len n'est pas null, c'est la longueur minimum requise de $prefix
+ * pour qu'on teste la correspondance. dans le cas contraire, la valeur de
+ * retour est toujours false, sauf s'il y a égalité. e.g
+ * - ends_with("c", "abc", 2) est false
+ * - ends_with("c", "c", 2) est true
+ */
+ static final function ends_with(?string $suffix, ?string $s, ?int $min_len=null): bool {
+ if ($s === null || $suffix === null) return false;
+ else return self::_ends_with($suffix, $s, $min_len);
+ }
+
+ /** Retourner $s sans le suffixe $suffix s'il existe */
+ static final function without_suffix(?string $suffix, ?string $s): ?string {
+ if ($s === null || $suffix === null) return $s;
+ if (self::_ends_with($suffix, $s)) $s = substr($s, 0, -strlen($suffix));
+ return $s;
+ }
+
+ /**
+ * modifier $s en place pour supprimer le suffixe $suffix s'il existe
+ *
+ * retourner true si le suffixe a été enlevé.
+ */
+ static final function del_suffix(?string &$s, ?string $suffix): bool {
+ if ($s === null || !self::_ends_with($suffix, $s)) return false;
+ $s = self::without_suffix($suffix, $s);
+ return true;
+ }
+
+ /**
+ * Retourner $s avec le suffixe $suffix
+ *
+ * Si $unless_exists, ne pas ajouter le suffixe s'il existe déjà
+ */
+ static final function with_suffix(?string $suffix, ?string $s, ?string $sep=null, bool $unless_exists=false): ?string {
+ if ($s === null || $suffix === null) return $s;
+ if (!self::_ends_with($suffix, $s) || !$unless_exists) $s = $s.$sep.$suffix;
+ return $s;
+ }
+
+ /**
+ * modifier $s en place pour ajouter le suffixe $suffix
+ *
+ * retourner true si le suffixe a été ajouté.
+ */
+ static final function add_suffix(?string &$s, ?string $suffix, bool $unless_exists=true): bool {
+ if (($s === null || self::_ends_with($suffix, $s)) && $unless_exists) return false;
+ $s = self::with_suffix($suffix, $s, null, $unless_exists);
+ return true;
+ }
+
+ /**
+ * ajouter $sep$prefix$text$suffix à $s si $text est non vide
+ *
+ * NB: ne rajouter $sep que si $s est non vide
+ */
+ static final function addsep(?string &$s, ?string $sep, ?string $text, ?string $prefix=null, ?string $suffix=null): void {
+ if (!$text) return;
+ if ($s) $s .= $sep;
+ $s .= $prefix.$text.$suffix;
+ }
+
+ /** si $text est non vide, ajouter $prefix$text$suffix à $s en séparant la valeur avec un espace */
+ static final function add(?string &$s, ?string $text, ?string $prefix=null, ?string $suffix=null): void {
+ self::addsep($s, " ", $text, $prefix, $suffix);
+ }
+
+ /** splitter $s en deux chaines séparées par $sep */
+ static final function split_pair(?string $s, string $sep=":"): array {
+ if ($s === null) return [null, null];
+ $parts = explode($sep, $s, 2);
+ if ($parts === false) return [null, null];
+ if (count($parts) < 2) $parts[] = null;
+ return $parts;
+ }
+
+ /** retourner $line sans son caractère de fin de ligne */
+ static final function strip_nl(?string $line): ?string {
+ if ($line === null) return null;
+ if (substr($line, -2) == "\r\n") {
+ $line = substr($line, 0, -2);
+ } elseif (substr($line, -1) == "\n") {
+ $line = substr($line, 0, -1);
+ } elseif (substr($line, -1) == "\r") {
+ $line = substr($line, 0, -1);
+ }
+ return $line;
+ }
+
+ /**
+ * normaliser le caractère de fin de ligne: tous les occurrences de [CR]LF et CR sont remplacées par LF
+ */
+ static final function norm_nl(?string $s): ?string {
+ if ($s === null) return null;
+ $s = str_replace("\r\n", "\n", $s);
+ $s = str_replace("\r", "\n", $s);
+ return $s;
+ }
+
+ /** découper la chaine sur tout ensemble de caractères espaces */
+ static final function split_tokens(?string $s): ?array {
+ $s = self::trim(self::norm_nl($s));
+ if ($s === null) return null;
+ elseif ($s === "") return [];
+ else return preg_split('/\s+/', $s);
+ }
+
+ /**
+ * joindre les éléments de $parts comme avec implode(), mais en ignorant les
+ * valeurs fausses (cela n'inclue pas la chaine "0")
+ *
+ * pour chaque valeur du tableau avec une clé associative, c'est la clé qui
+ * est utilisée mais uniquement si la valeur est vraie
+ */
+ static final function join(string $glue, ?iterable $values): ?string {
+ if ($values === null) return null;
+ $pieces = [];
+ $index = 0;
+ foreach ($values as $key => $value) {
+ if (is_array($value)) $value = self::join($glue, $value);
+ if ($key === $index) {
+ $index++;
+ if (cv::t($value)) $pieces[] = $value;
+ } elseif (cv::t($value)) {
+ $pieces[] = $key;
+ }
+ }
+ return implode($glue, $pieces);
+ }
+
+ /**
+ * comme {@link join()} mais en ignorant les valeurs fausses selon les règles
+ * de PHP
+ */
+ static final function pjoin(string $glue, ?iterable $values): ?string {
+ if ($values === null) return null;
+ $pieces = [];
+ $index = 0;
+ foreach ($values as $key => $value) {
+ if (is_array($value)) $value = self::join($glue, $value);
+ if ($key === $index) {
+ $index++;
+ if ($value) $pieces[] = $value;
+ } elseif ($value) {
+ $pieces[] = $key;
+ }
+ }
+ return implode($glue, $pieces);
+ }
+
+ const CAMEL_PATTERN0 = '/([A-Z0-9]+)$/A';
+ const CAMEL_PATTERN1 = '/([A-Z0-9]+)[A-Z]/A';
+ const CAMEL_PATTERN2 = '/([A-Z]?[^A-Z]+)/A';
+ const CAMEL_PATTERN3 = '/([A-Z][^A-Z]*)/A';
+
+ /**
+ * convertir une chaine de la forme "camelCase" en "under_score". le premier
+ * ensemble de caractères en majuscule est considéré comme étant en minuscule
+ *
+ * par exemple:
+ * - 'myCamelCase' devient 'my_camel_case'
+ * - 'AValue' devient 'a_value'
+ * - 'UPPERValue' devient 'upper_value'
+ * - 'UPPER' devient 'upper'
+ * - 'aXYZ' devient 'a_x_y_z'
+ *
+ * $delimiter est le séparateur en sortie ('_' par défaut)
+ * $upper indique s'il faut transformer le résultat en majuscule
+ */
+ static final function camel2us(?string $camel, bool $upper=false, string $delimiter="_"): ?string {
+ if ($camel === null || $camel === "") return $camel;
+ $prefix = null;
+ if (preg_match('/^(_+)(.*)/', $camel, $ms)) {
+ $prefix = $ms[1];
+ $camel = $ms[2];
+ }
+ $parts = [];
+ if (preg_match(self::CAMEL_PATTERN0, $camel, $ms, PREG_OFFSET_CAPTURE)) {
+ # que des majuscules
+ } elseif (preg_match(self::CAMEL_PATTERN1, $camel, $ms, PREG_OFFSET_CAPTURE)) {
+ # préfixe en majuscule
+ } elseif (preg_match(self::CAMEL_PATTERN2, $camel, $ms, PREG_OFFSET_CAPTURE)) {
+ # préfixe en minuscule
+ } else {
+ throw ValueException::invalid_kind($camel, "camel string");
+ }
+ $parts[] = strtolower($ms[1][0]);
+ $index = intval($ms[1][1]) + strlen($ms[1][0]);
+ while (preg_match(self::CAMEL_PATTERN3, $camel, $ms, PREG_OFFSET_CAPTURE, $index) !== false && $ms) {
+ $parts[] = strtolower($ms[1][0]);
+ $index = intval($ms[1][1]) + strlen($ms[1][0]);
+ }
+ $us = implode($delimiter, $parts);
+ if ($upper) $us = strtoupper($us);
+ return "$prefix$us";
+ }
+
+ const US_PATTERN = '/([ _\-\t\r\n\f\v])/';
+
+ /**
+ * convertir une chaine de la forme "under_score" en "camelCase"
+ *
+ * par exemple, 'my_camel_case' devient 'myCamelCalse'
+ * et 'UPPER_VALUE' devient 'upperValue'
+ *
+ * si la chaine de départ ne contient pas de delimiter, e.g 'myValue', elle
+ * est retournée inchangée
+ */
+ static final function us2camel(?string $us, ?string $delimiters=null): ?string {
+ if ($us === null || $us === "") return $us;
+ $prefix = null;
+ if (preg_match('/^(_+)(.*)/', $us, $ms)) {
+ $prefix = $ms[1];
+ $us = $ms[2];
+ }
+ if ($delimiters === null) $pattern = self::US_PATTERN;
+ else $pattern = '/(['.preg_quote($delimiters).'])/';
+ $parts = preg_split($pattern, $us);
+ $count = count($parts);
+ if ($count == 1) return $us;
+ for ($i = 0; $i < $count; $i++) {
+ $part = strtolower($parts[$i]);
+ if ($i > 0) $part = ucfirst($part);
+ $parts[$i] = $part;
+ }
+ return $prefix.implode("", $parts);
+ }
+}
diff --git a/php/src/text/Word.php b/php/src/text/Word.php
new file mode 100644
index 0000000..a91b03b
--- /dev/null
+++ b/php/src/text/Word.php
@@ -0,0 +1,279 @@
+fem = $fem;
+ $this->le = $le;
+ $this->ce = $ce;
+ $this->du = $du;
+ $this->au = $au;
+ $this->w = $spec;
+ }
+
+ /**
+ * retourner le mot sans article
+ *
+ * @param bool|int $amount nombre du nom, avec l'équivalence false===0 et
+ * true===2. à partir de 2, le mot est ecrit au pluriel
+ * @param bool|string $fem genre du nom avec lequel accorder les adjectifs,
+ * avec l'équivalence false==="M" et true==="F"
+ */
+ function w($amount=1, bool $upper1=false, $fem=null): string {
+ if ($amount === true) $amount = 2;
+ elseif ($amount === false) $amount = 0;
+ $amount = abs($amount);
+ $fem ??= $this->fem;
+ $w = $this->w;
+ # marque du nombre
+ if ($amount <= 1) {
+ $w = preg_replace('/#[sx]/', "", $w);
+ } else {
+ $w = preg_replace('/#([sx])/', "$1", $w);
+ }
+ # marque du genre
+ if ($fem === "f" || $fem === "F") $fem = true;
+ elseif ($fem === "m" || $fem === "M") $fem = false;
+ $repl = $fem? "$1": "";
+ $w = preg_replace('/#([e])/', $repl, $w);
+ # mise en majuscule
+ if ($upper1) {
+ if (strpos($w, "^") === false) {
+ # uniquement la première lettre
+ $w = txt::upper1($w);
+ } else {
+ # toutes les lettres qui suivent les occurences de ^
+ $w = preg_replace_callback('/\^([[:alpha:]])/u', function ($ms) {
+ return mb_strtoupper($ms[1]);
+ }, $w);
+ }
+ }
+ return $w;
+ }
+
+ /**
+ * retourner le mot sans article avec la première lettre en majuscule.
+ * alias pour $this->w($amount, true, $fem)
+ *
+ * @param bool|int $amount
+ */
+ function u($amount=1): string {
+ return $this->w($amount, true);
+ }
+
+ /**
+ * retourner l'adjectif accordé avec le genre spécifié.
+ * alias pour $this->w($amount, false, $fem)
+ *
+ * @param bool|int $amount
+ */
+ function a($fem=false, $amount=1): string {
+ return $this->w($amount, false, $fem);
+ }
+
+ /** retourner le mot sans article et avec la quantité */
+ function q(int $amount=1): string {
+ return $amount." ".$this->w($amount);
+ }
+
+ /** retourner le mot sans article et avec la quantité $amount/$max */
+ function r(int $amount, int $max): string {
+ return "$amount/$max ".$this->w($amount);
+ }
+
+ /** retourner le mot avec l'article indéfini et la quantité */
+ function un(int $amount=1): string {
+ $abs_amount = abs($amount);
+ if ($abs_amount == 0) {
+ $aucun = $this->fem? "aucune ": "aucun ";
+ return $aucun.$this->w($amount);
+ } elseif ($abs_amount == 1) {
+ $un = $this->fem? "une ": "un ";
+ return $un.$this->w($amount);
+ } else {
+ return "les $amount ".$this->w($amount);
+ }
+ }
+
+ /** retourner le mot avec l'article indéfini mais sans la quantité */
+ function _un(int $amount=1): string {
+ $abs_amount = abs($amount);
+ if ($abs_amount <= 1) {
+ $un = $this->fem? "une ": "un ";
+ return $un.$this->w($amount);
+ } else {
+ return "les ".$this->w($amount);
+ }
+ }
+
+ function le(int $amount=1): string {
+ $abs_amount = abs($amount);
+ if ($abs_amount == 0) {
+ $le = $this->fem? "la 0 ": "le 0 ";
+ return $le.$this->w($amount);
+ } elseif ($abs_amount == 1) {
+ return $this->le.$this->w($amount);
+ } else {
+ return "les $amount ".$this->w($amount);
+ }
+ }
+
+ function _le(int $amount=1): string {
+ $abs_amount = abs($amount);
+ if ($abs_amount <= 1) {
+ return $this->le.$this->w($amount);
+ } else {
+ return "les ".$this->w($amount);
+ }
+ }
+
+ function ce(int $amount=1): string {
+ $abs_amount = abs($amount);
+ if ($abs_amount == 0) {
+ $ce = $this->fem? "cette 0 ": "ce 0 ";
+ return $ce.$this->w($amount);
+ } elseif ($abs_amount == 1) {
+ return $this->ce.$this->w($amount);
+ } else {
+ return "ces $amount ".$this->w($amount);
+ }
+ }
+
+ function _ce(int $amount=1): string {
+ $abs_amount = abs($amount);
+ if ($abs_amount <= 1) {
+ return $this->ce.$this->w($amount);
+ } else {
+ return "ces ".$this->w($amount);
+ }
+ }
+
+ function du(int $amount=1): string {
+ $abs_amount = abs($amount);
+ if ($abs_amount == 0) {
+ $du = $this->fem? "de la 0 ": "du 0 ";
+ return $du.$this->w($amount);
+ } elseif ($abs_amount == 1) {
+ return $this->du.$this->w($amount);
+ } else {
+ return "des $amount ".$this->w($amount);
+ }
+ }
+
+ function _du(int $amount=1): string {
+ $abs_amount = abs($amount);
+ if ($abs_amount <= 1) {
+ return $this->du.$this->w($amount);
+ } else {
+ return "des ".$this->w($amount);
+ }
+ }
+
+ function au(int $amount=1): string {
+ $abs_amount = abs($amount);
+ if ($abs_amount == 0) {
+ $au = $this->fem? "à la 0 ": "au 0 ";
+ return $au.$this->w($amount);
+ } elseif ($abs_amount == 1) {
+ return $this->au.$this->w($amount);
+ } else {
+ return "aux $amount ".$this->w($amount);
+ }
+ }
+
+ function _au(int $amount=1): string {
+ $abs_amount = abs($amount);
+ if ($abs_amount <= 1) {
+ return $this->au.$this->w($amount);
+ } else {
+ return "aux ".$this->w($amount);
+ }
+ }
+}
diff --git a/php/src/text/words.php b/php/src/text/words.php
new file mode 100644
index 0000000..7906c30
--- /dev/null
+++ b/php/src/text/words.php
@@ -0,0 +1,19 @@
+q($count);
+ }
+
+ static function r(int $count, int $max, string $spec, bool $adjective=true): string {
+ $word = new Word($spec, $adjective);
+ return $word->r($count, $max);
+ }
+}
diff --git a/php/src/tools/BgLauncherApp.php b/php/src/tools/BgLauncherApp.php
new file mode 100644
index 0000000..88aa522
--- /dev/null
+++ b/php/src/tools/BgLauncherApp.php
@@ -0,0 +1,124 @@
+ "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;
+
+ protected ?array $args = null;
+
+ 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;
+ }
+ }
+}
diff --git a/php/src/tools/SteamTrainApp.php b/php/src/tools/SteamTrainApp.php
new file mode 100644
index 0000000..7e00ef0
--- /dev/null
+++ b/php/src/tools/SteamTrainApp.php
@@ -0,0 +1,53 @@
+ self::TITLE,
+ "description" => << 1,
+ "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());
+ }
+}
diff --git a/php/src/txt.php b/php/src/txt.php
new file mode 100644
index 0000000..9857473
--- /dev/null
+++ b/php/src/txt.php
@@ -0,0 +1,294 @@
+ $length) {
+ if ($ellips && $length > 3) $s = mb_substr($s, 0, $length - 3)."...";
+ else $s = mb_substr($s, 0, $length);
+ }
+ if ($suffix !== null) $s .= $suffix;
+ return $s;
+ }
+
+ /** trimmer $s */
+ static final function trim(?string $s): ?string {
+ if ($s === null) return null;
+ return trim($s);
+ }
+
+ /** trimmer $s à gauche */
+ static final function ltrim(?string $s): ?string {
+ if ($s === null) return null;
+ return ltrim($s);
+ }
+
+ /** trimmer $s à droite */
+ static final function rtrim(?string $s): ?string {
+ if ($s === null) return null;
+ return rtrim($s);
+ }
+
+ static final function left(?string $s, int $size): ?string {
+ if ($s === null) return null;
+ return mb_str_pad($s, $size);
+ }
+
+ static final function right(?string $s, int $size): ?string {
+ if ($s === null) return null;
+ return mb_str_pad($s, $size, " ", STR_PAD_LEFT);
+ }
+
+ static final function center(?string $s, int $size): ?string {
+ if ($s === null) return null;
+ return mb_str_pad($s, $size, " ", STR_PAD_BOTH);
+ }
+
+ static final function pad0(?string $s, int $size): ?string {
+ if ($s === null) return null;
+ return mb_str_pad($s, $size, "0", STR_PAD_LEFT);
+ }
+
+ static final function lower(?string $s): ?string {
+ if ($s === null) return null;
+ return mb_strtolower($s);
+ }
+
+ static final function lower1(?string $s): ?string {
+ if ($s === null) return null;
+ return mb_strtolower(mb_substr($s, 0, 1)).mb_substr($s, 1);
+ }
+
+ static final function upper(?string $s): ?string {
+ if ($s === null) return null;
+ return mb_strtoupper($s);
+ }
+
+ static final function upper1(?string $s): ?string {
+ if ($s === null) return null;
+ return mb_strtoupper(mb_substr($s, 0, 1)).mb_substr($s, 1);
+ }
+
+ static final function upperw(?string $s, ?string $delimiters=null): ?string {
+ if ($s === null) return null;
+ if ($delimiters === null) $delimiters = " _-\t\r\n\f\v";
+ $pattern = "/([".preg_quote($delimiters)."])/u";
+ $words = preg_split($pattern, $s, -1, PREG_SPLIT_DELIM_CAPTURE);
+ $max = count($words) - 1;
+ $ucwords = [];
+ for ($i = 0; $i < $max; $i += 2) {
+ $s = $words[$i];
+ $ucwords[] = mb_strtoupper(mb_substr($s, 0, 1)).mb_substr($s, 1);
+ $ucwords[] = $words[$i + 1];
+ }
+ $s = $words[$max];
+ $ucwords[] = mb_strtoupper(mb_substr($s, 0, 1)).mb_substr($s, 1);
+ return implode("", $ucwords);
+ }
+
+ protected static final function _starts_with(string $prefix, string $s, ?int $min_len=null): bool {
+ if ($prefix === $s) return true;
+ $len = mb_strlen($prefix);
+ if ($min_len !== null && ($len < $min_len || $len > mb_strlen($s))) return false;
+ return $len == 0 || $prefix === mb_substr($s, 0, $len);
+ }
+
+ /**
+ * tester si $s commence par $prefix
+ * par exemple:
+ * - starts_with("", "whatever") est true
+ * - starts_with("fi", "first") est true
+ * - starts_with("no", "yes") est false
+ *
+ * si $min_len n'est pas null, c'est la longueur minimum requise de $prefix
+ * pour qu'on teste la correspondance. dans le cas contraire, la valeur de
+ * retour est toujours false, sauf s'il y a égalité. e.g
+ * - starts_with("a", "abc", 2) est false
+ * - starts_with("a", "a", 2) est true
+ */
+ static final function starts_with(?string $prefix, ?string $s, ?int $min_len=null): bool {
+ if ($s === null || $prefix === null) return false;
+ else return self::_starts_with($prefix, $s, $min_len);
+ }
+
+ /** Retourner $s sans le préfixe $prefix s'il existe */
+ static final function without_prefix(?string $prefix, ?string $s): ?string {
+ if ($s === null || $prefix === null) return $s;
+ if (self::_starts_with($prefix, $s)) $s = mb_substr($s, mb_strlen($prefix));
+ return $s;
+ }
+
+ /**
+ * modifier $s en place pour supprimer le préfixe $prefix s'il existe
+ *
+ * retourner true si le préfixe a été enlevé.
+ */
+ static final function del_prefix(?string &$s, ?string $prefix): bool {
+ if ($s === null || !self::_starts_with($prefix, $s)) return false;
+ $s = self::without_prefix($prefix, $s);
+ return true;
+ }
+
+ /**
+ * Retourner $s avec le préfixe $prefix
+ *
+ * Si $unless_exists, ne pas ajouter le préfixe s'il existe déjà
+ */
+ static final function with_prefix(?string $prefix, ?string $s, ?string $sep=null, bool $unless_exists=false): ?string {
+ if ($s === null || $prefix === null) return $s;
+ if (!self::_starts_with($prefix, $s) || !$unless_exists) $s = $prefix.$sep.$s;
+ return $s;
+ }
+
+ /**
+ * modifier $s en place pour ajouter le préfixe $prefix
+ *
+ * retourner true si le préfixe a été ajouté.
+ */
+ static final function add_prefix(?string &$s, ?string $prefix, bool $unless_exists=true): bool {
+ if (($s === null || self::_starts_with($prefix, $s)) && $unless_exists) return false;
+ $s = self::with_prefix($prefix, $s, null, $unless_exists);
+ return true;
+ }
+
+ protected static final function _ends_with(string $suffix, string $s, ?int $min_len=null): bool {
+ if ($suffix === $s) return true;
+ $len = mb_strlen($suffix);
+ if ($min_len !== null && ($len < $min_len || $len > mb_strlen($s))) return false;
+ return $len == 0 || $suffix === mb_substr($s, -$len);
+ }
+
+ /**
+ * tester si $string se termine par $suffix
+ * par exemple:
+ * - ends_with("", "whatever") est true
+ * - ends_with("st", "first") est true
+ * - ends_with("no", "yes") est false
+ *
+ * si $min_len n'est pas null, c'est la longueur minimum requise de $prefix
+ * pour qu'on teste la correspondance. dans le cas contraire, la valeur de
+ * retour est toujours false, sauf s'il y a égalité. e.g
+ * - ends_with("c", "abc", 2) est false
+ * - ends_with("c", "c", 2) est true
+ */
+ static final function ends_with(?string $suffix, ?string $s, ?int $min_len=null): bool {
+ if ($s === null || $suffix === null) return false;
+ else return self::_ends_with($suffix, $s, $min_len);
+ }
+
+ /** Retourner $s sans le suffixe $suffix s'il existe */
+ static final function without_suffix(?string $suffix, ?string $s): ?string {
+ if ($s === null || $suffix === null) return $s;
+ if (self::_ends_with($suffix, $s)) $s = mb_substr($s, 0, -mb_strlen($suffix));
+ return $s;
+ }
+
+ /**
+ * modifier $s en place pour supprimer le suffixe $suffix s'il existe
+ *
+ * retourner true si le suffixe a été enlevé.
+ */
+ static final function del_suffix(?string &$s, ?string $suffix): bool {
+ if ($s === null || !self::_ends_with($suffix, $s)) return false;
+ $s = self::without_suffix($suffix, $s);
+ return true;
+ }
+
+ /**
+ * Retourner $s avec le suffixe $suffix
+ *
+ * Si $unless_exists, ne pas ajouter le suffixe s'il existe déjà
+ */
+ static final function with_suffix(?string $suffix, ?string $s, ?string $sep=null, bool $unless_exists=false): ?string {
+ if ($s === null || $suffix === null) return $s;
+ if (!self::_ends_with($suffix, $s) || !$unless_exists) $s = $s.$sep.$suffix;
+ return $s;
+ }
+
+ /**
+ * modifier $s en place pour ajouter le suffixe $suffix
+ *
+ * retourner true si le suffixe a été ajouté.
+ */
+ static final function add_suffix(?string &$s, ?string $suffix, bool $unless_exists=true): bool {
+ if (($s === null || self::_ends_with($suffix, $s)) && $unless_exists) return false;
+ $s = self::with_suffix($suffix, $s, null, $unless_exists);
+ return true;
+ }
+
+ /**
+ * ajouter $sep$prefix$text$suffix à $s si $text est non vide
+ *
+ * NB: ne rajouter $sep que si $s est non vide
+ */
+ static final function addsep(?string &$s, ?string $sep, ?string $text, ?string $prefix=null, ?string $suffix=null): void {
+ if (!$text) return;
+ if ($s) $s .= $sep;
+ $s .= $prefix.$text.$suffix;
+ }
+
+ /** si $text est non vide, ajouter $prefix$text$suffix à $s en séparant la valeur avec un espace */
+ static final function add(?string &$s, ?string $text, ?string $prefix=null, ?string $suffix=null): void {
+ self::addsep($s, " ", $text, $prefix, $suffix);
+ }
+
+ #############################################################################
+ # divers
+
+ /**
+ * supprimer les diacritiques de la chaine $text
+ *
+ * la translitération se fait avec les règles de la locale spécifiée.
+ * NB: la translitération ne fonctionne pas si LC_CTYPE == C ou POISX
+ */
+ static final function remove_diacritics(?string $text, string $locale="fr_FR.UTF-8"): ?string {
+ if ($text === null) return null;
+ #XXX est-ce thread-safe?
+ $olocale = setlocale(LC_CTYPE, 0);
+ try {
+ setlocale(LC_CTYPE, $locale);
+ $clean = @iconv("UTF-8", "US-ASCII//TRANSLIT", $text);
+ if ($clean === false) $clean = "";
+ return $clean;
+ } finally {
+ setlocale(LC_CTYPE, $olocale);
+ }
+ }
+}
diff --git a/php/src/web/curl/CurlException.php b/php/src/web/curl/CurlException.php
new file mode 100644
index 0000000..53fda92
--- /dev/null
+++ b/php/src/web/curl/CurlException.php
@@ -0,0 +1,22 @@
+ $value) {
+ if (is_array($value)) {
+ self::parse_files($files, "$pkey.$key", $value, $lastkey);
+ } else {
+ cl::pset($files, "$pkey.$key.$lastkey", $value);
+ }
+ }
+ }
+
+ /** @var array */
+ private static $_files;
+
+ static function _files(?array $_files=null): ?array {
+ if (self::$_files === null) {
+ $files = [];
+ if ($_files === null) $_files = $_FILES;
+ foreach ($_files as $pkey => $values) {
+ $name = $values["name"] ?? null;
+ $type = $values["type"] ?? null;
+ $error = $values["error"] ?? null;
+ if (is_scalar($name) && is_scalar($type) && is_scalar($error)) {
+ $files[$pkey] = $values;
+ } else {
+ self::parse_files($files, $pkey, $values["name"], "name");
+ self::parse_files($files, $pkey, $values["type"], "type");
+ self::parse_files($files, $pkey, $values["tmp_name"], "tmp_name");
+ self::parse_files($files, $pkey, $values["error"], "error");
+ self::parse_files($files, $pkey, $values["size"], "size");
+ $full_path = $values["full_path"] ?? null;
+ if ($full_path !== null) {
+ self::parse_files($files, $pkey, $full_path, "full_path");
+ }
+ }
+ }
+ self::$_files = $files;
+ }
+ return self::$_files;
+ }
+
+ static function get(string $pkey, bool $required=true, bool $check=true): Upload {
+ $_files = self::_files();
+ return new Upload(cl::pget($_files, $pkey), $required, $check);
+ }
+
+ static function all(string $pkey, bool $required=true, bool $check=true) {
+ $_files = self::_files();
+ $uploads = [];
+ foreach (cl::pget($_files, $pkey) as $file) {
+ $uploads[] = new Upload($file, $required, $check);
+ }
+ return $uploads;
+ }
+}
diff --git a/php/support/copy-templates b/php/support/copy-templates
new file mode 100755
index 0000000..a426860
--- /dev/null
+++ b/php/support/copy-templates
@@ -0,0 +1,39 @@
+#!/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
+
+declare -A DESTDIRS=(
+ [template-_bg_launcher.php]=sbin
+ [template-.launcher.php]=_cli
+ [template-_wrapper.sh]=_cli
+)
+declare -A MODES=(
+ [template-_bg_launcher.php]=+x
+ [template-.launcher.php]=
+ [template-_wrapper.sh]=+x
+)
+
+projdir=
+args=(
+ "copier les templates dans le projet en cours"
+ #"usage"
+ -d:,--projdir: .
+)
+parse_args "$@"; set -- "${args[@]}"
+
+if [ -n "$projdir" ]; then
+ cd "$projdir" || die
+fi
+
+[ -f composer.json ] || die "$(basename "$(dirname "$(pwd)")"): n'est pas un projet composer"
+
+setx -a templates=ls_files "$MYDIR" "template-*"
+for template in "${templates[@]}"; do
+ destdir="${DESTDIRS[$template]}"
+ [ -n "$destdir" ] || die "$template: la destination n'est pas configurée"
+ mode="${MODES[$template]}"
+ destname="${template#template-}"
+
+ tail -n+4 "$MYDIR/$template" >"$destdir/$destname"
+ [ -n "$mode" ] && chmod "$mode" "$destdir/$destname"
+done
diff --git a/php/support/template-.launcher.php b/php/support/template-.launcher.php
new file mode 100644
index 0000000..eeb2b41
--- /dev/null
+++ b/php/support/template-.launcher.php
@@ -0,0 +1,12 @@
+# TODO Faire une copie de ce script dans un répertoire de l'application web
+# (dans le répertoire _cli/ par défaut) et modifier les paramètres si nécessaire
+#-------------------------------------------------------------------------------
+ __DIR__ . '/..',
+ "appcode" => \app\config\bootstrap::APPCODE,
+];
+require __DIR__.'/../vendor/nulib/php/php/src/app/cli/include-launcher.php';
diff --git a/php/support/template-_bg_launcher.php b/php/support/template-_bg_launcher.php
new file mode 100644
index 0000000..88147f5
--- /dev/null
+++ b/php/support/template-_bg_launcher.php
@@ -0,0 +1,18 @@
+# TODO Faire une copie de ce script dans un répertoire de l'application web
+# (dans le répertoire sbin/ par défaut) et modifier les paramètres si nécessaire
+#-------------------------------------------------------------------------------
+ __DIR__.'/..',
+ "appcode" => \app\config\bootstrap::APPCODE,
+]);
+BgLauncherApp::run();
diff --git a/php/support/template-_wrapper.sh b/php/support/template-_wrapper.sh
new file mode 100644
index 0000000..22c34f6
--- /dev/null
+++ b/php/support/template-_wrapper.sh
@@ -0,0 +1,132 @@
+# TODO Faire une copie de ce script dans un répertoire de l'application web
+# (dans le répertoire _cli/ par défaut) et modifier les paramétres si nécessaire
+#-------------------------------------------------------------------------------
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+# S'assurer que le script PHP est lancé avec l'utilisateur www-data
+# Tous les chemins suivants sont relatifs au répertoire qui contient ce script
+
+# Chemin relatif de la racine du projet
+PROJPATH=..
+
+# Chemin relatif vers le lanceur PHP
+LAUNCHERPATH=.launcher.php
+
+# Chemin relatif des scripts PHP wrappés
+WRAPPEDPATH=
+
+# Nom du service dans docker-compose.yml utilisé pour lancer les commandes
+COMPOSE_SERVICE=web
+
+###############################################################################
+
+case "${RUNPHP_MODE:-auto}" in
+auto)
+ RUNPHP_MODE=
+ [ -f /.dockerenv ] && RUNPHP_MODE=docker
+ [ -z "$RUNPHP_MODE" ] &&
+ [ -f /proc/self/mountinfo ] &&
+ grep -q ' /docker/' /proc/self/mountinfo &&
+ RUNPHP_MODE=docker
+ [ -z "$RUNPHP_MODE" ] &&
+ [ -f /proc/1/cgroup ] &&
+ grep -q ':/docker/' /proc/1/cgroup &&
+ RUNPHP_MODE=docker
+ [ -n "$RUNPHP_MODE" ] || RUNPHP_MODE=host
+ ;;
+docker) RUNPHP_MODE=docker;;
+host) RUNPHP_MODE=host;;
+direct) RUNPHP_MODE=direct;;
+*) RUNPHP_MODE=host;;
+esac
+
+MYDIR="$(dirname -- "$0")"
+MYNAME="$(basename -- "$0")"
+if [ ! -L "$0" ]; then
+ echo "\
+$0
+Ce script doit être lancé en tant que lien symbolique avec un nom de la forme
+'monscript.php' et lance le script PHP du même nom situé dans le même répertoire
+avec l'utilisateur www-data"
+
+ if [ "$RUNPHP_MODE" == host -o "$RUNPHP_MODE" == direct ]; then
+ echo "\
+----------------------------------------
+Vérification des liens..."
+ cd "$MYDIR"
+ for i in *.php*; do
+ [ -f "$i" ] || continue
+ name="bin/${i%.*}.php"
+ dest="../_cli/_wrapper.sh"
+ link="../bin/${i%.*}.php"
+ if [ -L "$link" ]; then
+ echo "* $name OK"
+ elif [ -e "$link" ]; then
+ echo "* $name KO (not a link)"
+ else
+ echo "* $name NEW"
+ ln -s "$dest" "$link" || exit 1
+ fi
+ done
+ fi
+ exit 0
+fi
+
+MYTRUESELF="$(readlink -f "$0")"
+MYTRUEDIR="$(dirname -- "$MYTRUESELF")"
+PROJDIR="$(cd "$MYTRUEDIR${PROJPATH:+/$PROJPATH}"; pwd)"
+
+if [ "$RUNPHP_MODE" == host ]; then
+ args=(
+ docker compose run
+ ${RUNPHP_BUILD:+--build}
+ --rm
+ )
+ cwd="$(pwd)"
+ mounted=
+ if [ "$PROJDIR" == "$HOME" -o "${PROJDIR#$HOME/}" != "$PROJDIR" ]; then
+ # monter HOME
+ args+=(-v "$HOME:$HOME")
+ [ "${cwd#$HOME/}" != "$cwd" ] && mounted=1
+ else
+ # monter uniquement le répertoire du projet
+ args+=(-v "$PROJDIR:$PROJDIR")
+ [ "${cwd#$PROJDIR/}" != "$cwd" ] && mounted=1
+ fi
+ if [ -z "$mounted" ]; then
+ echo "Impossible de mapper le répertoire courant avec les montages du container"
+ exit 1
+ fi
+ args+=(
+ --workdir "$cwd"
+ "$COMPOSE_SERVICE"
+ exec "$MYNAME"
+ "$@"
+ )
+ cd "$PROJDIR"
+ exec "${args[@]}"
+fi
+
+launcher="$MYTRUEDIR/$LAUNCHERPATH"
+class="$MYTRUEDIR${WRAPPEDPATH:+/$WRAPPEDPATH}/${MYNAME%.php}.phpc"
+script="$MYTRUEDIR${WRAPPEDPATH:+/$WRAPPEDPATH}/${MYNAME%.php}.php"
+
+[ -f /g/init.env ] && source /g/init.env
+
+www_data="${DEVUSER_USERENT%%:*}"
+[ -n "$www_data" ] || www_data=www-data
+
+cmd=(php "$launcher")
+[ -n "$MEMPROF_PROFILE" ] && cmd+=(-dextension=memprof.so)
+if [ -f "$class" ]; then
+ cmd+=("$(<"$class")")
+else
+ cmd+=("$script")
+fi
+cmd+=("$@")
+
+if [ "$(id -u)" -eq 0 ]; then
+ su-exec "$www_data" "${cmd[@]}"
+else
+ exec "${cmd[@]}"
+fi
diff --git a/php/tests/app/argsTest.php b/php/tests/app/argsTest.php
new file mode 100644
index 0000000..90252d9
--- /dev/null
+++ b/php/tests/app/argsTest.php
@@ -0,0 +1,26 @@
+ false]));
+ self::assertSame(["--opt"], args::from_array(["opt" => true]));
+ self::assertSame(["--opt", "value"], args::from_array(["opt" => "value"]));
+ self::assertSame(["--opt", "42"], args::from_array(["opt" => 42]));
+ self::assertSame(["--opt", "1", "2", "3", "--"], args::from_array(["opt" => [1, 2, 3]]));
+
+ self::assertSame(["x", "1", "2", "3", "y"], args::from_array(["x", [1, 2, 3], "y"]));
+ }
+}
diff --git a/php/tests/appTest.php b/php/tests/appTest.php
new file mode 100644
index 0000000..8d86b6f
--- /dev/null
+++ b/php/tests/appTest.php
@@ -0,0 +1,132 @@
+ $projdir,
+ "vendor" => [
+ "bindir" => "$projdir/vendor/bin",
+ "autoload" => "$projdir/vendor/autoload.php",
+ ],
+ "appcode" => "nur-sery",
+ "cwd" => $cwd,
+ "datadir" => "$projdir/devel",
+ "etcdir" => "$projdir/devel/etc",
+ "vardir" => "$projdir/devel/var",
+ "logdir" => "$projdir/devel/log",
+ "profile" => "devel",
+ "appgroup" => null,
+ "name" => "my-application1",
+ "title" => null,
+ ], $app1->getParams());
+
+ $app2 = myapp::with(MyApplication2::class, $app1);
+ self::assertSame([
+ "projdir" => $projdir,
+ "vendor" => [
+ "bindir" => "$projdir/vendor/bin",
+ "autoload" => "$projdir/vendor/autoload.php",
+ ],
+ "appcode" => "nur-sery",
+ "cwd" => $cwd,
+ "datadir" => "$projdir/devel",
+ "etcdir" => "$projdir/devel/etc",
+ "vardir" => "$projdir/devel/var",
+ "logdir" => "$projdir/devel/log",
+ "profile" => "devel",
+ "appgroup" => null,
+ "name" => "my-application2",
+ "title" => null,
+ ], $app2->getParams());
+ }
+
+ function testInit() {
+ $projdir = config::get_projdir();
+ $cwd = getcwd();
+
+ myapp::reset();
+ myapp::init(MyApplication1::class);
+ self::assertSame([
+ "projdir" => $projdir,
+ "vendor" => [
+ "bindir" => "$projdir/vendor/bin",
+ "autoload" => "$projdir/vendor/autoload.php",
+ ],
+ "appcode" => "nur-sery",
+ "cwd" => $cwd,
+ "datadir" => "$projdir/devel",
+ "etcdir" => "$projdir/devel/etc",
+ "vardir" => "$projdir/devel/var",
+ "logdir" => "$projdir/devel/log",
+ "profile" => "devel",
+ "appgroup" => null,
+ "name" => "my-application1",
+ "title" => null,
+ ], myapp::get()->getParams());
+
+ myapp::init(MyApplication2::class);
+ self::assertSame([
+ "projdir" => $projdir,
+ "vendor" => [
+ "bindir" => "$projdir/vendor/bin",
+ "autoload" => "$projdir/vendor/autoload.php",
+ ],
+ "appcode" => "nur-sery",
+ "cwd" => $cwd,
+ "datadir" => "$projdir/devel",
+ "etcdir" => "$projdir/devel/etc",
+ "vardir" => "$projdir/devel/var",
+ "logdir" => "$projdir/devel/log",
+ "profile" => "devel",
+ "appgroup" => null,
+ "name" => "my-application2",
+ "title" => null,
+ ], myapp::get()->getParams());
+ }
+ }
+}
+
+namespace nulib\impl {
+
+ use nulib\app\cli\Application;
+ use nulib\os\path;
+ use nulib\app;
+
+ class config {
+ const PROJDIR = __DIR__.'/..';
+
+ static function get_projdir(): string {
+ return path::abspath(self::PROJDIR);
+ }
+ }
+
+ class myapp extends app {
+ static function reset(): void {
+ self::$app = null;
+ }
+ }
+
+ class MyApplication1 extends Application {
+ const PROJDIR = config::PROJDIR;
+
+ function main() {
+ }
+ }
+ class MyApplication2 extends Application {
+ const PROJDIR = null;
+
+ function main() {
+ }
+ }
+}
diff --git a/php/tests/db/_private/_baseTest.php b/php/tests/db/_private/_baseTest.php
new file mode 100644
index 0000000..cd2d758
--- /dev/null
+++ b/php/tests/db/_private/_baseTest.php
@@ -0,0 +1,22 @@
+ ["any", $values],
+ ], $sql, $bindings);
+ self::assertSame([
+ "value = any(array[:value, :value2])",
+ ], $sql);
+ self::assertSame([
+ "value" => 1,
+ "value2" => "string",
+ ], $bindings);
+ }
+}
diff --git a/php/tests/db/sqlite/.gitignore b/php/tests/db/sqlite/.gitignore
new file mode 100644
index 0000000..6ab0f32
--- /dev/null
+++ b/php/tests/db/sqlite/.gitignore
@@ -0,0 +1 @@
+/capacitor.db*
diff --git a/php/tests/db/sqlite/SqliteStorageTest.php b/php/tests/db/sqlite/SqliteStorageTest.php
new file mode 100644
index 0000000..e40ccdf
--- /dev/null
+++ b/php/tests/db/sqlite/SqliteStorageTest.php
@@ -0,0 +1,344 @@
+reset($channel);
+ $storage->charge($channel, "first");
+ $storage->charge($channel, "second");
+ $storage->charge($channel, "third");
+ $items = cl::all($storage->discharge($channel, false));
+ self::assertSame(["first", "second", "third"], $items);
+ }
+
+ function _testChargeArrays(SqliteStorage $storage, ?string $channel) {
+ $storage->reset($channel);
+ $storage->charge($channel, ["id" => 10, "name" => "first"]);
+ $storage->charge($channel, ["name" => "second", "id" => 20]);
+ $storage->charge($channel, ["name" => "third", "id" => "30"]);
+ }
+
+ function testChargeStrings() {
+ $storage = new SqliteStorage(__DIR__.'/capacitor.db');
+ $this->_testChargeStrings($storage, null);
+ $storage->close();
+ }
+
+ function testChargeArrays() {
+ $storage = new SqliteStorage(__DIR__.'/capacitor.db');
+ $storage->addChannel(new class extends CapacitorChannel {
+ const NAME = "arrays";
+ const COLUMN_DEFINITIONS = ["id" => "integer"];
+
+ function getItemValues($item): ?array {
+ return ["id" => $item["id"] ?? null];
+ }
+ });
+
+ $this->_testChargeStrings($storage, "strings");
+ $this->_testChargeArrays($storage, "arrays");
+ $storage->close();
+ }
+
+ function testEach() {
+ $storage = new SqliteStorage(__DIR__.'/capacitor.db');
+ $capacitor = new Capacitor($storage, new class extends CapacitorChannel {
+ const NAME = "each";
+ const COLUMN_DEFINITIONS = [
+ "age" => "integer",
+ "done" => "integer default 0",
+ ];
+
+ function getItemValues($item): ?array {
+ return [
+ "age" => $item["age"],
+ ];
+ }
+ });
+
+ $capacitor->reset();
+ $capacitor->charge(["name" => "first", "age" => 5]);
+ $capacitor->charge(["name" => "second", "age" => 10]);
+ $capacitor->charge(["name" => "third", "age" => 15]);
+ $capacitor->charge(["name" => "fourth", "age" => 20]);
+
+ $setDone = function ($item, $row, $suffix=null) {
+ $updates = ["done" => 1];
+ if ($suffix !== null) {
+ $item["name"] .= $suffix;
+ $updates["item"] = $item;
+ }
+ return $updates;
+ };
+ $capacitor->each(["age" => [">", 10]], $setDone, ["++"]);
+ $capacitor->each(["done" => 0], $setDone, null);
+
+ Txx(cl::all($capacitor->discharge(false)));
+ $capacitor->close();
+ self::assertTrue(true);
+ }
+
+ function testPrimayKey() {
+ $storage = new SqliteStorage(__DIR__.'/capacitor.db');
+ $capacitor = new Capacitor($storage, new class extends CapacitorChannel {
+ const NAME = "pk";
+ const COLUMN_DEFINITIONS = [
+ "id_" => "varchar primary key",
+ "done" => "integer default 0",
+ ];
+
+ function getItemValues($item): ?array {
+ return [
+ "id_" => $item["numero"],
+ ];
+ }
+ });
+
+ $capacitor->charge(["numero" => "a", "name" => "first", "age" => 5]);
+ $capacitor->charge(["numero" => "b", "name" => "second", "age" => 10]);
+ $capacitor->charge(["numero" => "c", "name" => "third", "age" => 15]);
+ $capacitor->charge(["numero" => "d", "name" => "fourth", "age" => 20]);
+ sleep(2);
+ $capacitor->charge(["numero" => "b", "name" => "second", "age" => 100]);
+ $capacitor->charge(["numero" => "d", "name" => "fourth", "age" => 200]);
+
+ $capacitor->close();
+ self::assertTrue(true);
+ }
+
+ function testSum() {
+ $storage = new SqliteStorage(__DIR__.'/capacitor.db');
+ $capacitor = new Capacitor($storage, new class extends CapacitorChannel {
+ const NAME = "sum";
+ const COLUMN_DEFINITIONS = [
+ "a__" => "varchar",
+ "b__" => "varchar",
+ "b__sum_" => self::SUM_DEFINITION,
+ ];
+
+ function getItemValues($item): ?array {
+ return [
+ "a" => $item["a"],
+ "b" => $item["b"],
+ ];
+ }
+ });
+
+ $capacitor->reset();
+ $capacitor->charge(["a" => null, "b" => null]);
+ $capacitor->charge(["a" => "first", "b" => "second"]);
+
+ Txx("=== all");
+ /** @var Sqlite $sqlite */
+ $sqlite = $capacitor->getStorage()->db();
+ Txx(cl::all($sqlite->all([
+ "select",
+ "from" => $capacitor->getChannel()->getTableName(),
+ ])));
+ Txx("=== each");
+ $capacitor->each(null, function ($item, $values) {
+ Txx($values);
+ });
+
+ $capacitor->close();
+ self::assertTrue(true);
+ }
+
+ function testEachValues() {
+ # tester que values contient bien toutes les valeurs de la ligne
+ $storage = new SqliteStorage(__DIR__.'/capacitor.db');
+ $capacitor = new Capacitor($storage, new class extends CapacitorChannel {
+ const NAME = "each_values";
+ const COLUMN_DEFINITIONS = [
+ "name" => "varchar primary key",
+ "age" => "integer",
+ "done" => "integer default 0",
+ "notes" => "text",
+ ];
+
+ function getItemValues($item): ?array {
+ return [
+ "name" => $item["name"],
+ "age" => $item["age"],
+ ];
+ }
+ });
+
+ $capacitor->reset();
+ $capacitor->charge(["name" => "first", "age" => 5], function($item, ?array $values, ?array $pvalues) {
+ self::assertSame("first", $item["name"]);
+ self::assertSame(5, $item["age"]);
+ self::assertnotnull($values);
+ self::assertSame(["name", "age", "item", "item__sum_", "created_", "modified_"], array_keys($values));
+ self::assertSame([
+ "name" => "first",
+ "age" => 5,
+ "item" => $item,
+ ], cl::select($values, ["name", "age", "item"]));
+ self::assertNull($pvalues);
+ });
+ $capacitor->charge(["name" => "first", "age" => 10], function($item, ?array $values, ?array $pvalues) {
+ self::assertSame("first", $item["name"]);
+ self::assertSame(10, $item["age"]);
+ self::assertnotnull($values);
+ self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($values));
+ self::assertSame([
+ "name" => "first",
+ "age" => 10,
+ "done" => 0,
+ "notes" => null,
+ "item" => $item,
+ ], cl::select($values, ["name", "age", "done", "notes", "item"]));
+ self::assertNotNull($pvalues);
+ self::assertSame([
+ "name" => "first",
+ "age" => 5,
+ "done" => 0,
+ "notes" => null,
+ "item" => ["name" => "first", "age" => 5],
+ ], cl::select($pvalues, ["name", "age", "done", "notes", "item"]));
+ });
+
+ $capacitor->each(null, function($item, ?array $values) {
+ self::assertSame("first", $item["name"]);
+ self::assertSame(10, $item["age"]);
+ self::assertnotnull($values);
+ self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($values));
+ self::assertSame([
+ "name" => "first",
+ "age" => 10,
+ "done" => 0,
+ "notes" => null,
+ "item" => $item,
+ ], cl::select($values, ["name", "age", "done", "notes", "item"]));
+ return [
+ "done" => 1,
+ "notes" => "modified",
+ ];
+ });
+ $capacitor->charge(["name" => "first", "age" => 10], function($item, ?array $values, ?array $pvalues) {
+ self::assertSame("first", $item["name"]);
+ self::assertSame(10, $item["age"]);
+ self::assertnotnull($values);
+ self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($values));
+ self::assertSame([
+ "name" => "first",
+ "age" => 10,
+ "done" => 1,
+ "notes" => "modified",
+ "item" => $item,
+ ], cl::select($values, ["name", "age", "done", "notes", "item"]));
+ self::assertNotNull($pvalues);
+ self::assertSame([
+ "name" => "first",
+ "age" => 10,
+ "done" => 1,
+ "notes" => "modified",
+ "item" => $item,
+ ], cl::select($pvalues, ["name", "age", "done", "notes", "item"]));
+ });
+
+ $capacitor->charge(["name" => "first", "age" => 20], function($item, ?array $values, ?array $pvalues) {
+ self::assertSame("first", $item["name"]);
+ self::assertSame(20, $item["age"]);
+ self::assertnotnull($values);
+ self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($values));
+ self::assertSame([
+ "name" => "first",
+ "age" => 20,
+ "done" => 1,
+ "notes" => "modified",
+ "item" => $item,
+ ], cl::select($values, ["name", "age", "done", "notes", "item"]));
+ self::assertNotNull($pvalues);
+ self::assertSame([
+ "name" => "first",
+ "age" => 10,
+ "done" => 1,
+ "notes" => "modified",
+ "item" => ["name" => "first", "age" => 10],
+ ], cl::select($pvalues, ["name", "age", "done", "notes", "item"]));
+ });
+ }
+
+ function testSetItemNull() {
+ # tester le forçage de $îtem à null pour économiser la place
+ $storage = new SqliteStorage(__DIR__.'/capacitor.db');
+ $capacitor = new Capacitor($storage, new class extends CapacitorChannel {
+ const NAME = "set_item_null";
+ const COLUMN_DEFINITIONS = [
+ "name" => "varchar primary key",
+ "age" => "integer",
+ "done" => "integer default 0",
+ "notes" => "text",
+ ];
+
+ function getItemValues($item): ?array {
+ return [
+ "name" => $item["name"],
+ "age" => $item["age"],
+ ];
+ }
+ });
+
+ $capacitor->reset();
+ $nbModified = $capacitor->charge(["name" => "first", "age" => 5], function ($item, ?array $values, ?array $pvalues) {
+ self::assertSame([
+ "name" => "first", "age" => 5,
+ "item" => $item,
+ ], cl::select($values, ["name", "age", "item"]));
+ return ["item" => null];
+ });
+ self::assertSame(1, $nbModified);
+ sleep(1);
+ # nb: on met des sleep() pour que la date de modification soit systématiquement différente
+
+ $nbModified = $capacitor->charge(["name" => "first", "age" => 10], function ($item, ?array $values, ?array $pvalues) {
+ self::assertSame([
+ "name" => "first", "age" => 10,
+ "item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc",
+ ], cl::select($values, ["name", "age", "item", "item__sum_"]));
+ self::assertSame([
+ "name" => "first", "age" => 5,
+ "item" => null, "item__sum_" => null,
+ ], cl::select($pvalues, ["name", "age", "item", "item__sum_"]));
+ return ["item" => null];
+ });
+ self::assertSame(1, $nbModified);
+ sleep(1);
+
+ # pas de modification ici
+ $nbModified = $capacitor->charge(["name" => "first", "age" => 10], function ($item, ?array $values, ?array $pvalues) {
+ self::assertSame([
+ "name" => "first", "age" => 10,
+ "item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc",
+ ], cl::select($values, ["name", "age", "item", "item__sum_"]));
+ self::assertSame([
+ "name" => "first", "age" => 10,
+ "item" => null, "item__sum_" => null,
+ ], cl::select($pvalues, ["name", "age", "item", "item__sum_"]));
+ return ["item" => null];
+ });
+ self::assertSame(0, $nbModified);
+ sleep(1);
+
+ $nbModified = $capacitor->charge(["name" => "first", "age" => 20], function ($item, ?array $values, ?array $pvalues) {
+ self::assertSame([
+ "name" => "first", "age" => 20,
+ "item" => $item, "item__sum_" => "001b91982b4e0883b75428c0eb28573a5dc5f7a5",
+ ], cl::select($values, ["name", "age", "item", "item__sum_"]));
+ self::assertSame([
+ "name" => "first", "age" => 10,
+ "item" => null, "item__sum_" => null,
+ ], cl::select($pvalues, ["name", "age", "item", "item__sum_"]));
+ return ["item" => null];
+ });
+ self::assertSame(1, $nbModified);
+ sleep(1);
+ }
+}
diff --git a/php/tests/db/sqlite/SqliteTest.php b/php/tests/db/sqlite/SqliteTest.php
new file mode 100644
index 0000000..b56855c
--- /dev/null
+++ b/php/tests/db/sqlite/SqliteTest.php
@@ -0,0 +1,146 @@
+ [
+ self::CREATE_PERSON,
+ self::INSERT_JEPHTE,
+ ],
+ ]);
+ self::assertSame("clain", $sqlite->get("select nom, age from person"));
+ self::assertSame([
+ "nom" => "clain",
+ "age" => 50,
+ ], $sqlite->get("select nom, age from person", null, true));
+
+ $sqlite->exec(self::INSERT_JEAN);
+ self::assertSame("payet", $sqlite->get("select nom, age from person where nom = 'payet'"));
+ self::assertSame([
+ "nom" => "payet",
+ "age" => 32,
+ ], $sqlite->get("select nom, age from person where nom = 'payet'", null, true));
+
+ self::assertSame([
+ ["key" => "0", "value" => self::CREATE_PERSON, "done" => 1],
+ ["key" => "1", "value" => self::INSERT_JEPHTE, "done" => 1],
+ ], iterator_to_array($sqlite->all("select key, value, done from _migration")));
+ }
+
+ function testException() {
+ $sqlite = new Sqlite(":memory:");
+ self::assertException(Exception::class, [$sqlite, "exec"], "prout");
+ self::assertException(SqliteException::class, [$sqlite, "exec"], ["prout"]);
+ }
+
+ protected function assertInserted(Sqlite $sqlite, array $row, array $query): void {
+ $sqlite->exec($query);
+ self::assertSame($row, $sqlite->one("select * from mapping where i = :i", [
+ "i" => $query["values"]["i"],
+ ]));
+ }
+ function testInsert() {
+ $sqlite = new Sqlite(":memory:", [
+ "migrate" => "create table mapping (i integer, s varchar)",
+ ]);
+ $sqlite->exec(["insert into mapping", "values" => ["i" => 1, "s" => "un"]]);
+ $sqlite->exec(["insert mapping", "values" => ["i" => 2, "s" => "deux"]]);
+ $sqlite->exec(["insert into", "into" => "mapping", "values" => ["i" => 3, "s" => "trois"]]);
+ $sqlite->exec(["insert", "into" => "mapping", "values" => ["i" => 4, "s" => "quatre"]]);
+ $sqlite->exec(["insert into mapping(i)", "values" => ["i" => 5, "s" => "cinq"]]);
+ $sqlite->exec(["insert into (i)", "into" => "mapping", "values" => ["i" => 6, "s" => "six"]]);
+ $sqlite->exec(["insert into mapping(i) values ()", "values" => ["i" => 7, "s" => "sept"]]);
+ $sqlite->exec(["insert into mapping(i) values (8)", "values" => ["i" => 42, "s" => "whatever"]]);
+ $sqlite->exec(["insert into mapping(i, s) values (9, 'neuf')", "values" => ["i" => 43, "s" => "garbage"]]);
+ $sqlite->exec(["insert into mapping", "cols" => ["i"], "values" => ["i" => 10, "s" => "dix"]]);
+
+ self::assertSame([
+ ["i" => 1, "s" => "un"],
+ ["i" => 2, "s" => "deux"],
+ ["i" => 3, "s" => "trois"],
+ ["i" => 4, "s" => "quatre"],
+ ["i" => 5, "s" => null/*"cinq"*/],
+ ["i" => 6, "s" => null/*"six"*/],
+ ["i" => 7, "s" => null/*"sept"*/],
+ ["i" => 8, "s" => null/*"huit"*/],
+ ["i" => 9, "s" => "neuf"],
+ ["i" => 10, "s" => null/*"dix"*/],
+ ], iterator_to_array($sqlite->all("select * from mapping")));
+ }
+
+ function testSelect() {
+ $sqlite = new Sqlite(":memory:", [
+ "migrate" => "create table user (name varchar, amount integer)",
+ ]);
+ $sqlite->exec(["insert into user", "values" => ["name" => "jclain1", "amount" => 1]]);
+ $sqlite->exec(["insert into user", "values" => ["name" => "jclain2", "amount" => 2]]);
+ $sqlite->exec(["insert into user", "values" => ["name" => "jclain5", "amount" => 5]]);
+ $sqlite->exec(["insert into user", "values" => ["name" => "fclain7", "amount" => 7]]);
+ $sqlite->exec(["insert into user", "values" => ["name" => "fclain9", "amount" => 9]]);
+ $sqlite->exec(["insert into user", "values" => ["name" => "fclain10", "amount" => 10]]);
+ self::assertSame([
+ "name" => "jclain1",
+ "amount" => 1,
+ ], $sqlite->one("select * from user where name = 'jclain1'"));
+ self::assertSame([
+ "name" => "jclain1",
+ "amount" => 1,
+ ], $sqlite->one(["select * from user where name = 'jclain1'"]));
+ self::assertSame([
+ "name" => "jclain1",
+ "amount" => 1,
+ ], $sqlite->one(["select from user where name = 'jclain1'"]));
+ self::assertSame([
+ "name" => "jclain1",
+ "amount" => 1,
+ ], $sqlite->one(["select from user where", "where" => ["name = 'jclain1'"]]));
+ self::assertSame([
+ "name" => "jclain1",
+ "amount" => 1,
+ ], $sqlite->one(["select from user", "where" => ["name = 'jclain1'"]]));
+ self::assertSame([
+ "name" => "jclain1",
+ "amount" => 1,
+ ], $sqlite->one(["select", "from" => "user", "where" => ["name = 'jclain1'"]]));
+ self::assertSame([
+ "name" => "jclain1",
+ "amount" => 1,
+ ], $sqlite->one(["select", "from" => "user", "where" => ["name" => "jclain1"]]));
+ self::assertSame([
+ "name" => "jclain1",
+ ], $sqlite->one(["select name", "from" => "user", "where" => ["name" => "jclain1"]]));
+ self::assertSame([
+ "name" => "jclain1",
+ ], $sqlite->one(["select", "cols" => "name", "from" => "user", "where" => ["name" => "jclain1"]]));
+ self::assertSame([
+ "name" => "jclain1",
+ ], $sqlite->one(["select", "cols" => ["name"], "from" => "user", "where" => ["name" => "jclain1"]]));
+ self::assertSame([
+ "plouf" => "jclain1",
+ ], $sqlite->one(["select", "cols" => ["plouf" => "name"], "from" => "user", "where" => ["name" => "jclain1"]]));
+ }
+
+ function testSelectGroupBy() {
+ $sqlite = new Sqlite(":memory:", [
+ "migrate" => "create table user (name varchar, amount integer)",
+ ]);
+ $sqlite->exec(["insert into user", "values" => ["name" => "jclain1", "amount" => 1]]);
+ $sqlite->exec(["insert into user", "values" => ["name" => "jclain2", "amount" => 1]]);
+ $sqlite->exec(["insert into user", "values" => ["name" => "jclain5", "amount" => 2]]);
+ $sqlite->exec(["insert into user", "values" => ["name" => "fclain7", "amount" => 2]]);
+ $sqlite->exec(["insert into user", "values" => ["name" => "fclain9", "amount" => 2]]);
+ $sqlite->exec(["insert into user", "values" => ["name" => "fclain10", "amount" => 3]]);
+
+ self::assertSame([
+ ["count" => 2],
+ ], iterator_to_array($sqlite->all(["select count(name) as count from user", "group by" => ["amount"], "having" => ["count(name) = 2"]])));
+ }
+}
diff --git a/php/tests/db/sqlite/_queryTest.php b/php/tests/db/sqlite/_queryTest.php
new file mode 100644
index 0000000..6d92412
--- /dev/null
+++ b/php/tests/db/sqlite/_queryTest.php
@@ -0,0 +1,125 @@
+ null], $sql, $params);
+ self::assertSame(["col is null"], $sql);
+ self::assertNull($params);
+
+ $sql = $params = null;
+ _query_base::parse_conds(["col = 'value'"], $sql, $params);
+ self::assertSame(["col = 'value'"], $sql);
+ self::assertNull($params);
+
+ $sql = $params = null;
+ _query_base::parse_conds([["col = 'value'"]], $sql, $params);
+ self::assertSame(["col = 'value'"], $sql);
+ self::assertNull($params);
+
+ $sql = $params = null;
+ _query_base::parse_conds(["int" => 42, "string" => "value"], $sql, $params);
+ self::assertSame(["(int = :int and string = :string)"], $sql);
+ self::assertSame(["int" => 42, "string" => "value"], $params);
+
+ $sql = $params = null;
+ _query_base::parse_conds(["or", "int" => 42, "string" => "value"], $sql, $params);
+ self::assertSame(["(int = :int or string = :string)"], $sql);
+ self::assertSame(["int" => 42, "string" => "value"], $params);
+
+ $sql = $params = null;
+ _query_base::parse_conds([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params);
+ self::assertSame(["((int = :int and string = :string) and (int = :int2 and string = :string2))"], $sql);
+ self::assertSame(["int" => 42, "string" => "value", "int2" => 24, "string2" => "eulav"], $params);
+
+ $sql = $params = null;
+ _query_base::parse_conds(["int" => ["is null"], "string" => ["<>", "value"]], $sql, $params);
+ self::assertSame(["(int is null and string <> :string)"], $sql);
+ self::assertSame(["string" => "value"], $params);
+
+ $sql = $params = null;
+ _query_base::parse_conds(["col" => ["between", "lower", "upper"]], $sql, $params);
+ self::assertSame(["col between :col and :col2"], $sql);
+ self::assertSame(["col" => "lower", "col2" => "upper"], $params);
+
+ $sql = $params = null;
+ _query_base::parse_conds(["col" => ["in", "one"]], $sql, $params);
+ self::assertSame(["col in (:col)"], $sql);
+ self::assertSame(["col" => "one"], $params);
+
+ $sql = $params = null;
+ _query_base::parse_conds(["col" => ["in", ["one", "two"]]], $sql, $params);
+ self::assertSame(["col in (:col, :col2)"], $sql);
+ self::assertSame(["col" => "one", "col2" => "two"], $params);
+
+ $sql = $params = null;
+ _query_base::parse_conds(["col" => ["=", ["one", "two"]]], $sql, $params);
+ self::assertSame(["col = :col and col = :col2"], $sql);
+ self::assertSame(["col" => "one", "col2" => "two"], $params);
+
+ $sql = $params = null;
+ _query_base::parse_conds(["or", "col" => ["=", ["one", "two"]]], $sql, $params);
+ self::assertSame(["col = :col or col = :col2"], $sql);
+ self::assertSame(["col" => "one", "col2" => "two"], $params);
+
+ $sql = $params = null;
+ _query_base::parse_conds(["col" => ["<>", ["one", "two"]]], $sql, $params);
+ self::assertSame(["col <> :col and col <> :col2"], $sql);
+ self::assertSame(["col" => "one", "col2" => "two"], $params);
+
+ $sql = $params = null;
+ _query_base::parse_conds(["or", "col" => ["<>", ["one", "two"]]], $sql, $params);
+ self::assertSame(["col <> :col or col <> :col2"], $sql);
+ self::assertSame(["col" => "one", "col2" => "two"], $params);
+ }
+
+ function testParseValues(): void {
+ $sql = $params = null;
+ _query_base::parse_set_values(null, $sql, $params);
+ self::assertNull($sql);
+ self::assertNull($params);
+
+ $sql = $params = null;
+ _query_base::parse_set_values([], $sql, $params);
+ self::assertNull($sql);
+ self::assertNull($params);
+
+ $sql = $params = null;
+ _query_base::parse_set_values(["col = 'value'"], $sql, $params);
+ self::assertSame(["col = 'value'"], $sql);
+ self::assertNull($params);
+
+ $sql = $params = null;
+ _query_base::parse_set_values([["col = 'value'"]], $sql, $params);
+ self::assertSame(["col = 'value'"], $sql);
+ self::assertNull($params);
+
+ $sql = $params = null;
+ _query_base::parse_set_values(["int" => 42, "string" => "value"], $sql, $params);
+ self::assertSame(["int = :int", "string = :string"], $sql);
+ self::assertSame(["int" => 42, "string" => "value"], $params);
+
+ $sql = $params = null;
+ _query_base::parse_set_values(["int" => 42, "string" => "value"], $sql, $params);
+ self::assertSame(["int = :int", "string = :string"], $sql);
+ self::assertSame(["int" => 42, "string" => "value"], $params);
+
+ $sql = $params = null;
+ _query_base::parse_set_values([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params);
+ self::assertSame(["int = :int", "string = :string", "int = :int2", "string = :string2"], $sql);
+ self::assertSame(["int" => 42, "string" => "value", "int2" => 24, "string2" => "eulav"], $params);
+ }
+}
diff --git a/php/tests/file/base/FileReaderTest.php b/php/tests/file/base/FileReaderTest.php
new file mode 100644
index 0000000..de36b56
--- /dev/null
+++ b/php/tests/file/base/FileReaderTest.php
@@ -0,0 +1,63 @@
+fread(10));
+ self::assertSame(10, $reader->ftell());
+ $reader->seek(30);
+ self::assertSame("abcdefghij", $reader->fread(10));
+ self::assertSame(40, $reader->ftell());
+ $reader->seek(10);
+ self::assertSame("ABCDEFGHIJ", $reader->fread(10));
+ self::assertSame(20, $reader->ftell());
+ $reader->seek(40);
+ self::assertSame("0123456789\n", $reader->getContents());
+ $reader->close();
+ ## avec BOM
+ $reader = new FileReader(__DIR__ . '/impl/avec_bom.txt');
+ self::assertSame("0123456789", $reader->fread(10));
+ self::assertSame(10, $reader->ftell());
+ $reader->seek(30);
+ self::assertSame("abcdefghij", $reader->fread(10));
+ self::assertSame(40, $reader->ftell());
+ $reader->seek(10);
+ self::assertSame("ABCDEFGHIJ", $reader->fread(10));
+ self::assertSame(20, $reader->ftell());
+ $reader->seek(40);
+ self::assertSame("0123456789\n", $reader->getContents());
+ $reader->close();
+ }
+
+ function testCsvAutoParams() {
+ $reader = new FileReader(__DIR__ . '/impl/msexcel.csv');
+ self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv());
+ self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv());
+ self::assertNull($reader->fgetcsv());
+ $reader->close();
+
+ $reader = new FileReader(__DIR__ . '/impl/ooffice.csv');
+ self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv());
+ self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv());
+ self::assertNull($reader->fgetcsv());
+ $reader->close();
+
+ $reader = new FileReader(__DIR__ . '/impl/weird.tsv');
+ self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv());
+ self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv());
+ self::assertNull($reader->fgetcsv());
+ $reader->close();
+
+ $reader = new FileReader(__DIR__ . '/impl/avec_bom.csv');
+ self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv());
+ self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv());
+ self::assertNull($reader->fgetcsv());
+ $reader->close();
+ }
+}
diff --git a/php/tests/file/base/impl/avec_bom.csv b/php/tests/file/base/impl/avec_bom.csv
new file mode 100644
index 0000000..d1512a2
--- /dev/null
+++ b/php/tests/file/base/impl/avec_bom.csv
@@ -0,0 +1,2 @@
+nom,prenom,age
+clain,jephte,50
diff --git a/php/tests/file/base/impl/avec_bom.txt b/php/tests/file/base/impl/avec_bom.txt
new file mode 100644
index 0000000..9e55899
--- /dev/null
+++ b/php/tests/file/base/impl/avec_bom.txt
@@ -0,0 +1 @@
+0123456789ABCDEFGHIJ0123456789abcdefghij0123456789
diff --git a/php/tests/file/base/impl/msexcel.csv b/php/tests/file/base/impl/msexcel.csv
new file mode 100644
index 0000000..b2d95c4
--- /dev/null
+++ b/php/tests/file/base/impl/msexcel.csv
@@ -0,0 +1,2 @@
+nom;prenom;age
+clain;jephte;50
diff --git a/php/tests/file/base/impl/ooffice.csv b/php/tests/file/base/impl/ooffice.csv
new file mode 100644
index 0000000..f00d4ff
--- /dev/null
+++ b/php/tests/file/base/impl/ooffice.csv
@@ -0,0 +1,2 @@
+nom,prenom,age
+clain,jephte,50
diff --git a/php/tests/file/base/impl/sans_bom.txt b/php/tests/file/base/impl/sans_bom.txt
new file mode 100644
index 0000000..f16e49f
--- /dev/null
+++ b/php/tests/file/base/impl/sans_bom.txt
@@ -0,0 +1 @@
+0123456789ABCDEFGHIJ0123456789abcdefghij0123456789
diff --git a/php/tests/file/base/impl/weird.tsv b/php/tests/file/base/impl/weird.tsv
new file mode 100644
index 0000000..cd8bf3a
--- /dev/null
+++ b/php/tests/file/base/impl/weird.tsv
@@ -0,0 +1,2 @@
+nom prenom age
+clain jephte 50
diff --git a/php/tests/php/access/KeyAccessTest.php b/php/tests/php/access/KeyAccessTest.php
new file mode 100644
index 0000000..dc5bd4c
--- /dev/null
+++ b/php/tests/php/access/KeyAccessTest.php
@@ -0,0 +1,67 @@
+ null, "false" => false, "empty" => ""];
+
+ #
+ $a = new KeyAccess($array, "inexistant");
+ self::assertFalse($a->exists());
+ self::assertFalse($a->available());
+ self::assertSame($default, $a->get($default));
+
+ $a = new KeyAccess($array, "null");
+ self::assertTrue($a->exists());
+ self::assertTrue($a->available());
+ self::assertSame(null, $a->get($default));
+
+ $a = new KeyAccess($array, "false");
+ self::assertTrue($a->exists());
+ self::assertFalse($a->available());
+ self::assertSame($default, $a->get($default));
+
+ $a = new KeyAccess($array, "empty");
+ self::assertTrue($a->exists());
+ self::assertTrue($a->available());
+ self::assertSame("", $a->get($default));
+
+ #
+ $a = new KeyAccess($array, "null", ["allow_null" => false]);
+ self::assertTrue($a->exists());
+ self::assertFalse($a->available());
+ self::assertSame($default, $a->get($default));
+
+ $a = new KeyAccess($array, "null", ["allow_null" => true]);
+ self::assertTrue($a->exists());
+ self::assertTrue($a->available());
+ self::assertSame(null, $a->get($default));
+
+ #
+ $a = new KeyAccess($array, "false", ["allow_false" => false]);
+ self::assertTrue($a->exists());
+ self::assertFalse($a->available());
+ self::assertSame($default, $a->get($default));
+
+ $a = new KeyAccess($array, "false", ["allow_false" => true]);
+ self::assertTrue($a->exists());
+ self::assertTrue($a->available());
+ self::assertSame(false, $a->get($default));
+
+ #
+ $a = new KeyAccess($array, "empty", ["allow_empty" => false]);
+ self::assertTrue($a->exists());
+ self::assertFalse($a->available());
+ self::assertSame($default, $a->get($default));
+
+ $a = new KeyAccess($array, "empty", ["allow_empty" => true]);
+ self::assertTrue($a->exists());
+ self::assertTrue($a->available());
+ self::assertSame("", $a->get($default));
+ }
+}
diff --git a/php/tests/php/access/ValueAccessTest.php b/php/tests/php/access/ValueAccessTest.php
new file mode 100644
index 0000000..a7d08c9
--- /dev/null
+++ b/php/tests/php/access/ValueAccessTest.php
@@ -0,0 +1,70 @@
+exists());
+ self::assertFalse($a->available());
+ self::assertSame($default, $a->get($default));
+
+ $i = false;
+ $a = new ValueAccess($i);
+ self::assertTrue($a->exists());
+ self::assertTrue($a->available());
+ self::assertSame(false, $a->get($default));
+
+ $i = "";
+ $a = new ValueAccess($i);
+ self::assertTrue($a->exists());
+ self::assertTrue($a->available());
+ self::assertSame("", $a->get($default));
+
+ #
+ $i = null;
+ $a = new ValueAccess($i, ["allow_null" => false]);
+ self::assertFalse($a->exists());
+ self::assertFalse($a->available());
+ self::assertSame($default, $a->get($default));
+
+ $i = null;
+ $a = new ValueAccess($i, ["allow_null" => true]);
+ self::assertTrue($a->exists());
+ self::assertTrue($a->available());
+ self::assertSame(null, $a->get($default));
+
+ #
+ $i = false;
+ $a = new ValueAccess($i, ["allow_false" => false]);
+ self::assertTrue($a->exists());
+ self::assertFalse($a->available());
+ self::assertSame($default, $a->get($default));
+
+ $i = false;
+ $a = new ValueAccess($i, ["allow_false" => true]);
+ self::assertTrue($a->exists());
+ self::assertTrue($a->available());
+ self::assertSame(false, $a->get($default));
+
+ #
+ $i = "";
+ $a = new ValueAccess($i, ["allow_empty" => false]);
+ self::assertTrue($a->exists());
+ self::assertFalse($a->available());
+ self::assertSame($default, $a->get($default));
+
+ $i = "";
+ $a = new ValueAccess($i, ["allow_empty" => true]);
+ self::assertTrue($a->exists());
+ self::assertTrue($a->available());
+ self::assertSame("", $a->get($default));
+ }
+}
diff --git a/php/tests/php/content/cTest.php b/php/tests/php/content/cTest.php
new file mode 100644
index 0000000..2f31b4a
--- /dev/null
+++ b/php/tests/php/content/cTest.php
@@ -0,0 +1,40 @@
+"));
+ self::assertSame("pouet", c::to_string(["pouet"]));
+ self::assertSame("hello world", c::to_string(["hello", "world"]));
+ self::assertSame("hello1 world", c::to_string(["hello1", "world"]));
+ self::assertSame("hello", c::to_string(["hello", ""]));
+ self::assertSame("world", c::to_string(["", "world"]));
+ self::assertSame("hello,world", c::to_string(["hello,", "world"]));
+ self::assertSame("hello world", c::to_string(["hello ", "world"]));
+ self::assertSame("hello. world", c::to_string(["hello.", "world"]));
+ self::assertSame("hello.", c::to_string(["hello.", ""]));
+
+ self::assertSame(
+ "title<q/>
hellobrave<q/>world
",
+ c::to_string([
+ [html::H1, "title"],
+ [html::P, [
+ "hello",
+ [html::SPAN, "brave"],
+ [html::SPAN, ["world"]],
+ ]],
+ ]));
+ }
+
+ function testXxx() {
+ $content = [[v::h1, "hello"]];
+ self::assertSame("hello
", c::to_string($content));
+ }
+}
+
diff --git a/php/tests/php/content/impl/AContent.php b/php/tests/php/content/impl/AContent.php
new file mode 100644
index 0000000..c53c376
--- /dev/null
+++ b/php/tests/php/content/impl/AContent.php
@@ -0,0 +1,10 @@
+content"];
+ }
+}
diff --git a/php/tests/php/content/impl/APrintable.php b/php/tests/php/content/impl/APrintable.php
new file mode 100644
index 0000000..7a75c2f
--- /dev/null
+++ b/php/tests/php/content/impl/APrintable.php
@@ -0,0 +1,10 @@
+printable
";
+ }
+}
diff --git a/php/tests/php/content/impl/ATag.php b/php/tests/php/content/impl/ATag.php
new file mode 100644
index 0000000..b4cc2ed
--- /dev/null
+++ b/php/tests/php/content/impl/ATag.php
@@ -0,0 +1,23 @@
+tag = $tag;
+ $this->content = $content;
+ }
+
+ protected $tag;
+ protected $content;
+
+ function getContent(): iterable {
+ return [
+ "<$this->tag>",
+ ...c::q($this->content),
+ "$this->tag>",
+ ];
+ }
+}
diff --git a/php/tests/php/content/impl/html.php b/php/tests/php/content/impl/html.php
new file mode 100644
index 0000000..3567b2e
--- /dev/null
+++ b/php/tests/php/content/impl/html.php
@@ -0,0 +1,14 @@
+",
+ false, null,
+ false, null,
+ ],
+ ["tsimple",
+ true, [false, "tsimple"],
+ true, [false, "tsimple"],
+ ],
+ ['nulib\php\impl\ntsimple',
+ true, [false, 'nulib\php\impl\ntsimple'],
+ true, [false, 'nulib\php\impl\ntsimple'],
+ ],
+ ['tmissing',
+ false, null,
+ true, [false, 'tmissing'],
+ ],
+ ["::tstatic",
+ false, null,
+ false, null,
+ ],
+ ["->tmethod",
+ false, null,
+ false, null,
+ ],
+ ["::tmissing",
+ false, null,
+ false, null,
+ ],
+ ["->tmissing",
+ false, null,
+ false, null,
+ ],
+ ["xxx::tmissing",
+ false, null,
+ false, null,
+ ],
+ ["xxx->tmissing",
+ false, null,
+ false, null,
+ ],
+ [SC::class."::tstatic",
+ false, null,
+ false, null,
+ ],
+ [SC::class."->tmethod",
+ false, null,
+ false, null,
+ ],
+ [SC::class."::tmissing",
+ false, null,
+ false, null,
+ ],
+ [SC::class."->tmissing",
+ false, null,
+ false, null,
+ ],
+ # tableaux avec un seul scalaire
+ [[],
+ false, null,
+ false, null,
+ ],
+ [[null],
+ false, null,
+ false, null,
+ ],
+ [[false],
+ false, null,
+ false, null,
+ ],
+ [[""],
+ false, null,
+ false, null,
+ ],
+ [["::"],
+ false, null,
+ false, null,
+ ],
+ [["->"],
+ false, null,
+ false, null,
+ ],
+ [["tsimple"],
+ false, null,
+ false, null,
+ ],
+ [['nulib\php\impl\ntsimple'],
+ false, null,
+ false, null,
+ ],
+ [["::tstatic"],
+ false, null,
+ false, null,
+ ],
+ [["->tmethod"],
+ false, null,
+ false, null,
+ ],
+ [["::tmissing"],
+ false, null,
+ false, null,
+ ],
+ [["->tmissing"],
+ false, null,
+ false, null,
+ ],
+ [["xxx::tmissing"],
+ false, null,
+ false, null,
+ ],
+ [["xxx->tmissing"],
+ false, null,
+ false, null,
+ ],
+ [[SC::class."::tstatic"],
+ false, null,
+ false, null,
+ ],
+ [[SC::class."->tmethod"],
+ false, null,
+ false, null,
+ ],
+ [[SC::class."::tmissing"],
+ false, null,
+ false, null,
+ ],
+ [[SC::class."->tmissing"],
+ false, null,
+ false, null,
+ ],
+ # tableaux avec deux scalaires
+ [[null, "tsimple"],
+ false, null,
+ false, null,
+ ],
+ [[null, 'nulib\php\impl\ntsimple'],
+ false, null,
+ false, null,
+ ],
+ [[null, "tmissing"],
+ false, null,
+ false, null,
+ ],
+ [[null, "::tstatic"],
+ false, null,
+ false, null,
+ ],
+ [[null, "->tmethod"],
+ false, null,
+ false, null,
+ ],
+ [[null, "::tmissing"],
+ false, null,
+ false, null,
+ ],
+ [[null, "->tmissing"],
+ false, null,
+ false, null,
+ ],
+ [[false, "tsimple"],
+ true, [false, "tsimple"],
+ true, [false, "tsimple"],
+ ],
+ [[false, 'nulib\php\impl\ntsimple'],
+ true, [false, 'nulib\php\impl\ntsimple'],
+ true, [false, 'nulib\php\impl\ntsimple'],
+ ],
+ [[false, "tmissing"],
+ false, null,
+ true, [false, "tmissing"],
+ ],
+ [[false, "::tstatic"],
+ false, null,
+ false, null,
+ ],
+ [[false, "->tmethod"],
+ false, null,
+ false, null,
+ ],
+ [[false, "::tmissing"],
+ false, null,
+ false, null,
+ ],
+ [[false, "->tmissing"],
+ false, null,
+ false, null,
+ ],
+ [["", "tsimple"],
+ false, null,
+ false, null,
+ ],
+ [["", 'nulib\php\impl\ntsimple'],
+ false, null,
+ false, null,
+ ],
+ [["", "tmissing"],
+ false, null,
+ false, null,
+ ],
+ [["", "::tstatic"],
+ false, null,
+ false, null,
+ ],
+ [["", "->tmethod"],
+ false, null,
+ false, null,
+ ],
+ [["", "::tmissing"],
+ false, null,
+ false, null,
+ ],
+ [["", "->tmissing"],
+ false, null,
+ false, null,
+ ],
+ [["xxx", "tmissing"],
+ false, null,
+ false, null,
+ ],
+ [["xxx", "::tmissing"],
+ false, null,
+ false, null,
+ ],
+ [["xxx", "->tmissing"],
+ false, null,
+ false, null,
+ ],
+ [[SC::class, "tstatic"],
+ false, null,
+ false, null,
+ ],
+ [[SC::class, "::tstatic"],
+ false, null,
+ false, null,
+ ],
+ [[SC::class, "tmethod"],
+ false, null,
+ false, null,
+ ],
+ [[SC::class, "->tmethod"],
+ false, null,
+ false, null,
+ ],
+ [[SC::class, "tmissing"],
+ false, null,
+ false, null,
+ ],
+ [[SC::class, "::tmissing"],
+ false, null,
+ false, null,
+ ],
+ [[SC::class, "->tmissing"],
+ false, null,
+ false, null,
+ ],
+ ];
+
+ function testFunction() {
+ foreach (self::FUNCTION_TESTS as $args) {
+ [$func,
+ $verifix1, $func1,
+ $verifix2, $func2,
+ ] = $args;
+ if ($func === ["", "tsimple"]) {
+ //echo "breakpoint";
+ }
+
+ $workf = $func;
+ $msg = var_export($func, true)." (strict)";
+ self::assertSame($verifix1, func::verifix_function($workf, true), "$msg --> verifix");
+ if ($verifix1) {
+ self::assertSame($func1, $workf, "$msg --> func");
+ }
+
+ $workf = $func;
+ $msg = var_export($func, true)." (lenient)";
+ self::assertSame($verifix2, func::verifix_function($workf, false), "$msg --> verifix");
+ if ($verifix2) {
+ self::assertSame($func2, $workf, "$msg --> func");
+ }
+ }
+ }
+
+ const STATIC_TESTS = [
+ # scalaires
+ [null,
+ false, null, null,
+ false, null, null,
+ ],
+ [false,
+ false, null, null,
+ false, null, null,
+ ],
+ ["",
+ false, null, null,
+ false, null, null,
+ ],
+ ["::",
+ false, null, null,
+ false, null, null,
+ ],
+ ["->",
+ false, null, null,
+ false, null, null,
+ ],
+ ["tsimple",
+ false, null, null,
+ false, null, null,
+ ],
+ ['nulib\php\impl\ntsimple',
+ false, null, null,
+ false, null, null,
+ ],
+ ['tmissing',
+ false, null, null,
+ false, null, null,
+ ],
+ ["::tstatic",
+ true, false, [null, "tstatic"],
+ true, false, [null, "tstatic"],
+ ],
+ ["->tmethod",
+ false, null, null,
+ false, null, null,
+ ],
+ ["::tmissing",
+ true, false, [null, "tmissing"],
+ true, false, [null, "tmissing"],
+ ],
+ ["->tmissing",
+ false, null, null,
+ false, null, null,
+ ],
+ ["xxx::tmissing",
+ false, null, null,
+ true, true, ["xxx", "tmissing"],
+ ],
+ ["xxx->tmissing",
+ false, null, null,
+ false, null, null,
+ ],
+ [SC::class."::tstatic",
+ true, true, [SC::class, "tstatic"],
+ true, true, [SC::class, "tstatic"],
+ ],
+ [SC::class."->tmethod",
+ false, null, null,
+ false, null, null,
+ ],
+ [SC::class."::tmissing",
+ false, null, null,
+ true, true, [SC::class, "tmissing"],
+ ],
+ [SC::class."->tmissing",
+ false, null, null,
+ false, null, null,
+ ],
+ # tableaux avec un seul scalaire
+ [[],
+ false, null, null,
+ false, null, null,
+ ],
+ [[null],
+ false, null, null,
+ false, null, null,
+ ],
+ [[false],
+ false, null, null,
+ false, null, null,
+ ],
+ [[""],
+ false, null, null,
+ false, null, null,
+ ],
+ [["::"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["->"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["tsimple"],
+ false, null, null,
+ false, null, null,
+ ],
+ [['nulib\php\impl\ntsimple'],
+ false, null, null,
+ false, null, null,
+ ],
+ [["::tstatic"],
+ true, false, [null, "tstatic"],
+ true, false, [null, "tstatic"],
+ ],
+ [["->tmethod"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["::tmissing"],
+ true, false, [null, "tmissing"],
+ true, false, [null, "tmissing"],
+ ],
+ [["->tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["xxx::tmissing"],
+ false, null, null,
+ true, true, ["xxx", "tmissing"],
+ ],
+ [["xxx->tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[SC::class."::tstatic"],
+ true, true, [SC::class, "tstatic"],
+ true, true, [SC::class, "tstatic"],
+ ],
+ [[SC::class."->tmethod"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[SC::class."::tmissing"],
+ false, null, null,
+ true, true, [SC::class, "tmissing"],
+ ],
+ [[SC::class."->tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ # tableaux avec deux scalaires
+ [[null, "tsimple"],
+ true, false, [null, "tsimple"],
+ true, false, [null, "tsimple"],
+ ],
+ [[null, 'nulib\php\impl\ntsimple'],
+ false, null, null,
+ false, null, null,
+ ],
+ [[null, "tmissing"],
+ true, false, [null, "tmissing"],
+ true, false, [null, "tmissing"],
+ ],
+ [[null, "::tstatic"],
+ true, false, [null, "tstatic"],
+ true, false, [null, "tstatic"],
+ ],
+ [[null, "->tmethod"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[null, "::tmissing"],
+ true, false, [null, "tmissing"],
+ true, false, [null, "tmissing"],
+ ],
+ [[null, "->tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[false, "tsimple"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[false, 'nulib\php\impl\ntsimple'],
+ false, null, null,
+ false, null, null,
+ ],
+ [[false, "tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[false, "::tstatic"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[false, "->tmethod"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[false, "::tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[false, "->tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["", "tsimple"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["", 'nulib\php\impl\ntsimple'],
+ false, null, null,
+ false, null, null,
+ ],
+ [["", "tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["", "::tstatic"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["", "->tmethod"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["", "::tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["", "->tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["xxx", "tmissing"],
+ false, null, null,
+ true, true, ["xxx", "tmissing"],
+ ],
+ [["xxx", "::tmissing"],
+ false, null, null,
+ true, true, ["xxx", "tmissing"],
+ ],
+ [["xxx", "->tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[SC::class, "tstatic"],
+ true, true, [SC::class, "tstatic"],
+ true, true, [SC::class, "tstatic"],
+ ],
+ [[SC::class, "::tstatic"],
+ true, true, [SC::class, "tstatic"],
+ true, true, [SC::class, "tstatic"],
+ ],
+ [[SC::class, "tmethod"],
+ true, true, [SC::class, "tmethod"],
+ true, true, [SC::class, "tmethod"],
+ ],
+ [[SC::class, "->tmethod"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[SC::class, "tmissing"],
+ false, null, null,
+ true, true, [SC::class, "tmissing"],
+ ],
+ [[SC::class, "::tmissing"],
+ false, null, null,
+ true, true, [SC::class, "tmissing"],
+ ],
+ [[SC::class, "->tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ ];
+
+ function testStatic() {
+ foreach (self::STATIC_TESTS as $args) {
+ [$func,
+ $verifix1, $bound1, $func1,
+ $verifix2, $bound2, $func2,
+ ] = $args;
+ if ($func === ["", "tsimple"]) {
+ //echo "breakpoint";
+ }
+
+ $workf = $func;
+ $msg = var_export($func, true)." (strict)";
+ self::assertSame($verifix1, func::verifix_static($workf, true, $bound), "$msg --> verifix");
+ if ($verifix1) {
+ self::assertSame($bound1, $bound, "$msg --> bound");
+ self::assertSame($func1, $workf, "$msg --> func");
+ }
+
+ $workf = $func;
+ $msg = var_export($func, true)." (lenient)";
+ self::assertSame($verifix2, func::verifix_static($workf, false, $bound), "$msg --> verifix");
+ if ($verifix2) {
+ self::assertSame($bound2, $bound, "$msg --> bound");
+ self::assertSame($func2, $workf, "$msg --> func");
+ }
+ }
+ }
+
+ const METHOD_TESTS = [
+ # scalaires
+ [null,
+ false, null, null,
+ false, null, null,
+ ],
+ [false,
+ false, null, null,
+ false, null, null,
+ ],
+ ["",
+ false, null, null,
+ false, null, null,
+ ],
+ ["::",
+ false, null, null,
+ false, null, null,
+ ],
+ ["->",
+ false, null, null,
+ false, null, null,
+ ],
+ ["tsimple",
+ false, null, null,
+ false, null, null,
+ ],
+ ['nulib\php\impl\ntsimple',
+ false, null, null,
+ false, null, null,
+ ],
+ ['tmissing',
+ false, null, null,
+ false, null, null,
+ ],
+ ["::tstatic",
+ false, null, null,
+ false, null, null,
+ ],
+ ["->tmethod",
+ true, false, [null, "tmethod"],
+ true, false, [null, "tmethod"],
+ ],
+ ["::tmissing",
+ false, null, null,
+ false, null, null,
+ ],
+ ["->tmissing",
+ true, false, [null, "tmissing"],
+ true, false, [null, "tmissing"],
+ ],
+ ["xxx::tmissing",
+ false, null, null,
+ false, null, null,
+ ],
+ ["xxx->tmissing",
+ false, null, null,
+ true, true, ["xxx", "tmissing"],
+ ],
+ [SC::class."::tstatic",
+ false, null, null,
+ false, null, null,
+ ],
+ [SC::class."->tmethod",
+ true, true, [SC::class, "tmethod"],
+ true, true, [SC::class, "tmethod"],
+ ],
+ [SC::class."::tmissing",
+ false, null, null,
+ false, null, null,
+ ],
+ [SC::class."->tmissing",
+ false, null, null,
+ true, true, [SC::class, "tmissing"],
+ ],
+ # tableaux avec un seul scalaire
+ [[],
+ false, null, null,
+ false, null, null,
+ ],
+ [[null],
+ false, null, null,
+ false, null, null,
+ ],
+ [[false],
+ false, null, null,
+ false, null, null,
+ ],
+ [[""],
+ false, null, null,
+ false, null, null,
+ ],
+ [["::"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["->"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["tsimple"],
+ false, null, null,
+ false, null, null,
+ ],
+ [['nulib\php\impl\ntsimple'],
+ false, null, null,
+ false, null, null,
+ ],
+ [["::tstatic"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["->tmethod"],
+ true, false, [null, "tmethod"],
+ true, false, [null, "tmethod"],
+ ],
+ [["::tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["->tmissing"],
+ true, false, [null, "tmissing"],
+ true, false, [null, "tmissing"],
+ ],
+ [["xxx::tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["xxx->tmissing"],
+ false, null, null,
+ true, true, ["xxx", "tmissing"],
+ ],
+ [[SC::class."::tstatic"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[SC::class."->tmethod"],
+ true, true, [SC::class, "tmethod"],
+ true, true, [SC::class, "tmethod"],
+ ],
+ [[SC::class."::tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[SC::class."->tmissing"],
+ false, null, null,
+ true, true, [SC::class, "tmissing"],
+ ],
+ # tableaux avec deux scalaires
+ [[null, "tsimple"],
+ true, false, [null, "tsimple"],
+ true, false, [null, "tsimple"],
+ ],
+ [[null, 'nulib\php\impl\ntsimple'],
+ false, null, null,
+ false, null, null,
+ ],
+ [[null, "tmissing"],
+ true, false, [null, "tmissing"],
+ true, false, [null, "tmissing"],
+ ],
+ [[null, "::tstatic"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[null, "->tmethod"],
+ true, false, [null, "tmethod"],
+ true, false, [null, "tmethod"],
+ ],
+ [[null, "::tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[null, "->tmissing"],
+ true, false, [null, "tmissing"],
+ true, false, [null, "tmissing"],
+ ],
+ [[false, "tsimple"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[false, 'nulib\php\impl\ntsimple'],
+ false, null, null,
+ false, null, null,
+ ],
+ [[false, "tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[false, "::tstatic"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[false, "->tmethod"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[false, "::tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[false, "->tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["", "tsimple"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["", 'nulib\php\impl\ntsimple'],
+ false, null, null,
+ false, null, null,
+ ],
+ [["", "tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["", "::tstatic"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["", "->tmethod"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["", "::tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["", "->tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["xxx", "tmissing"],
+ false, null, null,
+ true, true, ["xxx", "tmissing"],
+ ],
+ [["xxx", "::tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [["xxx", "->tmissing"],
+ false, null, null,
+ true, true, ["xxx", "tmissing"],
+ ],
+ [[SC::class, "tstatic"],
+ true, true, [SC::class, "tstatic"],
+ true, true, [SC::class, "tstatic"],
+ ],
+ [[SC::class, "::tstatic"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[SC::class, "tmethod"],
+ true, true, [SC::class, "tmethod"],
+ true, true, [SC::class, "tmethod"],
+ ],
+ [[SC::class, "->tmethod"],
+ true, true, [SC::class, "tmethod"],
+ true, true, [SC::class, "tmethod"],
+ ],
+ [[SC::class, "tmissing"],
+ false, null, null,
+ true, true, [SC::class, "tmissing"],
+ ],
+ [[SC::class, "::tmissing"],
+ false, null, null,
+ false, null, null,
+ ],
+ [[SC::class, "->tmissing"],
+ false, null, null,
+ true, true, [SC::class, "tmissing"],
+ ],
+ ];
+
+ function testMethod() {
+ foreach (self::METHOD_TESTS as $args) {
+ [$func,
+ $verifix1, $bound1, $func1,
+ $verifix2, $bound2, $func2,
+ ] = $args;
+
+ $workf = $func;
+ $msg = var_export($func, true)." (strict)";
+ self::assertSame($verifix1, func::verifix_method($workf, true, $bound), "$msg --> verifix");
+ if ($verifix1) {
+ self::assertSame($bound1, $bound, "$msg --> bound");
+ self::assertSame($func1, $workf, "$msg --> func");
+ }
+
+ $workf = $func;
+ $msg = var_export($func, true)." (lenient)";
+ self::assertSame($verifix2, func::verifix_method($workf, false, $bound), "$msg --> verifix");
+ if ($verifix2) {
+ self::assertSame($bound2, $bound, "$msg --> bound");
+ self::assertSame($func2, $workf, "$msg --> func");
+ }
+ }
+ }
+
+ function testInvokeFunction() {
+ # m1
+ self::assertSame([null], func::call("tm1"));
+ self::assertSame([null], func::call("tm1", null));
+ self::assertSame([null], func::call("tm1", null, null));
+ self::assertSame([null], func::call("tm1", null, null, null));
+ self::assertSame([null], func::call("tm1", null, null, null, null));
+ self::assertSame([1], func::call("tm1", 1));
+ self::assertSame([1], func::call("tm1", 1, 2));
+ self::assertSame([1], func::call("tm1", 1, 2, 3));
+ self::assertSame([1], func::call("tm1", 1, 2, 3, 4));
+
+ # o1
+ self::assertSame([9], func::call("to1"));
+ self::assertSame([null], func::call("to1", null));
+ self::assertSame([null], func::call("to1", null, null));
+ self::assertSame([null], func::call("to1", null, null, null));
+ self::assertSame([null], func::call("to1", null, null, null, null));
+ self::assertSame([1], func::call("to1", 1));
+ self::assertSame([1], func::call("to1", 1, 2));
+ self::assertSame([1], func::call("to1", 1, 2, 3));
+ self::assertSame([1], func::call("to1", 1, 2, 3, 4));
+
+ # v
+ self::assertSame([], func::call("tv"));
+ self::assertSame([null], func::call("tv", null));
+ self::assertSame([null, null], func::call("tv", null, null));
+ self::assertSame([null, null, null], func::call("tv", null, null, null));
+ self::assertSame([null, null, null, null], func::call("tv", null, null, null, null));
+ self::assertSame([1], func::call("tv", 1));
+ self::assertSame([1, 2], func::call("tv", 1, 2));
+ self::assertSame([1, 2, 3], func::call("tv", 1, 2, 3));
+ self::assertSame([1, 2, 3, 4], func::call("tv", 1, 2, 3, 4));
+
+ # m1o1
+ self::assertSame([null, 9], func::call("tm1o1"));
+ self::assertSame([null, 9], func::call("tm1o1", null));
+ self::assertSame([null, null], func::call("tm1o1", null, null));
+ self::assertSame([null, null], func::call("tm1o1", null, null, null));
+ self::assertSame([null, null], func::call("tm1o1", null, null, null, null));
+ self::assertSame([1, 9], func::call("tm1o1", 1));
+ self::assertSame([1, 2], func::call("tm1o1", 1, 2));
+ self::assertSame([1, 2], func::call("tm1o1", 1, 2, 3));
+ self::assertSame([1, 2], func::call("tm1o1", 1, 2, 3, 4));
+
+ # m1v
+ self::assertSame([null], func::call("tm1v"));
+ self::assertSame([null], func::call("tm1v", null));
+ self::assertSame([null, null], func::call("tm1v", null, null));
+ self::assertSame([null, null, null], func::call("tm1v", null, null, null));
+ self::assertSame([null, null, null, null], func::call("tm1v", null, null, null, null));
+ self::assertSame([1], func::call("tm1v", 1));
+ self::assertSame([1, 2], func::call("tm1v", 1, 2));
+ self::assertSame([1, 2, 3], func::call("tm1v", 1, 2, 3));
+ self::assertSame([1, 2, 3, 4], func::call("tm1v", 1, 2, 3, 4));
+
+ # m1o1v
+ self::assertSame([null, 9], func::call("tm1o1v"));
+ self::assertSame([null, 9], func::call("tm1o1v", null));
+ self::assertSame([null, null], func::call("tm1o1v", null, null));
+ self::assertSame([null, null, null], func::call("tm1o1v", null, null, null));
+ self::assertSame([null, null, null, null], func::call("tm1o1v", null, null, null, null));
+ self::assertSame([1, 9], func::call("tm1o1v", 1));
+ self::assertSame([1, 2], func::call("tm1o1v", 1, 2));
+ self::assertSame([1, 2, 3], func::call("tm1o1v", 1, 2, 3));
+ self::assertSame([1, 2, 3, 4], func::call("tm1o1v", 1, 2, 3, 4));
+
+ # o1v
+ self::assertSame([9], func::call("to1v"));
+ self::assertSame([null], func::call("to1v", null));
+ self::assertSame([null, null], func::call("to1v", null, null));
+ self::assertSame([null, null, null], func::call("to1v", null, null, null));
+ self::assertSame([null, null, null, null], func::call("to1v", null, null, null, null));
+ self::assertSame([1], func::call("to1v", 1));
+ self::assertSame([1, 2], func::call("to1v", 1, 2));
+ self::assertSame([1, 2, 3], func::call("to1v", 1, 2, 3));
+ self::assertSame([1, 2, 3, 4], func::call("to1v", 1, 2, 3, 4));
+ }
+
+ function testInvokeClass() {
+ $func = func::with(SC::class);
+ self::assertInstanceOf(SC::class, $func->invoke());
+ self::assertInstanceOf(SC::class, $func->invoke([]));
+ self::assertInstanceOf(SC::class, $func->invoke([1]));
+ self::assertInstanceOf(SC::class, $func->invoke([1, 2]));
+ self::assertInstanceOf(SC::class, $func->invoke([1, 2, 3]));
+
+ $func = func::with(C0::class);
+ self::assertInstanceOf(C0::class, $func->invoke());
+ self::assertInstanceOf(C0::class, $func->invoke([]));
+ self::assertInstanceOf(C0::class, $func->invoke([1]));
+ self::assertInstanceOf(C0::class, $func->invoke([1, 2]));
+ self::assertInstanceOf(C0::class, $func->invoke([1, 2, 3]));
+
+ $func = func::with(C1::class);
+ /** @var C1 $i1 */
+ $i1 = $func->invoke();
+ self::assertInstanceOf(C1::class, $i1); self::assertSame(0, $i1->base);
+ $i1 = $func->invoke([]);
+ self::assertInstanceOf(C1::class, $i1); self::assertSame(0, $i1->base);
+ $i1 = $func->invoke([1]);
+ self::assertInstanceOf(C1::class, $i1); self::assertSame(1, $i1->base);
+ $i1 = $func->invoke([1, 2]);
+ self::assertInstanceOf(C1::class, $i1); self::assertSame(1, $i1->base);
+ }
+
+ private static function invoke_asserts(): array {
+ $inv_ok = function($func) {
+ return func::with($func)->invoke();
+ };
+ $inv_ko = function($func) use ($inv_ok) {
+ return function() use ($func, $inv_ok) {
+ return $inv_ok($func);
+ };
+ };
+ $bind_ok = function($func, $objet) {
+ return func::with($func)->bind($objet)->invoke();
+ };
+ $bind_ko = function($func, $object) use ($bind_ok) {
+ return function() use ($func, $object, $bind_ok) {
+ return $bind_ok($func, $object);
+ };
+ };
+ return [$inv_ok, $inv_ko, $bind_ok, $bind_ko];
+ }
+
+ function testInvokeStatic() {
+ [$inv_ok, $inv_ko, $bind_ok, $bind_ko] = self::invoke_asserts();
+ $sc = new SC();
+
+ self::assertSame(10, $inv_ok([SC::class, "tstatic"]));
+ self::assertSame(10, $inv_ok([SC::class, "::tstatic"]));
+ self::assertSame(10, $inv_ok([SC::class, "->tstatic"]));
+
+ self::assertSame(10, $inv_ok([$sc, "tstatic"]));
+ self::assertSame(10, $inv_ok([$sc, "::tstatic"]));
+ self::assertSame(10, $inv_ok([$sc, "->tstatic"]));
+
+ self::assertException(ValueException::class, $inv_ko([null, "tstatic"]));
+ self::assertException(ValueException::class, $inv_ko([null, "::tstatic"]));
+ self::assertException(ValueException::class, $inv_ko([null, "->tstatic"]));
+
+ self::assertSame(10, $bind_ok([null, "tstatic"], SC::class));
+ self::assertSame(10, $bind_ok([null, "::tstatic"], SC::class));
+ self::assertSame(10, $bind_ok([null, "->tstatic"], SC::class));
+
+ self::assertSame(10, $bind_ok([null, "tstatic"], $sc));
+ self::assertSame(10, $bind_ok([null, "::tstatic"], $sc));
+ self::assertSame(10, $bind_ok([null, "->tstatic"], $sc));
+ }
+
+ function testInvokeMethod() {
+ [$inv_ok, $inv_ko, $bind_ok, $bind_ko] = self::invoke_asserts();
+ $sc = new SC();
+
+ self::assertException(ReflectionException::class, $inv_ko([SC::class, "tmethod"]));
+ self::assertException(ReflectionException::class, $inv_ko([SC::class, "::tmethod"]));
+ self::assertException(ReflectionException::class, $inv_ko([SC::class, "->tmethod"]));
+
+ self::assertSame(11, $inv_ok([$sc, "tmethod"]));
+ self::assertException(ReflectionException::class, $inv_ko([$sc, "::tmethod"]));
+ self::assertSame(11, $inv_ok([$sc, "->tmethod"]));
+
+ self::assertException(ValueException::class, $inv_ko([null, "tmethod"]));
+ self::assertException(ValueException::class, $inv_ko([null, "::tmethod"]));
+ self::assertException(ValueException::class, $inv_ko([null, "->tmethod"]));
+
+ self::assertException(ReflectionException::class, $bind_ko([null, "tmethod"], SC::class));
+ self::assertException(ReflectionException::class, $bind_ko([null, "::tmethod"], SC::class));
+ self::assertException(ReflectionException::class, $bind_ko([null, "->tmethod"], SC::class));
+
+ self::assertSame(11, $bind_ok([null, "tmethod"], $sc));
+ self::assertException(ReflectionException::class, $bind_ko([null, "::tmethod"], $sc));
+ self::assertSame(11, $bind_ok([null, "->tmethod"], $sc));
+ }
+
+ function testArgs() {
+ $func = function(int $a, int $b, int $c): int {
+ return $a + $b + $c;
+ };
+
+ self::assertSame(6, func::call($func, 1, 2, 3));
+ self::assertSame(6, func::call($func, 1, 2, 3, 4));
+
+ self::assertSame(6, func::with($func)->invoke([1, 2, 3]));
+ self::assertSame(6, func::with($func, [1])->invoke([2, 3]));
+ self::assertSame(6, func::with($func, [1, 2])->invoke([3]));
+ self::assertSame(6, func::with($func, [1, 2, 3])->invoke());
+ self::assertSame(6, func::with($func, [1, 2, 3, 4])->invoke());
+ }
+
+ function testRebind() {
+ $func = func::with([C1::class, "tmethod"]);
+ self::assertSame(11, $func->bind(new C1(0))->invoke());
+ self::assertSame(12, $func->bind(new C1(1))->invoke());
+ self::assertException(ValueException::class, function() use ($func) {
+ $func->bind(new C0())->invoke();
+ });
+ }
+ }
+}
+
+namespace {
+ function tsimple(): int { return 0; }
+ function tm1($a): array { return [$a]; }
+ function to1($b=9): array { return [$b]; }
+ function tv(...$c): array { return [...$c]; }
+ function tm1o1($a, $b=9): array { return [$a, $b]; }
+ function tm1v($a, ...$c): array { return [$a, ...$c]; }
+ function tm1o1v($a, $b=9, ...$c): array { return [$a, $b, ...$c]; }
+ function to1v($b=9, ...$c): array { return [$b, ...$c]; }
+}
+
+namespace nulib\php\impl {
+ function ntsimple(): int { return 0; }
+
+ class SC {
+ static function tstatic(): int {
+ return 10;
+ }
+
+ function tmethod(): int {
+ return 11;
+ }
+ }
+
+ class C0 {
+ function __construct() {
+ }
+
+ static function tstatic(): int {
+ return 10;
+ }
+
+ function tmethod(): int {
+ return 11;
+ }
+ }
+
+ class C1 {
+ function __construct(int $base=0) {
+ $this->base = $base;
+ }
+
+ public int $base;
+
+ static function tstatic(): int {
+ return 10;
+ }
+
+ function tmethod(): int {
+ return 11 + $this->base;
+ }
+ }
+}
diff --git a/php/tests/php/nur_funcTest.php b/php/tests/php/nur_funcTest.php
new file mode 100644
index 0000000..44fa744
--- /dev/null
+++ b/php/tests/php/nur_funcTest.php
@@ -0,0 +1,292 @@
+"));
+ self::assertFalse(nur_func::is_method([]));
+ self::assertFalse(nur_func::is_method([""]));
+ self::assertFalse(nur_func::is_method([null, "->"]));
+ self::assertFalse(nur_func::is_method(["xxx", "->"]));
+
+ self::assertTrue(nur_func::is_method("->xxx"));
+ self::assertTrue(nur_func::is_method(["->xxx"]));
+ self::assertTrue(nur_func::is_method([null, "->yyy"]));
+ self::assertTrue(nur_func::is_method(["xxx", "->yyy"]));
+ self::assertTrue(nur_func::is_method([null, "->yyy", "aaa"]));
+ self::assertTrue(nur_func::is_method(["xxx", "->yyy", "aaa"]));
+ }
+
+ function testFix_method() {
+ $object = new \stdClass();
+ $func= "->xxx";
+ nur_func::fix_method($func, $object);
+ self::assertSame([$object, "xxx"], $func);
+ $func= ["->xxx"];
+ nur_func::fix_method($func, $object);
+ self::assertSame([$object, "xxx"], $func);
+ $func= [null, "->yyy"];
+ nur_func::fix_method($func, $object);
+ self::assertSame([$object, "yyy"], $func);
+ $func= ["xxx", "->yyy"];
+ nur_func::fix_method($func, $object);
+ self::assertSame([$object, "yyy"], $func);
+ $func= [null, "->yyy", "aaa"];
+ nur_func::fix_method($func, $object);
+ self::assertSame([$object, "yyy", "aaa"], $func);
+ $func= ["xxx", "->yyy", "aaa"];
+ nur_func::fix_method($func, $object);
+ self::assertSame([$object, "yyy", "aaa"], $func);
+ }
+
+ function testCall() {
+ self::assertSame(36, nur_func::call("func36"));
+ self::assertSame(12, nur_func::call(TC::class."::method"));
+ self::assertSame(12, nur_func::call([TC::class, "method"]));
+ $closure = function() {
+ return 21;
+ };
+ self::assertSame(21, nur_func::call($closure));
+ }
+
+ function test_prepare_fill() {
+ # vérifier que les arguments sont bien remplis, en fonction du fait qu'ils
+ # soient obligatoires, facultatifs ou variadiques
+
+ # m1
+ self::assertSame([null], nur_func::call("func_m1"));
+ self::assertSame([null], nur_func::call("func_m1", null));
+ self::assertSame([null], nur_func::call("func_m1", null, null));
+ self::assertSame([null], nur_func::call("func_m1", null, null, null));
+ self::assertSame([null], nur_func::call("func_m1", null, null, null, null));
+ self::assertSame([1], nur_func::call("func_m1", 1));
+ self::assertSame([1], nur_func::call("func_m1", 1, 2));
+ self::assertSame([1], nur_func::call("func_m1", 1, 2, 3));
+ self::assertSame([1], nur_func::call("func_m1", 1, 2, 3, 4));
+
+ # o1
+ self::assertSame([9], nur_func::call("func_o1"));
+ self::assertSame([null], nur_func::call("func_o1", null));
+ self::assertSame([null], nur_func::call("func_o1", null, null));
+ self::assertSame([null], nur_func::call("func_o1", null, null, null));
+ self::assertSame([null], nur_func::call("func_o1", null, null, null, null));
+ self::assertSame([1], nur_func::call("func_o1", 1));
+ self::assertSame([1], nur_func::call("func_o1", 1, 2));
+ self::assertSame([1], nur_func::call("func_o1", 1, 2, 3));
+ self::assertSame([1], nur_func::call("func_o1", 1, 2, 3, 4));
+
+ # v
+ self::assertSame([], nur_func::call("func_v"));
+ self::assertSame([null], nur_func::call("func_v", null));
+ self::assertSame([null, null], nur_func::call("func_v", null, null));
+ self::assertSame([null, null, null], nur_func::call("func_v", null, null, null));
+ self::assertSame([null, null, null, null], nur_func::call("func_v", null, null, null, null));
+ self::assertSame([1], nur_func::call("func_v", 1));
+ self::assertSame([1, 2], nur_func::call("func_v", 1, 2));
+ self::assertSame([1, 2, 3], nur_func::call("func_v", 1, 2, 3));
+ self::assertSame([1, 2, 3, 4], nur_func::call("func_v", 1, 2, 3, 4));
+
+ # m1o1
+ self::assertSame([null, 9], nur_func::call("func_m1o1"));
+ self::assertSame([null, 9], nur_func::call("func_m1o1", null));
+ self::assertSame([null, null], nur_func::call("func_m1o1", null, null));
+ self::assertSame([null, null], nur_func::call("func_m1o1", null, null, null));
+ self::assertSame([null, null], nur_func::call("func_m1o1", null, null, null, null));
+ self::assertSame([1, 9], nur_func::call("func_m1o1", 1));
+ self::assertSame([1, 2], nur_func::call("func_m1o1", 1, 2));
+ self::assertSame([1, 2], nur_func::call("func_m1o1", 1, 2, 3));
+ self::assertSame([1, 2], nur_func::call("func_m1o1", 1, 2, 3, 4));
+
+ # m1v
+ self::assertSame([null], nur_func::call("func_m1v"));
+ self::assertSame([null], nur_func::call("func_m1v", null));
+ self::assertSame([null, null], nur_func::call("func_m1v", null, null));
+ self::assertSame([null, null, null], nur_func::call("func_m1v", null, null, null));
+ self::assertSame([null, null, null, null], nur_func::call("func_m1v", null, null, null, null));
+ self::assertSame([1], nur_func::call("func_m1v", 1));
+ self::assertSame([1, 2], nur_func::call("func_m1v", 1, 2));
+ self::assertSame([1, 2, 3], nur_func::call("func_m1v", 1, 2, 3));
+ self::assertSame([1, 2, 3, 4], nur_func::call("func_m1v", 1, 2, 3, 4));
+
+ # m1o1v
+ self::assertSame([null, 9], nur_func::call("func_m1o1v"));
+ self::assertSame([null, 9], nur_func::call("func_m1o1v", null));
+ self::assertSame([null, null], nur_func::call("func_m1o1v", null, null));
+ self::assertSame([null, null, null], nur_func::call("func_m1o1v", null, null, null));
+ self::assertSame([null, null, null, null], nur_func::call("func_m1o1v", null, null, null, null));
+ self::assertSame([1, 9], nur_func::call("func_m1o1v", 1));
+ self::assertSame([1, 2], nur_func::call("func_m1o1v", 1, 2));
+ self::assertSame([1, 2, 3], nur_func::call("func_m1o1v", 1, 2, 3));
+ self::assertSame([1, 2, 3, 4], nur_func::call("func_m1o1v", 1, 2, 3, 4));
+
+ # o1v
+ self::assertSame([9], nur_func::call("func_o1v"));
+ self::assertSame([null], nur_func::call("func_o1v", null));
+ self::assertSame([null, null], nur_func::call("func_o1v", null, null));
+ self::assertSame([null, null, null], nur_func::call("func_o1v", null, null, null));
+ self::assertSame([null, null, null, null], nur_func::call("func_o1v", null, null, null, null));
+ self::assertSame([1], nur_func::call("func_o1v", 1));
+ self::assertSame([1, 2], nur_func::call("func_o1v", 1, 2));
+ self::assertSame([1, 2, 3], nur_func::call("func_o1v", 1, 2, 3));
+ self::assertSame([1, 2, 3, 4], nur_func::call("func_o1v", 1, 2, 3, 4));
+ }
+
+ function testCall_all() {
+ $c1 = new C1();
+ $c2 = new C2();
+ $c3 = new C3();
+
+ self::assertSameValues([11, 12], nur_func::call_all(C1::class));
+ self::assertSameValues([11, 12, 21, 22], nur_func::call_all($c1));
+ self::assertSameValues([13, 11, 12], nur_func::call_all(C2::class));
+ self::assertSameValues([13, 23, 11, 12, 21, 22], nur_func::call_all($c2));
+ self::assertSameValues([111, 13, 12], nur_func::call_all(C3::class));
+ self::assertSameValues([111, 121, 13, 23, 12, 22], nur_func::call_all($c3));
+
+ $options = "conf";
+ self::assertSameValues([11], nur_func::call_all(C1::class, $options));
+ self::assertSameValues([11, 21], nur_func::call_all($c1, $options));
+ self::assertSameValues([11], nur_func::call_all(C2::class, $options));
+ self::assertSameValues([11, 21], nur_func::call_all($c2, $options));
+ self::assertSameValues([111], nur_func::call_all(C3::class, $options));
+ self::assertSameValues([111, 121], nur_func::call_all($c3, $options));
+
+ $options = ["prefix" => "conf"];
+ self::assertSameValues([11], nur_func::call_all(C1::class, $options));
+ self::assertSameValues([11, 21], nur_func::call_all($c1, $options));
+ self::assertSameValues([11], nur_func::call_all(C2::class, $options));
+ self::assertSameValues([11, 21], nur_func::call_all($c2, $options));
+ self::assertSameValues([111], nur_func::call_all(C3::class, $options));
+ self::assertSameValues([111, 121], nur_func::call_all($c3, $options));
+
+ self::assertSameValues([11, 12], nur_func::call_all($c1, ["include" => "x"]));
+ self::assertSameValues([11, 21], nur_func::call_all($c1, ["include" => "y"]));
+ self::assertSameValues([11, 12, 21], nur_func::call_all($c1, ["include" => ["x", "y"]]));
+
+ self::assertSameValues([21, 22], nur_func::call_all($c1, ["exclude" => "x"]));
+ self::assertSameValues([12, 22], nur_func::call_all($c1, ["exclude" => "y"]));
+ self::assertSameValues([22], nur_func::call_all($c1, ["exclude" => ["x", "y"]]));
+
+ self::assertSameValues([12], nur_func::call_all($c1, ["include" => "x", "exclude" => "y"]));
+ }
+
+ function testCons() {
+ $obj1 = nur_func::cons(WoCons::class, 1, 2, 3);
+ self::assertInstanceOf(WoCons::class, $obj1);
+
+ $obj2 = nur_func::cons(WithEmptyCons::class, 1, 2, 3);
+ self::assertInstanceOf(WithEmptyCons::class, $obj2);
+
+ $obj3 = nur_func::cons(WithCons::class, 1, 2, 3);
+ self::assertInstanceOf(WithCons::class, $obj3);
+ self::assertSame(1, $obj3->first);
+ }
+ }
+
+ class WoCons {
+ }
+ class WithEmptyCons {
+ function __construct() {
+ }
+ }
+ class WithCons {
+ public $first;
+ function __construct($first) {
+ $this->first = $first;
+ }
+ }
+
+ class TC {
+ static function method() {
+ return 12;
+ }
+ }
+
+ class C1 {
+ static function confps1_xy() {
+ return 11;
+ }
+ static function ps2_x() {
+ return 12;
+ }
+ function confp1_y() {
+ return 21;
+ }
+ function p2() {
+ return 22;
+ }
+ }
+ class C2 extends C1 {
+ static function ps3() {
+ return 13;
+ }
+ function p3() {
+ return 23;
+ }
+ }
+ class C3 extends C2 {
+ static function confps1_xy() {
+ return 111;
+ }
+ function confp1_y() {
+ return 121;
+ }
+ }
+}
diff --git a/php/tests/php/time/DateTest.php b/php/tests/php/time/DateTest.php
new file mode 100644
index 0000000..857ca33
--- /dev/null
+++ b/php/tests/php/time/DateTest.php
@@ -0,0 +1,85 @@
+format());
+ self::assertSame("05/04/2024", strval($date));
+ self::assertSame(2024, $date->year);
+ self::assertSame(4, $date->month);
+ self::assertSame(5, $date->day);
+ self::assertSame(0, $date->hour);
+ self::assertSame(0, $date->minute);
+ self::assertSame(0, $date->second);
+ self::assertSame(5, $date->wday);
+ self::assertSame(14, $date->wnum);
+ self::assertSame("+04:00", $date->timezone);
+ self::assertSame("05/04/2024 00:00:00", $date->datetime);
+ self::assertSame("05/04/2024", $date->date);
+ }
+
+ function testClone() {
+ $date = self::dt("now");
+ $clone = Date::clone($date);
+ self::assertInstanceOf(DateTime::class, $clone);
+ }
+
+ function testConstruct() {
+ $y = date("Y");
+ self::assertSame("05/04/$y", strval(new Date("5/4")));
+ self::assertSame("05/04/2024", strval(new Date("5/4/24")));
+ self::assertSame("05/04/2024", strval(new Date("5/4/2024")));
+ self::assertSame("05/04/2024", strval(new Date("05/04/2024")));
+ self::assertSame("05/04/2024", strval(new Date("20240405")));
+ self::assertSame("05/04/2024", strval(new Date("240405")));
+ self::assertSame("05/04/2024", strval(new Date("20240405T091523")));
+ self::assertSame("05/04/2024", strval(new Date("20240405T091523Z")));
+ self::assertSame("05/04/2024", strval(new Date("5/4/2024 9:15:23")));
+ self::assertSame("05/04/2024", strval(new Date("5/4/2024 9.15.23")));
+ self::assertSame("05/04/2024", strval(new Date("5/4/2024 9:15")));
+ self::assertSame("05/04/2024", strval(new Date("5/4/2024 9.15")));
+ self::assertSame("05/04/2024", strval(new Date("5/4/2024 9h15")));
+ self::assertSame("05/04/2024", strval(new Date("5/4/2024 09:15:23")));
+ self::assertSame("05/04/2024", strval(new Date("5/4/2024 09:15")));
+ self::assertSame("05/04/2024", strval(new Date("5/4/2024 09h15")));
+ }
+
+ function testCompare() {
+ $a = new Date("10/02/2024");
+ $b = new Date("15/02/2024");
+ $c = new Date("20/02/2024");
+ $a2 = new Date("10/02/2024");
+ $b2 = new Date("15/02/2024");
+ $c2 = new Date("20/02/2024");
+
+ self::assertTrue($a == $a2);
+ self::assertFalse($a === $a2);
+ self::assertTrue($b == $b2);
+ self::assertTrue($c == $c2);
+
+ self::assertFalse($a < $a);
+ self::assertTrue($a < $b);
+ self::assertTrue($a < $c);
+
+ self::assertTrue($a <= $a);
+ self::assertTrue($a <= $b);
+ self::assertTrue($a <= $c);
+
+ self::assertFalse($c > $c);
+ self::assertTrue($c > $b);
+ self::assertTrue($c > $a);
+
+ self::assertTrue($c >= $c);
+ self::assertTrue($c >= $b);
+ self::assertTrue($c >= $a);
+ }
+}
diff --git a/php/tests/php/time/DateTimeTest.php b/php/tests/php/time/DateTimeTest.php
new file mode 100644
index 0000000..088bc79
--- /dev/null
+++ b/php/tests/php/time/DateTimeTest.php
@@ -0,0 +1,109 @@
+format());
+ self::assertEquals("05/04/2024 09:15:23", strval($date));
+ self::assertSame(2024, $date->year);
+ self::assertSame(4, $date->month);
+ self::assertSame(5, $date->day);
+ self::assertSame(9, $date->hour);
+ self::assertSame(15, $date->minute);
+ self::assertSame(23, $date->second);
+ self::assertSame(5, $date->wday);
+ self::assertSame(14, $date->wnum);
+ self::assertEquals("+04:00", $date->timezone);
+ self::assertSame("05/04/2024 09:15:23", $date->datetime);
+ self::assertSame("05/04/2024", $date->date);
+ self::assertSame("20240405", $date->Ymd);
+ self::assertSame("20240405T091523", $date->YmdHMS);
+ self::assertSame("20240405T091523+04:00", $date->YmdHMSZ);
+ }
+
+ function testDateTimeZ() {
+ $date = new DateTime("20240405T091523Z");
+ self::assertSame("20240405T091523", $date->YmdHMS);
+ self::assertSame("20240405T091523Z", $date->YmdHMSZ);
+ }
+
+ function testClone() {
+ $date = self::dt("now");
+ $clone = DateTime::clone($date);
+ self::assertInstanceOf(DateTime::class, $clone);
+ }
+
+ function testConstruct() {
+ $y = date("Y");
+ self::assertSame("05/04/$y 00:00:00", strval(new DateTime("5/4")));
+ self::assertSame("05/04/2024 00:00:00", strval(new DateTime("5/4/24")));
+ self::assertSame("05/04/2024 00:00:00", strval(new DateTime("5/4/2024")));
+ self::assertSame("05/04/2024 00:00:00", strval(new DateTime("05/04/2024")));
+ self::assertSame("05/04/2024 00:00:00", strval(new DateTime("20240405")));
+ self::assertSame("05/04/2024 00:00:00", strval(new DateTime("240405")));
+ self::assertSame("05/04/2024 09:15:23", strval(new DateTime("20240405T091523")));
+ self::assertSame("05/04/2024 09:15:23", strval(new DateTime("20240405T091523Z")));
+ self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 9:15:23")));
+ self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 9.15.23")));
+ self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 9:15")));
+ self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 9.15")));
+ self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 9h15")));
+ self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 09:15:23")));
+ self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 09:15")));
+ self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 09h15")));
+ }
+
+ function testCompare() {
+ $a = new DateTime("10/02/2024");
+ $a2 = new DateTime("10/02/2024 8:30");
+ $a3 = new DateTime("10/02/2024 15:45");
+ $b = new DateTime("15/02/2024");
+ $b2 = new DateTime("15/02/2024 8:30");
+ $b3 = new DateTime("15/02/2024 15:45");
+ $x = new DateTime("10/02/2024");
+ $x2 = new DateTime("10/02/2024 8:30");
+ $x3 = new DateTime("10/02/2024 15:45");
+
+ self::assertTrue($a == $x);
+ self::assertFalse($a === $x);
+ self::assertTrue($a2 == $x2);
+ self::assertTrue($a3 == $x3);
+
+ self::assertFalse($a < $a);
+ self::assertTrue($a < $a2);
+ self::assertTrue($a < $a3);
+ self::assertTrue($a < $b);
+ self::assertTrue($a < $b2);
+ self::assertTrue($a < $b3);
+
+ self::assertTrue($a <= $a);
+ self::assertTrue($a <= $a2);
+ self::assertTrue($a <= $a3);
+ self::assertTrue($a <= $b);
+ self::assertTrue($a <= $b2);
+ self::assertTrue($a <= $b3);
+
+ self::assertTrue($b > $a);
+ self::assertTrue($b > $a2);
+ self::assertTrue($b > $a3);
+ self::assertFalse($b > $b);
+ self::assertFalse($b > $b2);
+ self::assertFalse($b > $b3);
+
+ self::assertTrue($b >= $a);
+ self::assertTrue($b >= $a2);
+ self::assertTrue($b >= $a3);
+ self::assertTrue($b >= $b);
+ self::assertFalse($b >= $b2);
+ self::assertFalse($b >= $b3);
+ }
+}
diff --git a/php/tests/php/time/DelayTest.php b/php/tests/php/time/DelayTest.php
new file mode 100644
index 0000000..132bc4d
--- /dev/null
+++ b/php/tests/php/time/DelayTest.php
@@ -0,0 +1,83 @@
+getDest());
+
+ $delay = new Delay("10", $from);
+ self::assertEquals(self::dt("2024-04-05 09:15:33"), $delay->getDest());
+
+ $delay = new Delay("10s", $from);
+ self::assertEquals(self::dt("2024-04-05 09:15:33"), $delay->getDest());
+
+ $delay = new Delay("s", $from);
+ self::assertEquals(self::dt("2024-04-05 09:15:24"), $delay->getDest());
+
+ $delay = new Delay("5m", $from);
+ self::assertEquals(self::dt("2024-04-05 09:20:00"), $delay->getDest());
+
+ $delay = new Delay("5m0", $from);
+ self::assertEquals(self::dt("2024-04-05 09:20:00"), $delay->getDest());
+
+ $delay = new Delay("5m2", $from);
+ self::assertEquals(self::dt("2024-04-05 09:20:02"), $delay->getDest());
+
+ $delay = new Delay("m", $from);
+ self::assertEquals(self::dt("2024-04-05 09:16:00"), $delay->getDest());
+
+ $delay = new Delay("5h", $from);
+ self::assertEquals(self::dt("2024-04-05 14:00:00"), $delay->getDest());
+
+ $delay = new Delay("5h0", $from);
+ self::assertEquals(self::dt("2024-04-05 14:00:00"), $delay->getDest());
+
+ $delay = new Delay("5h2", $from);
+ self::assertEquals(self::dt("2024-04-05 14:02:00"), $delay->getDest());
+
+ $delay = new Delay("h", $from);
+ self::assertEquals(self::dt("2024-04-05 10:00:00"), $delay->getDest());
+
+ $delay = new Delay("5d", $from);
+ self::assertEquals(self::dt("2024-04-10 05:00:00"), $delay->getDest());
+
+ $delay = new Delay("5d2", $from);
+ self::assertEquals(self::dt("2024-04-10 02:00:00"), $delay->getDest());
+
+ $delay = new Delay("5d0", $from);
+ self::assertEquals(self::dt("2024-04-10 00:00:00"), $delay->getDest());
+
+ $delay = new Delay("d", $from);
+ self::assertEquals(self::dt("2024-04-06 05:00:00"), $delay->getDest());
+
+ $delay = new Delay("2w", $from);
+ self::assertEquals(self::dt("2024-04-21 05:00:00"), $delay->getDest());
+
+ $delay = new Delay("2w2", $from);
+ self::assertEquals(self::dt("2024-04-21 02:00:00"), $delay->getDest());
+
+ $delay = new Delay("2w0", $from);
+ self::assertEquals(self::dt("2024-04-21 00:00:00"), $delay->getDest());
+
+ $delay = new Delay("w", $from);
+ self::assertEquals(self::dt("2024-04-07 05:00:00"), $delay->getDest());
+ }
+
+ function testElapsed() {
+ $delay = new Delay(5);
+ sleep(2);
+ self::assertFalse($delay->isElapsed());
+ sleep(5);
+ self::assertTrue($delay->isElapsed());
+ }
+}
diff --git a/php/tests/schema/_scalar/ScalarSchemaTest.php b/php/tests/schema/_scalar/ScalarSchemaTest.php
new file mode 100644
index 0000000..e004168
--- /dev/null
+++ b/php/tests/schema/_scalar/ScalarSchemaTest.php
@@ -0,0 +1,64 @@
+ [null],
+ "default" => null,
+ "title" => null,
+ "required" => false,
+ "nullable" => true,
+ "desc" => null,
+ "analyzer_func" => null,
+ "extractor_func" => null,
+ "parser_func" => null,
+ "normalizer_func" => null,
+ "messages" => null,
+ "formatter_func" => null,
+ "format" => null,
+ "" => ["scalar"],
+ "name" => null,
+ "pkey" => null,
+ "header" => null,
+ "composite" => null,
+ ];
+
+ static function schema(array $schema): array {
+ return array_merge(self::NULL_SCHEMA, $schema);
+ }
+
+ function testNormalize() {
+ self::assertSame(self::NULL_SCHEMA, ScalarSchema::normalize(null));
+ self::assertSame(self::NULL_SCHEMA, ScalarSchema::normalize([]));
+ self::assertSame(self::NULL_SCHEMA, ScalarSchema::normalize([null]));
+ self::assertException(SchemaException::class, function () {
+ ScalarSchema::normalize([[]]);
+ });
+ self::assertException(SchemaException::class, function () {
+ ScalarSchema::normalize([[null]]);
+ });
+
+ $string = self::schema(["type" => ["string"], "nullable" => false]);
+ self::assertSame($string, ScalarSchema::normalize("string"));
+ self::assertSame($string, ScalarSchema::normalize(["string"]));
+
+ $nstring = self::schema(["type" => ["string"]]);
+ self::assertSame($nstring, ScalarSchema::normalize(["?string"]));
+ self::assertSame($nstring, ScalarSchema::normalize(["?string|null"]));
+ self::assertSame($nstring, ScalarSchema::normalize(["string|null"]));
+ self::assertSame($nstring, ScalarSchema::normalize([["?string", "null"]]));
+ self::assertSame($nstring, ScalarSchema::normalize([["string", "null"]]));
+ self::assertSame($nstring, ScalarSchema::normalize([["string", null]]));
+
+ $key = self::schema(["type" => ["string", "int"], "nullable" => false]);
+ self::assertSame($key, ScalarSchema::normalize("string|int"));
+
+ $nkey = self::schema(["type" => ["string", "int"], "nullable" => true]);
+ self::assertSame($nkey, ScalarSchema::normalize("?string|int"));
+ self::assertSame($nkey, ScalarSchema::normalize("string|?int"));
+ }
+}
diff --git a/php/tests/schema/types/boolTest.php b/php/tests/schema/types/boolTest.php
new file mode 100644
index 0000000..8f990e3
--- /dev/null
+++ b/php/tests/schema/types/boolTest.php
@@ -0,0 +1,111 @@
+set(true);
+ self::assertSame(true, $destv->get());
+ self::assertSame(true, $dest);
+ self::assertSame("Oui", $destv->format());
+ self::assertSame("Oui", $destv->format("OuiNonNull"));
+ self::assertSame("O", $destv->format("ON"));
+ self::assertSame("O", $destv->format("ONN"));
+
+ $destv->set(false);
+ self::assertSame(false, $destv->get());
+ self::assertSame(false, $dest);
+ self::assertSame("Non", $destv->format());
+ self::assertSame("Non", $destv->format("OuiNonNull"));
+ self::assertSame("N", $destv->format("ON"));
+ self::assertSame("N", $destv->format("ONN"));
+
+ $destv->set("yes");
+ self::assertSame(true, $destv->get());
+
+ $destv->set(" yes ");
+ self::assertSame(true, $destv->get());
+
+ $destv->set("12");
+ self::assertSame(true, $destv->get());
+
+ $destv->set(12);
+ self::assertSame(true, $destv->get());
+
+ $destv->set("no");
+ self::assertSame(false, $destv->get());
+
+ $destv->set(" no ");
+ self::assertSame(false, $destv->get());
+
+ $destv->set("0");
+ self::assertSame(false, $destv->get());
+
+ $destv->set(0);
+ self::assertSame(false, $destv->get());
+
+ $destv->set(12.34);
+ self::assertSame(true, $destv->get());
+
+ self::assertException(Exception::class, $destvSetter("a"));
+ self::assertException(Exception::class, $destvSetter([]));
+ self::assertException(Exception::class, $destvSetter(["a"]));
+
+ }
+
+ function testBool() {
+ /** @var ScalarValue $destv */
+ Schema::nv($destv, $dest, null, $schema, "bool");
+ $destvSetter = function($value) use($destv) {
+ return function() use($destv, $value) {
+ $destv->set($value);
+ };
+ };
+
+ self::assertException(Exception::class, $destvSetter(null));
+ self::assertException(Exception::class, $destvSetter(""));
+ self::assertException(Exception::class, $destvSetter(" "));
+
+ $this->commonTests($destv, $dest, $destvSetter);
+ }
+
+ function testNbool() {
+ /** @var ScalarValue $destv */
+ Schema::nv($destv, $dest, null, $schema, "?bool");
+ $destvSetter = function($value) use($destv) {
+ return function() use($destv, $value) {
+ $destv->set($value);
+ };
+ };
+
+ $destv->set(null);
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("Non", $destv->format());
+ self::assertSame("", $destv->format("OuiNonNull"));
+ self::assertSame("N", $destv->format("ON"));
+ self::assertSame("", $destv->format("ONN"));
+
+ $destv->set("");
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("Non", $destv->format());
+ self::assertSame("", $destv->format("OuiNonNull"));
+ self::assertSame("N", $destv->format("ON"));
+ self::assertSame("", $destv->format("ONN"));
+
+ $destv->set(" ");
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("Non", $destv->format());
+ self::assertSame("", $destv->format("OuiNonNull"));
+ self::assertSame("N", $destv->format("ON"));
+ self::assertSame("", $destv->format("ONN"));
+
+ $this->commonTests($destv, $dest, $destvSetter);
+ }
+}
diff --git a/php/tests/schema/types/floatTest.php b/php/tests/schema/types/floatTest.php
new file mode 100644
index 0000000..193d407
--- /dev/null
+++ b/php/tests/schema/types/floatTest.php
@@ -0,0 +1,139 @@
+set(12);
+ self::assertSame(12.0, $destv->get());
+ self::assertSame(12.0, $dest);
+ self::assertSame("12", $destv->format());
+ self::assertSame("0012", $destv->format("%04u"));
+
+ $destv->set("12");
+ self::assertSame(12.0, $destv->get());
+
+ $destv->set(" 12 ");
+ self::assertSame(12.0, $destv->get());
+
+ $destv->set(12.34);
+ self::assertSame(12.34, $destv->get());
+
+ $destv->set(true);
+ self::assertSame(1.0, $destv->get());
+
+ self::assertException(Exception::class, $destvSetter("a"));
+ self::assertException(Exception::class, $destvSetter([]));
+ self::assertException(Exception::class, $destvSetter(["a"]));
+ }
+
+ function testFloat() {
+ /** @var ScalarValue $destv */
+ Schema::nv($destv, $dest, null, $schema, "float");
+ $destvSetter = function($value) use($destv) {
+ return function() use($destv, $value) {
+ $destv->set($value);
+ };
+ };
+
+ self::assertException(Exception::class, $destvSetter(null));
+ self::assertException(Exception::class, $destvSetter(""));
+ self::assertException(Exception::class, $destvSetter(" "));
+
+ // valeur non requise donc retourne null
+ $destv->set(false);
+ self::assertNull($destv->get());
+
+ $this->commonTests($destv, $dest, $destvSetter);
+ }
+
+ function testRequiredFloat() {
+ /** @var ScalarValue $destv */
+ Schema::nv($destv, $dest, null, $schema, [
+ "float", null,
+ "required" => true,
+ ]);
+ $destvSetter = function($value) use($destv) {
+ return function() use($destv, $value) {
+ $destv->set($value);
+ };
+ };
+
+ self::assertException(Exception::class, $destvSetter(null));
+ self::assertException(Exception::class, $destvSetter(""));
+ self::assertException(Exception::class, $destvSetter(" "));
+
+ // valeur requise donc lance une exception
+ self::assertException(Exception::class, $destvSetter(false));
+
+ $this->commonTests($destv, $dest, $destvSetter);
+ }
+
+ function testNfloat() {
+ /** @var ScalarValue $destv */
+ Schema::nv($destv, $dest, null, $schema, "?float");
+ $destvSetter = function($value) use($destv) {
+ return function() use($destv, $value) {
+ $destv->set($value);
+ };
+ };
+
+ $destv->set(null);
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("", $destv->format());
+
+ $destv->set("");
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("", $destv->format());
+
+ $destv->set(" ");
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("", $destv->format());
+
+ // valeur non requise donc retourne null
+ $destv->set(false);
+ self::assertNull($destv->get());
+
+ $this->commonTests($destv, $dest, $destvSetter);
+ }
+
+ function testRequiredNfloat() {
+ /** @var ScalarValue $destv */
+ Schema::nv($destv, $dest, null, $schema, [
+ "?float", null,
+ "required" => true,
+ ]);
+ $destvSetter = function($value) use($destv) {
+ return function() use($destv, $value) {
+ $destv->set($value);
+ };
+ };
+
+ $destv->set(null);
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("", $destv->format());
+
+ $destv->set("");
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("", $destv->format());
+
+ $destv->set(" ");
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("", $destv->format());
+
+ // valeur requise donc lance une exception
+ self::assertException(Exception::class, $destvSetter(false));
+
+ $this->commonTests($destv, $dest, $destvSetter);
+ }
+}
diff --git a/php/tests/schema/types/intTest.php b/php/tests/schema/types/intTest.php
new file mode 100644
index 0000000..29de7ce
--- /dev/null
+++ b/php/tests/schema/types/intTest.php
@@ -0,0 +1,139 @@
+set(12);
+ self::assertSame(12, $destv->get());
+ self::assertSame(12, $dest);
+ self::assertSame("12", $destv->format());
+ self::assertSame("0012", $destv->format("%04u"));
+
+ $destv->set("12");
+ self::assertSame(12, $destv->get());
+
+ $destv->set(" 12 ");
+ self::assertSame(12, $destv->get());
+
+ $destv->set(12.34);
+ self::assertSame(12, $destv->get());
+
+ $destv->set(true);
+ self::assertSame(1, $destv->get());
+
+ self::assertException(Exception::class, $destvSetter("a"));
+ self::assertException(Exception::class, $destvSetter([]));
+ self::assertException(Exception::class, $destvSetter(["a"]));
+ }
+
+ function testInt() {
+ /** @var ScalarValue $destv */
+ Schema::nv($destv, $dest, null, $schema, "int");
+ $destvSetter = function($value) use($destv) {
+ return function() use($destv, $value) {
+ $destv->set($value);
+ };
+ };
+
+ self::assertException(Exception::class, $destvSetter(null));
+ self::assertException(Exception::class, $destvSetter(""));
+ self::assertException(Exception::class, $destvSetter(" "));
+
+ // valeur non requise donc retourne null
+ $destv->set(false);
+ self::assertNull($destv->get());
+
+ $this->commonTests($destv, $dest, $destvSetter);
+ }
+
+ function testRequiredInt() {
+ /** @var ScalarValue $destv */
+ Schema::nv($destv, $dest, null, $schema, [
+ "int", null,
+ "required" => true,
+ ]);
+ $destvSetter = function($value) use($destv) {
+ return function() use($destv, $value) {
+ $destv->set($value);
+ };
+ };
+
+ self::assertException(Exception::class, $destvSetter(null));
+ self::assertException(Exception::class, $destvSetter(""));
+ self::assertException(Exception::class, $destvSetter(" "));
+
+ // valeur requise donc lance une exception
+ self::assertException(Exception::class, $destvSetter(false));
+
+ $this->commonTests($destv, $dest, $destvSetter);
+ }
+
+ function testNint() {
+ /** @var ScalarValue $destv */
+ Schema::nv($destv, $dest, null, $schema, "?int");
+ $destvSetter = function($value) use($destv) {
+ return function() use($destv, $value) {
+ $destv->set($value);
+ };
+ };
+
+ $destv->set(null);
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("", $destv->format());
+
+ $destv->set("");
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("", $destv->format());
+
+ $destv->set(" ");
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("", $destv->format());
+
+ // valeur non requise donc retourne null
+ $destv->set(false);
+ self::assertNull($destv->get());
+
+ $this->commonTests($destv, $dest, $destvSetter);
+ }
+
+ function testRequiredNint() {
+ /** @var ScalarValue $destv */
+ Schema::nv($destv, $dest, null, $schema, [
+ "?int", null,
+ "required" => true,
+ ]);
+ $destvSetter = function($value) use($destv) {
+ return function() use($destv, $value) {
+ $destv->set($value);
+ };
+ };
+
+ $destv->set(null);
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("", $destv->format());
+
+ $destv->set("");
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("", $destv->format());
+
+ $destv->set(" ");
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("", $destv->format());
+
+ // valeur requise donc lance une exception
+ self::assertException(Exception::class, $destvSetter(false));
+
+ $this->commonTests($destv, $dest, $destvSetter);
+ }
+}
diff --git a/php/tests/schema/types/strTest.php b/php/tests/schema/types/strTest.php
new file mode 100644
index 0000000..45eda17
--- /dev/null
+++ b/php/tests/schema/types/strTest.php
@@ -0,0 +1,123 @@
+set("");
+ self::assertSame("", $destv->get());
+ self::assertSame("", $dest);
+
+ $destv->set(" ");
+ self::assertSame(" ", $destv->get());
+ self::assertSame(" ", $dest);
+
+ $destv->set("a");
+ self::assertSame("a", $destv->get());
+ self::assertSame("a", $dest);
+
+ $destv->set("12");
+ self::assertSame("12", $destv->get());
+
+ $destv->set(" 12 ");
+ self::assertSame(" 12 ", $destv->get());
+
+ $destv->set(12);
+ self::assertSame("12", $destv->get());
+
+ $destv->set(12.34);
+ self::assertSame("12.34", $destv->get());
+
+ $destv->set(true);
+ self::assertSame("1", $destv->get());
+
+ self::assertException(Exception::class, $destvSetter([]));
+ self::assertException(Exception::class, $destvSetter(["a"]));
+ }
+
+ function testStr() {
+ /** @var ScalarValue $destv */
+ Schema::nv($destv, $dest, null, $schema, "string");
+ $destvSetter = function($value) use($destv) {
+ return function() use($destv, $value) {
+ $destv->set($value);
+ };
+ };
+
+ self::assertException(Exception::class, $destvSetter(null));
+
+ // valeur non requise donc retourne null
+ $destv->set(false);
+ self::assertNull($destv->get());
+
+ $this->commonTests($destv, $dest, $destvSetter);
+ }
+
+ function testRequiredStr() {
+ /** @var ScalarValue $destv */
+ Schema::nv($destv, $dest, null, $schema, [
+ "string", null,
+ "required" => true,
+ ]);
+ $destvSetter = function($value) use($destv) {
+ return function() use($destv, $value) {
+ $destv->set($value);
+ };
+ };
+
+ self::assertException(Exception::class, $destvSetter(null));
+
+ // valeur requise donc lance une exception
+ self::assertException(Exception::class, $destvSetter(false));
+
+ $this->commonTests($destv, $dest, $destvSetter);
+ }
+
+ function testNstr() {
+ /** @var ScalarValue $destv */
+ Schema::nv($destv, $dest, null, $schema, "?string");
+ $destvSetter = function($value) use($destv) {
+ return function() use($destv, $value) {
+ $destv->set($value);
+ };
+ };
+
+ $destv->set(null);
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("", $destv->format());
+
+ // valeur non requise donc retourne null
+ $destv->set(false);
+ self::assertNull($destv->get());
+
+ $this->commonTests($destv, $dest, $destvSetter);
+ }
+
+ function testRequiredNstr() {
+ /** @var ScalarValue $destv */
+ Schema::nv($destv, $dest, null, $schema, [
+ "?string", null,
+ "required" => true,
+ ]);
+ $destvSetter = function($value) use($destv) {
+ return function() use($destv, $value) {
+ $destv->set($value);
+ };
+ };
+
+ $destv->set(null);
+ self::assertNull($destv->get());
+ self::assertNull($dest);
+ self::assertSame("", $destv->format());
+
+ // valeur requise donc lance une exception
+ self::assertException(Exception::class, $destvSetter(false));
+
+ $this->commonTests($destv, $dest, $destvSetter);
+ }
+}
diff --git a/php/tests/schema/types/unionTest.php b/php/tests/schema/types/unionTest.php
new file mode 100644
index 0000000..c208087
--- /dev/null
+++ b/php/tests/schema/types/unionTest.php
@@ -0,0 +1,29 @@
+set("12");
+ self::assertSame("12", $si);
+ $siv->set(12);
+ self::assertSame(12, $si);
+
+ # int puis string
+ Schema::nv($isv, $is, null, $iss, "int|string");
+
+ $isv->set("12");
+ self::assertSame("12", $is);
+ $isv->set(12);
+ self::assertSame(12, $is);
+ }
+}
diff --git a/php/tests/strTest.php b/php/tests/strTest.php
new file mode 100644
index 0000000..2d69303
--- /dev/null
+++ b/php/tests/strTest.php
@@ -0,0 +1,36 @@
+ [
+ 'name' => '',
+ 'type' => '',
+ 'tmp_name' => '',
+ 'error' => 4,
+ 'size' => 0,
+ ],
+ # name=multiple[], name=multiple[]
+ 'multiple' => [
+ 'name' => [
+ 0 => '',
+ 1 => '',
+ ],
+ 'type' => [
+ 0 => '',
+ 1 => '',
+ ],
+ 'tmp_name' => [
+ 0 => '',
+ 1 => '',
+ ],
+ 'error' => [
+ 0 => 4,
+ 1 => 4,
+ ],
+ 'size' => [
+ 0 => 0,
+ 1 => 0,
+ ],
+ ],
+ # name=onelevel[a], name=onelevel[b]
+ 'onelevel' => [
+ 'name' => [
+ 'a' => '',
+ 'b' => '',
+ ],
+ 'type' => [
+ 'a' => '',
+ 'b' => '',
+ ],
+ 'tmp_name' => [
+ 'a' => '',
+ 'b' => '',
+ ],
+ 'error' => [
+ 'a' => 4,
+ 'b' => 4,
+ ],
+ 'size' => [
+ 'a' => 0,
+ 'b' => 0,
+ ],
+ ],
+ # name=multiplelevel[a][], name=multiplelevel[a][], name=multiplelevel[b][], name=multiplelevel[b][]
+ 'multiplelevel' => [
+ 'name' => [
+ 'a' => [
+ 0 => '',
+ 1 => '',
+ ],
+ 'b' => [
+ 0 => '',
+ 1 => '',
+ ],
+ ],
+ 'type' => [
+ 'a' => [
+ 0 => '',
+ 1 => '',
+ ],
+ 'b' => [
+ 0 => '',
+ 1 => '',
+ ],
+ ],
+ 'tmp_name' => [
+ 'a' => [
+ 0 => '',
+ 1 => '',
+ ],
+ 'b' => [
+ 0 => '',
+ 1 => '',
+ ],
+ ],
+ 'error' => [
+ 'a' => [
+ 0 => 4,
+ 1 => 4,
+ ],
+ 'b' => [
+ 0 => 4,
+ 1 => 4,
+ ],
+ ],
+ 'size' => [
+ 'a' => [
+ 0 => 0,
+ 1 => 0,
+ ],
+ 'b' => [
+ 0 => 0,
+ 1 => 0,
+ ],
+ ],
+ ],
+ ];
+
+ const PARSED = [
+ # name="simple"
+ 'simple' => [
+ 'name' => '',
+ 'type' => '',
+ 'tmp_name' => '',
+ 'error' => 4,
+ 'size' => 0,
+ ],
+ # name=multiple[], name=multiple[]
+ 'multiple' => [
+ 0 => [
+ 'name' => '',
+ 'type' => '',
+ 'tmp_name' => '',
+ 'error' => 4,
+ 'size' => 0,
+ ],
+ 1 => [
+ 'name' => '',
+ 'type' => '',
+ 'tmp_name' => '',
+ 'error' => 4,
+ 'size' => 0,
+ ],
+ ],
+ # name=onelevel[a], name=onelevel[b]
+ 'onelevel' => [
+ 'a' => [
+ 'name' => '',
+ 'type' => '',
+ 'tmp_name' => '',
+ 'error' => 4,
+ 'size' => 0,
+ ],
+ 'b' => [
+ 'name' => '',
+ 'type' => '',
+ 'tmp_name' => '',
+ 'error' => 4,
+ 'size' => 0,
+ ],
+ ],
+ # name=multiplelevel[a][], name=multiplelevel[a][], name=multiplelevel[b][], name=multiplelevel[b][]
+ 'multiplelevel' => [
+ 'a' => [
+ 0 => [
+ 'name' => '',
+ 'type' => '',
+ 'tmp_name' => '',
+ 'error' => 4,
+ 'size' => 0,
+ ],
+ 1 => [
+ 'name' => '',
+ 'type' => '',
+ 'tmp_name' => '',
+ 'error' => 4,
+ 'size' => 0,
+ ],
+ ],
+ 'b' => [
+ 0 => [
+ 'name' => '',
+ 'type' => '',
+ 'tmp_name' => '',
+ 'error' => 4,
+ 'size' => 0,
+ ],
+ 1 => [
+ 'name' => '',
+ 'type' => '',
+ 'tmp_name' => '',
+ 'error' => 4,
+ 'size' => 0,
+ ],
+ ],
+ ],
+ ];
+
+ function test_files() {
+ self::assertSame(self::PARSED, uploads::_files(self::_FILES));
+ }
+}
diff --git a/runphp/Dockerfile.runphp b/runphp/Dockerfile.runphp
new file mode 100644
index 0000000..f6bcfbc
--- /dev/null
+++ b/runphp/Dockerfile.runphp
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+ARG NDIST=12
+ARG REGISTRY=pubdocker.univ-reunion.fr/dist
+
+FROM $REGISTRY/src/base AS base
+FROM $REGISTRY/src/php AS php
+
+################################################################################
+FROM debian:${NDIST}-slim AS builder
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=base /src/ /src/
+RUN /g/build core lite _builder
+RUN /g/build _su-exec_builder
+
+################################################################################
+FROM debian:${NDIST}-slim
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=builder /src/su-exec/su-exec /g/
+RUN /g/build
+
+COPY --from=php /g/ /g/
+RUN /g/build @php-cli php-utils
+
+ENTRYPOINT ["/g/entrypoint"]
diff --git a/runphp/Dockerfile.runphp+ic b/runphp/Dockerfile.runphp+ic
new file mode 100644
index 0000000..48f376b
--- /dev/null
+++ b/runphp/Dockerfile.runphp+ic
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+ARG NDIST=12
+ARG REGISTRY=pubdocker.univ-reunion.fr/dist
+
+FROM $REGISTRY/src/base AS base
+FROM $REGISTRY/src/legacytools AS legacytools
+FROM $REGISTRY/src/instantclient AS instantclient
+FROM $REGISTRY/src/php AS php
+
+################################################################################
+FROM debian:${NDIST}-slim AS builder
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=base /src/ /src/
+RUN /g/build core lite _builder
+RUN /g/build _su-exec_builder
+
+COPY --from=instantclient /g/ /g/
+COPY --from=instantclient /src/ /src/
+RUN /g/build _instantclient_builder
+
+################################################################################
+FROM debian:${NDIST}-slim
+ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE
+ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE
+
+COPY --from=base /g/ /g/
+COPY --from=builder /src/su-exec/su-exec /g/
+RUN /g/build
+
+COPY --from=legacytools /g/ /g/
+RUN /g/build nutools
+
+COPY --from=php /g/ /g/
+RUN /g/build @php-cli php-utils
+
+COPY --from=instantclient /g/ /g/
+COPY --from=builder /opt/oracle/ /opt/oracle/
+RUN /g/build instantclient
+
+ENTRYPOINT ["/g/entrypoint"]
diff --git a/runphp/build b/runphp/build
new file mode 100755
index 0000000..4e324d0
--- /dev/null
+++ b/runphp/build
@@ -0,0 +1,240 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+MYDIR="$(cd "$(dirname -- "$0")"; pwd)"
+RUNPHP="$MYDIR/runphp"
+"$RUNPHP" --bs --ue --ci || exit 1
+RUNPHP_STANDALONE=
+PROJDIR=; COMPOSERDIR=; COMPOSERPHAR=; VENDORDIR=; BUILDENV0=; BUILDENV=
+BUILD_IMAGES=(php-apache mariadb10); export BUILD_FLAVOUR=; DIST=; IMAGENAME=
+DISTFILES=()
+source "$RUNPHP" || exit 1
+source "$PROJDIR/$VENDORDIR/nulib/php/load.sh" || exit 1
+require: template
+
+BUILD_ARGS=(
+ DIST NDIST
+ REGISTRY
+ APT_PROXY
+ APT_MIRROR
+ SEC_MIRROR
+ TIMEZONE
+)
+
+function dklsnet() {
+ docker network ls --no-trunc --format '{{.Name}}' -f name="$1" 2>/dev/null
+}
+
+function dklsimg() {
+ local image="$1" version="$2"
+ docker image ls --no-trunc --format '{{.Repository}}:{{.Tag}}' "$image${version:+:$version}" 2>/dev/null
+}
+
+function dklsct() {
+ # afficher le container dont l'image correspondante est $1
+ docker ps --no-trunc --format '{{.Image}} {{.Names}}' | awk -v image="$1" '$1 == image { print $2 }'
+}
+
+function dkrunning() {
+ # vérifier si le container d'image $1 tourne
+ [ -n "$(dklsct "$@")" ]
+}
+
+function dclsct() {
+ # afficher les containers correspondant à $1(=docker-compose.yml)
+ docker compose ${1:+-f "$1"} ps -q
+}
+
+function dcrunning() {
+ # vérifier si les containers correspondant à $1(=docker-compose.yml) tournent
+ # si $2 est spécifié, c'est le nombre de service qui doit tourner
+ if [ -n "$2" ]; then
+ [ "$(dclsct "${@:1:1}" | wc -l)" -eq "$2" ]
+ else
+ [ -n "$(dclsct "${@:1:1}")" ]
+ fi
+}
+
+function build_check_env() {
+ eval "$(template_locals)"
+ local updatedenv distfile distname
+ local -a updatedfiles distfiles
+
+ if template_copy_missing "$PROJDIR/$BUILDENV0"; then
+ updated=1
+ updatedenv=1
+ fi
+ for distfile in "${DISTFILES[@]}"; do
+ if [ -f "$PROJDIR/$distfile" ]; then
+ if template_copy_missing "$PROJDIR/$distfile"; then
+ updated=1
+ setx distname=basename -- "$distfile"
+ distname="${distname#.}"; distname="${distname%.dist}"
+ setx distfile=dirname -- "$distfile"
+ distfile="$distfile/$distname"
+ updatedfiles+=("$distfile")
+ fi
+ elif [ -d "$PROJDIR/$distfile" ]; then
+ local distdir="$PROJDIR/$distfile"
+ setx -a distfiles=find "$distdir" -type f -name ".*.dist"
+ for distfile in "${distfiles[@]}"; do
+ if template_copy_missing "$distfile"; then
+ updated=1
+ setx distname=basename -- "$distfile"
+ distname="${distname#.}"; distname="${distname%.dist}"
+ # ignorer les fichiers binaires
+ #XXX remplacer par un code plus robuste, peut-être à
+ # intégrer directement dans template:
+ case "$distname" in
+ *.png|*.jpg) ;;
+ *)
+ setx distfile=dirname -- "$distfile"
+ distfile="$distfile/$distname"
+ updatedfiles+=("${distfile#$PROJDIR/}")
+ ;;
+ esac
+ fi
+ done
+ else
+ ewarn "$distfile: fichier introuvable"
+ fi
+ done
+ template_process_userfiles
+
+ if [ -n "$updated" ]; then
+ enote "IMPORTANT: vous devez paramétrer certains fichiers avant de pouvoir construire les images"
+ if [ -n "$updatedenv" ]; then
+ if [ $(id -u) -ne 0 ]; then
+ setx userent=getent passwd "$(id -un)"
+ setx userent=qval "$userent"
+ setx groupent=getent group "$(id -gn)"
+ setx groupent=qval "$groupent"
+ sed -i "
+/^#DEVUSER_.*=/s/^#//
+/^DEVUSER_USERENT=/s/=.*/=${userent//\//\\\/}/
+/^DEVUSER_GROUPENT=/s/=.*/=${groupent//\//\\\/}/
+" "$PROJDIR/$BUILDENV"
+ fi
+ einfo "\
+Veuillez vérifier le fichier $BUILDENV
+ ${EDITOR:-nano} $BUILDENV"
+ fi
+ [ ${#updatedfiles[*]} -gt 0 ] && einfo "\
+Le cas échéant, veuillez vérifier ce(s) fichier(s)
+ ${EDITOR:-nano} $(qvals "${updatedfiles[@]}")"
+ enote "ENSUITE, vous pourrez relancer la commande"
+ return 1
+ fi
+}
+
+function _build() {
+ local dockerfile image="${PRIVAREG:+$PRIVAREG/}${IMAGENAME%/*}/$1"
+ if [ -n "$ForceBuild" -o -z "$(dklsimg "$image")" ]; then
+ estep "Construction de $image"
+ dockerfiles=(
+ "$MYDIR/Dockerfile.$1.local"
+ "$MYDIR/Dockerfile.$1$BUILD_FLAVOUR"
+ "$PROJDIR/$VENDORDIR/nulib/php/dockerfiles/Dockerfile.$1$BUILD_FLAVOUR"
+ "$MYDIR/Dockerfile.$1"
+ "$PROJDIR/$VENDORDIR/nulib/php/dockerfiles/Dockerfile.$1"
+ )
+ for dockerfile in "${dockerfiles[@]}"; do
+ [ -f "$dockerfile" ] && break
+ done
+ args=(
+ -f "$dockerfile"
+ ${Pull:+--pull}
+ ${NoCache:+--no-cache}
+ ${PlainOutput:+--progress plain}
+ -t "$image"
+ )
+ for arg in "${BUILD_ARGS[@]}"; do
+ args+=(--build-arg "$arg=${!arg}")
+ done
+ for arg in "${!PROXY_VARS[@]}"; do
+ args+=(--build-arg "$arg=${PROXY_VARS[$arg]}")
+ done
+ for host in "${HOST_MAPPINGS[@]}"; do
+ args+=(--add-host "$host")
+ done
+ docker build "${args[@]}" "$PROJDIR" || die
+ if [ -n "$Push" ]; then
+ if [ -n "$PRIVAREG" ]; then
+ estep "Poussement de $image"
+ docker push "$image" || die
+ else
+ ewarn "PRIVAREG non défini: impossible de pousser l'image"
+ fi
+ fi
+ fi
+}
+function build_images() {
+ local image sourced
+
+ [ $# -gt 0 ] || set -- runphp "${BUILD_IMAGES[@]}"
+ for image in "$@"; do
+ case "$image" in
+ runphp)
+ [ ${#Configs[*]} -gt 0 ] && export RUNPHP_FORCE_BUILDENVS="${Configs[*]}"
+ local -a args=(--bs)
+ [ "$ForceBuild" != all ] && args+=(--ue)
+ [ -n "$Pull" ] && args+=(--pull)
+ [ -n "$NoCache" ] && args+=(--no-cache)
+ "$RUNPHP" "${args[@]}" || die
+ ;;
+ *)
+ if [ -z "$sourced" ]; then
+ [ ${#Configs[*]} -gt 0 ] || Configs=("$PROJDIR/$BUILDENV")
+ for config in "${Configs[@]}"; do
+ source "$config"
+ done
+ after_source_buildenv
+ read -a HOST_MAPPINGS <<<"${HOST_MAPPINGS//
+/ }"
+ sourced=1
+ fi
+ _build "$image"
+ ;;
+ esac
+ done
+}
+
+action=build
+Configs=()
+ForceBuild=
+Pull=
+NoCache=
+PlainOutput=
+Push=
+args=(
+ "Construire les images pour le projet"
+ #"usage"
+ --check-only action=none "++Ne faire que la vérification de l'environnement"
+ -c:,--config:BUILDENV Configs "Spécifier un fichier d'environnement pour le build"
+ -r,--rebuild ForceBuild=1 "Forcer la (re)construction des images"
+ -R,--rebuild-all ForceBuild=all "++Comme --rebuild, mais reconstruire aussi runphp"
+ -U,--pull Pull=1 "++Forcer le re-téléchargement des images dépendantes"
+ -j,--no-cache NoCache=1 "++Construire l'image en invalidant le cache"
+ -D,--plain-output PlainOutput=1 "++Afficher le détail du build"
+ -p,--push Push=1 "Pousser les images vers le registry après construction"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+if [ ${#Configs[*]} -gt 0 ]; then
+ aconfigs=()
+ for config in "${Configs[@]}"; do
+ setx config=abspath "$config"
+ aconfigs+=("$config")
+ done
+ Configs=("${aconfigs[@]}")
+ # pas de vérification d'environnement si on spécifie Configs
+ # ne pas oublier d'implémenter un traitement spécifique si build_check_env()
+ # contient d'autres vérifications
+else
+ build_check_env || die
+fi
+[ "$action" == none ] && exit 0
+
+case "$action" in
+build) build_images "$@";;
+*) die "$action: action non implémentée";;
+esac
diff --git a/runphp/dot-build.env.dist b/runphp/dot-build.env.dist
new file mode 100644
index 0000000..7baed50
--- /dev/null
+++ b/runphp/dot-build.env.dist
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+# Source des paquets et proxy
+APT_PROXY=
+APT_MIRROR=default
+SEC_MIRROR=default
+
+# Timezone du serveur
+TIMEZONE=Europe/Paris
+
+# registre docker privé d'après lequel sont nommées les images
+PRIVAREG=
+
+################################################################################
+# Ne pas toucher à partir d'ici
+
+REGISTRY=pubdocker.univ-reunion.fr/dist
+DIST=d12
+IMAGENAME=nulib/
+#DEVUSER_USERENT=user:x:1000:1000:User,,,:/home/user:/bin/bash
+#DEVUSER_GROUPENT=user:x:1000:
diff --git a/runphp/dot-dkbuild.env.dist b/runphp/dot-dkbuild.env.dist
new file mode 100644
index 0000000..b3319ea
--- /dev/null
+++ b/runphp/dot-dkbuild.env.dist
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+# Source des paquets et proxy
+setenv APT_PROXY=
+setenv APT_MIRROR=default
+setenv SEC_MIRROR=default
+
+# Timezone du serveur
+setenv TIMEZONE=Europe/Paris
+
+setenv REGISTRY=pubdocker.univ-reunion.fr/dist
+setenv PRIVAREG=
diff --git a/runphp/dot-runphp.conf b/runphp/dot-runphp.conf
new file mode 100644
index 0000000..0c2f16a
--- /dev/null
+++ b/runphp/dot-runphp.conf
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+# Chemin vers runphp, e.g sbin/runphp
+RUNPHP=
+
+# Si RUNPHP n'est pas défini, les variables suivantes peuvent être définies
+#DIST=d12
+#REGISTRY=pubdocker.univ-reunion.fr/dist
diff --git a/runphp/runphp b/runphp/runphp
new file mode 100755
index 0000000..1a03b5f
--- /dev/null
+++ b/runphp/runphp
@@ -0,0 +1,641 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+# Script permettant de lancer une commande dans docker et/ou de bootstrapper
+# l'utilisation de nulib dans un projet PHP
+# Les fichiers suivants doivent être copiés à un endroit quelconque du projet:
+# - runphp (ce script, à générer avec update-runphp.sh)
+# - Dockerfile.runphp
+# Les fichiers suivants peuvent être intégrés dans le projet comme exemples:
+# - dot-build.env.dist (à renommer en .build.env.dist)
+# - dot-dkbuild.env.dist (indiquer qu'il faut le copier en ~/.dkbuild.env)
+# Par défaut, ce script assume que runphp est copié dans le répertoire sbin/
+# du projet, et que le fichier composer.json et le répertoire vendor/ sont à la
+# racine du projet. Le cas échéant, modifier les valeurs ci-dessous
+(return 0 2>/dev/null) && _sourced=1 || _sourced=
+
+###############################################################################
+# Modifier les valeurs suivantes si nécessaire
+#SOF:runphp.userconf:ne pas modifier cette ligne
+
+# répertoire du projet. ce chemin doit être absolu. s'il est relatif, il est
+# exprimé par rapport au répertoire de ce script
+PROJDIR=
+
+# composer: répertoire du projet composer (celui qui contient le fichier
+# composer.json), chemin de composer.phar et répertoire vendor. ces chemins
+# doivent être relatifs à $PROJDIR
+COMPOSERDIR=
+COMPOSERPHAR=
+VENDORDIR=
+
+# fichier de configuration pour le build
+BUILDENV0=
+BUILDENV=
+
+# Listes des images que le script build construit automatiquement
+BUILD_IMAGES=(php-apache mariadb10)
+BUILD_FLAVOUR=
+
+## En ce qui concerne DIST et IMAGENAME, les valeurs dans BUILDENV prennent le
+## dessus. si BUILDENV *n'est pas* utilisé, ces valeurs peuvent être spécifiées
+## ici
+
+# version de debian à utiliser pour l'image
+# d12=php8.2, d11=php7.4, d10=php7.3
+DIST=
+
+# Nom de base de l'image (sans le registry), e.g prefix/
+IMAGENAME=
+
+## Fichiers .dist
+## Lors du build, les fichiers de la forme .name.dist sont copiés vers un
+## fichier name sauf s'il existe déjà
+
+# Liste de fichiers (ou de répertoirs à considérer). Pour chaque répertoire, les
+# fichiers .*.dist dans l'arborescence du répertoire sont recherchés
+DISTFILES=()
+
+#EOF:runphp.userconf:ne pas modifier cette ligne
+################################################################################
+
+# Ne pas modifier à partir d'ici
+
+if [ -n "$_sourced" ]; then
+ if [ "${0#-}" != "$0" ]; then
+ # sourcé depuis la ligne de commande
+ MYSELF="${BASH_SOURCE[1]}"
+ else
+ # sourcé depuis un script
+ MYSELF="${BASH_SOURCE[0]}"
+ fi
+ MYDIR="$(cd "$(dirname -- "$MYSELF")"; pwd)"
+ MYNAME="$(basename -- "$MYSELF")"
+else
+ MYDIR="$(cd "$(dirname -- "$0")"; pwd)"
+ MYNAME="$(basename -- "$0")"
+fi
+if [ -f "$MYDIR/runphp.userconf.local" ]; then
+ source "$MYDIR/runphp.userconf.local"
+fi
+
+DEFAULT_DIST=d12
+if [ -n "$RUNPHP_STANDALONE" ]; then
+ PROJDIR="$RUNPHP_PROJDIR"
+
+ COMPOSERDIR=.
+ COMPOSERPHAR=
+ VENDORDIR=vendor
+ BUILDENV0=
+ BUILDENV=
+ DIST="${RUNPHP_DIST:-$DEFAULT_DIST}"
+ IMAGENAME=nulib/
+
+ PRIVAREG=docker.io
+ REGISTRY="$RUNPHP_REGISTRY"
+
+ [ -n "$RUNPHP_BUILD_FLAVOUR" ] && BUILD_FLAVOUR="$RUNPHP_BUILD_FLAVOUR"
+
+else
+ [ -n "$PROJDIR" ] || PROJDIR="$(dirname -- "$MYDIR")"
+ [ "${PROJDIR#/}" != "$PROJDIR" ] || PROJDIR="$(cd "$MYDIR/$PROJDIR"; pwd)"
+
+ [ -n "$COMPOSERDIR" ] || COMPOSERDIR=.
+ [ -n "$COMPOSERPHAR" ] || COMPOSERPHAR=sbin/composer.phar
+ [ -n "$VENDORDIR" ] || VENDORDIR=vendor
+ [ -n "$BUILDENV0" ] || BUILDENV0=.build.env.dist
+ [ -n "$BUILDENV" ] || BUILDENV=build.env
+ [ -n "$DIST" ] || DIST="$DEFAULT_DIST"
+ [ -n "$IMAGENAME" ] || IMAGENAME=nulib/
+
+ [ "$COMPOSERPHAR" == none ] && COMPOSERPHAR=
+ [ "$BUILDENV0" == none ] && BUILDENV0=
+ [ "$BUILDENV" == none ] && BUILDENV=
+fi
+[ "$BUILD_FLAVOUR" == none ] && BUILD_FLAVOUR=
+
+function after_source_buildenv() {
+ NDIST="${DIST#d}"
+}
+after_source_buildenv
+
+[ -n "$_sourced" ] && return 0
+
+function eecho() { echo "$*" 1>&2; }
+function eerror() { eecho "ERROR: $*"; }
+function die() { [ $# -gt 0 ] && eerror "$*"; exit 1; }
+function is_defined() { [ -n "$(declare -p "$1" 2>/dev/null)" ]; }
+function in_path() { [ -n "$1" -a -x "$(which "$1" 2>/dev/null)" ]; }
+
+function composer() {
+ if [ -n "$PROJDIR" -a -n "$COMPOSERDIR" ]; then
+ cd "$PROJDIR/$COMPOSERDIR" || exit 1
+ fi
+ if [ -n "$PROJDIR" -a -n "$COMPOSERPHAR" -a -x "$PROJDIR/$COMPOSERPHAR" ]; then
+ "$PROJDIR/$COMPOSERPHAR" "$@"
+ elif in_path composer; then
+ command composer "$@"
+ elif [ -x /usr/bin/composer ]; then
+ /usr/bin/composer "$@"
+ elif [ -x /usr/local/bin/composer ]; then
+ /usr/local/bin/composer "$@"
+ else
+ die "impossible de trouver composer"
+ fi
+ if [ -n "$PROJDIR" -a -z "$RUNPHP_STANDALONE" -a -f composer.lock ]; then
+ cp composer.lock "$PROJDIR/.composer.lock.runphp"
+ fi
+}
+
+function host_parse_args() {
+ # analyser les arguments et calculer les opérations à lancer: bootstrap,
+ # composer install et/ou commande
+
+ # faut-il lancer bootstrap?
+ Bootstrap=auto
+ # faut-il installer les dépendances?
+ Composer=
+ # commande à lancer
+ Cmd=()
+
+ if [ "$1" == --runphp-bootstrap -o "$1" == --bs ]; then
+ Bootstrap=1
+ shift
+ elif [ "$1" == --runphp-exec ]; then
+ Bootstrap=
+ shift
+ elif [ "$1" == --runphp-install -o "$1" == --ci ]; then
+ Composer=ci
+ shift
+ elif [ "$1" == --runphp-update -o "$1" == --cu ]; then
+ Composer=cu
+ shift
+ fi
+ Cmd=("$@")
+}
+
+function host_ensure_image() {
+ # calculer le nom de l'image runphp
+ local dfdir suffix dockerfiles dockerfile
+ local privareg imagename
+ if [ -z "$Image" ]; then
+ [ -n "$RUNPHP_STANDALONE" ] && dfdir="$RUNPHP_STANDALONE/runphp" || dfdir="$MYDIR"
+ dockerfiles=(
+ "_local:$dfdir/Dockerfile.runphp.local"
+ "${BUILD_FLAVOUR//+/_}:$dfdir/Dockerfile.runphp$BUILD_FLAVOUR"
+ ":$dfdir/Dockerfile.runphp"
+ )
+ for dockerfile in "${dockerfiles[@]}"; do
+ suffix="${dockerfile%:*}"
+ dockerfile="${dockerfile##*:}"
+ [ -f "$dockerfile" ] && break
+ done
+ Dockerfile="$dockerfile"
+
+ [[ "$IMAGENAME" == */ ]] && imagename=runphp || imagename="${IMAGENAME%/*}/runphp"
+ privareg="$PRIVAREG"
+ if [ "$imagename" == runphp ]; then
+ [ -z "$privareg" -o "$privareg" == docker.io ] && privareg=docker.io/library
+ else
+ [ -z "$privareg" ] && privareg=docker.io
+ fi
+ Image="$privareg/$imagename$suffix:$DIST"
+ fi
+}
+
+function host_check_image() {
+ # vérifier que l'image runphp existe
+ local image="$Image"
+ for prefix in docker.io/library/ docker.io; do
+ if [ "${image#$prefix}" != "$image" ]; then
+ image="${image#$prefix}"
+ break
+ fi
+ done
+ [ -n "$(docker image ls --no-trunc --format '{{.Repository}}:{{.Tag}}' "$image" 2>/dev/null)" ]
+}
+
+function host_check_projdir() {
+ # vérifier le projet pour voir s'il faut installer les dépendances
+ [ -n "$RUNPHP_STANDALONE" ] && return
+
+ local install
+ if [ ! -f "$PROJDIR/$VENDORDIR/nulib/php/load.sh" ]; then
+ install=1
+ elif [ ! -f "$PROJDIR/.composer.lock.runphp" ]; then
+ install=1
+ elif ! diff -q "$PROJDIR/$COMPOSERDIR/composer.lock" "$PROJDIR/.composer.lock.runphp" >&/dev/null; then
+ install=1
+ fi
+ if [ -n "$install" ]; then
+ [ -n "$Composer" ] || Composer=ci
+ fi
+}
+
+function host_init_env() {
+ ## Charger ~/.dkbuild.env
+
+ APT_PROXY=
+ APT_MIRROR=
+ SEC_MIRROR=
+ TIMEZONE=
+ PRIVAREG=
+ REGISTRY=
+ PROFILE=
+ HOST_MAPPINGS=()
+ function default_profile() {
+ PROFILE="$1"
+ }
+ function profile() {
+ local profile
+ for profile in "$@"; do
+ [ "$profile" == "$PROFILE" ] && return 0
+ done
+ return 1
+ }
+ function setenv() {
+ eval "export $1"
+ }
+ function default() {
+ local command="$1"; shift
+ local nv n v
+ case "$command" in
+ docker)
+ for nv in "$@"; do
+ [[ "$nv" == *=* ]] || continue
+ n="${nv%%=*}"
+ v="${nv#*=}"
+ case "$n" in
+ host-mappings)
+ read -a ns <<<"$v"
+ for v in "${ns[@]}"; do
+ HOST_MAPPINGS+=("$v")
+ done
+ ;;
+ esac
+ done
+ ;;
+ esac
+ }
+ envs=()
+ if [ -z "$RUNPHP_IGNORE_DEFAULTS" ]; then
+ envs+=(
+ /etc/default/dkbuild
+ ~/etc/default.${HOSTNAME%%.*}/dkbuild
+ ~/etc/default/dkbuild
+ )
+ fi
+ envs+=(~/.dkbuild.env)
+ for env in "${envs[@]}"; do
+ [ -f "$env" ] && source "$env"
+ done
+ [ -n "$APT_PROXY" ] || APT_PROXY=
+ [ -n "$APT_MIRROR" ] || APT_MIRROR=default
+ [ -n "$SEC_MIRROR" ] || SEC_MIRROR=default
+ [ -n "$TIMEZONE" ] || TIMEZONE=Europe/Paris
+ [ -n "$PRIVAREG" ] || PRIVAREG=
+ [ -n "$REGISTRY" ] || REGISTRY=pubdocker.univ-reunion.fr/dist
+
+ ## Charger la configuration
+
+ # Recenser les valeur de proxy
+ declare -A PROXY_VARS
+ for var in {HTTPS,ALL,NO}_PROXY {http,https,all,no}_proxy; do
+ is_defined "$var" && PROXY_VARS[${var,,}]="${!var}"
+ done
+
+ # Paramètres de montage
+ if [ -n "$RUNPHP_NO_USE_RSLAVE" ]; then
+ UseRslave=
+ elif [ -n "$RUNPHP_USE_RSLAVE" ]; then
+ UseRslave=1
+ elif [ -e /proc/sys/fs/binfmt_misc/WSLInterop ]; then
+ # pas de mount propagation sous WSL
+ UseRslave=
+ else
+ UseRslave=1
+ fi
+
+ Image=
+ if [ -n "$RUNPHP_FORCE_BUILDENVS" ]; then
+ eval "Configs=($RUNPHP_FORCE_BUILDENVS)"
+ elif [ -n "$BUILDENV" -a -f "$PROJDIR/$BUILDENV" ]; then
+ Configs=("$PROJDIR/$BUILDENV")
+ elif [ -n "$BUILDENV0" -a -f "$PROJDIR/$BUILDENV0" ]; then
+ Configs=("$PROJDIR/$BUILDENV0")
+ else
+ Configs=()
+ fi
+ for config in "${Configs[@]}"; do
+ source "$config" || exit 1
+ done
+ after_source_buildenv
+
+ Chdir=
+ Verbose="$RUNPHP_VERBOSE"
+ Exec=
+}
+
+function host_docker_build() {
+ BUILD_ARGS=(
+ DIST NDIST
+ REGISTRY
+ APT_PROXY
+ APT_MIRROR
+ SEC_MIRROR
+ TIMEZONE
+ )
+
+ SOPTS=+d:9876543210:c:UjDx:z:r:p
+ LOPTS=help,dist:,d19,d18,d17,d16,d15,d14,d13,d12,d11,d10,config:,ue,unless-exists,pull,nc,no-cache,po,plain-output,apt-proxy:,timezone:,privareg:,push,ci,no-use-rslave
+ args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args"
+
+ Dist=
+ UnlessExists=
+ Pull=
+ NoCache=
+ PlainOutput=
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --) shift; break;;
+ --help)
+ eecho "\
+runphp: construire l'image docker
+
+USAGE
+ $MYNAME --bs [options...]
+
+OPTIONS
+ -c, --config build.env
+ -d, --dist DIST
+ --ue, --unless-exists
+ -U, --pull
+ -j, --nc, --no-cache
+ -D, --po, --plain-output
+ -x, --apt-proxy APT_PROXY
+ -z, --timezone TIMEZONE
+ -r, --privareg PRIVAREG
+ -p, --push
+ paramètres pour la consruction de l'image
+ --ci
+ --cu
+ lancer composer install (resp. update) s'il y a eu bootstrap
+ --no-use-rslave
+ paramètre montage des volumes"
+ exit 0
+ ;;
+ -c|--config) shift; Configs+=("$1");;
+ -d|--dist) shift; Dist="$1";;
+ -[0-9]) Dist="d1${1#-}";;
+ --d*) Dist="${1#--}";;
+ --ue|--unless-exists) UnlessExists=1;;
+ -U|--pull) Pull=1;;
+ -j|--nc|--no-cache) NoCache=1;;
+ -D|--po|--plain-output) PlainOutput=1;;
+ -x|--apt-proxy) shift; APT_PROXY="$1";;
+ -z|--timezone) shift; TIMEZONE="$1";;
+ -r|--privareg) shift; PRIVAREG="$1";;
+ -p|--push) Push=1;;
+ --ci) Composer=ci;;
+ --cu) Composer=cu;;
+ --no-use-rslave) UseRslave=;;
+ *) die "$1: option non configurée";;
+ esac
+ shift
+ done
+ Cmd=("$@")
+
+ for config in "${Configs[@]}"; do
+ if [ "$config" == none ]; then
+ Configs=()
+ break
+ fi
+ done
+ if [ ${#Configs[*]} -gt 0 ]; then
+ for config in "${Configs[@]}"; do
+ source "$config" || exit 1
+ done
+ after_source_buildenv
+ fi
+ [ -n "$Dist" ] && DIST="$Dist"
+
+ Image=
+ host_ensure_image
+ host_check_image && exists=1 || exists=
+
+ if [ -z "$UnlessExists" -o -z "$exists" ]; then
+ eecho "== bootstrapping $Image"
+ args=(
+ -f "$Dockerfile"
+ ${Pull:+--pull}
+ ${NoCache:+--no-cache}
+ ${BuildPlain:+--progress plain}
+ -t "$Image"
+ )
+ for arg in "${BUILD_ARGS[@]}"; do
+ args+=(--build-arg "$arg=${!arg}")
+ done
+ for arg in "${!PROXY_VARS[@]}"; do
+ args+=(--build-arg "$arg=${PROXY_VARS[$arg]}")
+ done
+ for host in "${HOST_MAPPINGS[@]}"; do
+ args+=(--add-host "$host")
+ done
+ mkdir -p /tmp/runphp-build
+ docker build "${args[@]}" /tmp/runphp-build || exit 1
+
+ if [ -n "$Push" -a -n "$PRIVAREG" ]; then
+ eecho "== Pushing $Image"
+ docker push "$Image" || exit 1
+ fi
+ else
+ # s'il n'y a pas eu bootstrap, alors ne pas lancer la commande composer
+ Composer=
+ fi
+}
+
+function host_docker_run() {
+ # lancer une commande avec docker
+ SOPTS=+w:
+ LOPTS=help,chdir:,no-use-rslave
+ args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args"
+
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --) shift; break;;
+ --help)
+ eecho "\
+runphp: lancer une commande dans un environnement PHP déterminé
+
+USAGE
+ $MYNAME --bs ...
+ $MYNAME ci|cu|composer
+ $MYNAME [options] command [args...]
+
+COMMANDES SPECIALES
+ --bs
+ faire un bootstrap de l'image runphp
+
+COMMANDES COMPOSER
+ ci
+ cu
+ installer/mettre à jour les dépendances du projet avec composer
+ composer [args...]
+ lancer composer avec les arguments spécifiés.
+
+pour les commandes ci-dessus, l'option --chdir est ignorée: le répertoire
+courant est forcé au répertoire du projet composer
+
+OPTIONS
+ -w, --chdir CHDIR
+ aller dans le répertoire spécifié avant de lancer la commande
+ --no-use-rslave
+ paramètre montage des volumes"
+ exit 0
+ ;;
+ -w|--chdir) shift; Chdir="$1";;
+ --no-use-rslave) UseRslave=;;
+ *) die "$1: option non configurée";;
+ esac
+ shift
+ done
+
+ args=(
+ run -it --rm
+ --name "runphp-$(basename -- "$1")-$$"
+ -e RUNPHP_MODE=docker
+ )
+ for arg in "${!PROXY_VARS[@]}"; do
+ args+=(-e "$arg=${PROXY_VARS[$arg]}")
+ done
+ if [ -n "$RUNPHP_STANDALONE" ]; then
+ args+=(
+ -e "RUNPHP_STANDALONE=$RUNPHP_STANDALONE"
+ -e "RUNPHP_PROJDIR=$PROJDIR"
+ )
+ fi
+ for host in "${HOST_MAPPINGS[@]}"; do
+ args+=(--add-host "$host")
+ done
+
+ # monter le répertoire qui contient $PROJDIR
+ mount_composer=
+ mount_runphp=1
+ if [ -z "$PROJDIR" -o "${PROJDIR#$HOME/}" != "$PROJDIR" -o "$PROJDIR" == "$HOME" ]; then
+ # bind mount $HOME
+ args+=(-v "$HOME:$HOME${UseRslave:+:rslave}")
+ [ -n "$RUNPHP_STANDALONE" ] &&
+ [ "${RUNPHP_STANDALONE#$HOME/}" != "$RUNPHP_STANDALONE" ] &&
+ mount_runphp=
+ elif [ -n "$PROJDIR" ]; then
+ # bind mount uniquement le répertoire du projet
+ args+=(-v "$PROJDIR:$PROJDIR${UseRslave:+:rslave}")
+ mount_composer=1
+ [ "$RUNPHP_STANDALONE" == "$PROJDIR" ] && mount_runphp=
+ fi
+ if [ -n "$mount_composer" -a -d "$HOME/.composer" ]; then
+ # monter la configuration de composer
+ args+=(-v "$HOME/.composer:$HOME/.composer")
+ fi
+ if [ -n "$RUNPHP_STANDALONE" -a -n "$mount_runphp" ]; then
+ args+=(-v "$RUNPHP_STANDALONE:$RUNPHP_STANDALONE")
+ fi
+ args+=(-w "$(pwd)")
+
+ # lancer avec l'utilisateur courant
+ if [ $(id -u) -ne 0 ]; then
+ # si c'est un utilisateur lambda, il faut monter les informations
+ # nécessaires. composer est déjà monté via $HOME
+ args+=(
+ -e DEVUSER_USERENT="$(getent passwd "$(id -un)")"
+ -e DEVUSER_GROUPENT="$(getent group "$(id -gn)")"
+ )
+ fi
+
+ args+=(
+ "$Image"
+ exec "$0" ${Chdir:+-w "$Chdir"}
+ )
+ [ -n "$Verbose" ] && eecho "\$ docker ${args[*]} $*"
+ ${Exec:+exec} docker "${args[@]}" "$@"
+}
+
+function container_exec() {
+ # lancer la commande $@. cette fonction doit être lancée dans le container
+ # docker
+ if [ -n "$DEVUSER_USERENT" ]; then
+ user="${DEVUSER_USERENT%%:*}"
+ export DEVUSER_USERENT=
+ export DEVUSER_GROUPENT=
+ if in_path su-exec; then
+ exec su-exec "$user" "$0" "$@"
+ else
+ exec runuser -u "$user" -- "$0" "$@"
+ fi
+ fi
+
+ SOPTS=+w:
+ LOPTS=chdir:
+ args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args"
+
+ chdir=
+ action=
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --) shift; break;;
+ -w|--chdir) shift; chdir="$1";;
+ *) die "$1: option non configurée";;
+ esac
+ shift
+ done
+
+ if [ $# -eq 0 ]; then
+ die "no command specified"
+ elif [ "$1" == ci ]; then
+ eecho "== installing composer dependencies"
+ shift
+ composer i "$@"
+ elif [ "$1" == cu ]; then
+ eecho "== upgrading composer dependencies"
+ shift
+ composer u "$@"
+ elif [ "$1" == composer ]; then
+ "$@"
+ else
+ if [ -n "$chdir" ]; then
+ cd "$chdir" || exit 1
+ fi
+ exec "$@"
+ fi
+}
+
+################################################################################
+
+if [ "$RUNPHP_MODE" != docker ]; then
+ # Lancement depuis l'extérieur du container
+ host_parse_args "$@"
+ host_init_env
+
+ Image=
+ if [ "$Bootstrap" == auto ]; then
+ host_ensure_image
+ host_check_image && Bootstrap= || Bootstrap=1
+ fi
+ if [ -n "$Bootstrap" ]; then
+ host_docker_build "${Cmd[@]}" || exit $?
+ else
+ host_ensure_image
+ fi
+
+ host_check_projdir
+ if [ -n "$Composer" ]; then
+ host_docker_run "$Composer" || exit $?
+ fi
+
+ if [ ${#Cmd[*]} -gt 0 ]; then
+ Exec=1 host_docker_run "${Cmd[@]}"
+ fi
+
+else
+ # Lancement depuis l'intérieur du container
+ container_exec "$@"
+fi
diff --git a/runphp/runphp.userconf.local b/runphp/runphp.userconf.local
new file mode 100644
index 0000000..a8aa3ed
--- /dev/null
+++ b/runphp/runphp.userconf.local
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+
+BUILD_FLAVOUR=+ic
diff --git a/runphp/template.sh b/runphp/template.sh
new file mode 100755
index 0000000..233c527
--- /dev/null
+++ b/runphp/template.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+# Modèle de script utilisant runphp pour lancer un traitement dans un container
+# RUNPHP est le chemin relatif vers runphp à partir du chemin du script
+RUNPHP=sbin/runphp
+
+MYDIR="$(dirname -- "$0")"; MYNAME="$(basename -- "$0")"
+if [ -z "$_RUNPHP_IN_DOCKER" ]; then
+ "$MYDIR/$RUNPHP" --bs --ue --ci || exit 1
+ exec "$MYDIR/$RUNPHP" "$0" "$@"
+fi
+source "$MYDIR/$RUNPHP" || exit 1
+source "$PROJDIR/$VENDORDIR/nulib/php/load.sh" || exit 1
+
+args=(
+ "description"
+ #"usage"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+echo "je tourne dans un container..."
+sleep 1000
diff --git a/runphp/update-runphp.sh b/runphp/update-runphp.sh
new file mode 100755
index 0000000..05403fe
--- /dev/null
+++ b/runphp/update-runphp.sh
@@ -0,0 +1,125 @@
+#!/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 parse_runphp() {
+ local runphp="$1" preamble="$2" conf="$3" postamble="$4"
+ local conf0
+ ac_set_tmpfile conf0
+ # extraire la configuration depuis le fichier
+ <"$runphp" awk -v preamble="$preamble" -v conf="$conf0" -v postamble="$postamble" '
+BEGIN { out = preamble }
+{
+ if ($0 ~ /SOF:runphp.userconf:/) {
+ if (out != "") print >out
+ out = conf
+ } else if ($0 ~ /EOF:runphp.userconf:/) {
+ out = postamble
+ if (out != "") print >out
+ } else {
+ if (out != "") print >out
+ }
+}
+'
+ # mettre en forme le fichier conf: pas de lignes vides avant et après
+ <"$conf0" >"$conf" awk '
+BEGIN { p = 0; have_pending = 0; pending = "" }
+$0 != "" { p = 1 }
+p == 1 {
+ if ($0 != "") {
+ if (have_pending) print pending
+ print
+ have_pending = 0
+ pending = ""
+ } else {
+ if (!have_pending) have_pending = 1
+ else pending = pending "\n"
+ }
+}
+'
+ ac_clean "$conf0"
+}
+
+projdir=
+args=(
+ "Mettre à jour le script runphp"
+ "[path/to/runphp]"
+ -d:,--projdir:PROJDIR . "Copier les fichiers pour un projet de l'université de la Réunion"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+if [ -n "$projdir" ]; then
+ setx projdir=abspath "$projdir"
+ set -- "$projdir/sbin/runphp"
+fi
+
+runphp="${1:-.}"
+if [ -d "$runphp" ]; then
+ runphp="$runphp/runphp"
+elif [ ! -e "$runphp" ]; then
+ [ "${runphp%/runphp}" != "$runphp" ] || runphp="$runphp/runphp"
+fi
+
+setx runphp=abspath "$runphp"
+setx rundir=dirname -- "$runphp"
+[ "$rundir" == "$MYDIR" ] && exit 0
+[ -d "$rundir" ] || mkdir -p "$rundir"
+
+# Tout d'abord, isoler chaque partie du fichier local
+ac_set_tmpfile preamble
+ac_set_tmpfile conf
+ac_set_tmpfile postamble
+parse_runphp "$MYDIR/runphp" "$preamble" "$conf" "$postamble"
+
+# Puis analyser le fichier destination
+if [ -f "$runphp" ]; then
+ ac_set_tmpfile userconf
+ parse_runphp "$runphp" "" "$userconf" ""
+ initial_config=
+
+elif [ -n "$projdir" ]; then
+ # forcer BUILDENV0=..env.dist et BUILDENV=.env pour les projets de
+ # l'université de la Réunion
+ ac_set_tmpfile userconf
+ sed <"$conf" >"$userconf" '
+/^BUILDENV0=/s/=.*/=..env.dist/
+/^BUILDENV=/s/=.*/=.env/
+'
+ initial_config=1
+
+else
+ userconf="$conf"
+ initial_config=1
+fi
+
+# (Re)construire le fichier destination
+(
+ cat "$preamble"
+ echo
+ cat "$userconf"
+ echo
+ cat "$postamble"
+) >"$runphp"
+[ -x "$runphp" ] || chmod +x "$runphp"
+
+rsync -lpt "$MYDIR/Dockerfile.runphp" "$rundir/"
+
+if [ -n "$projdir" ]; then
+ if testdiff "$rundir/build" "$MYDIR/build"; then
+ cp "$MYDIR/build" "$rundir/build"
+ chmod +x "$rundir/build"
+ fi
+ if [ ! -f "$projdir/..env.dist" ]; then
+ sed <"$MYDIR/dot-build.env.dist" >"$projdir/..env.dist" '
+/^IMAGENAME=/s/=.*\//='"$(basename -- "$projdir")"'\//
+'
+ initial_config=1
+ fi
+ if [ ! -f "$projdir/.runphp.conf" ]; then
+ sed <"$MYDIR/dot-runphp.conf" >"$projdir/.runphp.conf" '
+/^RUNPHP=/s/=.*/=sbin\/runphp/
+'
+ fi
+fi
+
+[ -n "$initial_config" ]
diff --git a/sbin/composer.phar b/sbin/composer.phar
new file mode 100755
index 0000000000000000000000000000000000000000..7a44c36752f9df9e5a22e853c9f4d2e7e79922eb
GIT binary patch
literal 2384623
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