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) + 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), + "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 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

)KI(tE8vLz1>EYY==C_bXUcP}f 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 B9zg&A delta 714871 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 z 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�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