From 23bf2ad042f18a33027680b3a67ed7846f01dc0a Mon Sep 17 00:00:00 2001
From: Jephte Clain
Date: Mon, 25 Nov 2024 14:10:13 +0400
Subject: [PATCH] =?UTF-8?q?impl=C3=A9mentation=20des=20outils=20de=20base?=
=?UTF-8?q?=20de=20nur-sery?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.composer.lock.runphp | 1798 +++++++++++++++++
.composer.yaml | 4 -
.idea/.gitignore | 8 -
.idea/codeception.xml | 15 +
.idea/nulib.iml | 2 +-
.idea/php.xml | 50 +-
.idea/phpspec.xml | 13 +
.idea/phpunit.xml | 10 +
.idea/vcs.xml | 1 -
.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 | 217 ++
bash/src/nulib.sh | 1 +
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/_runphp_build-all | 30 +
bin/nlman | 69 +
bin/nlshell | 38 +
bin/np | 61 +
bin/runphp | 89 +-
bin/templ.md | 38 +
bin/templ.sh | 56 +
bin/templ.sql | 40 +
bin/templ.yml | 38 +
bin_wip/donk | 25 +
bin_wip/npci | 30 +
bin_wip/npp | 22 +
bin_wip/npu | 147 ++
composer.json | 9 +-
composer.lock | 259 +--
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 | 16 +
lib/profile.d/nulib | 2 +
lib/setup.sh | 11 +
lib/uinst/conf | 6 +
lib/uinst/rootconf | 5 +
load.sh | 182 ++
php/src/A.php | 235 +++
php/{src_base => src}/AccessException.php | 11 +-
php/src/ExceptionShadow.php | 101 +
php/src/ExitError.php | 31 +
php/src/IArrayWrapper.php | 11 +
php/src/NoMoreDataException.php | 10 +
php/{src_base => src}/StateException.php | 4 +-
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_base => src}/cl.php | 424 +++-
php/{src_base => src}/cv.php | 13 +-
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 | 262 +++
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/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 | 279 +++
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 | 61 +
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/json.php | 67 +
php/src/ext/json/JsonException.php | 20 +
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 | 476 +++++
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 | 56 +
php/src/file/csv/AbstractBuilder.php | 173 ++
php/src/file/csv/AbstractReader.php | 129 ++
php/src/file/csv/CsvBuilder.php | 32 +
php/src/file/csv/CsvReader.php | 39 +
php/src/file/csv/IBuilder.php | 14 +
php/src/file/csv/IReader.php | 7 +
php/src/file/csv/TAbstractBuilder.php | 55 +
php/src/file/csv/TAbstractReader.php | 54 +
php/src/file/csv/csv_flavours.php | 59 +
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 | 265 +++
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_base/cstr.php => src/str.php} | 78 +-
php/src/text/Word.php | 212 ++
php/src/text/words.php | 14 +
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 | 67 +
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/src_base/ValueException.php | 48 -
php/tests/app/argsTest.php | 26 +
php/tests/appTest.php | 132 ++
php/tests/cstrTest.php | 56 -
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 | 28 +
php/tests/web/uploadsTest.php | 200 ++
runphp/Dockerfile.runphp | 30 +
runphp/Dockerfile.runphp+ic | 43 +
runphp/build | 193 ++
runphp/dot-build.env.dist | 21 +
runphp/dot-dkbuild.env.dist | 28 +
runphp/dot-runphp.conf | 8 +
runphp/runphp | 583 ++++++
runphp/runphp.1preamble | 18 +
runphp/runphp.2postamble | 534 +++++
runphp/runphp.userconf | 29 +
runphp/runphp.userconf.local | 3 +
runphp/template.sh | 22 +
runphp/update-runphp.sh | 94 +
sbin/composer.phar | Bin 2861074 -> 2384623 bytes
299 files changed, 28160 insertions(+), 398 deletions(-)
create mode 100644 .composer.lock.runphp
delete mode 100644 .idea/.gitignore
create mode 100644 .idea/codeception.xml
create mode 100644 .idea/phpspec.xml
create mode 100644 .idea/phpunit.xml
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/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/_runphp_build-all
create mode 100755 bin/nlman
create mode 100755 bin/nlshell
create mode 100755 bin/np
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 100755 bin_wip/donk
create mode 100755 bin_wip/npci
create mode 100755 bin_wip/npp
create mode 100755 bin_wip/npu
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/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 100644 php/src/A.php
rename php/{src_base => src}/AccessException.php (76%)
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
rename php/{src_base => src}/StateException.php (90%)
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
rename php/{src_base => src}/cl.php (53%)
rename php/{src_base => src}/cv.php (95%)
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/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/json.php
create mode 100644 php/src/ext/json/JsonException.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/AbstractBuilder.php
create mode 100644 php/src/file/csv/AbstractReader.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/IBuilder.php
create mode 100644 php/src/file/csv/IReader.php
create mode 100644 php/src/file/csv/TAbstractBuilder.php
create mode 100644 php/src/file/csv/TAbstractReader.php
create mode 100644 php/src/file/csv/csv_flavours.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
rename php/{src_base/cstr.php => src/str.php} (83%)
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
delete mode 100644 php/src_base/ValueException.php
create mode 100644 php/tests/app/argsTest.php
create mode 100644 php/tests/appTest.php
delete mode 100644 php/tests/cstrTest.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.1preamble
create mode 100644 runphp/runphp.2postamble
create mode 100644 runphp/runphp.userconf
create mode 100644 runphp/runphp.userconf.local
create mode 100755 runphp/template.sh
create mode 100755 runphp/update-runphp.sh
diff --git a/.composer.lock.runphp b/.composer.lock.runphp
new file mode 100644
index 0000000..bf3295e
--- /dev/null
+++ b/.composer.lock.runphp
@@ -0,0 +1,1798 @@
+{
+ "_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": "356c1dcfe9eee39e9e6eadff4f63cdfe",
+ "packages": [],
+ "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.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
+ "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
+ "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.0"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-06-12T14:39:25+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v5.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b",
+ "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b",
+ "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.3.1"
+ },
+ "time": "2024-10-08T18:51:32+00:00"
+ },
+ {
+ "name": "nulib/tests",
+ "version": "7.4",
+ "source": {
+ "type": "git",
+ "url": "https://git.univ-reunion.fr/sda-php/nulib-tests.git",
+ "reference": "6ce8257560b42e8fb3eea03eba84d3877c9648ca"
+ },
+ "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": "2024-03-26T10:56:17+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.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa",
+ "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa",
+ "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.0",
+ "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.21"
+ },
+ "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-09-19T10:50:18+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": {
+ "php": "^7.4"
+ },
+ "platform-dev": {
+ "ext-posix": "*",
+ "ext-pcntl": "*",
+ "ext-curl": "*"
+ },
+ "plugin-api-version": "2.2.0"
+}
diff --git a/.composer.yaml b/.composer.yaml
index 81afa48..ea509c7 100644
--- a/.composer.yaml
+++ b/.composer.yaml
@@ -1,8 +1,4 @@
# -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8
-composer_php_min: '7.3'
-composer_php_max: '8.0'
-composer_registry: pubdocker.univ-reunion.fr
-composer_image: image/phpbuilder:d10
require:
branch:
master:
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 13566b8..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/codeception.xml b/.idea/codeception.xml
new file mode 100644
index 0000000..9da3754
--- /dev/null
+++ b/.idea/codeception.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/nulib.iml b/.idea/nulib.iml
index cfcc99b..2f97961 100644
--- a/.idea/nulib.iml
+++ b/.idea/nulib.iml
@@ -2,8 +2,8 @@
-
+
diff --git a/.idea/php.xml b/.idea/php.xml
index 988f6b9..430b858 100644
--- a/.idea/php.xml
+++ b/.idea/php.xml
@@ -12,39 +12,39 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
+
diff --git a/.idea/phpspec.xml b/.idea/phpspec.xml
new file mode 100644
index 0000000..ec7e1d4
--- /dev/null
+++ b/.idea/phpspec.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
index 8306744..35eb1dd 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -2,6 +2,5 @@
-
\ No newline at end of file
diff --git a/.runphp.conf b/.runphp.conf
new file mode 100644
index 0000000..89af070
--- /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
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..d120126
--- /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 "$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..a879f28
--- /dev/null
+++ b/bash/src/git.sh
@@ -0,0 +1,217 @@
+# -*- 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_check_gitvcs ""
+function git_check_gitvcs() {
+ git rev-parse --show-toplevel >&/dev/null
+}
+
+function: git_ensure_gitvcs ""
+function git_ensure_gitvcs() {
+ git_check_gitvcs || edie "Ce 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)" ]
+}
+
+function: git_ensure_cleancheckout ""
+function git_ensure_cleancheckout() {
+ git_check_cleancheckout ||
+ edie "Vous avez des modifications locales. Enregistrez ces modifications avant de continuer" || 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/pretty.sh b/bash/src/pretty.sh
new file mode 100644
index 0000000..1d24a5b
--- /dev/null
+++ b/bash/src/pretty.sh
@@ -0,0 +1,194 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: pretty "Affichage en couleur"
+require: base
+
+################################################################################
+# Gestion des couleurs
+
+function __get_color() {
+ [ -z "$*" ] && set RESET
+ echo_ $'\e['
+ local sep
+ while [ -n "$1" ]; do
+ [ -n "$sep" ] && echo_ ";"
+ case "$1" in
+ z|RESET) echo_ "0";;
+ o|BLACK) echo_ "30";;
+ r|RED) echo_ "31";;
+ g|GREEN) echo_ "32";;
+ y|YELLOW) echo_ "33";;
+ b|BLUE) echo_ "34";;
+ m|MAGENTA) echo_ "35";;
+ c|CYAN) echo_ "36";;
+ w|WHITE) echo_ "37";;
+ DEFAULT) echo_ "39";;
+ O|BLACK_BG) echo_ "40";;
+ R|RED_BG) echo_ "41";;
+ G|GREEN_BG) echo_ "42";;
+ Y|YELLOW_BG) echo_ "43";;
+ B|BLUE_BG) echo_ "44";;
+ M|MAGENTA_BG) echo_ "45";;
+ C|CYAN_BG) echo_ "46";;
+ W|WHITE_BG) echo_ "47";;
+ DEFAULT_BG) echo_ "49";;
+ @|BOLD) echo_ "1";;
+ -|FAINT) echo_ "2";;
+ _|UNDERLINED) echo_ "4";;
+ ~|REVERSE) echo_ "7";;
+ n|NORMAL) echo_ "22";;
+ esac
+ sep=1
+ shift
+ done
+ echo_ "m"
+}
+function get_color() {
+ [ -n "$NO_COLORS" ] && return
+ __get_color "$@"
+}
+function __set_no_colors() {
+ if [ -z "$1" ]; then
+ if [ -n "$NULIB_NO_COLORS" ]; then NO_COLORS=1
+ elif out_isatty && err_isatty; then NO_COLORS=
+ else NO_COLORS=1
+ fi
+ else
+ is_yes "$1" && NO_COLORS=1 || NO_COLORS=
+ fi
+
+ COULEUR_ROUGE="$(get_color RED BOLD)"
+ COULEUR_VERTE="$(get_color GREEN BOLD)"
+ COULEUR_JAUNE="$(get_color YELLOW BOLD)"
+ COULEUR_BLEUE="$(get_color BLUE BOLD)"
+ COULEUR_BLANCHE="$(get_color WHITE BOLD)"
+ COULEUR_NORMALE="$(get_color RESET)"
+ if [ -n "$NO_COLORS" ]; then
+ nulib__load: _output_vanilla
+ else
+ nulib__load: _output_color
+ fi
+}
+__set_no_colors
+
+# 5=afficher les messages de debug; 4=afficher les message verbeux;
+# 3=afficher les message informatifs; 2=afficher les warnings et les notes;
+# 1=afficher les erreurs; 0=ne rien afficher
+export __verbosity
+[ -z "$__verbosity" ] && __verbosity=3
+function set_verbosity() {
+ [ -z "$__verbosity" ] && __verbosity=3
+ case "$1" in
+ -Q|--very-quiet) __verbosity=0;;
+ -q|--quiet) [ "$__verbosity" -gt 0 ] && let __verbosity=$__verbosity-1;;
+ -v|--verbose) [ "$__verbosity" -lt 5 ] && let __verbosity=$__verbosity+1;;
+ -c|--default) __verbosity=3;;
+ -D|--debug) __verbosity=5; DEBUG=1;;
+ *) return 1;;
+ esac
+ return 0
+}
+# 3=interaction maximale; 2=interaction par défaut
+# 1= interaction minimale; 0=pas d'interaction
+export __interaction
+[ -z "$__interaction" ] && __interaction=2
+function set_interaction() {
+ [ -z "$__interaction" ] && __interaction=2
+ case "$1" in
+ -b|--batch) __interaction=0;;
+ -y|--automatic) [ "$__interaction" -gt 0 ] && let __interaction=$__interaction-1;;
+ -i|--interactive) [ "$__interaction" -lt 3 ] && let __interaction=$__interaction+1;;
+ -c|--default) __interaction=2;;
+ *) return 1;;
+ esac
+ return 0
+}
+
+# Variable à inclure pour lancer automatiquement set_verbosity et
+# set_interaction en fonction des arguments de la ligne de commande. A utiliser
+# de cette manière:
+# parse_opts ... "${PRETTYOPTS[@]}" @ args -- ...
+PRETTYOPTS=(
+ -L:,--log-to: '$elogto $value_'
+ -Q,--very-quiet,-q,--quiet,-v,--verbose,-D,--debug '$set_verbosity $option_'
+ -b,--batch,-y,--automatic,-i,--interactive '$set_interaction $option_'
+)
+
+function show_error() { [ "$__verbosity" -ge 1 ]; }
+function show_warn() { [ "$__verbosity" -ge 2 ]; }
+function show_info() { [ "$__verbosity" -ge 3 ]; }
+function show_verbose() { [ "$__verbosity" -ge 4 ]; }
+function show_debug() { [ -n "$DEBUG" -o "$__verbosity" -ge 5 ]; }
+
+# Vérifier le niveau de verbosité actuel par rapport à l'argument. $1 peut valoir:
+# -Q retourner true si on peut afficher les messages d'erreur
+# -q retourner true si on peut afficher les messages d'avertissement
+# -c retourner true si on peut afficher les message normaux
+# -v retourner true si on peut afficher les messages verbeux
+# -D retourner true si on peut afficher les messages de debug
+function check_verbosity() {
+ case "$1" in
+ -Q|--very-quiet) [ "$__verbosity" -ge 1 ];;
+ -q|--quiet) [ "$__verbosity" -ge 2 ];;
+ -c|--default) [ "$__verbosity" -ge 3 ];;
+ -v|--verbose) [ "$__verbosity" -ge 4 ];;
+ -D|--debug) [ -n "$DEBUG" -o "$__verbosity" -ge 5 ];;
+ *) return 0;;
+ esac
+}
+# Retourner l'option correspondant au niveau de verbosité actuel
+function get_verbosity_option() {
+ case "$__verbosity" in
+ 0) echo --very-quiet;;
+ 1) echo --quiet --quiet;;
+ 2) echo --quiet;;
+ 4) echo --verbose;;
+ 5) echo --debug;;
+ esac
+}
+
+# Vérifier le niveau d'interaction autorisé par rapport à l'argument. Par
+# exemple, 'check_interaction -y' signifie "Il ne faut interagir avec
+# l'utilisateur qu'à partir du niveau d'interaction -y. Suis-je dans les
+# condition voulues pour autoriser l'interaction?"
+# $1 peut valoir:
+# -b retourner true
+# -y retourner true si on est au moins en interaction minimale
+# -c retourner true si on est au moins en interaction normale
+# -i retourner true si on est au moins en interaction maximale
+function check_interaction() {
+ case "$1" in
+ -b|--batch) return 0;;
+ -y|--automatic) [ -n "$__interaction" -a "$__interaction" -ge 1 ];;
+ -c|--default) [ -n "$__interaction" -a "$__interaction" -ge 2 ];;
+ -i|--interactive) [ -n "$__interaction" -a "$__interaction" -ge 3 ];;
+ *) return 0;;
+ esac
+}
+# Vérifier le niveau d'interaction dans lequel on se trouve actuellement. $1
+# peut valoir:
+# -b retourner true si l'une des options -b ou -yy a été spécifiée
+# -Y retourner true si l'une des options -b, -yy, ou -y a été spécifiée
+# -y retourner true si l'option -y a été spécifiée
+# -c retourner true si aucune option n'a été spécifiée
+# -i retourner true si l'option -i a été spécifiée
+# -C retourner true si aucune option ou l'option -i ont été spécifiés
+function is_interaction() {
+ case "$1" in
+ -b|--batch) [ -n "$__interaction" -a "$__interaction" -eq 0 ];;
+ -Y) [ -n "$__interaction" -a "$__interaction" -le 1 ];;
+ -y|--automatic) [ -n "$__interaction" -a "$__interaction" -eq 1 ];;
+ -c|--default) [ -n "$__interaction" -a "$__interaction" -eq 2 ];;
+ -i|--interactive) [ -n "$__interaction" -a "$__interaction" -eq 3 ];;
+ -C) [ -n "$__interaction" -a "$__interaction" -ge 2 ];;
+ *) return 1;;
+ esac
+}
+# Retourner l'option correspondant au niveau d'interaction actuel
+function get_interaction_option() {
+ case "$__interaction" in
+ 0) echo --batch;;
+ 1) echo --automatic;;
+ 3) echo --interactive;;
+ esac
+}
diff --git a/bash/src/sysinfos.sh b/bash/src/sysinfos.sh
new file mode 100644
index 0000000..341a528
--- /dev/null
+++ b/bash/src/sysinfos.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: sysinfos "Informations sur le système courant"
+require: base
diff --git a/bash/src/template.sh b/bash/src/template.sh
new file mode 100644
index 0000000..3201b6a
--- /dev/null
+++ b/bash/src/template.sh
@@ -0,0 +1,225 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+##@cooked nocomments
+module: template "Mise à jour de templates à partir de modèles"
+
+function: template_locals "\
+Afficher les variables qui doivent être locales
+
+Utiliser de cette façon:
+~~~
+eval \$(template_locals)
+~~~"
+function template_locals() {
+ echo "local -a userfiles; local updated"
+}
+
+function: template_copy_replace "\
+Copier \$1 vers \$2 de façon inconditionnelle
+
+Si \$2 n'est pas spécifié, on assume que \$1 est de la forme '.file.ext'
+et \$2 vaudra alors 'file'
+
+si un fichier \${2#.}.local existe, prendre ce fichier à la place comme source
+
+Ajouter file au tableau userfiles"
+function template_copy_replace() {
+ local src="$1" dest="$2"
+ local srcdir srcname lsrcname
+ setx srcdir=dirname "$src"
+ setx srcname=basename "$src"
+ if [ -z "$dest" ]; then
+ dest="${srcname#.}"
+ dest="${dest%.*}"
+ dest="$srcdir/$dest"
+ fi
+
+ lsrcname="${srcname#.}.local"
+ [ -e "$srcdir/$lsrcname" ] && src="$srcdir/$lsrcname"
+
+ userfiles+=("$dest")
+ cp -P "$src" "$dest"
+ return 0
+}
+
+function: template_copy_missing "\
+Copier \$1 vers \$2 si ce fichier n'existe pas déjà
+
+Si \$2 n'est pas spécifié, on assume que \$1 est de la forme '.file.ext'
+et \$2 vaudra alors 'file'
+
+si un fichier \${2#.}.local existe, prendre ce fichier à la place comme source
+
+Ajouter file au tableau userfiles"
+function template_copy_missing() {
+ local src="$1" dest="$2"
+ local srcdir srcname lsrcname
+ setx srcdir=dirname "$src"
+ setx srcname=basename "$src"
+ if [ -z "$dest" ]; then
+ dest="${srcname#.}"
+ dest="${dest%.*}"
+ dest="$srcdir/$dest"
+ fi
+
+ userfiles+=("$dest")
+ if [ ! -e "$dest" ]; then
+ lsrcname="${srcname#.}.local"
+ [ -e "$srcdir/$lsrcname" ] && src="$srcdir/$lsrcname"
+
+ cp -P "$src" "$dest"
+ return 0
+ fi
+ return 1
+}
+
+function: template_dump_vars "\
+Lister les variables mentionnées dans les fichiers \$@
+
+Seules sont prises en compte les variables dont le nom est de la forme
+[A-Z][A-Za-z_]*
+
+Cette fonction est utilisée par template_source_envs(). Elle utilise la
+fonction outil _template_dump_vars() qui peut être redéfinie si nécessaire."
+function template_dump_vars() {
+ _template_dump_vars "$@"
+}
+function _template_dump_vars() {
+ [ $# -gt 0 ] || return 0
+ cat "$@" |
+ grep -E '^[A-Z][A-Za-z_]*=' |
+ sed 's/=.*//' |
+ sort -u
+}
+
+function: template_source_envs "\
+Cette fonction doit être implémentée par l'utilisateur et doit:
+- initialiser le tableau template_vars qui donne la liste des variables scalaires
+- initialiser te tableau template_lists qui donne la liste des variables listes
+- charger ces variables depuis les fichiers \$@
+
+Cette fonction utilise la fonction outil _template_source_envs() qui peut être
+redéfinie si nécessaire."
+function template_source_envs() {
+ _template_source_envs "$@"
+}
+function _template_source_envs() {
+ local e_
+ for e_ in "$@"; do
+ [ -f "$e_" ] && source "$e_"
+ done
+ setx -a template_vars=template_dump_vars "$@"
+ template_lists=()
+}
+
+function: template_resolve_scripts "\
+Générer le script awk \$1 et le script sed \$2 qui remplacent dans les fichiers
+destination les marqueurs @@VAR@@ par la valeur de la variable \$VAR
+correspondante
+
+Les fichiers \$3..@ contiennent les valeurs des variables
+
+Les marqueurs supportés sont les suivants et sont évalués dans cet ordre:
+- XXXRANDOMXXX remplacer cette valeur par une chaine de 16 caractères au hasard
+- @@FOR:VARS@@ VARS étant une liste de valeurs séparées par des espaces:
+ dupliquer la ligne autant de fois qu'il y a de valeurs dans \$VARS
+ dans chaque ligne, remplacer les occurrences de @@VAR@@ par la valeur
+ de l'itération courante
+- #@@IF:VAR@@ afficher la ligne si VAR est non vide, supprimer la ligne sinon
+- #@@UL:VAR@@ afficher la ligne si VAR est vide, supprimer la ligne sinon
+- #@@if:VAR@@
+- #@@ul:VAR@@ variantes qui ne suppriment pas la ligne mais sont remplacées par #
+- @@VAR:-string@@ remplacer par 'string' si VAR a une valeur vide ou n'est pas défini, \$VAR sinon
+- @@VAR:+string@@ remplacer par 'string' si VAR est défini a une valeur non vide
+"
+function template_generate_scripts() {
+ local awkscript="$1"; shift
+ local sedscript="$1"; shift
+ (
+ template_source_envs "$@"
+
+ NL=$'\n'
+ # random, for
+ exec >"$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/_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/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/np b/bin/np
new file mode 100755
index 0000000..e567f2b
--- /dev/null
+++ b/bin/np
@@ -0,0 +1,61 @@
+#!/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 status "$@" 2>&1)"; r=$?
+ if [ -n "$status" ]; then
+ setx cwd=ppath2 "$(pwd)" "$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
+
+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
+ args=()
+ isatty || args+=(--porcelain)
+ git_status "${args[@]}"
+fi
diff --git a/bin/runphp b/bin/runphp
index a3aa6dc..a938ba3 100755
--- a/bin/runphp
+++ b/bin/runphp
@@ -1,52 +1,51 @@
#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
-MYDIR="$(dirname -- "$0")"; MYNAME="$(basename -- "$0")"
-function die() { echo 1>&2 "ERROR: $*"; exit 1; }
+source "$(dirname -- "$0")/../load.sh" || exit 1
-case "$MYNAME" in
-runphp) ;;
-composer)
- if [ -f "$MYDIR/composer.phar" ]; then
- set -- "$MYDIR/composer.phar" "$@"
- elif [ -f "$MYDIR/../sbin/composer.phar" ]; then
- set -- "$MYDIR/../sbin/composer.phar" "$@"
- elif [ -f "/usr/bin/composer" ]; then
- set -- "/usr/bin/composer" "$@"
- else
- set -- "" "$@"
+owd="$(pwd)"
+PROJDIR=
+while true; do
+ cwd="$(pwd)"
+ if [ -f .runphp.conf ]; then
+ PROJDIR="$cwd"
+ break
+ elif [ -f composer.json ]; then
+ PROJDIR="$cwd"
+ break
fi
- ;;
-*) die "$MYNAME: nom de script invalide";;
-esac
-
-function runphp_help() {
- echo "$MYNAME: lance un programme PHP en sélectionnant une version en particulier
-
-USAGE
- $MYNAME [options] [args...]
-
-OPTIONS
- -s, --min PHP_MIN
- -m, --max PHP_MAX
- -i, --image IMAGE"
-}
-
-SOPTS=+smi
-LOPTS=help,php-min,min,php-max,max,image
-args="$(getopt -n runphp -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args"
-
-while [ $# -gt 0 ]; do
- case "$1" in
- --) shift; break;;
- --help) runphp_help; exit 0;;
- *) die "$1: option non configurée";;
- esac
- shift
+ if [ "$cwd" == "$HOME" -o "$cwd" == / ]; then
+ cd "$owd"
+ break
+ fi
+ cd ..
done
-script="$1"; shift
-[ -n "$script" ] || die "vous devez spécifier le script à lancer"
-[ -f "$script" ] || die "$script: script introuvable"
+if [ -z "$PROJDIR" ]; then
+ # s'il n'y a pas de projet, --bs est l'action par défaut
+ [ $# -gt 0 ] || set -- --bs
+elif [ "$MYNAME" == composer ]; then
+ set -- composer "$@"
+else
+ case "$1" in
+ *.php|*.phar) set -- php "$@";;
+ esac
+fi
-scriptdir="$(dirname -- "$script")"
-scritname="$(basename -- "$script")"
+if [ -n "$PROJDIR" ]; then
+ export RUNPHP_STANDALONE=
+ RUNPHP=; DIST=; REGISTRY=
+ if [ -f "$PROJDIR/.runphp.conf" ]; then
+ source "$PROJDIR/.runphp.conf"
+ [ -n "$RUNPHP" ] && exec "$PROJDIR/$RUNPHP" "$@"
+ elif [ -f "$PROJDIR/sbin/runphp" ]; then
+ exec "$PROJDIR/sbin/runphp" "$@"
+ elif [ -f "$PROJDIR/runphp" ]; then
+ exec "$PROJDIR/runphp" "$@"
+ fi
+fi
+
+export RUNPHP_STANDALONE="$NULIBDIR"
+export RUNPHP_PROJDIR="$PROJDIR"
+export RUNPHP_REGISTRY="$REGISTRY"
+export RUNPHP_DIST="$DIST"
+exec "$MYDIR/../runphp/runphp" "$@"
diff --git a/bin/templ.md b/bin/templ.md
new file mode 100755
index 0000000..a7b4934
--- /dev/null
+++ b/bin/templ.md
@@ -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
+require: fndate
+
+: ${EDITOR:=vim}
+
+autoext=1
+args=(
+ "créer un nouveau fichier .markdown"
+ ""
+ -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/bin_wip/donk b/bin_wip/donk
new file mode 100755
index 0000000..b93ad81
--- /dev/null
+++ b/bin_wip/donk
@@ -0,0 +1,25 @@
+#!/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: donk.help
+
+# par défaut, c'est l'action build
+case "$1" in
+-h*|--help|--help=*|--help++) ;;
+-*) set -- build "$@";;
+esac
+
+args=(
+ "construire des images docker"
+ "ACTION [options]
+$(_donk_show_actions)"
+ +
+ -h::section,--help '$_donk_show_help' "Afficher l'aide de la section spécifiée.
+Les sections valides sont: ${DONK_HELP_SECTIONS[*]%%:*}"
+ --help++ '$_donk_show_help' "++Afficher l'aide"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+action="$1"; shift
+require: "donk.$action" || die
+"donk_$action" "$@"
diff --git a/bin_wip/npci b/bin_wip/npci
new file mode 100755
index 0000000..bc02cc3
--- /dev/null
+++ b/bin_wip/npci
@@ -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
+require: git
+
+projdir=
+remote=
+what=auto
+push=auto
+clobber=ask
+args=(
+ "\
+valider les modifications locales
+
+si la branche courante est une branche wip, écraser les modifications distantes éventuelles après un avertissement.
+sinon, ne mettre à jour la branche locale qu'en mode fast-forward"
+ "MESSAGE [FILES...]"
+ -d:,--projdir projdir= "spécifier le projet dans lequel faire la mise à jour"
+ -o:,--remote remote= "spécifier le remote depuis lequel faire le fetch et vers lequel pousser les modifications"
+ --auto what=auto "calculer les modifications à valider: soit les fichiers mentionnés, soit ceux de l'index, soit les fichiers modifiés. c'est l'option par défaut"
+ -a,--all what=all "valider les modifications sur les fichiers modifiés uniquement"
+ -A,--all-new what=new "valider les modifications sur les fichiers modifiés et rajouter aussi les nouveaux fichiers"
+ --current push=auto "pousser les modifications sur la branche courante après validation. c'est l'option par défaut"
+ -p,--push push=1 "pousser les modifications de toutes les branches après la validation"
+ -l,--no-push push= "ne pas pousser les modifications après la validation"
+ --clobber clobber=1 "écraser les modifications distantes si la branche courante est une branche wip"
+ -n,--no-clobber clobber= "ne jamais écraser les modifications distantes, même si la branche courante est une branche wip"
+)
+parse_args "$@"; set -- "${args[@]}"
+
diff --git a/bin_wip/npp b/bin_wip/npp
new file mode 100755
index 0000000..4023184
--- /dev/null
+++ b/bin_wip/npp
@@ -0,0 +1,22 @@
+#!/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
+
+projdir=
+remote=
+clobber=ask
+args=(
+ "\
+pousser les modifications locales
+
+si la branche courante est une branche wip, écraser les modifications distantes éventuelles après un avertissement.
+sinon, ne mettre à jour la branche locale qu'en mode fast-forward"
+ "MESSAGE [FILES...]"
+ -d:,--projdir projdir= "spécifier le projet dans lequel faire la mise à jour"
+ -o:,--remote remote= "spécifier le remote depuis lequel faire le fetch et vers lequel pousser les modifications"
+ --clobber clobber=1 "écraser les modifications distantes si la branche courante est une branche wip"
+ -n,--no-clobber clobber= "ne jamais écraser les modifications distantes, même si la branche courante est une branche wip"
+)
+parse_args "$@"; set -- "${args[@]}"
+
diff --git a/bin_wip/npu b/bin_wip/npu
new file mode 100755
index 0000000..7c227b6
--- /dev/null
+++ b/bin_wip/npu
@@ -0,0 +1,147 @@
+#!/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_update() {
+ local branch
+ local -a prbranches crbranches dbranches
+
+ setx -a prbranches=git_list_rbranches
+ git fetch -p "$@" || return
+ setx -a crbranches=git_list_rbranches
+
+ # vérifier s'il y a des branches distantes qui ont été supprimées
+ for branch in "${prbranches[@]}"; do
+ if ! array_contains crbranches "$branch"; then
+ array_add dbranches "${branch#*/}"
+ fi
+ done
+ if [ ${#dbranches[*]} -gt 0 ]; then
+ setx -a branches=git_list_branches
+ setx branch=git_get_branch
+
+ eimportant "One or more distant branches where deleted"
+ if git_check_cleancheckout; then
+ einfo "Delete the obsolete local branches with these commands:"
+ else
+ ewarn "Take care of uncommitted local changes first"
+ einfo "Then delete the obsolete local branches with these commands:"
+ fi
+ if array_contains dbranches "$branch"; then
+ # si la branche courante est l'une des branches à supprimer, il faut
+ # basculer vers develop ou master
+ local swto
+ if [ -z "$swto" ] && array_contains branches develop && ! array_contains dbranches develop; then
+ swto=develop
+ fi
+ if [ -z "$swto" ] && array_contains branches master && ! array_contains dbranches master; then
+ swto=master
+ fi
+ [ -n "$swto" ] && qvals git checkout "$swto"
+ fi
+ qvals git branch -D "${dbranches[@]}"
+ return 1
+ fi
+
+ # intégrer les modifications des branches locales
+ if ! git_check_cleancheckout; then
+ setx branch=git_get_branch
+ setx remote=git_get_branch_remote "$branch"
+ setx rbranch=git_get_branch_rbranch "$branch" "$remote"
+ pbranch="${rbranch#refs/remotes/}"
+ if git merge -q --ff-only "$rbranch"; then
+ enote "There are uncommitted local changes: only CURRENT branch were updated"
+ fi
+ return 0
+ fi
+
+ setx -a branches=git_list_branches
+ restore_branch=
+ for branch in "${branches[@]}"; do
+ setx remote=git_get_branch_remote "$branch"
+ setx rbranch=git_get_branch_rbranch "$branch" "$remote"
+ pbranch="${rbranch#refs/remotes/}"
+ [ -n "$remote" -a -n "$rbranch" ] || continue
+ if git_is_ancestor "$branch" "$rbranch"; then
+ if git_should_ff "$branch" "$rbranch"; then
+ einfo "Fast-forwarding $branch -> $pbranch"
+ git checkout -q "$branch"
+ git merge -q --ff-only "$rbranch"
+ restore_branch=1
+ fi
+ else
+ if [ "$branch" == "$orig_branch" ]; then
+ echo "* Cannot fast-forward CURRENT branch $branch from $pbranch
+Try to merge manually with: git merge $pbranch"
+ else
+ echo "* Cannot fast-forward local branch $branch from $pbranch
+You can merge manually with: git checkout $branch; git merge $pbranch"
+ fi
+ fi
+ done
+ [ -n "$restore_branch" ] && git checkout -q "$orig_branch"
+ return 0
+}
+
+function git_update() {
+ local cwd r
+ setx cwd=ppath2 "$(pwd)" "$OrigCwd"
+ etitle "$cwd"
+ _git_update "$@"; r=$?
+ eend
+ return $r
+}
+
+chdir=
+all=
+Remote=
+Autoff=1
+Reset=ask
+args=(
+ "\
+mettre à jour les branches locales
+
+si la branche courante est une branche wip, écraser les modifications locales éventuelles après un avertissement.
+sinon, ne mettre à jour la branche locale qu'en mode fast-forward"
+ #"usage"
+ -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"
+ -o:,--remote Remote= "spécifier le remote depuis lequel faire le fetch"
+ --autoff Autoff=1 "s'il n'y a pas de modifications locales, faire un fast-forward de toutes les branches traquées. c'est l'option par défaut."
+ -l,--no-autoff Autoff= "ne pas faire de fast-forward automatique des branches traquées. seule la branche courante est mise à jour"
+ --reset Reset=1 "écraser les modifications locales si la branche courante est une branche wip"
+ -n,--no-reset Reset= "ne jamais écraser les modifications locales, même si la branche courante est une branche wip"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+
+setx OrigCwd=pwd
+if [ -n "$chdir" ]; then
+ cd "$chdir" || die
+fi
+
+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_update || die
+ cd "$cwd"
+ done
+else
+ # répertoire courant uniquement
+ args=()
+ isatty || args+=(--porcelain)
+ git_update "${args[@]}"
+fi
diff --git a/composer.json b/composer.json
index a075909..530fed3 100644
--- a/composer.json
+++ b/composer.json
@@ -9,14 +9,17 @@
}
],
"require": {
- "php": ">=7.3"
+ "php": "^7.4"
},
"require-dev": {
- "nulib/tests": "7.3"
+ "nulib/tests": "7.4",
+ "ext-posix": "*",
+ "ext-pcntl": "*",
+ "ext-curl": "*"
},
"autoload": {
"psr-4": {
- "nulib\\": "php/src_base"
+ "nulib\\": "php/src"
}
},
"autoload-dev": {
diff --git a/composer.lock b/composer.lock
index ba7dfde..bf3295e 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "a83db90dff9c8a1e44abc608738042c3",
+ "content-hash": "356c1dcfe9eee39e9e6eadff4f63cdfe",
"packages": [],
"packages-dev": [
{
@@ -79,16 +79,16 @@
},
{
"name": "myclabs/deep-copy",
- "version": "1.11.1",
+ "version": "1.12.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
+ "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
- "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
+ "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
"shasum": ""
},
"require": {
@@ -96,11 +96,12 @@
},
"conflict": {
"doctrine/collections": "<1.6.8",
- "doctrine/common": "<2.13.3 || >=3,<3.2.2"
+ "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",
@@ -126,7 +127,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
- "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1"
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0"
},
"funding": [
{
@@ -134,29 +135,31 @@
"type": "tidelift"
}
],
- "time": "2023-03-08T13:26:56+00:00"
+ "time": "2024-06-12T14:39:25+00:00"
},
{
"name": "nikic/php-parser",
- "version": "v4.17.1",
+ "version": "v5.3.1",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d"
+ "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d",
- "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b",
+ "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b",
"shasum": ""
},
"require": {
+ "ext-ctype": "*",
+ "ext-json": "*",
"ext-tokenizer": "*",
- "php": ">=7.0"
+ "php": ">=7.4"
},
"require-dev": {
"ircmaxell/php-yacc": "^0.0.7",
- "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
+ "phpunit/phpunit": "^9.0"
},
"bin": [
"bin/php-parse"
@@ -164,7 +167,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.9-dev"
+ "dev-master": "5.0-dev"
}
},
"autoload": {
@@ -188,17 +191,17 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1"
},
- "time": "2023-08-13T19:53:39+00:00"
+ "time": "2024-10-08T18:51:32+00:00"
},
{
"name": "nulib/tests",
- "version": "7.3",
+ "version": "7.4",
"source": {
"type": "git",
"url": "https://git.univ-reunion.fr/sda-php/nulib-tests.git",
- "reference": "8902035bef6ddfe9864675a00844dd14872f6d13"
+ "reference": "6ce8257560b42e8fb3eea03eba84d3877c9648ca"
},
"require": {
"php": ">=7.3",
@@ -207,12 +210,12 @@
"type": "library",
"autoload": {
"psr-4": {
- "mur\\tests\\": "src"
+ "nulib\\tests\\": "src"
}
},
"autoload-dev": {
"psr-4": {
- "mur\\tests\\": "tests"
+ "nulib\\tests\\": "tests"
}
},
"authors": [
@@ -222,24 +225,25 @@
}
],
"description": "fonctions et classes pour les tests",
- "time": "2023-10-01T11:57:55+00:00"
+ "time": "2024-03-26T10:56:17+00:00"
},
{
"name": "phar-io/manifest",
- "version": "2.0.3",
+ "version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/phar-io/manifest.git",
- "reference": "97803eca37d319dfa7826cc2437fc020857acb53"
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53",
- "reference": "97803eca37d319dfa7826cc2437fc020857acb53",
+ "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",
@@ -280,9 +284,15 @@
"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.3"
+ "source": "https://github.com/phar-io/manifest/tree/2.0.4"
},
- "time": "2021-07-20T11:28:43+00:00"
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+00:00"
},
{
"name": "phar-io/version",
@@ -337,35 +347,35 @@
},
{
"name": "phpunit/php-code-coverage",
- "version": "9.2.29",
+ "version": "9.2.32",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76"
+ "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76",
- "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76",
+ "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.15",
+ "nikic/php-parser": "^4.19.1 || ^5.1.0",
"php": ">=7.3",
- "phpunit/php-file-iterator": "^3.0.3",
- "phpunit/php-text-template": "^2.0.2",
- "sebastian/code-unit-reverse-lookup": "^2.0.2",
- "sebastian/complexity": "^2.0",
- "sebastian/environment": "^5.1.2",
- "sebastian/lines-of-code": "^1.0.3",
- "sebastian/version": "^3.0.1",
- "theseer/tokenizer": "^1.2.0"
+ "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.3"
+ "phpunit/phpunit": "^9.6"
},
"suggest": {
"ext-pcov": "PHP extension that provides line coverage",
@@ -374,7 +384,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "9.2-dev"
+ "dev-main": "9.2.x-dev"
}
},
"autoload": {
@@ -403,7 +413,7 @@
"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.29"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32"
},
"funding": [
{
@@ -411,7 +421,7 @@
"type": "github"
}
],
- "time": "2023-09-19T04:57:46+00:00"
+ "time": "2024-08-22T04:23:01+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -656,45 +666,45 @@
},
{
"name": "phpunit/phpunit",
- "version": "9.6.13",
+ "version": "9.6.21",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be"
+ "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3d767f7f9e191eab4189abe41ab37797e30b1be",
- "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa",
+ "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa",
"shasum": ""
},
"require": {
- "doctrine/instantiator": "^1.3.1 || ^2",
+ "doctrine/instantiator": "^1.5.0 || ^2",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
- "myclabs/deep-copy": "^1.10.1",
- "phar-io/manifest": "^2.0.3",
- "phar-io/version": "^3.0.2",
+ "myclabs/deep-copy": "^1.12.0",
+ "phar-io/manifest": "^2.0.4",
+ "phar-io/version": "^3.2.1",
"php": ">=7.3",
- "phpunit/php-code-coverage": "^9.2.28",
- "phpunit/php-file-iterator": "^3.0.5",
+ "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.3",
- "phpunit/php-timer": "^5.0.2",
- "sebastian/cli-parser": "^1.0.1",
- "sebastian/code-unit": "^1.0.6",
+ "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.3",
- "sebastian/environment": "^5.1.3",
- "sebastian/exporter": "^4.0.5",
- "sebastian/global-state": "^5.0.1",
- "sebastian/object-enumerator": "^4.0.3",
- "sebastian/resource-operations": "^3.0.3",
- "sebastian/type": "^3.2",
+ "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": {
@@ -739,7 +749,7 @@
"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.13"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21"
},
"funding": [
{
@@ -755,20 +765,20 @@
"type": "tidelift"
}
],
- "time": "2023-09-19T05:39:22+00:00"
+ "time": "2024-09-19T10:50:18+00:00"
},
{
"name": "sebastian/cli-parser",
- "version": "1.0.1",
+ "version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/cli-parser.git",
- "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2"
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2",
- "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
"shasum": ""
},
"require": {
@@ -803,7 +813,7 @@
"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.1"
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2"
},
"funding": [
{
@@ -811,7 +821,7 @@
"type": "github"
}
],
- "time": "2020-09-28T06:08:49+00:00"
+ "time": "2024-03-02T06:27:43+00:00"
},
{
"name": "sebastian/code-unit",
@@ -1000,20 +1010,20 @@
},
{
"name": "sebastian/complexity",
- "version": "2.0.2",
+ "version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/complexity.git",
- "reference": "739b35e53379900cc9ac327b2147867b8b6efd88"
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88",
- "reference": "739b35e53379900cc9ac327b2147867b8b6efd88",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a",
"shasum": ""
},
"require": {
- "nikic/php-parser": "^4.7",
+ "nikic/php-parser": "^4.18 || ^5.0",
"php": ">=7.3"
},
"require-dev": {
@@ -1045,7 +1055,7 @@
"homepage": "https://github.com/sebastianbergmann/complexity",
"support": {
"issues": "https://github.com/sebastianbergmann/complexity/issues",
- "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2"
+ "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3"
},
"funding": [
{
@@ -1053,20 +1063,20 @@
"type": "github"
}
],
- "time": "2020-10-26T15:52:27+00:00"
+ "time": "2023-12-22T06:19:30+00:00"
},
{
"name": "sebastian/diff",
- "version": "4.0.5",
+ "version": "4.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
- "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131"
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131",
- "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc",
"shasum": ""
},
"require": {
@@ -1111,7 +1121,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
- "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5"
+ "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6"
},
"funding": [
{
@@ -1119,7 +1129,7 @@
"type": "github"
}
],
- "time": "2023-05-07T05:35:17+00:00"
+ "time": "2024-03-02T06:30:58+00:00"
},
{
"name": "sebastian/environment",
@@ -1186,16 +1196,16 @@
},
{
"name": "sebastian/exporter",
- "version": "4.0.5",
+ "version": "4.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d"
+ "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
- "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
+ "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
"shasum": ""
},
"require": {
@@ -1251,7 +1261,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
- "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5"
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6"
},
"funding": [
{
@@ -1259,20 +1269,20 @@
"type": "github"
}
],
- "time": "2022-09-14T06:03:37+00:00"
+ "time": "2024-03-02T06:33:00+00:00"
},
{
"name": "sebastian/global-state",
- "version": "5.0.6",
+ "version": "5.0.7",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
- "reference": "bde739e7565280bda77be70044ac1047bc007e34"
+ "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34",
- "reference": "bde739e7565280bda77be70044ac1047bc007e34",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
+ "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
"shasum": ""
},
"require": {
@@ -1315,7 +1325,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/global-state/issues",
- "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6"
+ "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7"
},
"funding": [
{
@@ -1323,24 +1333,24 @@
"type": "github"
}
],
- "time": "2023-08-02T09:26:13+00:00"
+ "time": "2024-03-02T06:35:11+00:00"
},
{
"name": "sebastian/lines-of-code",
- "version": "1.0.3",
+ "version": "1.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/lines-of-code.git",
- "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc"
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc",
- "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5",
"shasum": ""
},
"require": {
- "nikic/php-parser": "^4.6",
+ "nikic/php-parser": "^4.18 || ^5.0",
"php": ">=7.3"
},
"require-dev": {
@@ -1372,7 +1382,7 @@
"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.3"
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4"
},
"funding": [
{
@@ -1380,7 +1390,7 @@
"type": "github"
}
],
- "time": "2020-11-28T06:42:11+00:00"
+ "time": "2023-12-22T06:20:34+00:00"
},
{
"name": "sebastian/object-enumerator",
@@ -1559,16 +1569,16 @@
},
{
"name": "sebastian/resource-operations",
- "version": "3.0.3",
+ "version": "3.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/resource-operations.git",
- "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8"
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
- "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
"shasum": ""
},
"require": {
@@ -1580,7 +1590,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-main": "3.0-dev"
}
},
"autoload": {
@@ -1601,8 +1611,7 @@
"description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations",
"support": {
- "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
- "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3"
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4"
},
"funding": [
{
@@ -1610,7 +1619,7 @@
"type": "github"
}
],
- "time": "2020-09-28T06:45:17+00:00"
+ "time": "2024-03-14T16:00:52+00:00"
},
{
"name": "sebastian/type",
@@ -1723,16 +1732,16 @@
},
{
"name": "theseer/tokenizer",
- "version": "1.2.1",
+ "version": "1.2.3",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
- "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e"
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e",
- "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
"shasum": ""
},
"require": {
@@ -1761,7 +1770,7 @@
"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.1"
+ "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
},
"funding": [
{
@@ -1769,7 +1778,7 @@
"type": "github"
}
],
- "time": "2021-07-28T10:34:58+00:00"
+ "time": "2024-03-03T12:36:25+00:00"
}
],
"aliases": [],
@@ -1778,8 +1787,12 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
- "php": ">=7.3"
+ "php": "^7.4"
},
- "platform-dev": [],
- "plugin-api-version": "2.6.0"
+ "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..96df10b
--- /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
+
+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..609ee69
--- /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
+
+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..f26efd5
--- /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
+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..3d5adf4
--- /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
+
+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..9c3cd80
--- /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
+
+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..ef17f83
--- /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
+
+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..b380090
--- /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
+
+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..f9b4db9
--- /dev/null
+++ b/dockerfiles/Dockerfile.postgres15
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+ARG REGISTRY=pubdocker.univ-reunion.fr
+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/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/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_base/AccessException.php b/php/src/AccessException.php
similarity index 76%
rename from php/src_base/AccessException.php
rename to php/src/AccessException.php
index 0fe1c6d..6996667 100644
--- a/php/src_base/AccessException.php
+++ b/php/src/AccessException.php
@@ -1,13 +1,18 @@
$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..99ebabf
--- /dev/null
+++ b/php/src/app/cli/include-launcher.php
@@ -0,0 +1,29 @@
+ $name,
+ ]);
+ require $app;
+}
diff --git a/php/src_base/cl.php b/php/src/cl.php
similarity index 53%
rename from php/src_base/cl.php
rename to php/src/cl.php
index 960abde..8bb3b37 100644
--- a/php/src_base/cl.php
+++ b/php/src/cl.php
@@ -2,50 +2,125 @@
namespace nulib;
use ArrayAccess;
+use nulib\php\nur_func;
use Traversable;
/**
- * Class cl: gestion de tableau de valeurs scalaires
+ * Class cl: gestion de tableaux ou d'instances de {@link ArrayAccess} le cas
+ * échéant
+ *
+ * contrairement à {@link A}, les méthodes de cette classes sont plutôt conçues
+ * pour retourner un nouveau tableau
*/
class cl {
+ /**
+ * retourner un array avec les éléments retournés par l'itérateur. les clés
+ * numériques sont réordonnées, les clés chaine sont laissées en l'état
+ */
+ static final function all(?iterable $iterable): array {
+ if ($iterable === null) return [];
+ if (is_array($iterable)) return $iterable;
+ $array = [];
+ foreach ($iterable as $key => $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 iterator_to_array($array);
+ 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 iterator_to_array($array);
+ elseif ($array instanceof Traversable) return self::all($array);
else return [$array];
}
- /**
- * s'assurer que $array est un array non null. retourner true si $array n'a
- * pas été modifié (s'il était déjà un array), false sinon.
- */
- static final function ensure_array(&$array): bool {
- if (is_array($array)) return true;
- elseif ($array === null || $array === false) $array = [];
- elseif ($array instanceof Traversable) $array = iterator_to_array($array);
- else $array = [$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;
}
/**
- * 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).
+ * 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 ensure_narray(&$array): bool {
- if ($array === null || is_array($array)) return true;
- elseif ($array === false) $array = [];
- elseif ($array instanceof Traversable) $array = iterator_to_array($array);
- else $array = [$array];
- return false;
+ 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;
}
/**
@@ -76,6 +151,128 @@ class cl {
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
@@ -119,29 +316,66 @@ class cl {
/**
* 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) {
- self::ensure_narray($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 $keys est vide ou null, retourner true
+ * si $pkey est vide ou null, retourner true
*/
static final function phas($array, $pkey): bool {
- if ($pkey !== null && !is_array($pkey)) {
+ # 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));
}
- if ($pkey === null || $pkey === []) return true;
+ # phas
$first = true;
foreach($pkey as $key) {
if ($key === "" && $first) {
@@ -174,13 +408,18 @@ class cl {
/**
* obtenir la valeur correspondant au chemin $keys dans $array
*
- * si $keys est vide ou null, retourner $default
+ * si $pkey est vide ou null, retourner $default
*/
static final function pget($array, $pkey, $default=null) {
- if ($pkey !== null && !is_array($pkey)) {
+ # 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));
}
- if ($pkey === null || $pkey === []) return true;
+ # pget
$value = $array;
$first = true;
foreach($pkey as $key) {
@@ -211,25 +450,78 @@ class cl {
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
+ * - 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 $keys est vide ou null, $array est remplacé par $value
+ * si $pkey est vide ou null, $array est remplacé par $value
*/
static final function pset(&$array, $pkey, $value): void {
- if ($pkey !== null && !is_array($pkey)) {
- $pkey = explode(".", strval($pkey));
- }
+ # 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));
}
- self::ensure_array($array);
+ # pset
+ A::ensure_array($array);
$current =& $array;
$key = null;
$last = count($pkey) - 1;
@@ -245,7 +537,7 @@ class cl {
$current = [$current];
}
} else {
- self::ensure_array($current[$key]);
+ A::ensure_array($current[$key]);
$current =& $current[$key];
}
$i++;
@@ -262,28 +554,27 @@ class cl {
}
}
- /**
- * supprimer la valeur au chemin $keys fourni sous forme de tableau
- */
- static final function pdel_a(&$array, ?array $pkey): void {
- }
-
/**
* supprimer la valeur au chemin de clé $keys dans $array
*
* si $array vaut null ou false, sa valeur est inchangée.
- * si $keys est vide ou null, $array devient null
+ * si $pkey est vide ou null, $array devient null
*/
static final function pdel(&$array, $pkey): void {
- if ($array === false || $array === null) return;
- if ($pkey !== null && !is_array($pkey)) {
- $pkey = explode(".", strval($pkey));
- }
- if ($pkey === null || $pkey === []) {
+ # 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));
}
- self::ensure_array($array);
+ # pdel
+ A::ensure_array($array);
$current =& $array;
$key = null;
$last = count($pkey) - 1;
@@ -322,9 +613,12 @@ class cl {
/**
* 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): ?array {
+ 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];
@@ -333,6 +627,19 @@ class cl {
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 */
@@ -420,15 +727,12 @@ class cl {
#############################################################################
static final function sorted(?array $array, int $flags=SORT_REGULAR, bool $assoc=false): ?array {
- if ($array === null) return null;
- if ($assoc) asort($array, $flags);
- else sort($array, $flags);
+ A::sort($array, $flags, $assoc);
return $array;
}
static final function ksorted(?array $array, int $flags=SORT_REGULAR): ?array {
- if ($array === null) return null;
- ksort($array, $flags);
+ A::ksort($array, $flags);
return $array;
}
@@ -445,10 +749,10 @@ class cl {
static final function compare(array $keys): callable {
return function ($a, $b) use ($keys) {
foreach ($keys as $key) {
- if (cstr::del_prefix($key, "+")) $w = 1;
- elseif (cstr::del_prefix($key, "-")) $w = -1;
- elseif (cstr::del_suffix($key, "|asc")) $w = 1;
- elseif (cstr::del_suffix($key, "|desc")) $w = -1;
+ 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;
@@ -459,9 +763,7 @@ class cl {
}
static final function usorted(?array $array, array $keys, bool $assoc=false): ?array {
- if ($array === null) return null;
- if ($assoc) uasort($array, self::compare($keys));
- else usort($array, self::compare($keys));
+ A::usort($array, $keys, $assoc);
return $array;
}
}
diff --git a/php/src_base/cv.php b/php/src/cv.php
similarity index 95%
rename from php/src_base/cv.php
rename to php/src/cv.php
index 42b5eb5..8fdcc4b 100644
--- a/php/src_base/cv.php
+++ b/php/src/cv.php
@@ -79,6 +79,15 @@ class cv {
#############################################################################
+ /** échanger les deux valeurs */
+ static final function swap(&$a, &$b): void {
+ $tmp = $a;
+ $a = $b;
+ $b = $tmp;
+ }
+
+ #############################################################################
+
/** mettre à jour $dest avec $value si $cond($value) est vrai */
static final function set_if(&$dest, $value, callable $cond) {
if ($cond($value)) $dest = $value;
@@ -181,7 +190,7 @@ class cv {
$index = is_int($value)? $value : null;
$key = is_string($value)? $value : null;
if ($index === null && $key === null && $throw_exception) {
- throw ValueException::invalid($value, "key", $prefix);
+ throw ValueException::invalid_kind($value, "key", $prefix);
} else {
return [$index, $key];
}
@@ -198,7 +207,7 @@ class cv {
$string = is_string($value)? $value : null;
$array = is_array($value)? $value : null;
if ($bool === null && $string === null && $array === null && $throw_exception) {
- throw ValueException::invalid($value, "value", $prefix);
+ throw ValueException::invalid_kind($value, "value", $prefix);
} else {
return [$bool, $string, $array];
}
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 "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..9545077
--- /dev/null
+++ b/php/src/db/sqlite/_query_base.php
@@ -0,0 +1,61 @@
+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/json/JsonException.php b/php/src/ext/json/JsonException.php
new file mode 100644
index 0000000..a534188
--- /dev/null
+++ b/php/src/ext/json/JsonException.php
@@ -0,0 +1,20 @@
+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();
+ }
+ }
+
+ function unserialize(?array $options=null, bool $close=true, bool $alreadyLocked=false) {
+ $args = [$this->getContents($close, $alreadyLocked)];
+ 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..1c599e5
--- /dev/null
+++ b/php/src/file/_IFile.php
@@ -0,0 +1,56 @@
+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;
+ $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 ?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): 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 &$value) {
+ # formatter les dates
+ if ($value instanceof DateTime) {
+ $value = $value->format();
+ } elseif ($value instanceof DateTimeInterface) {
+ $value = DateTime::with($value)->format();
+ }
+ }; unset($value);
+ }
+ return $row;
+ }
+
+ function write(?array $row): void {
+ $row = $this->cookRow($row);
+ if ($row === null) return;
+ $this->writeHeaders(array_keys($row));
+ $this->_write($row);
+ }
+
+ function writeAll(?iterable $rows=null): void {
+ $unsetRows = false;
+ if ($rows === null) {
+ $rows = $this->rows;
+ $unsetRows = true;
+ }
+ if ($rows !== null) {
+ foreach ($rows as $row) {
+ $this->write(cl::with($row));
+ }
+ }
+ 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/csv/AbstractReader.php b/php/src/file/csv/AbstractReader.php
new file mode 100644
index 0000000..cf6221a
--- /dev/null
+++ b/php/src/file/csv/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/csv/CsvBuilder.php b/php/src/file/csv/CsvBuilder.php
new file mode 100644
index 0000000..c8f6947
--- /dev/null
+++ b/php/src/file/csv/CsvBuilder.php
@@ -0,0 +1,32 @@
+csvFlavour = csv_flavours::verifix($csvFlavour);
+ parent::__construct($output, $params);
+ }
+
+ protected function _write(array $row): 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..d05b2e0
--- /dev/null
+++ b/php/src/file/csv/CsvReader.php
@@ -0,0 +1,39 @@
+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/IBuilder.php b/php/src/file/csv/IBuilder.php
new file mode 100644
index 0000000..fae647a
--- /dev/null
+++ b/php/src/file/csv/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/csv/TAbstractReader.php b/php/src/file/csv/TAbstractReader.php
new file mode 100644
index 0000000..ed59776
--- /dev/null
+++ b/php/src/file/csv/TAbstractReader.php
@@ -0,0 +1,54 @@
+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/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/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..9238b97
--- /dev/null
+++ b/php/src/php/time/DateTime.php
@@ -0,0 +1,265 @@
+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)) {
+ throw new InvalidArgumentException("datetime must be a string or an instance of DateTimeInterface");
+ } else {
+ $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);
+ }
+ }
+
+ 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 @@
+ 0) $part = ucfirst($part);
$parts[$i] = $part;
}
- return implode("", $parts);
+ return $prefix.implode("", $parts);
}
}
diff --git a/php/src/text/Word.php b/php/src/text/Word.php
new file mode 100644
index 0000000..7369b5f
--- /dev/null
+++ b/php/src/text/Word.php
@@ -0,0 +1,212 @@
+fem = $fem;
+ $this->le = $le;
+ $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=false): string {
+ if ($amount === true) $amount = 2;
+ elseif ($amount === false) $amount = 0;
+ $amount = abs($amount);
+ $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, $fem=false): string {
+ return $this->w($amount, true, $fem);
+ }
+
+ /**
+ * 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, $fem=false): string {
+ return $amount." ".$this->w($amount, $fem);
+ }
+
+ /** retourner le mot sans article et avec la quantité $amount/$max */
+ function r(int $amount, int $max, $fem=false): string {
+ return "$amount/$max ".$this->w($amount, $fem);
+ }
+
+ /** retourner le mot avec l'article indéfini et la quantité */
+ function un(int $amount=1, $fem=false): string {
+ $abs_amount = abs($amount);
+ if ($abs_amount == 0) {
+ $aucun = $this->fem? "aucune ": "aucun ";
+ return $aucun.$this->w($amount, $fem);
+ } elseif ($abs_amount == 1) {
+ $un = $this->fem? "une ": "un ";
+ return $un.$this->w($amount, $fem);
+ } else {
+ return "les $amount ".$this->w($amount, $fem);
+ }
+ }
+
+ function le(int $amount=1, $fem=false): string {
+ $abs_amount = abs($amount);
+ if ($abs_amount == 0) {
+ $le = $this->fem? "la 0 ": "le 0 ";
+ return $le.$this->w($amount, $fem);
+ } elseif ($abs_amount == 1) {
+ return $this->le.$this->w($amount, $fem);
+ } else {
+ return "les $amount ".$this->w($amount, $fem);
+ }
+ }
+
+ function du(int $amount=1, $fem=false): string {
+ $abs_amount = abs($amount);
+ if ($abs_amount == 0) {
+ $du = $this->fem? "de la 0 ": "du 0 ";
+ return $du.$this->w($amount, $fem);
+ } elseif ($abs_amount == 1) {
+ return $this->du.$this->w($amount, $fem);
+ } else {
+ return "des $amount ".$this->w($amount, $fem);
+ }
+ }
+
+ function au(int $amount=1, $fem=false): string {
+ $abs_amount = abs($amount);
+ if ($abs_amount == 0) {
+ $au = $this->fem? "à la 0 ": "au 0 ";
+ return $au.$this->w($amount, $fem);
+ } elseif ($abs_amount == 1) {
+ return $this->au.$this->w($amount, $fem);
+ } else {
+ return "aux $amount ".$this->w($amount, $fem);
+ }
+ }
+}
diff --git a/php/src/text/words.php b/php/src/text/words.php
new file mode 100644
index 0000000..f11c392
--- /dev/null
+++ b/php/src/text/words.php
@@ -0,0 +1,14 @@
+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..c3ed581
--- /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..3827fe4
--- /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/src_base/ValueException.php b/php/src_base/ValueException.php
deleted file mode 100644
index 665549c..0000000
--- a/php/src_base/ValueException.php
+++ /dev/null
@@ -1,48 +0,0 @@
-";
- } elseif (is_array($value)) {
- $values = $value;
- $parts = [];
- foreach ($values as $value) {
- $parts[] = self::value($value);
- }
- return "[".implode(", ", $parts)."]";
- } 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, " is null"));
- }
-
- static final function invalid($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 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/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/cstrTest.php b/php/tests/cstrTest.php
deleted file mode 100644
index 132a538..0000000
--- a/php/tests/cstrTest.php
+++ /dev/null
@@ -1,56 +0,0 @@
-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..92785fc
--- /dev/null
+++ b/php/tests/strTest.php
@@ -0,0 +1,28 @@
+ [
+ '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..ef17f83
--- /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
+
+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..b380090
--- /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
+
+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..c1f1cbc
--- /dev/null
+++ b/runphp/build
@@ -0,0 +1,193 @@
+#!/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=
+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)"
+
+ template_copy_missing "$PROJDIR/$BUILDENV0" && updated=1
+ template_process_userfiles
+
+ if [ -n "$updated" ]; 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
+
+ enote "IMPORTANT: Veuillez faire le paramétrage en éditant le fichier $BUILDENV
+ ${EDITOR:-nano} $BUILDENV
+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..15f02fd
--- /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=d12
+IMAGENAME=runphp
+#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..06a0348
--- /dev/null
+++ b/runphp/dot-dkbuild.env.dist
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+default_profile "${DKBUILD_PROFILE:-prod}"
+
+# Source des paquets et proxy
+setenv APT_PROXY=
+setenv APT_MIRROR=default
+setenv SEC_MIRROR=default
+
+# Timezone du serveur
+setenv TIMEZONE=Europe/Paris
+
+if profile prod test; then
+ setenv REGISTRY=pubdocker.univ-reunion.fr
+ setenv PRIVAREG=pridocker.univ-reunion.fr
+ host_mappings=(
+ pridocker.univ-reunion.fr:10.85.1.56
+ pubdocker.univ-reunion.fr:10.85.1.57
+ repos.univ-reunion.fr:10.85.1.57
+ git.univ-reunion.fr:10.85.1.55
+ )
+ default docker host-mappings="${host_mappings[*]}"
+elif profile devel; then
+ setenv REGISTRY=docker.devel.self
+ setenv PRIVAREG=docker.devel.self
+else
+ setenv REGISTRY=pubdocker.univ-reunion.fr
+ setenv PRIVAREG=
+fi
diff --git a/runphp/dot-runphp.conf b/runphp/dot-runphp.conf
new file mode 100644
index 0000000..b409a45
--- /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
diff --git a/runphp/runphp b/runphp/runphp
new file mode 100755
index 0000000..87691d7
--- /dev/null
+++ b/runphp/runphp
@@ -0,0 +1,583 @@
+#!/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=()
+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=
+
+#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() {
+ cd "$PROJDIR/$COMPOSERDIR" || exit 1
+ if [ -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 [ -z "$RUNPHP_STANDALONE" -a -f composer.lock ]; then
+ cp composer.lock "$PROJDIR/.composer.lock.runphp"
+ fi
+}
+function ensure_image() {
+ 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 check_image() {
+ 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)" ]
+}
+
+## Arguments initiaux
+
+Bootstrap=
+ComposerInstall=
+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
+ ComposerInstall=1
+ shift
+fi
+
+ForcedBootstrap=
+if [ -z "$Bootstrap" -a -z "$RUNPHP_STANDALONE" ]; then
+ # si vendor/ n'existe pas, alors on doit faire bootstrap
+ if [ ! -f "$PROJDIR/$VENDORDIR/nulib/php/load.sh" ]; then
+ ForcedBootstrap=1
+ elif [ ! -f "$PROJDIR/.composer.lock.runphp" ]; then
+ ForcedBootstrap=1
+ elif ! diff -q "$PROJDIR/$COMPOSERDIR/composer.lock" "$PROJDIR/.composer.lock.runphp" >&/dev/null; then
+ ForcedBootstrap=1
+ fi
+ if [ -n "$ForcedBootstrap" ]; then
+ [ "$RUNPHP_MODE" != docker ] && eecho "== bootstrapping runphp"
+ Bootstrap=1
+ ComposerInstall=1
+ fi
+fi
+
+if [ "$RUNPHP_MODE" != docker ]; then
+ ############################################################################
+ # Lancement depuis l'extérieur du container
+ ############################################################################
+
+ ## 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
+ }
+ [ -f ~/.dkbuild.env ] && source ~/.dkbuild.env
+ [ -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
+
+ ## 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
+
+ # Toujours vérifier l'existence de l'image
+ Image=
+ if [ -z "$Bootstrap" ]; then
+ if [ -n "$RUNPHP_FORCE_BUILDENVS" ]; then
+ eval "Configs=($RUNPHP_FORCE_BUILDENVS)"
+ for config in "${Configs[@]}"; do
+ source "$config" || exit 1
+ done
+ after_source_buildenv
+ elif [ -n "$BUILDENV" -a -f "$PROJDIR/$BUILDENV" ]; then
+ source "$PROJDIR/$BUILDENV" || exit 1
+ after_source_buildenv
+ elif [ -n "$BUILDENV0" -a -f "$PROJDIR/$BUILDENV0" ]; then
+ source "$PROJDIR/$BUILDENV0" || exit 1
+ after_source_buildenv
+ fi
+ ensure_image
+ check_image || Bootstrap=1
+ fi
+
+ Chdir=
+ Verbose="$RUNPHP_VERBOSE"
+ if [ -n "$Bootstrap" ]; then
+ ## Mode bootstrap de l'image ###########################################
+ # Ici, on a déterminé que l'image doit être construite
+
+ 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=
+ 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
+ UnlessExists=
+ Pull=
+ NoCache=
+ PlainOutput=
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --) shift; break;;
+ --help)
+ eecho "\
+runphp: construire l'image docker
+
+USAGE
+ $MYNAME --bootstrap [options...]
+
+OPTIONS
+ -c, --config build.env
+ --unless-exists
+ -U, --pull
+ -j, --no-cache
+ -D, --plain-output
+ -x, --apt-proxy APT_PROXY
+ -z, --timezone TIMEZONE
+ -r, --privareg PRIVAREG
+ -p, --push
+ paramètres pour la consruction de l'image"
+ exit 0
+ ;;
+ -d|--dist) shift; Dist="$1";;
+ -[0-9]) Dist="d1${1#-}";;
+ --d*) Dist="${1#--}";;
+ -c|--config) shift; Configs+="$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) ComposerInstall=1;;
+ --no-use-rslave) UseRslave=;;
+ *) die "$1: option non configurée";;
+ esac
+ shift
+ done
+
+ 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"
+
+ ensure_image
+ check_image && exists=1 || exists=
+ if [ -z "$UnlessExists" -o -z "$exists" ]; then
+ eecho "== Building $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
+ fi
+ if [ -z "$RUNPHP_STANDALONE" -a ! -f "$PROJDIR/$VENDORDIR/nulib/php/load.sh" ]; then
+ # Forcer l'installation des dépendances si nécessaire
+ ComposerInstall=1
+ fi
+ [ -z "$ComposerInstall" -o -n "$UnlessExists" ] && exit 0
+
+ else
+ ## Mode exécution de commande ##########################################
+ # Ici, on a déterminé qu'il faut lancer une commande
+
+ 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 ci|cu|composer
+ $MYNAME [options] command [args...]
+
+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"
+ exit 0
+ ;;
+ -w|--chdir) shift; Chdir="$1";;
+ --no-use-rslave) UseRslave=;;
+ *) die "$1: option non configurée";;
+ esac
+ shift
+ done
+
+ if [ -z "$RUNPHP_STANDALONE" -a ! -f "$PROJDIR/$VENDORDIR/nulib/php/load.sh" ]; then
+ # Forcer l'installation des dépendances si nécessaire
+ ComposerInstall=1
+ fi
+ fi
+
+ ## Lancer la commande
+
+ 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=
+ else
+ # 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 "$ComposerInstall" ] && set -- ci
+ [ -n "$Verbose" ] && eecho "\$ docker ${args[*]} $*"
+ exec docker "${args[@]}" "$@"
+
+else
+ ############################################################################
+ # Lancement depuis l'intérieur du container
+ ############################################################################
+
+ 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 [ -z "$1" ]; then
+ die "no command specified"
+ elif [ "$1" == ci ]; then
+ eecho "== installing composer dependencies"
+ composer i
+ elif [ "$1" == cu ]; then
+ eecho "== upgrading composer dependencies"
+ composer u
+ elif [ "$1" == composer ]; then
+ "$@"
+ else
+ if [ -n "$chdir" ]; then
+ cd "$chdir" || exit 1
+ fi
+ exec "$@"
+ fi
+fi
diff --git a/runphp/runphp.1preamble b/runphp/runphp.1preamble
new file mode 100644
index 0000000..f290705
--- /dev/null
+++ b/runphp/runphp.1preamble
@@ -0,0 +1,18 @@
+#!/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
diff --git a/runphp/runphp.2postamble b/runphp/runphp.2postamble
new file mode 100644
index 0000000..83f987b
--- /dev/null
+++ b/runphp/runphp.2postamble
@@ -0,0 +1,534 @@
+#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() {
+ cd "$PROJDIR/$COMPOSERDIR" || exit 1
+ if [ -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 [ -z "$RUNPHP_STANDALONE" -a -f composer.lock ]; then
+ cp composer.lock "$PROJDIR/.composer.lock.runphp"
+ fi
+}
+function ensure_image() {
+ 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 check_image() {
+ 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)" ]
+}
+
+## Arguments initiaux
+
+Bootstrap=
+ComposerInstall=
+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
+ ComposerInstall=1
+ shift
+fi
+
+ForcedBootstrap=
+if [ -z "$Bootstrap" -a -z "$RUNPHP_STANDALONE" ]; then
+ # si vendor/ n'existe pas, alors on doit faire bootstrap
+ if [ ! -f "$PROJDIR/$VENDORDIR/nulib/php/load.sh" ]; then
+ ForcedBootstrap=1
+ elif [ ! -f "$PROJDIR/.composer.lock.runphp" ]; then
+ ForcedBootstrap=1
+ elif ! diff -q "$PROJDIR/$COMPOSERDIR/composer.lock" "$PROJDIR/.composer.lock.runphp" >&/dev/null; then
+ ForcedBootstrap=1
+ fi
+ if [ -n "$ForcedBootstrap" ]; then
+ [ "$RUNPHP_MODE" != docker ] && eecho "== bootstrapping runphp"
+ Bootstrap=1
+ ComposerInstall=1
+ fi
+fi
+
+if [ "$RUNPHP_MODE" != docker ]; then
+ ############################################################################
+ # Lancement depuis l'extérieur du container
+ ############################################################################
+
+ ## 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
+ }
+ [ -f ~/.dkbuild.env ] && source ~/.dkbuild.env
+ [ -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
+
+ ## 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
+
+ # Toujours vérifier l'existence de l'image
+ Image=
+ if [ -z "$Bootstrap" ]; then
+ if [ -n "$RUNPHP_FORCE_BUILDENVS" ]; then
+ eval "Configs=($RUNPHP_FORCE_BUILDENVS)"
+ for config in "${Configs[@]}"; do
+ source "$config" || exit 1
+ done
+ after_source_buildenv
+ elif [ -n "$BUILDENV" -a -f "$PROJDIR/$BUILDENV" ]; then
+ source "$PROJDIR/$BUILDENV" || exit 1
+ after_source_buildenv
+ elif [ -n "$BUILDENV0" -a -f "$PROJDIR/$BUILDENV0" ]; then
+ source "$PROJDIR/$BUILDENV0" || exit 1
+ after_source_buildenv
+ fi
+ ensure_image
+ check_image || Bootstrap=1
+ fi
+
+ Chdir=
+ Verbose="$RUNPHP_VERBOSE"
+ if [ -n "$Bootstrap" ]; then
+ ## Mode bootstrap de l'image ###########################################
+ # Ici, on a déterminé que l'image doit être construite
+
+ 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=
+ 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
+ UnlessExists=
+ Pull=
+ NoCache=
+ PlainOutput=
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --) shift; break;;
+ --help)
+ eecho "\
+runphp: construire l'image docker
+
+USAGE
+ $MYNAME --bootstrap [options...]
+
+OPTIONS
+ -c, --config build.env
+ --unless-exists
+ -U, --pull
+ -j, --no-cache
+ -D, --plain-output
+ -x, --apt-proxy APT_PROXY
+ -z, --timezone TIMEZONE
+ -r, --privareg PRIVAREG
+ -p, --push
+ paramètres pour la consruction de l'image"
+ exit 0
+ ;;
+ -d|--dist) shift; Dist="$1";;
+ -[0-9]) Dist="d1${1#-}";;
+ --d*) Dist="${1#--}";;
+ -c|--config) shift; Configs+="$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) ComposerInstall=1;;
+ --no-use-rslave) UseRslave=;;
+ *) die "$1: option non configurée";;
+ esac
+ shift
+ done
+
+ 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"
+
+ ensure_image
+ check_image && exists=1 || exists=
+ if [ -z "$UnlessExists" -o -z "$exists" ]; then
+ eecho "== Building $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
+ fi
+ if [ -z "$RUNPHP_STANDALONE" -a ! -f "$PROJDIR/$VENDORDIR/nulib/php/load.sh" ]; then
+ # Forcer l'installation des dépendances si nécessaire
+ ComposerInstall=1
+ fi
+ [ -z "$ComposerInstall" -o -n "$UnlessExists" ] && exit 0
+
+ else
+ ## Mode exécution de commande ##########################################
+ # Ici, on a déterminé qu'il faut lancer une commande
+
+ 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 ci|cu|composer
+ $MYNAME [options] command [args...]
+
+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"
+ exit 0
+ ;;
+ -w|--chdir) shift; Chdir="$1";;
+ --no-use-rslave) UseRslave=;;
+ *) die "$1: option non configurée";;
+ esac
+ shift
+ done
+
+ if [ -z "$RUNPHP_STANDALONE" -a ! -f "$PROJDIR/$VENDORDIR/nulib/php/load.sh" ]; then
+ # Forcer l'installation des dépendances si nécessaire
+ ComposerInstall=1
+ fi
+ fi
+
+ ## Lancer la commande
+
+ 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=
+ else
+ # 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 "$ComposerInstall" ] && set -- ci
+ [ -n "$Verbose" ] && eecho "\$ docker ${args[*]} $*"
+ exec docker "${args[@]}" "$@"
+
+else
+ ############################################################################
+ # Lancement depuis l'intérieur du container
+ ############################################################################
+
+ 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 [ -z "$1" ]; then
+ die "no command specified"
+ elif [ "$1" == ci ]; then
+ eecho "== installing composer dependencies"
+ composer i
+ elif [ "$1" == cu ]; then
+ eecho "== upgrading composer dependencies"
+ composer u
+ elif [ "$1" == composer ]; then
+ "$@"
+ else
+ if [ -n "$chdir" ]; then
+ cd "$chdir" || exit 1
+ fi
+ exec "$@"
+ fi
+fi
diff --git a/runphp/runphp.userconf b/runphp/runphp.userconf
new file mode 100644
index 0000000..b849b4b
--- /dev/null
+++ b/runphp/runphp.userconf
@@ -0,0 +1,29 @@
+# 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=
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..c17a544
--- /dev/null
+++ b/runphp/update-runphp.sh
@@ -0,0 +1,94 @@
+#!/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
+
+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:-.}"
+[ -d "$runphp" ] && runphp="$runphp/runphp"
+
+setx rundir=dirname -- "$runphp"
+[ -d "$rundir" ] || mkdir -p "$rundir"
+
+if [ -f "$runphp" ]; then
+ ac_set_tmpfile userconf
+ <"$runphp" awk '
+# extraire la configuration depuis le fichier
+BEGIN { p = 0 }
+/SOF:runphp.userconf:/ { p = 1; next }
+/EOF:runphp.userconf:/ { p = 0; next }
+p == 1 { print }
+' | awk '
+# mettre en forme le fichier: pas de lignes vides avant et après
+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"
+ }
+}
+' >"$userconf"
+ initial_config=
+
+elif [ -n "$projdir" ]; then
+ # forcer BUILDENV0=..env.dist et BUILDENV=.env pour les projets de
+ # l'université de la Réunion
+ initial_config=1
+ ac_set_tmpfile userconf
+ sed <"$MYDIR/runphp.userconf" >"$userconf" '
+/^BUILDENV0=/s/=.*/=..env.dist/
+/^BUILDENV=/s/=.*/=.env/
+'
+
+else
+ initial_config=1
+ userconf="$MYDIR/runphp.userconf"
+fi
+
+(
+ cat "$MYDIR/runphp.1preamble"
+ echo
+ cat "$userconf"
+ echo
+ cat "$MYDIR/runphp.2postamble"
+) >"$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
index 03c724bd9e6c32d89ffff2ea5c8f55d889c2e428..7a44c36752f9df9e5a22e853c9f4d2e7e79922eb 100755
GIT binary patch
delta 312106
zcmbq+2V7Lw@_+8V%K}R~uqv5H
z)WoP5qnp?@y=$7u(-@e`~83KeLj9K_p~`PbLPys|?R
z@gOgGtd_A~BGp2|lpp6-$;(?tQp<$Ga!0RUtD-rZqT-39VGQ7VMAI#iF`xbhW!?%z9bCj_FGH&Vy8&?g1?Wv#yg0u
z4_6BbzZs>SB^PF?g`avyvMszNVbya5Rr1ANYGH{_G<#dGwj_M<%WRPILX7eXZ>_xB
z$G~oSsfC2~duP8X4+@BvJNO2(^s#CIVbR^#4f2gNwaaO~5iF;-T1c2V-tSL&t8W{&
zN~69cOj;JRTVWKD@1@f-(*U)Qa9GX88S=hhm52geINQn8Lc;Z_E4sTU9@lfKtu)_W
zRb{O%BmO?+qt+ulb>;n!J@tCkRhKoXHy}{0N0{0!vKLJ%zIUa4oVBt^tz|m37GZYf
zxpki2`q-;%P3i@4sSU$c{flb=;fk`?
zf<250^VhRkA!;kaM^k$)^%Rc6!jJgSgxA{sa6lepCLu(#E?jJc1utpZ@B$w>Js^}N
z8r3cdS060R#p$qvTsl@``(}y5Pqj=wbH|;r9pbO-cKzg?EmR*rpSAkqs&^{RE|FF)LMjd
zFYSr)bfIT@Hr1pS5_X-_=2Q7xVWd2e`Lpi+Y60Psf4}LaCr_8%Ya5#IVyV`*8oA{!ykz4Wdl3)XS
zk1r^~Bj3j7T%^Ux7SXpC}&>)5$-D
z#<8z>EyAv!|8_z)8b(BZ$6pdIYM#S_#s?tW@=%v3Y}#IaD@@P&daHdB{+t~jqs%=kB2_
z+)Pqb2@AvR_DV8TL;0d6y!6gLcOkp8S$3}4BjK(OSAVYvhHLzi
z(6S<8*}(#}72)0wmp!BukPIG=@MB5bJ`le5cCuauk$Sd;
zD=6XW&GRP8D^paZ=@S*ghH=Ibc8>^4^c1ea!XdsYv4kPRG@p5h{WdC?b>YMk?j5?I
zHLen++o&P-$|>shThmExPx!#`kKU2{8oRR%d_=;reJ-w+HyV4hzI-_lo_n|JI%PSS
zqJ!AoD76d1N5B4RhqAin#Q4dj(FT^oR~X>~@48YH`l#^v=t%Z1z9StZ+|2Q@y^(`yvsn@9IIzReA0y8XU9%O%MmG;#Ky5AZp{eqm?Iunj6F?Z
ze{3*2)m`n9aAwXWyD}aX{vI2`;<&LV{NvTI^@^%dVYfKO0(5Fi!jI==rJ)t+!JM3Y
z!q1=byW#2X)3^|3=NutC)$Q|EiaSHyMVrFdBEHWPKC|$%)t;8)O(Cp+&yDcaNm)O8
zrtrKen6>B1NBF_%Ucb5vpH4Cs3YEA?hwydZ$1WL
zW<=o_g*r!kJWJxTBAi;9Xp*p$Bj95&eL)mw)`nhWh&t_|IA*CyIQ1tB9Y0NiC&?KJH%T*h4|KhH{ZEb@5RdRGx
zcdOG@Fp#))lG|j$cDG;tL#{+3`y~f4FFq{cnACmm%cpxs%JY*M8x^TiPq-qx?k8C&
zh?I{ehp@|B%Lun<+Bdqz0lToSoo~3}OFwi~5n)P+31q2>XsJDB=4(
z0|wC2QOg45%@!~ADDRiB`h1s@isz!V^_4GJ0$2f`1>u3`J}Z*9q$bLymZ2;;TBVY(
z`pvuFp}kQ$p3pLafsa~7_}HOb(($$|iL;e3WA;GHN;oX%vn+g>E$`0oVLp6N!f_!-
zuHh1nmvb|MqHX=H)z%5BsS4w}JmFU_m2JiiB8T~KA_#Z7*2(y?rTloNft})A5?Wa1
z+w$Jbd=|%R6D}SyDofdSiR9c2UpXx+l>NaeB)l`p=L+guXZ9%XitxZML;ESaA#W8o
zs=|?1Q(9iZH{T0sguqzh|VLzyysMvPuvqvk8
z4Y(u;&*py-?iuj$RuOEGuc~E)eIp+jp;%IC8QwZb(Wt?^0^tE|>pjZ;tX7C-pL2C1
zJl6epv$8)^VOi@i+`!bX3AMiridRek7Dn`|sGdYr+qp6m7Cmjs^|TIZ>n;D-I*2`y
zs`fxwG`spRN;qDo!q#n!Y&d5wVGrZeF`hYNh1dt3_Ue9~ZAs+om+-^^doV3CMYpK0
zciQOL94<-1cayqQdL|jvHkxhan+;*y?y?d&yKNqOs-H?4VW%wp9>t8&%y+ldv6v*a
zkZ{M(Prf5B?O>EIWN3{|M3$bQ)*&>`jU21&QWx3=$*JuE*d@+%!ZkZK4dR79az(pP
z){=XVgpXxzT;Ns;c~83_md`bYaCO-;R*$f+wu@jda&Zxk7==8@>tK;{+UwahK7Ybw
z`?h%sgII@f`Fwjnme)e1l<-W|FBegZfh{lQ1zk
zKG~3pEH%mXof!L?D;eSb*WVa{bB~gbclMTF>J-VA@m7TAzsWRWt1M>YtR!qXY*vjN
z-?Xsm#ke(e3XmV~?8i3tRry3Xx~JtvY&n=Sl`!^h=>f9t^119}){VC!
z9DV5g4rNVV@9HNn?4oC_xFb(k``(YsX#bR{@ck}fEQaqQgf4T^4PF=^mv;?iN7t)!
zB&><_PEZu=de;ERZ}19M?94N~IpHm7_g)MssP7E6h;y0nbEluAIAk>F;BFC2=I$Kf
zsoWdSDjcA~P2ED-quc-yE_iOo24#|Sazf>6-Ec0PcZ6fScAZrmGAhi;iD*_hE$N5%
z6msX}B(PMz$q{}SH|Z?;3u7M*R^>wY*x3Q`%9w@u2KjoPA3LE}3kh5G`nOG?77HOY
zMypPC2&am0UCN1`?&iiO8*KnLSA>hb53ZCqch6^bP86ZzlU>ir)*fa#v%5|<<$E(L
zS2MzMM{C|gYiyRE%8z8ZTxNtLw=awKNc&oTD06XY3FBJr{8(B2#Gwv7qS&9@A``yZ
zFm)-8*N(~DKoQQ}@XcmVcW?IyWzV-)Cr`M^Z?eu)7}7J8b_E$;^l*hO;eV4K74tf;luC?@jHHY(wSE#A?T
zB5S*{C%7;O^Ku4XQ(|T6cyMnc`;6OV!pQpI_fULhd28=D_7ES9u=2OOUAVnl*g9UD
zF#Mxmo<)-wDJS#^XXkls!q=~ap2RINQr^_ZPp<0Y!wz$~5`JWOx~tM^6SflgK1f(w
zG(%Kc6+Y+{n^#v`-p5wyP`97S3F@o}ec!*6g0?-CUFJR`VW$^QeW!GaZRJq~A*`00
zIYRvp14T)wIaKTqo3
z7Xv^uYssBC!ufM!-%vaQqAjCuI1BHs4ngP}RFX~6fkJpjUtjs*zW!_iXEdSf=G`62
zmQ1Y=_6=qIxqcCT@pb?0p4PwjHL&))knqNO4D{Sx&+6wLGr7*@tSzjuPgQg*h8qRK
zlS3M0M_QCZ#H@b7Y(D26;fuZdyxyYc2ffvngde+R&iBl#xTPsOOGC9C3#iEwk)=j-rgoUzhcJKkPXMO2jqsf`G`
zm7N{uQL+mI^sF1-xd_kP^xEPfIeuU$n=(LcN!b4UfezSnZ1fO37ppE+>G^?kgfP_6
zsLB!eyCb0K8Q17hTdhO}W%@tTAy26rfxHw;N<
zEBJCCyz%ZwUGZfL`OXlZ$a2mj!up8|tK50^M7h-{z1(`Jo;7gihOp;U%{SOITHZ5M
zC(jt_r!wI2pmX{q!>)o%?AH4wj6n{l(P{>a;1BZg`~)!wiG
z=F9g5o&3JjaQ2v&mR_zcc?|T#!prs
zCQqnUAHJW!M;TH+N$E2!0sS1|v2=OmKE3=!dA{X`UF6M^bn^NwVLk<1
zUF8)O)oN$rL_V7u_ruo{>(rvrlkDol`;++mx>oWx4V9dPsH!PyohPa$@(!yj)CYTY
zsY1vdlt=&N^M94kJgb-e##!VjdyrZq-TsjJ@VK3G^17W3|$*8(ZYq-jPSW}`lZ11$H59^((GCPsD
zuTHK^l#}Xs--UI&(dxRsYTa+^vek#gDSX9LPsvx`9GKEwefV<0nM7OnvQ4ZXn$xB!WZM#3{wm{4${6^&CGt*>C>8!rsXDs@bSZ;
zA?}&ArkPoqH*L}^l8b}zQ2PfD{LPH+Cxgvg283IV?yOdm&+^kJf^EM;OZHf>02InkckmhMTl0>VF>`852eSBSYX>EnQ+DZC#
znyV*a{QiN0dtUcww5LsRMRjRqU74~ZnzFRjqmPZ17tcsjm)+qRBh`oShYQ#jZB^|e
zyfFuk$<|pB^0^tt$bPn|s@HprJJf`ECC_K0N`%U%AMT*`;Wx9N`Y?KCdu6GcaenL4
z`tQIUFI29d86=;W8KTzsbf#({#-cIU_}wc#WXI}6`Sh$HmO4_UfpFLEX?T9}qETjZ
znS6D&ANw{zEhJp=^xLO!@6*f2<|MPvxVJ{Q{U7O_c|n9cac&SB#T^O4!}XdH+~%lD
zooskCh<(q+M>zQ0$2MeZANB!nLD*&4u&c-joTeNkUs>QMk5~}MmhYv==wPO3*O34
z-mox)xnk8q!cuwi969J^wdGF>gP9Pm781@LH=~QvGHX!`Yv6W;&^!KtH_&&EOsE&XRzVnZefx`Y&awe)KA#BTZ%0{g+3_?f
zm5+R4Su)Gz?igY3owvzMqqtCi<}Ht4)3|aGzT5Hj({k}^YT+l#Ls>m^RL{S__c2`luhjN2E&9cR;CQZydNk7{iDEZ1Pd
zPm<^D!?x;*{0^BF_xP3wIr=4qtQfTozc}+YZ$^0WgAsV2loP*XC3oj`uFPj2p`%CJ
z6ydY=j|^56kLF*!3h(l*@@Azi)Cz?8#w1L_DGMx&Dl{STAs!PF4vt@*sj9uwkAd~)
z@dM$lr9KNieH>jC!bZoa0}$?QKl29aSt9XJ_LU>qY925XJ}_q1V0pJPiBLt_N4Qf=
zIR1y*@thCQY!26c!o~AO2l7dUHla6=+i1e|b-z5xNixc_o(N!X<6y)TLhJOnXn78(
zByE!$e!dB5YBjf!ySz7lob}vqq|hD@0
za6OT5mb<$pHEES`BM@l2-jw{CLGk6Mxl8QS1Vj
zBjLgYufMIx5es7n)KyjvtgGeBe~V9+@X4%e!APAXxRL*s+1Fi{yO
zcb%Sv^BxEv_(_}O5fxFgbRA>sIfn={wqCueEJh;LzFsG%t`BBYxH1tgeqltRXD;^j
z@$53U(}bBZA1{};tnbL)=L8W}_6lN(*!?!BVt>GlN198YnIgZf+PcUg<>-f#rhUWx
z0>Z~mF8fa5$%7jWtO)HGDF$Kv*{3xMyT91zEkC(2n0>$}NSIXp+8Jd<5&d6m3}feb
zdX8|%_0n&ZwNHf&o4w`AO$PP>@0!rLsZ)j0H5IbW!E7gI4dLuA8&(tdEoHWGbrUkX
z*NsP~zoL3VKl=oJ6`__Z1!30*%FmOkm8rFRP#cWK2@4_pGV0*Yv?F;gzbZb-TC9Il|-9
z`^DgvWbDkVx_9PGrTYQ)+Fo|ND(=~^iQF}ON`w`UUHXMY-{~*S?HD*styP9@)_N^(
zO_;7LYc0oaiOC}7d1__$sH>`)=J}I`6nP`UJ*lt!K)YUtdxz|0t+iJEbwGcZ#%TS#$HdcpVA49JAx}RVa|oieMwW={$*TD3)kAJn-P4Nn^MA5Yf><762^}A
zHpCS@aD0DT6`tQTYdj0(PMS^l<4!x?Z(3^Xc5mZh)n&Hv71g#fyvnb*^3A$f#~DUA
z_r`-K$hDdMw_WtN)t1|v3%RM7s{j4NXa7|vJ;OIqG&4`46ZZUZ!r5)^UJCwBvm0f|
zyJ@b6e<>y*wd3b3gpZ$WIb8n70ry=0Qa|%AZ<=i*5@#FXt1ovS?xwf@|D0Y`F0-2m
z<}X7M(NnprMmX!|gxT(q`u=Z6!huxrH5;bTz19bCQ%0CPXa6GiIOx#d-?seS7MS*I
zj2yJhSI*t4Xg1wlFQ42}&@9vMxKeabv8<<`^21h-(c+Z5!F2M~;NwOfUB2u;b-c+e3*W)8A0+o~gxMF|MDzbW#)T
z{F$?ca8{emqi}~b$|cWur{JuLTa*1Njyo@SxE8R7PCFAT(uR+(Cod!)j0N0S--!mAU0S#~*F&f1pEmU4;-
zkKNjEK#@OED{tHu!xDKRVOT@l`)=N0p`ZNQwoo>MS0J48kDCwCzC?4CyFQ()IEV95
z*Nz@@Za{06fG_=8s!Z!;t*ET4v3V>I3F>)nxe4oIBL*vRlG>N5ztk7$FJW}=>FM(9
z?bN5P)j&LWwo-74n_r==rm6yyK+U?ga6KlR8n?qHpWGTFU*G9Pt+N!rg@5C8Ke`Ta
zx(L_aF|0+lCA982)lp+}I&nKJ{3vcdg5LP_X`L(7x9hulzHZ}NR
zd|5hC@$qrm<|cIq+xRhv#9b@G9ohXYZfT`AsXW|TS!ZjemGANC6BaqzE}QI|Xu#QO0?MmX`kPG@N&B$=+-=_^0?OavRl`9b);{k7NSv}aT0+s_!;
zR9-+B^ovx8GEbF#GF`S^sl-rG1C1=pWl;lmJCd;Qu6olEyHE$ExWLThcgvj7*h
z6>m!rqY^o1347-ZJ?hpqqRV?n6dT2dCrpV-`W=l`AJ&e)BwTkTF0^T*7qL+dCylVy
zC#ueEQLxhlqymR;O2R4L8)5YFZ}y@EB|Qq1hwhAJE%}59#U-0Ix)%%4w|i$8JI#k8
z40*(-ls8UeLB6Ua2nWv0{}vhAi`n>4gqv4g{N62TrMG88St{oS;jdr3W0F&zv#`HJ
zK@`iP=;9R}&usD`UpHj@wVA54V!sVl+Y)}&`neoDU>L6Ekbc}ZlwBRBmJ*iS={QJET
z{omYBQd*~9j)d)P5`z3`3RAg}m3P0~1q{C4>J7a(&O5@{*8+Db>PEdkamam^b|{B!
z;I#;2|9SFVT;(mB2}B8&DRa8)aB{P+@veAfeM1tIFB}eIH#rG}r3b%ym2)FlK7PbY
z&OCw_+WFo>IQ65n9dhB3Y}SLbo^Z-%SGS^R!vo`&@EgJ-de(uvQ-tHrJLaQoJFyBL
z`xAbo+w!X1@@Pinmcgnh2+w}>%*%}?b@I`8WB0m>O8&jeMIPZ2-e_O2T1n+xK$#6V
zs*R)+y^=eBkgps&zgEoBCN>use;1K{ZnDfQST6W;U8P%9)F&dJKmZB
z>O;Ra@bQ?qaJVi6dTFQhx0l&cWVmgHK3)8SL0V^sgu-p%
zgW+^1|2Vnd9Us^*G&E$*p8*5|S_ILgkFR+SLog
zB4N%K0h;|g?*1vjq?O)!ctH>rr<-f7H4|*LJt}I_OlD`PwYtz*!>?33OwV}wZ8QBlYcJ6I_@m10RVsSmnjJLZ?GDyG|#O+cX_)
zcp)Ge>}Mo1yzM27)F1(+kfjx7@L9W_(+XXL{Cs3vyVghrPQ19r=BUK;z?9_T;vNNq
zi;I)fOvz;xHPzNCTS{?puY!I(i;FGz(yWH=wDVwl*I932h9I_U4L|w{?O>s=P=V&
zE~U&uf2#uRD6IAYhJ?*i^y)=*n
z8%K7+V|t-=s)s6_4zE|?fo!=cMSYj-aMq-^B@WX=RxD^LwGjG*3M~YSt2$J$
z2!_8WcEr8Jnn_kp2MQ6%nNVY|bBs=|o#wD5kHNp)lm8zO|7E#v7`u0dFeXfBF&n$V
zA2}e|BVA3khR=&D2Wq=1!&H<^Gc=FG?>?HmNJvgAs!s5zbQ5(#(x@)=j_s1Miihga
z>XX+1oZ%SAi}#I!uO1OPfp>%u1}*0aeh?ZiBzkk&pl^gQDnr-UIvF>0yi#hbDJm+c
zc49iE(pEOyRzsetGkFY7C7}p%J`0b458et74@L5_b(5TUkuJs41+)qi3}Gg#lhZ`Y
z&s>V@$6}fRd!mGv!D*)2nmU^$Ls^;08Rk;gjVR#}0g_$_2!iECK??(nLLkhY;cL)l
z>t^Er;k_848>}xAf?VTcgnC*9q6u8k%^E9j9ANONsPX<@m8Qw`hVg|@DX
z@xnfd-CO2b-U9V!B-g(Gt?95>h&Wx1t+uYF+Ej{JvL;-hGC1>GOc<=_ljtAdUOQw*
zQowY;9}Q);fJusbR#Bs3s@@{$U|p@9zQG)d`pHRbl(zdt(tXyMGA&b5v*l*d&1hU<
z=2|$>qJ?)c{uBp&_RrURiur#{6p52q-(+1|8WjKvql9Arit(nD_zI`fhD?I2o#BRI
z9*yBDOWI|2sK(NU>@(TW;8Qbhf-3B$q@>2Fds@YJZPE@dT-F=ljWx#9aQ8ZPS44e7
zWphA07!57%U9|2>qdioR0KZ5QVqnpU=&;!nZ2xC2nVseK`aHKu9$~Gi#x1l_fX%pv
zVp3-AwyzQ`p7+9}2pAX{;N@DLEc6u&9_vetbJv7_rwDClbES04w6{vVy{2>LQZ%o%
z_^F)ZK61VvfuI$KS%
zz0a+S|6LuL?uQ=NrRf5(WW$+xz6p@kPUr=P+6ht6@>Xm({Mbp*!Hst4Ap~R#k?=vR
zV1gO#1wBk3W7I*w(P*7~_mVeUs}`81vcg#lj^q9jzN7(|rl`t>-~es3E4qVlMD+C6
zxv?ufDwcUXZuw5KMA#}hIUBBb5vJjwUvw0*1Kd|U$SttIw64NgC^u`vptPG{R=YVK
zAb7!{Zo;eZk8AosSe+xJWhQ6qRDGr8YoZ0IOc|9{*SM+S-ho>kZWuFNujdG#3h;h^
z9QEb?LO?K{6WFS0kHNb(Q!0?@aQ8@bglD2Ka;ji}Thj$!ICtAGz?IoucvJ97Mn5?j
zPW}`a4Q+b}J6s7pg)c=ISRfpPQ+_fA20aa$KL>B1H-h?&n#rP<_{)M
ztIu#@GMw)c>;tzK#_E;gfr_ccn=+NefE06`(_ctmz=WX;@)l-;eRj30e
zyc;Vdz~~l2hv0iRpETEj5yEUKtf&a>ZFAodcNSPtjQjPjiDAL8VwBLvwRW@+Ai(w_
z!IWJ@bPQ3pIdxbRV_~1d;^9394=ETpptzuisT-=pz2lMbgxqiaA|!l*_$OL~4xNri
zOU0^|7n4#H;|+9Fi?U>k>EVY>@qbsGLOY*{|BuX6Q;OjFi(v-1Z54dr{URa3OQ{Ig
zi-Z#9ZlYRtG-!omgis+K_I2`)hB;$|;K;%ugR|Nud-|pZ<|=YSfsH~9SXG9rWNuwp
z7#u7UVxi~OFa{r&VGrL__#3rNYlhj-4BS(5uuW*u(oF*)Zr(a&L{v$xzfIh%hf
zLyUV*aZ@D53Sq`ZW>Bw+-FDZ5yscrOP~T37?a+j0eDM5!c|(Tu&nr}hS5~DdgBA(@
z-^`+F^siiF#t4NHyDyuAi_H8YmZEH^2XL$e9i=73_%ki{mZ>akJ5kX4H4;1%ZZIL#
zziBTpXq=D{R?6ML!M4eD6*V?@)U{-sumzkY7?-A%3ekfyTej4hTAFel)*5RSdAYcw
znTnLD6qMm%f5rF;Tg~6qP~P*6YKpz04FArAelvwg?~(Qro1FM|6*mNM6xj
zULrT!Wwp=B!btj!5ke9~oyU<6_~ORlUnca?x_6dg6NPq0^7a&Wg$y1lY?DX}woF8_
z_q{8ITFg@@l}hU{%hVNZB=QSg?G&@4#_mAVp+p^^LQ!1Rs>+-2RTtvMPU
z?jf4C1sA+<&^Hk}O%t+U+!0METqySSf)ky6B3xUh2@wK$1#uvBM83N5W`b
zh#$0?AuI@N9D{bQ=DUu8wpWyn+e1-&Qw#h#L)fh_4@Q0+6bmcLm>0a)1=A@1dRQ>H
z+2T7BN1ZS_AON77Aus?v7+U0HaCAE8J5w;i)whGfVdYF*PCspn_LIlIZlruqIBa>$
z*dH8&F|V`b4}Ul{OGt*xm$Z@Lbycdz=0r^)TdnA9l@;zBiH?Mq)BP9>o-G^?@%Vwr
zK-C4IoKV<9*vb?0dWxFG7M{1wBZfvnC?mSVPJFHKBO5Z88#{2nuG&^_DjMQY_RfOp
z@pfWfZMg*o{Sa#adATvKO?o$?-I-#+2YY3uy`Fw8K>1VI0h2R}GE#6`e;5tY!{y}`
zb0!A-IV+6C=qPMjVeAa0qZ2}4|59TxXulU@ghJ3PGbXjDHo1c&lM=<#a>TWv>>M67
zf&piw1bu0J8Iq1Za3ztJqNIC%Sz&xQf~z_C6sYJpK0$eTGF-?GX)zk{IMUgDk#~IMdCkeuyrN*#eHCR$oIBGNa{Rv~=@hH{`J67h~m$L2H
zoQmIde?lm{p>R!6N4LpBGt$H}ZKT>=861z`b{eq8$Z|YnDHg1`0-<3ef))R-iMHYE
z!Ui5e0W?KEh}E8HT-sc{9W&W$OiI42
z>1ro3OcU@kr!Ed;9g=F6kpQ%!1WErj%rS{
z^+sc8$84E`I*S1)OpjTlvQ0Lar?>
zM`zmiCF8rUb1xY;2vPBT7QAS5GNq?Rn=8gxVD}+o4*Yt^*c*l)HlBbzXSBYMa>RHO
zEJuy8aOmsgKp1}17!9|}jo~bZbhEQ5`C&Zguc*afDEL-fBrH2>tb{+08e1B1-dWKGKZv=5|Ja{4J})O+Q{
z>_jzytn)rGu9|0z=+<9a5)cZf2YdTtB93lNRo6PxpkcdlDojX-*F)h8i5jUo#p3F{
z!|3e=VLO=tN}k7P(DA%+BdoolF+l8Yqo1F-%vOmhZzRy-%G+&>7XZFyp#jcG6%IP8
z!){Y7fCYMZat}77LMVRTsD=J}jlR@bf7tYbF%v%9Yiyx*8`xmaKd<|L|dli02QJa#LhMuOnXSqwOYKy6YS_CIcm=TVD>g!F@h*PKqq&62$EEZCr
zYBTa+#anuB$hsS(0Y{BN3k`$)bua~lcX9TKp_;r2n03Uqi{SEFjCqL#J`+k&8EY%k
zT+^Nu-Vh<~oj@Zx?TGe7k1P}fG_dt
zFI0{ZQz7?3F%VupBeum{gwT3Tidh4jH%VH!xJ5{WFCUTGL6@yUcQGRwwoVX3;m0YM
z#@e-27zbION*Xv(FZsA)pAz~DiYKIYdHk>x2B)_P>%jDeUplOKUNS(?&$w4)oY!e#
zbG;@Sa$}_e^pRr0e~N!3tZ4AXyiZU9>WPdA9>*$8y85)`k%YFe;&;kz~h}em)?7#_V0l&)kGQ)d2h2tPPC4=kWv%&xYeci1W
z#aLJ3F3MoICxA-d-#gf4+AS;>p#6-XK}1xnt&@r6nS{+*1viq;GbhO?Ib%%DF=jcFru*M~a|
z0pZZ_LQtp=e`Rr%?Gsvvu(*S_KRmf#_?Nmhtb0*-M$HlpK7ic4yImg!(bv6vVABD?
zs&Gc5L^WroN_}9&K_N9*wW{Vyd#SaOe!{{6C-!*d!b`inTEpo-g&=Ugh2o5T#j6#p
zcM0_(o&a3D7!(Q%G+F~(uki7Pz#XXX{r6**VL=|AW3of|G~oz^HHU?H$_ZJZqsekJ
zJLG*|>*3^KVX-gnOiEhO=^A%LSS-NsoA|pA$GS1UCi++My+J*Cg*m$=A2|IM8lVPm
z@AmLP&mc7QD>0I2^^QCogccHJ8DPC`~40y@w06*P!@d*fPUU
z1M}V!dcwkOXz&{*h4_V}po^$P*yNTfd33;=9g&cE8okcXt|te>oUR&N&$-C)q|H(~
z9_V5$dNLOeO+P;^MEQD}L#qLP82#v^U~ldrL1B0AaPT{$4af6M;U!qn-J8)_ug10N
zBOz6QcAKSG>Mk98KM|s#)iIqOI-mCqfF7R+rEt#2Uk?|m#m=z(GvPW(Q4bwI5)xhK
zKSwhe!uQ4W^lE#$6;pS(9gaCC4D}v9R(7
zp}mjWuY)-kH4!lB!)RZK$=4VT{wO%b*~$kRD1Q_tZjm*VbH>0#d7FZapqW-F7j%nl
zQ!ysRIKf>}se^$EIvIH0g$G~O%I@|W>?0pHW@obMNjIyU-t8}SR}V<^?w3p)_XGqK
zbE~8%BiyGE4UmFIj}%DUTNwt#B}RB9w{V(FF#Zajsw=WOGzmTL!df#PnN?L(TPuk=
zIC4ek7SUu}of>r4SCmoU7&k3G0&f4q15-}LNT0Iu)wu1H3t7Q
z9VK3M3X5RUpF&g=4ySZc?68)l=xkFRm2|$C+!=R}l?!ppe5Xbe6j+#--#4#!Pm3i@
z*Bbk6DT*18_lMBIN2m0LCt@XS#3)P+2PM0X{~?$Jh`J@rZ?Do;T4}GgQ4$tCaEp54
zNm&eYwatbXj2rV@{DlQ(-xf;9{_6ZFv^SY6@m~up@(+l2nePa{N-$i)z3jyE2^vUe
zLBVj%SL+3HBr&B2g+{gHy}4&a5;m9F>+n_MK=`iAPVpiBEDf0%cld-=@$Nny;e^8*
zK4O?JQmHBvFpU_6yJMj5>
z2K5R=DV`0~G!g2PztSGY2tl%X{?JUqY~o%ovseSs<`>MGJqYr8d*;#$=>wV`4qm29itCanR(4m4DSoG}dnhh!(h#8O>cd
zTmeRC*cBZMSs!|9fGwA}Bc-@cjv%oRE^PMc2(xYo`Oq~@Y)?Lv1vaON)_Z&?c2ej8
zBhy6}vWDC}*VYX2Apw3f>cW6+(R7C6^_U5vWkJ#Gz8YV>h*8PhEOZQezZ8!7vgbs-
zt07C=DfuA-m95cfN`;fLViv53*T%xG_M$IbZzqNadW5ei8LYS|cvx_wt=LDRW0bFY
zYdXNdcf7n{`T|WXtZ65{oy={q`@COKYqDXAv^!U}}5uvT`#AcG_7qJX{i_gKHhd
z0N2(IVwmVzyeHrCHo%DvVgQ`$B-V%-Dd2gd!_!kWzBIw+Y%xcJYtRBmI*SSSZ#@)-
zXN%GQgDx&+i!>|
z2f&H9b=Sp5pJU@)!gW$x4&_FoxAv!;q4ZeY}BRNJir`7uZl{za7hSX^D6c^FF6GBj*~?^^L`cO
zVReYpRHyy2L%agtT@ph6TFndmUK7KkJiXz~oeWcQCgxbnGbZ9suO_>s8qwf60*G{q
z(aKs|A)$1hd_*+Bamm{craHx?ZXMOuif0<@{81|&f;aQMLsg5=`g_a}-F*#(lsN?>
z$+7if5Okd)M){zYk>?N()24_6!O>Cl!f^MkDPk%Lxz!{eyhBqj=4o-_7`iA^hS=9d
zt!sTfvd=y8+f&5>Ztu$iH)>Ip@4PO0L!0mMV14#N3{x+}dTC(mG;wG%cg|3XN%D=VV-D=Q}VIu;Z;`?X#hVcR+5NyHn=-aEQO^fM3W!-mbG{W
z*w;4A0_F3?y%4ex_t#Gsh(T_7^nX;0MDa~{RE(#>7`VL@m*eS2#VEWHLsvL;$QFAk
zehiV0_cv|!x?1I$7`GP{&f;qPjJT#kNzY^Q1gXO7Z@OeWxJ~D$nCT9*3QZb;b-CCg
z#$9PbMQwTAxOA(d;@&C}R)Li#wFcjAoJTs?dK^8YvkSyPenl~v4jM>0PJ3NVsUn;*
z^vLN3o;zy6POw!Z%w8;d$D^f0;&4p-MVWM*SFIhrh$aI!8NtG*h?Qx8^4ePEB!Unf
z`TO@T(ROlXp{Vz4Cmw>`{El8EMg}x*9wdtqU|l2*7Av9B
zi=gX!jm%e%c+tHKUMR-n)kIzLxQgokRu9EJ_c2lLju}yg&AK7SG#PgR`d#dFo+$Yb
zqb5>yQtkgXteGk*d&_^T*|M=FB|)73tv;OTA%_3mbcm_A5rzI;d1m8an0~v5zxPpY
zXjm+U^8>cz`|Ce^wOBMZRe?x&?q_`vJbzl70mVziCK?&7Y9vXGj`%Sr)yV25%9zAO
z?pF8lc&?+U{j@4_=rMFd2cW5@E1H=1qShUp34hhZaCmnqZE6s^OpN>+
zp2fnuE5zt%VjSHy#RaY=aGKDr=&rJYFX}Z0UE`!03r-%;MBH0|ZZhf~-eFI7pCHU9l~1t`HM={d8ZD>2CPR8{zLp=an{rjKl|I@o;u>xm3Z%uQ-S
zgu(`_duE^3bHYtVRpB1qg;#zPGaa+_&05_@1wLIRw)m?GlqpAIib7dnmMn%fRb|!u
zx^?J3S+g9Z9M#fXg+W(^dfYOcE?hJ^h>9g9>+iP2O@*X`jv)4imsg7!(SJ3?%9iYO
zmg6pbXSLWLPCq5my@%if+D2-z{RpK%;R`qVnB=!s;NeuGw3UI>ii%(w{*77Z6MN7n
zaXgKoam6~^a&oqb%+>8lF+gH!LPR+;q)*o@t`Qr=^J1b(qE7WW8apQo(agHWkv!?p
zsR;#tsgy*OFd3|TuYuR_{!?AW{(2mn|1h$$J-EG||4jqnnWEkLbW{>Fg+|bQ^%~P3Q&lbYMM&k<61RR(nJv9!~nO)b>A9|qAIbA
z{;t?LT^)r5lm_{4G+JFxH>xP@HZvU*+B1rdxSiZx%Dz`=%_(Sz+7SrV0*!{#82F><{l7@@Wu;bsN$C2NDniFxX)5s;a3pqkZbm1UdrO?
zv|sG)xk`%-TVr2^62^Tc8!<I`!`4+$hJkqs^#isyzSl7R5RFm}pVI-*G*ETnxn={3&e^bU!5;Rv*=dn@4xS
zJ(-LJ+51yFbL9v#{{YQiK_PG{KqCMIU_FTECzByu{Np!h=zF4R>)
z-VpGEcuqNEgClQA{z`2nxsQM8VAzl1QW$g%)idq~?9cat;3qk8vFH7XlS=$a%!A51
zcq4j?moI}49@KfmjGx5ZWcP7X)1x(v&G1rXDgAD{TPfkBC`H28qJ+6T{70|k3~e<%
zFuHK-C>;M42byz5G{Nt`V2pg{g&-fy8n2KrS$FY@Xb3S^+xbU3o+h~^w7LB3V}|}$
zMc*!F)dA^)K?PQ!kOlLR_-z**!l}O_?{2BwRIyi<N)M3w$j}Nm<;AbKBCm
z$>8BlbEi;cAx=*tRjNaKZXuEqQCf3_BQOF^X{0kMZ?<|#yFE`aFik5}D}TCv)k-G?
zIGq}%alP&%eJlNMWfmSFJq3j~1&up}k`3<_iYa`XR?j8yJ4NXw*UN!YrU?3FXl1V7
zj_CrI8Gce39*lw>d?zk8)KOYpTUqR^n1E+EDMJc+&vIPvzjFX
zv|2b6DYd`HNDi8S+aI0Pz{na+d*~qJZJ0NmngA$vYEGe^E7vBTs?{Vxc(k;}CtcTb
z;82}K2WNczW7HIyUyRfis;d(kZ)AjXW1kn=4=zOFJscCmj0;|xsDwg09)T$rCQcXl
zqrLhtR0AdFgJYrpjL;OkkD&>qyEwk^X^vOEdf=Uf|BY72U6&dU?;qE+gO0>gBdm|J<%k@b^@pWX8PNx<@${n4b18%`NP%v*htr|e$r&Y
z{oNoBCPpVaot4Gz$U^b9AbJ2^D!SGm|LH$KDuc(O@%F;00eA$O7^sPH{V_nw5cp+p
ze$HWWwJMZ!B6J@tB|-II=|LX!Q3UChDDH*oBDTP>Vkz8paj?`^@@kjWCaX2H8ZNcW
z<_@5$45O6~l8_v{<#5mUI9>39SDUQ1N*
zk)bD7$A_dHUS91o+GMnLeLGh2^MPe!rNE&H524#i-J4Qon0d%B(}JF;36IKZZ6@5Y
z(@b@a2{qO-`WBFA?uX)Q;^#iFuUrZ*%;#Txm@or9$!V&`
zJgG?;KGlRtIa8G#9TA7k?x4GFP8k)77YOu`6PIVd5Au9ZD|v
z;D1@}cQih+QZ|(QO!olEKXu>g>a89wML48~`A(T!hBxKi$sE=28sKGzq`$}JT3e%$
z-F!yl?fSzZ#fY#a)vJZ8!HG7*ZE*3P+6=rLb9J+(sPWJe-j309b;V7UmWi(IGo)xi
zK6N+b@BGUE)xT8t^K3Z&uv7qhXG;FA5i_N&!s^XJpYCSN_>HsG3>=?NpWiCE3N!z>
z(2mzYXfk-qQaRV5lVkUVDSYNhur#n*gz)fWlqNbSn7zUx_fH?7qp*2~)WxNnBXz(F
zdj+`5(8mUJQm1TV1DZPfd_&X03y(S??PPEt(HjP5tx1SgyzMV&Bxn`7zbB7F-VCIhq%yK
ze|U9?6asQvEq)cTMA8LOzEM33QY%irqK}78i=>^9vQ!!eE0^LXlF%AQTE0lq9ehj@
z1y|3z_$Pd9m;J+O64bAkc
z4_qDXtA($ZqZ^={cxSDU@bjo(hCB88^PQJe+#
zF}(kqwZm%+EN2=o*Tpr`+k(`>ne6&}t@MEa*S`shgg4ho_}{KU7adP*FpFuYpKjp#
zqj;3AJL{xJ1(@}q`%0&B|8mZaz!tHP32%Tp9bNjUp_F#`ECw2c%Y!
z%9rMS_}L?hn$jHut1IzLuKhtNJ&W&J&N_z!v+1~-s%Keh+Tt&7O`XAd%PZ8iwTBhN5rplF823RE<2I8*J?Ljha39;So;p}DypsR=bSS&C&|f4
z?>XrSNgzRbNg(tddM^P21VR!LddG%diXf6j6Tu2f2}Q>S1g?S&1Pe+{P(($gh@c?l
zfA^l5a{}tU_y3>&e$N*KFdneLTX`unUjLxq?pBG>G*$@K6x!5j`Eh;dQs&*B=ePQNB$I;aFQu=zmQHN
znhQTxJPVV<)N55YoW+M+F$nW!Rs!0{Bl(pMmmghrxc=VZk@r}T7hT%z)tsh%0{Jy+
zuPxi@#T>@G7=U2}(x-c2bdO#x23yOU1I!p5wbBpplGuH=?)Ogu?U8*p)uJt`vOQ;E
zFycTeJ79aCK3jv#w~xQHHqc)GH?$D8R)8v|v+rYu@_cI!mHFYIEx{ba=UhHP6nDrr
zgkmqp25L_qvbC`K-Di#mIp|u!D21cq~+!df1kzi@4_w+ZIt}h?nRD_zzQy2_eWL77(KR
zG&z!5Zo`&3?1*iw70hsiaZP)*;<$CmM
z+YIV|!ZyMjz&B6WvPB-ZqNmQn&sG1Vtqp$@OA1XrX&Z-cP<;{#BYQa8@(OGl7%ZR+PT6XpzJ6;9)z+M~?Y4+~o7Z~P*w$EmSdJFnhA-RZbnzS8
zGZH(5*#DewZ3WTn-x=AZtioz*pejnrdh^s-Bc85F=WQdP#j{&K_ML5(&FquqM_WAX
zki;KtCoGIV<0PanZ28woEk$q-v$ECp_29DHfRl?F?3{#WsA|
z{E=++wTGwg({_RvmA4aO$@PjDN_H@W&aX_TEhV}<7<|O{AtH9P;t8PNBm&rEaO3~JI6AoErzJ_2X
zXGwsO&Bzm4)M|p%r8q#+^Mqgpcv~=`qx1effz#A4qH~X@1<<~pf^PuVbTD+#n-L3x
ztvXpBJuS4C;AIIYpTcNe-9EK+S7tgb%@acC+OTLRb?q(qbM$+#jz!QHC@|MBDzH*h
zDdd`MGM}`vafn*4Qe$Y_HCvdExr1}9iq2nyg1jgt+Q(Lg9*qCZwoVvdQdmqr*I~>A
zlwl;tT!%kqI&jXUP$23IS7S
zKxXg-Omp#0ftakO=wL_L_;|6%1tWIh)4MtEGUfFVI{YV#({Z20i5D0#%hBq-a7A-!
z)>lYp4+NgRaN?ASz0FZT7*-ZjX<1*P2@U;6ir?hJUcKSA7aMG`ZLKJRMmy^HRdT?JoQn#bDG{2|
z)K&Hn3LPtWJ571a7Sv<}+h)-_V_}q4_i_4ByEh^*=f(+vBA=w{44RpX)}}
zJMbxd(g1g@Jca;YzdN?Mrk=9k3Rs7JxC0Z|1H^YbyrP#w)NR-=w4qk*VZnE87|S8Z
z6AhQm(?UDgsS@q|!CR(zQ}O5D_wXS2C>pLz7ectDH6taNGLacyA%irf~fcB%Rc%9O=6OlfVmAxgx+<
zyWtQ(-^VchBG-fTgs9S=tVj(;@Aj
ze5S>e8Y8$2lgLoJ3>rqmtwZ}ySR%BqV}!MqaCdqcmOQX&wQ|eoNUj|G5Ig!mC6e*$
zdCQf}gcigKET&-D1J_^W
z9g7%bybcRrmrOHS8KP!;oG{bymm$lH?KQ>7V@hTbn5OYU3*FP?y9Y`pyA!R87wVX+
za!D?nj(g)E>wk|I`Wg#~A&aPIf)H!ADo+reMrWi%=;VyNmH5r7Vv$fVI8o^7mI2!m
zg?oB~canrQ=GFI-1Q>D@pDc7V4;3ZzL(7td3aXqb%H;hCgyN$rEUo(8one$S+dCo%
z^I_zaQ4)W4fQm%mh7+omw9fU3580k}c)5
zyswm4*Lb9-Ep#lNh30uJV-4hEmQ(AoN`M!8hTdt;59s%Tx82V@2(?#Cnz3Sxhqy1p
zyJCwsGV6_7QO`i1NV?p#jzr5<|8PAtWIiXAIZXp;{sqNf(w{^<-*6^U%(IFj@&mLn
z3CjC}Zgh9f`7aJvLzX4dkqT0UP@y;6Z`7pJmP02}g;@^>iu(XEL#X`;YoOLLO?V0P
z9(9FrG;6i!N3-iAG<0UL}I|`_3R%rPVS)JCJ7H@!q5Z3au$`)5IgV3$ALMY5h(xQA7bPwbY%*xJI{7o
zvQ3Smqx46}a3VC5@o%qoonr`eQ=0Rf{Z?8d+%GJYj?GPUBnx#q)&^4K9Hn4+8(w
z|6%m;A9qv71G_09t-c3+{IK9~3!T#sL#g7F(()t1469o}DO$y20w^iCz?!;HI7V0I
z%b`AqyfC8Q9xQJ-Rh#~VFxc{!4fxX)|Gww^r#P3FXX~Y9i{XZNC