From 950919d07ee77494d3390f358d2047edfce5b99b Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Thu, 29 May 2025 09:59:39 +0200 Subject: [PATCH] importation initiale --- .composer.pman.yml | 8 + .gitignore | 5 + .idea/.gitignore | 8 + .idea/misc.xml | 17 + .idea/php-docker-settings.xml | 23 + .idea/php.xml | 4 + .pman.conf | 11 + .runphp.conf | 8 + .udir | 30 + CHANGES.md | 1 + README.md | 31 + TODO.md | 15 + VERSION.txt | 1 + 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 | 554 +++++ 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 | 582 +++++ bash/src/base.num.sh | 30 + bash/src/base.output.sh | 603 ++++++ bash/src/base.path.sh | 304 +++ bash/src/base.sh | 22 + bash/src/base.split.sh | 188 ++ bash/src/base.str.sh | 140 ++ bash/src/base.tools.sh | 101 + bash/src/donk.build.sh | 4 + bash/src/donk.common.sh | 3 + bash/src/donk.help.sh | 41 + bash/src/fndate.sh | 45 + bash/src/git.sh | 226 ++ bash/src/nulib.sh | 1 + bash/src/pman.conf.sh | 14 + bash/src/pman.sh | 473 ++++ bash/src/pman74.conf.sh | 16 + bash/src/pman82.conf.sh | 16 + bash/src/pretty.sh | 200 ++ bash/src/sysinfos.sh | 4 + bash/src/template.sh | 240 +++ 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-interaction.sh | 29 + bash/tests/test-output.sh | 168 ++ bash/tests/test-template.sh | 29 + bash/tests/test-verbosity.sh | 24 + bin/_merge82 | 4 + bin/_pman-composer_local_deps.php | 14 + bin/_pman-composer_select_profile.php | 22 + bin/_runphp_build-all | 30 + bin/composer | 1 + bin/nlman | 69 + bin/nlshell | 38 + bin/p | 95 + bin/pman | 371 ++++ bin/pmer | 262 +++ bin/prel | 292 +++ bin/pwip | 60 + bin/runphp | 51 + bin/templ.md | 38 + bin/templ.sh | 56 + bin/templ.sql | 40 + bin/templ.yml | 38 + composer.json | 55 + composer.lock | 2038 ++++++++++++++++++ dockerfiles/Dockerfile.adminer | 31 + dockerfiles/Dockerfile.adminer+ic | 40 + dockerfiles/Dockerfile.mariadb10 | 19 + dockerfiles/Dockerfile.php-apache | 31 + dockerfiles/Dockerfile.php-apache+ic | 44 + dockerfiles/Dockerfile.php-cli | 30 + dockerfiles/Dockerfile.php-cli+ic | 43 + dockerfiles/Dockerfile.postgres15 | 17 + lib/completion.d/pman | 37 + lib/profile.d/nulib | 2 + lib/setup.sh | 11 + lib/uinst/conf | 20 + lib/uinst/rootconf | 5 + load.sh | 182 ++ php/run-tests | 5 + php/src/A.php | 351 +++ php/src/AccessException.php | 36 + php/src/ExceptionShadow.php | 101 + php/src/ExitError.php | 31 + php/src/IArrayWrapper.php | 11 + php/src/NoMoreDataException.php | 10 + php/src/StateException.php | 23 + php/src/StopException.php | 12 + php/src/UserException.php | 90 + php/src/ValueException.php | 76 + php/src/app/LockFile.php | 89 + php/src/app/RunFile.php | 497 +++++ php/src/app/args.php | 39 + php/src/app/cli/include-launcher.php | 29 + php/src/cl.php | 926 ++++++++ php/src/cv.php | 250 +++ php/src/db/Capacitor.php | 182 ++ php/src/db/CapacitorChannel.php | 492 +++++ php/src/db/CapacitorStorage.php | 728 +++++++ php/src/db/IDatabase.php | 26 + php/src/db/ITransactor.php | 33 + php/src/db/TODO.md | 7 + php/src/db/_private/Tbindings.php | 36 + php/src/db/_private/Tvalues.php | 35 + php/src/db/_private/_base.php | 85 + php/src/db/_private/_common.php | 255 +++ php/src/db/_private/_config.php | 36 + php/src/db/_private/_create.php | 54 + php/src/db/_private/_delete.php | 48 + php/src/db/_private/_generic.php | 24 + php/src/db/_private/_insert.php | 91 + php/src/db/_private/_migration.php | 99 + php/src/db/_private/_select.php | 182 ++ php/src/db/_private/_update.php | 53 + php/src/db/conds.php | 48 + php/src/db/mysql/Mysql.php | 14 + php/src/db/mysql/MysqlStorage.php | 65 + php/src/db/mysql/_mysqlMigration.php | 31 + php/src/db/mysql/_mysqlQuery.php | 8 + php/src/db/pdo/Pdo.php | 272 +++ php/src/db/pdo/_pdoQuery.php | 30 + php/src/db/pgsql/Pgsql.php | 309 +++ php/src/db/pgsql/PgsqlException.php | 15 + php/src/db/pgsql/PgsqlStorage.php | 60 + php/src/db/pgsql/_pgsqlMigration.php | 24 + php/src/db/pgsql/_pgsqlQuery.php | 44 + php/src/db/sqlite/Sqlite.php | 332 +++ php/src/db/sqlite/SqliteException.php | 18 + php/src/db/sqlite/SqliteStorage.php | 82 + php/src/db/sqlite/_sqliteMigration.php | 23 + php/src/db/sqlite/_sqliteQuery.php | 39 + php/src/ext/JsonException.php | 20 + php/src/ext/json.php | 66 + php/src/ext/yaml.php | 35 + php/src/file.php | 91 + php/src/file/FileReader.php | 51 + php/src/file/FileWriter.php | 31 + php/src/file/IReader.php | 45 + php/src/file/IWriter.php | 30 + php/src/file/MemoryStream.php | 21 + php/src/file/SharedFile.php | 15 + php/src/file/Stream.php | 507 +++++ php/src/file/TStreamFilter.php | 49 + php/src/file/TempStream.php | 28 + php/src/file/TmpfileWriter.php | 99 + php/src/file/_File.php | 39 + php/src/file/_IFile.php | 57 + php/src/file/csv/CsvBuilder.php | 34 + php/src/file/csv/CsvReader.php | 48 + php/src/file/csv/csv_flavours.php | 59 + php/src/file/tab/AbstractBuilder.php | 169 ++ php/src/file/tab/AbstractReader.php | 129 ++ php/src/file/tab/IBuilder.php | 14 + php/src/file/tab/IReader.php | 7 + php/src/file/tab/TAbstractBuilder.php | 56 + php/src/file/tab/TAbstractReader.php | 55 + php/src/file/web/Upload.php | 123 ++ php/src/os/EOFException.php | 14 + php/src/os/IOException.php | 42 + php/src/os/README.md | 7 + php/src/os/TODO.md | 8 + php/src/os/path.php | 318 +++ php/src/os/proc/AbstractCmd.php | 201 ++ php/src/os/proc/AbstractCmdList.php | 53 + php/src/os/proc/Cmd.php | 19 + php/src/os/proc/CmdAnd.php | 13 + php/src/os/proc/CmdOr.php | 13 + php/src/os/proc/CmdPipe.php | 81 + php/src/os/proc/ICmd.php | 82 + php/src/os/sh.php | 363 ++++ php/src/output/IMessenger.php | 107 + php/src/output/TODO.md | 35 + php/src/output/_messenger.php | 74 + php/src/output/console.php | 28 + php/src/output/log.php | 28 + php/src/output/msg.php | 67 + 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 | 168 ++ php/src/php/func.php | 726 +++++++ php/src/php/iter/AbstractIterator.php | 154 ++ php/src/php/mprop.php | 122 ++ php/src/php/oprop.php | 152 ++ php/src/php/time/Date.php | 20 + php/src/php/time/DateInterval.php | 59 + php/src/php/time/DateTime.php | 343 +++ php/src/php/time/Delay.php | 179 ++ 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_input.php | 42 + php/src/ref/schema/ref_schema.php | 86 + php/src/ref/schema/ref_types.php | 11 + php/src/ref/web/ref_mimetypes.php | 12 + php/src/str.php | 467 ++++ php/src/text/Word.php | 279 +++ php/src/text/words.php | 19 + php/src/tools/BgLauncherApp.php | 124 ++ php/src/tools/SteamTrainApp.php | 53 + php/src/tools/pman/ComposerFile.php | 172 ++ php/src/tools/pman/ComposerPmanFile.php | 110 + php/src/txt.php | 294 +++ php/src/web/curl/CurlException.php | 22 + php/src/web/curl/curl.php | 59 + php/src/web/http.php | 144 ++ php/src/web/params/F.php | 76 + php/src/web/params/G.php | 32 + php/src/web/params/P.php | 33 + php/src/web/params/R.php | 23 + php/src/web/uploads.php | 61 + php/support/copy-templates | 39 + php/support/template-.launcher.php | 12 + php/support/template-_bg_launcher.php | 18 + php/support/template-_wrapper.sh | 132 ++ php/tbin/.gitignore | 1 + php/tbin/test_mysql.php | 42 + php/tbin/test_pgsql.php | 41 + php/tbin/test_sqlite.php | 34 + php/tests/app/argsTest.php | 26 + php/tests/clTest.php | 37 + php/tests/db/_private/_baseTest.php | 22 + php/tests/db/sqlite/.gitignore | 1 + php/tests/db/sqlite/ChannelMigrationTest.php | 75 + php/tests/db/sqlite/SqliteStorageTest.php | 344 +++ php/tests/db/sqlite/SqliteTest.php | 146 ++ php/tests/db/sqlite/_queryTest.php | 125 ++ php/tests/db/sqlite/impl/MyChannel.php | 33 + php/tests/db/sqlite/impl/MyChannelV2.php | 14 + php/tests/db/sqlite/impl/MyChannelV3.php | 17 + 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/funcTest.php | 1200 +++++++++++ php/tests/php/time/DateTest.php | 85 + php/tests/php/time/DateTimeTest.php | 109 + php/tests/php/time/DelayTest.php | 83 + php/tests/strTest.php | 51 + php/tests/web/uploadsTest.php | 200 ++ runphp/Dockerfile.runphp | 30 + runphp/Dockerfile.runphp+ic | 43 + runphp/build | 256 +++ runphp/dot-build.env.dist | 21 + runphp/dot-dkbuild.env.dist | 12 + runphp/dot-runphp.conf | 8 + runphp/runphp | 662 ++++++ runphp/runphp.userconf.local | 3 + runphp/template.sh | 22 + runphp/update-runphp.sh | 125 ++ wip/_pci | 30 + wip/_pp | 22 + wip/_pu | 147 ++ wip/donk | 25 + wip/pman.md | 14 + 293 files changed, 30948 insertions(+) create mode 100644 .composer.pman.yml create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/misc.xml create mode 100644 .idea/php-docker-settings.xml create mode 100644 .idea/php.xml create mode 100644 .pman.conf create mode 100644 .runphp.conf create mode 100644 .udir create mode 100644 CHANGES.md create mode 100644 README.md create mode 100644 TODO.md create mode 100644 VERSION.txt create mode 100644 awk/src/base.array.awk create mode 100644 awk/src/base.awk create mode 100644 awk/src/base.core.awk create mode 100644 awk/src/base.date.awk create mode 100644 awk/src/base.tools.awk create mode 100644 awk/src/csv.awk create mode 100644 awk/src/enc.base64.awk create mode 100644 bash/TODO.md create mode 100644 bash/src/TEMPLATE create mode 100644 bash/src/_output_color.sh create mode 100644 bash/src/_output_vanilla.sh create mode 100644 bash/src/base.args.sh create mode 100644 bash/src/base.array.sh create mode 100644 bash/src/base.bool.sh create mode 100644 bash/src/base.core.sh create mode 100644 bash/src/base.init.sh create mode 100644 bash/src/base.input.sh create mode 100644 bash/src/base.num.sh create mode 100644 bash/src/base.output.sh create mode 100644 bash/src/base.path.sh create mode 100644 bash/src/base.sh create mode 100644 bash/src/base.split.sh create mode 100644 bash/src/base.str.sh create mode 100644 bash/src/base.tools.sh create mode 100644 bash/src/donk.build.sh create mode 100644 bash/src/donk.common.sh create mode 100644 bash/src/donk.help.sh create mode 100644 bash/src/fndate.sh create mode 100644 bash/src/git.sh create mode 120000 bash/src/nulib.sh create mode 100644 bash/src/pman.conf.sh create mode 100644 bash/src/pman.sh create mode 100644 bash/src/pman74.conf.sh create mode 100644 bash/src/pman82.conf.sh create mode 100644 bash/src/pretty.sh create mode 100644 bash/src/sysinfos.sh create mode 100644 bash/src/template.sh create mode 100644 bash/src/tests.sh create mode 100644 bash/tests/.gitignore create mode 100644 bash/tests/_template-dest.txt create mode 100644 bash/tests/_template-source.txt create mode 100755 bash/tests/_template-source_envs create mode 100644 bash/tests/_template-values.env create mode 100755 bash/tests/test-args-autodebug.sh create mode 100755 bash/tests/test-args-autohelp.sh create mode 100755 bash/tests/test-args-base.sh create mode 100755 bash/tests/test-args-help.sh create mode 100755 bash/tests/test-input.sh create mode 100755 bash/tests/test-interaction.sh create mode 100755 bash/tests/test-output.sh create mode 100755 bash/tests/test-template.sh create mode 100755 bash/tests/test-verbosity.sh create mode 100755 bin/_merge82 create mode 100755 bin/_pman-composer_local_deps.php create mode 100755 bin/_pman-composer_select_profile.php create mode 100755 bin/_runphp_build-all create mode 120000 bin/composer create mode 100755 bin/nlman create mode 100755 bin/nlshell create mode 100755 bin/p create mode 100755 bin/pman create mode 100755 bin/pmer create mode 100755 bin/prel create mode 100755 bin/pwip create mode 100755 bin/runphp create mode 100755 bin/templ.md create mode 100755 bin/templ.sh create mode 100755 bin/templ.sql create mode 100755 bin/templ.yml create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 dockerfiles/Dockerfile.adminer create mode 100644 dockerfiles/Dockerfile.adminer+ic create mode 100644 dockerfiles/Dockerfile.mariadb10 create mode 100644 dockerfiles/Dockerfile.php-apache create mode 100644 dockerfiles/Dockerfile.php-apache+ic create mode 100644 dockerfiles/Dockerfile.php-cli create mode 100644 dockerfiles/Dockerfile.php-cli+ic create mode 100644 dockerfiles/Dockerfile.postgres15 create mode 100644 lib/completion.d/pman create mode 100644 lib/profile.d/nulib create mode 100755 lib/setup.sh create mode 100644 lib/uinst/conf create mode 100644 lib/uinst/rootconf create mode 100644 load.sh create mode 100755 php/run-tests create mode 100644 php/src/A.php create mode 100644 php/src/AccessException.php create mode 100644 php/src/ExceptionShadow.php create mode 100644 php/src/ExitError.php create mode 100644 php/src/IArrayWrapper.php create mode 100644 php/src/NoMoreDataException.php create mode 100644 php/src/StateException.php create mode 100644 php/src/StopException.php create mode 100644 php/src/UserException.php create mode 100644 php/src/ValueException.php create mode 100644 php/src/app/LockFile.php create mode 100644 php/src/app/RunFile.php create mode 100644 php/src/app/args.php create mode 100644 php/src/app/cli/include-launcher.php create mode 100644 php/src/cl.php create mode 100644 php/src/cv.php create mode 100644 php/src/db/Capacitor.php create mode 100644 php/src/db/CapacitorChannel.php create mode 100644 php/src/db/CapacitorStorage.php create mode 100644 php/src/db/IDatabase.php create mode 100644 php/src/db/ITransactor.php create mode 100644 php/src/db/TODO.md create mode 100644 php/src/db/_private/Tbindings.php create mode 100644 php/src/db/_private/Tvalues.php create mode 100644 php/src/db/_private/_base.php create mode 100644 php/src/db/_private/_common.php create mode 100644 php/src/db/_private/_config.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/_migration.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/conds.php create mode 100644 php/src/db/mysql/Mysql.php create mode 100644 php/src/db/mysql/MysqlStorage.php create mode 100644 php/src/db/mysql/_mysqlMigration.php create mode 100644 php/src/db/mysql/_mysqlQuery.php create mode 100644 php/src/db/pdo/Pdo.php create mode 100644 php/src/db/pdo/_pdoQuery.php create mode 100644 php/src/db/pgsql/Pgsql.php create mode 100644 php/src/db/pgsql/PgsqlException.php create mode 100644 php/src/db/pgsql/PgsqlStorage.php create mode 100644 php/src/db/pgsql/_pgsqlMigration.php create mode 100644 php/src/db/pgsql/_pgsqlQuery.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/_sqliteMigration.php create mode 100644 php/src/db/sqlite/_sqliteQuery.php create mode 100644 php/src/ext/JsonException.php create mode 100644 php/src/ext/json.php create mode 100644 php/src/ext/yaml.php create mode 100644 php/src/file.php create mode 100644 php/src/file/FileReader.php create mode 100644 php/src/file/FileWriter.php create mode 100644 php/src/file/IReader.php create mode 100644 php/src/file/IWriter.php create mode 100644 php/src/file/MemoryStream.php create mode 100644 php/src/file/SharedFile.php create mode 100644 php/src/file/Stream.php create mode 100644 php/src/file/TStreamFilter.php create mode 100644 php/src/file/TempStream.php create mode 100644 php/src/file/TmpfileWriter.php create mode 100644 php/src/file/_File.php create mode 100644 php/src/file/_IFile.php create mode 100644 php/src/file/csv/CsvBuilder.php create mode 100644 php/src/file/csv/CsvReader.php create mode 100644 php/src/file/csv/csv_flavours.php create mode 100644 php/src/file/tab/AbstractBuilder.php create mode 100644 php/src/file/tab/AbstractReader.php create mode 100644 php/src/file/tab/IBuilder.php create mode 100644 php/src/file/tab/IReader.php create mode 100644 php/src/file/tab/TAbstractBuilder.php create mode 100644 php/src/file/tab/TAbstractReader.php create mode 100644 php/src/file/web/Upload.php create mode 100644 php/src/os/EOFException.php create mode 100644 php/src/os/IOException.php create mode 100644 php/src/os/README.md create mode 100644 php/src/os/TODO.md create mode 100644 php/src/os/path.php create mode 100644 php/src/os/proc/AbstractCmd.php create mode 100644 php/src/os/proc/AbstractCmdList.php create mode 100644 php/src/os/proc/Cmd.php create mode 100644 php/src/os/proc/CmdAnd.php create mode 100644 php/src/os/proc/CmdOr.php create mode 100644 php/src/os/proc/CmdPipe.php create mode 100644 php/src/os/proc/ICmd.php create mode 100644 php/src/os/sh.php create mode 100644 php/src/output/IMessenger.php create mode 100644 php/src/output/TODO.md create mode 100644 php/src/output/_messenger.php create mode 100644 php/src/output/console.php create mode 100644 php/src/output/log.php create mode 100644 php/src/output/msg.php create mode 100644 php/src/output/out.php create mode 100644 php/src/output/say.php create mode 100644 php/src/output/std/ProxyMessenger.php create mode 100644 php/src/output/std/StdMessenger.php create mode 100644 php/src/output/std/StdOutput.php create mode 100644 php/src/output/std/_IMessenger.php create mode 100644 php/src/php/ICloseable.php create mode 100644 php/src/php/README.md create mode 100644 php/src/php/akey.php create mode 100644 php/src/php/coll/AutoArray.php create mode 100644 php/src/php/coll/BaseArray.php create mode 100644 php/src/php/content/IContent.php create mode 100644 php/src/php/content/IPrintable.php create mode 100644 php/src/php/content/Printer.php create mode 100644 php/src/php/content/README.md create mode 100644 php/src/php/content/c.php create mode 100644 php/src/php/func.php create mode 100644 php/src/php/iter/AbstractIterator.php create mode 100644 php/src/php/mprop.php create mode 100644 php/src/php/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_input.php create mode 100644 php/src/ref/schema/ref_schema.php create mode 100644 php/src/ref/schema/ref_types.php create mode 100644 php/src/ref/web/ref_mimetypes.php create mode 100644 php/src/str.php create mode 100644 php/src/text/Word.php create mode 100644 php/src/text/words.php create mode 100644 php/src/tools/BgLauncherApp.php create mode 100644 php/src/tools/SteamTrainApp.php create mode 100644 php/src/tools/pman/ComposerFile.php create mode 100644 php/src/tools/pman/ComposerPmanFile.php create mode 100644 php/src/txt.php create mode 100644 php/src/web/curl/CurlException.php create mode 100644 php/src/web/curl/curl.php create mode 100644 php/src/web/http.php create mode 100644 php/src/web/params/F.php create mode 100644 php/src/web/params/G.php create mode 100644 php/src/web/params/P.php create mode 100644 php/src/web/params/R.php create mode 100644 php/src/web/uploads.php create mode 100755 php/support/copy-templates create mode 100644 php/support/template-.launcher.php create mode 100644 php/support/template-_bg_launcher.php create mode 100644 php/support/template-_wrapper.sh create mode 100644 php/tbin/.gitignore create mode 100644 php/tbin/test_mysql.php create mode 100644 php/tbin/test_pgsql.php create mode 100644 php/tbin/test_sqlite.php create mode 100644 php/tests/app/argsTest.php create mode 100644 php/tests/clTest.php create mode 100644 php/tests/db/_private/_baseTest.php create mode 100644 php/tests/db/sqlite/.gitignore create mode 100644 php/tests/db/sqlite/ChannelMigrationTest.php 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/db/sqlite/impl/MyChannel.php create mode 100644 php/tests/db/sqlite/impl/MyChannelV2.php create mode 100644 php/tests/db/sqlite/impl/MyChannelV3.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/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/strTest.php create mode 100644 php/tests/web/uploadsTest.php create mode 100644 runphp/Dockerfile.runphp create mode 100644 runphp/Dockerfile.runphp+ic create mode 100755 runphp/build create mode 100644 runphp/dot-build.env.dist create mode 100644 runphp/dot-dkbuild.env.dist create mode 100644 runphp/dot-runphp.conf create mode 100755 runphp/runphp create mode 100644 runphp/runphp.userconf.local create mode 100755 runphp/template.sh create mode 100755 runphp/update-runphp.sh create mode 100755 wip/_pci create mode 100755 wip/_pp create mode 100755 wip/_pu create mode 100755 wip/donk create mode 100644 wip/pman.md diff --git a/.composer.pman.yml b/.composer.pman.yml new file mode 100644 index 0000000..12513d0 --- /dev/null +++ b/.composer.pman.yml @@ -0,0 +1,8 @@ +# -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8 + +composer: + profiles: [ dev, dist ] + dev: + link: true + dist: + link: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3c469c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ + +.~lock*# +.*.swp +/php/vendor/ +/.phpunit.result.cache diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 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/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..3a1e22c --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + Angular + + + + + + \ No newline at end of file diff --git a/.idea/php-docker-settings.xml b/.idea/php-docker-settings.xml new file mode 100644 index 0000000..bd786be --- /dev/null +++ b/.idea/php-docker-settings.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..f6159c8 --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.pman.conf b/.pman.conf new file mode 100644 index 0000000..ba5edad --- /dev/null +++ b/.pman.conf @@ -0,0 +1,11 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +UPSTREAM= +DEVELOP=dev74 +FEATURE=wip74/ +RELEASE=rel74- +MAIN=dist74 +TAG_SUFFIX=p74 +HOTFIX=hotf74- +DIST= +NOAUTO= diff --git a/.runphp.conf b/.runphp.conf new file mode 100644 index 0000000..75c5696 --- /dev/null +++ b/.runphp.conf @@ -0,0 +1,8 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +# Chemin vers runphp, e.g sbin/runphp +RUNPHP= + +# Si RUNPHP n'est pas défini, les variables suivantes peuvent être définies +DIST=d11 +#REGISTRY=pubdocker.univ-reunion.fr/dist diff --git a/.udir b/.udir new file mode 100644 index 0000000..f0bc74b --- /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-base) +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/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..bea6044 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1 @@ +29/05/2025 Renommer le répertoire de nulib à nulib-base et le package de nulib/php à nulib/base diff --git a/README.md b/README.md new file mode 100644 index 0000000..ddd0939 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# nulib/base + + +## Release + +Exemple: release de la version 0.6.0 +~~~sh +version=0.6.0 + +## branche dev74 +git checkout dev74 + +prel -v$version + +_merge82 + +## branche dev82 +git checkout dev82 + +prel -C + +commit="$(git log --grep="Init changelog . version ${version}p82" --format=%H)" && +echo "commit=$commit" + +git checkout dev74 + +git cherry-pick "$commit" +pp -a +~~~ + +-*- 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/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/VERSION.txt b/VERSION.txt new file mode 100644 index 0000000..a918a2a --- /dev/null +++ b/VERSION.txt @@ -0,0 +1 @@ +0.6.0 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..afe6428 --- /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" - + + recho "$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" + recho "$COULEUR_BLEUE$line-$COULEUR_NORMALE" + done + recho "$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} + recho "${prefix}${COULEUR_BLEUE}T $line$COULEUR_NORMALE" + done + maxlen=$((maxlen + 2)) + recho "${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 + recho "${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" = + + recho "$COULEUR_ROUGE$lsep" + length=$((length - 1)) + setx -a lines=echo "$1" + for line in "" "${lines[@]}" ""; do + setx line=__complete "$prefix= $line" "$length" + recho "$line=" + done + recho "$lsep$COULEUR_NORMALE" +} +function __eimportant() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}!${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __eattention() { recho "$(__edate)$(__eindent0)${COULEUR_JAUNE}*${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __eerror() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}E${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __ewarn() { recho "$(__edate)$(__eindent0)${COULEUR_JAUNE}W${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __enote() { recho "$(__edate)$(__eindent0)${COULEUR_VERTE}N${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __einfo() { recho "$(__edate)$(__eindent0)${COULEUR_BLEUE}I${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __edebug() { recho "$(__edate)$(__eindent0)${COULEUR_BLANCHE}D${COULEUR_NORMALE} $(__eindent "$1" " ")"; } + +function __estep() { recho "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepe() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepw() { recho "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepn() { recho "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepi() { recho "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estep_() { recho_ "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepe_() { recho_ "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepw_() { recho_ "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepn_() { recho_ "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepi_() { recho_ "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } + +function __action() { recho "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __asuccess() { recho "$(__edate)$(__eindent0)${COULEUR_VERTE}✔${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __afailure() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}✘${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __adone() { recho "$(__edate)$(__eindent0)$(__eindent "$1")"; } diff --git a/bash/src/_output_vanilla.sh b/bash/src/_output_vanilla.sh new file mode 100644 index 0000000..cbd466f --- /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" - + + recho "$lsep" + [ -n "$*" ] || return 0 + length=$((length - 1)) + setx -a lines=echo "$1" + for line in "${lines[@]}"; do + setx line=__complete "$prefix- $line" "$length" + recho "$line-" + done + recho "$lsep" +} +function __etitle() { + local p="TITLE: " i=" " + recho "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")" +} +function __edesc() { + local p="DESC: " i=" " + recho "$(__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" = + + recho "$lsep" + length=$((length - 1)) + setx -a lines=echo "$1" + for line in "" "${lines[@]}" ""; do + setx line=__complete "$prefix= $line" "$length" + recho "$line=" + done + recho "$lsep" +} +function __eimportant() { recho "$(__edate)$(__eindent0)IMPORTANT! $(__eindent "$1" " ")"; } +function __eattention() { recho "$(__edate)$(__eindent0)ATTENTION! $(__eindent "$1" " ")"; } +function __eerror() { recho "$(__edate)$(__eindent0)ERROR: $(__eindent "$1" " ")"; } +function __ewarn() { recho "$(__edate)$(__eindent0)WARNING: $(__eindent "$1" " ")"; } +function __enote() { recho "$(__edate)$(__eindent0)NOTE: $(__eindent "$1" " ")"; } +function __einfo() { recho "$(__edate)$(__eindent0)INFO: $(__eindent "$1" " ")"; } +function __edebug() { recho "$(__edate)$(__eindent0)DEBUG: $(__eindent "$1" " ")"; } +function __eecho() { recho "$(__edate)$(__eindent0)$(__eindent "$1")"; } +function __eecho_() { recho_ "$(__edate)$(__eindent0)$(__eindent "$1")"; } + +function __estep() { recho "$(__edate)$(__eindent0). $(__eindent "$1" " ")"; } +function __estepe() { recho "$(__edate)$(__eindent0).E $(__eindent "$1" " ")"; } +function __estepw() { recho "$(__edate)$(__eindent0).W $(__eindent "$1" " ")"; } +function __estepn() { recho "$(__edate)$(__eindent0).N $(__eindent "$1" " ")"; } +function __estepi() { recho "$(__edate)$(__eindent0).I $(__eindent "$1" " ")"; } +function __estep_() { recho_ "$(__edate)$(__eindent0). $(__eindent "$1" " ")"; } +function __estepe_() { recho_ "$(__edate)$(__eindent0).E $(__eindent "$1" " ")"; } +function __estepw_() { recho_ "$(__edate)$(__eindent0).W $(__eindent "$1" " ")"; } +function __estepn_() { recho_ "$(__edate)$(__eindent0).N $(__eindent "$1" " ")"; } +function __estepi_() { recho_ "$(__edate)$(__eindent0).I $(__eindent "$1" " ")"; } + +function __action() { recho "$(__edate)$(__eindent0)ACTION: $(__eindent "$1" " ")"; } +function __asuccess() { recho "$(__edate)$(__eindent0)(OK) $(__eindent "$1" " ")"; } +function __afailure() { recho "$(__edate)$(__eindent0)(KO) $(__eindent "$1" " ")"; } +function __adone() { recho "$(__edate)$(__eindent0)$(__eindent "$1")"; } diff --git a/bash/src/base.args.sh b/bash/src/base.args.sh new file mode 100644 index 0000000..43e63ae --- /dev/null +++ b/bash/src/base.args.sh @@ -0,0 +1,554 @@ +# -*- 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" + [ -n "$NULIB_ARGS_ONERROR_RETURN" ] && set_die_return + local __r= + + 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=("$@") + __nulib_args_parse_args || __r=1 + fi + eval "$NULIB__ENABLE_SET_X" + if [ -n "$__r" ]; then + die || return + fi +} +function __nulib_args_add_sopt() { + local noauto="$1"; shift + local def + for def in "$@"; do + array_contains "$noauto" "$def" || __sopts="$__sopts$def" + done +} +function __nulib_args_add_lopt() { + local noauto="$1"; shift + local def + for def in "$@"; do + array_contains "$noauto" "$def" || __lopts="$__lopts${__lopts:+,}$def" + done +} +function __nulib_args_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 -a __NOAUTOL __NOAUTOLOGTO # faut-il rajouter les options pour gérer la journalisation? + local -a __NOAUTOV __NOAUTOVERBOSITY # options de verbosité qui ont été définies par l'utilisateur + local -a __NOAUTOI __NOAUTOINTERACTION # options d'interaction qui ont été définies par l'utilisateur + 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;; + *) die "Invalid arg definition: expected option, got '$1'" || return;; + esac + # est-ce que l'option prend un argument? + local __def __longdef __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%%:*}" + __longdef= + if [[ "$__def" == --* ]]; then + # --longopt + __def="${__def#--}" + __lopts="$__lopts${__lopts:+,}$__def$__witharg" + __longdef=1 + elif [[ "$__def" == -* ]] && [ ${#__def} -eq 2 ]; then + # -o + __def="${__def#-}" + __sopts="$__sopts$__def$__witharg" + case "$__def" in + h) __AUTOH=;; + L) __NOAUTOL+=("-$__def");; + Q|q|v|D) __NOAUTOV+=("-$__def");; + b|y|i) __NOAUTOI+=("-$__def");; + esac + else + # -longopt ou longopt + __def="${__def#-}" + __lopts="$__lopts${__lopts:+,}$__def$__witharg" + __longdef=1 + fi + if [ -n "$__longdef" ]; then + case "$__def" in + help|help++) __AUTOHELP=;; + log-to) __NOAUTOLOGTO+=("--$__def");; + very-quiet|quiet|verbose|debug) __NOAUTOVERBOSITY+=("--$__def");; + batch|automatic|interactive) __NOAUTOINTERACTION+=("--$__def");; + esac + fi + 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++" + __nulib_args_add_sopt __NOAUTOL L + __nulib_args_add_lopt __NOAUTOLOGTO log-to + __nulib_args_add_sopt __NOAUTOV Q q v D + __nulib_args_add_lopt __NOAUTOVERBOSITY very-quiet quiet verbose debug + __nulib_args_add_sopt __NOAUTOI b y i + __nulib_args_add_lopt __NOAUTOINTERACTION batch automatic interactive + + __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 + die || return + 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 + __nulib_args_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 __nulib_args_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 + case "$option_" in + -h) + if [ -n "$__AUTOH" ]; then + __action='showhelp@' + return 0 + fi + ;; + --help) + if [ -n "$__AUTOHELP" ]; then + __action='showhelp@' + return 0 + fi + ;; + --help++) + if [ -n "$__AUTOHELP" ]; then + __action='showhelp@ ++' + return 0 + fi + ;; + -L) + if ! array_contains __NOAUTOL "$option_"; then + __action='elogto $value_' + return 0 + fi + ;; + --log-to) + if ! array_contains __NOAUTOL "$option_"; then + __action='elogto $value_' + return 0 + fi + ;; + -Q|-q|-v|-D) + if ! array_contains __NOAUTOV "$option_"; then + __action='set_verbosity $option_' + return 0 + fi + ;; + --very-quiet|--quiet|--verbose|--debug) + if ! array_contains __NOAUTOVERBOSITY "$option_"; then + __action='set_verbosity $option_' + return 0 + fi + ;; + -b|-y|-i) + if ! array_contains __NOAUTOI "$option_"; then + __action='set_interaction $option_' + return 0 + fi + ;; + --batch|--automatic|--interactive) + if ! array_contains __NOAUTOINTERACTION "$option_"; then + __action='set_interaction $option_' + return 0 + fi + ;; + esac + # ici, l'option n'a pas été trouvée, on ne devrait pas arriver ici + die "Unexpected option '$option_'" || return +} 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..c81fbc8 --- /dev/null +++ b/bash/src/base.input.sh @@ -0,0 +1,582 @@ +# -*- 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 + echo_ " $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 + echo_ ": " 1>&2 + uread -e ${__rv_d:+-i"$__rv_d"} "${__rv_opts[@]}" __rv_r + else + if [ -n "$__rv_d" ]; then + if [ -n "$__rv_showdef" ]; then + echo_ " [$__rv_d]" 1>&2 + else + echo_ " [****]" 1>&2 + fi + fi + echo_ ": " 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 + echo + 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 + echo_ ": " 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 + echo 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 + recho "$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 + echo_ ": " 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 + echo 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 + echo "$i*- $option" 1>&2 + else + echo "$i - $option" 1>&2 + fi + let i=$i+1 + done + __estepn_ "Actions disponibles: " 1>&2 + recho "$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 + echo_ ": " 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 + echo "" 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..15f2040 --- /dev/null +++ b/bash/src/base.output.sh @@ -0,0 +1,603 @@ +# -*- 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 uecho() { +# $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_() { +# $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 +} + +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=recho "$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() { + case "$1" in + -D|--debug) NULIB_DEBUG=1;; + esac +} +function check_verbosity() { return 0; } +function get_verbosity_option() { :;} + +# tester respectivement si on doit afficher les messages d'erreur, +# d'avertissement, d'information, de debug +function show_error() { return 0; } +function show_warn() { return 0; } +function show_info() { return 0; } +function show_verbose() { return 0; } +function show_debug() { [ -n "$NULIB_DEBUG" ]; } + +# note: toutes les fonctions d'affichage e* écrivent sur stderr + +function esection() { +# Afficher une section. Toutes les indentations sont remises à zéro + show_info || return + eval "$NULIB__DISABLE_SET_X" + NULIB__ESTACK= + __esection "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} + +function etitle() { +# Afficher le titre $1. Le contenu sous des titres imbriqués est affiché +# indenté. +# - si $2..$* est spécifié, c'est une commande qui est lancée dans le contexte +# du titre, ensuite le titre est automatiquement terminé +# - sinon il faut terminer le titre explicitement avec eend + local title="$1"; shift + # etitle + NULIB__ESTACK="$NULIB__ESTACK:t" + if show_info; then + eval "$NULIB__DISABLE_SET_X" + __etitle "$title" 1>&2 + eval "$NULIB__ENABLE_SET_X" + fi + # commande + local r=0 + if [ $# -gt 0 ]; then + "$@"; r=$? + eend + fi + return $r +} + + +function eend() { +# Terminer un titre + if [ "${NULIB__ESTACK%:t}" != "$NULIB__ESTACK" ]; then + NULIB__ESTACK="${NULIB__ESTACK%:t}" + fi +} + +function edesc() { +# Afficher une description sous le titre courant + show_info || return + eval "$NULIB__DISABLE_SET_X" + __edesc "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} + +function ebanner() { +# Afficher un message très important encadré, puis attendre 5 secondes + show_error || return + eval "$NULIB__DISABLE_SET_X" + __ebanner "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" + sleep 5 +} + +function eimportant() { +# Afficher un message très important + show_error || return + eval "$NULIB__DISABLE_SET_X" + __eimportant "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} +function qimportant() { eimportant "$*"; quietc_echo "IMPORTANT: $*"; } + +function eattention() { +# Afficher un message important + show_warn || return + eval "$NULIB__DISABLE_SET_X" + __eattention "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} +function qattention() { eattention "$*"; quietc_echo "ATTENTION: $*"; } + +function eerror() { +# Afficher un message d'erreur + show_error || return + eval "$NULIB__DISABLE_SET_X" + __eerror "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} +function qerror() { eerror "$*"; quietc_echo "ERROR: $*"; } + +function eerror_unless() { + # Afficher $1 avec eerror() si la commande $2..@ retourne FAUX. dans tous les cas, retourner le code de retour de la commande. + local msg="$1"; shift + local r=1 + if [ $# -eq 0 ]; then + [ -n "$msg" ] && eerror "$msg" + else + "$@"; r=$? + if [ $r -ne 0 -a -n "$msg" ]; then + eerror "$msg" + fi + fi + return $r +} + +function eerror_if() { + # Afficher $1 avec eerror() si la commande $2..@ retourne VRAI. dans tous les cas, retourner le code de retour de la commande. + local msg="$1"; shift + local r=0 + if [ $# -gt 0 ]; then + "$@"; r=$? + if [ $r -eq 0 -a -n "$msg" ]; then + eerror "$msg" + fi + fi + return $r +} + +function set_die_return() { + NULIB__DIE="return 1" +} +function die() { + [ $# -gt 0 ] && eerror "$@" + local die="${NULIB__DIE:-exit 1}" + eval "$die" || return +} + +function die_with { + [ $# -gt 0 ] && eerror "$1" + shift + [ $# -gt 0 ] && "$@" + local die="${NULIB__DIE:-exit 1}" + eval "$die" || return +} + +function die_unless() { + # Afficher $1 et quitter le script avec die() si la commande $2..@ retourne FAUX + local msg="$1"; shift + local die="${NULIB__DIE:-exit 1}" + local r=1 + if [ $# -eq 0 ]; then + [ -n "$msg" ] && eerror "$msg" + else + "$@"; r=$? + if [ $r -ne 0 -a -n "$msg" ]; then + eerror "$msg" + fi + fi + if [ $r -ne 0 ]; then + eval "${die% *} $r" || return $r + fi + return $r +} + +function die_if() { + # Afficher $1 et quitter le script avec die() si la commande $2..@ retourne VRAI. sinon, retourner le code de retour de la commande + local msg="$1"; shift + local die="${NULIB__DIE:-exit 1}" + local r=0 + if [ $# -gt 0 ]; then + "$@"; r=$? + if [ $r -eq 0 -a -n "$msg" ]; then + eerror "$msg" + fi + fi + if [ $r -eq 0 ]; then + eval "${die% *} $r" || return $r + fi + return $r +} + +function exit_with { + if [ $# -gt 0 ]; then "$@"; fi + exit $? +} + +function ewarn() { +# Afficher un message d'avertissement + show_warn || return + eval "$NULIB__DISABLE_SET_X" + __ewarn "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} +function qwarn() { ewarn "$*"; quietc_echo "WARN: $*"; } + +function enote() { +# Afficher un message d'information de même niveau qu'un avertissement + show_info || return + eval "$NULIB__DISABLE_SET_X" + __enote "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} +function qnote() { enote "$*"; quietc_echo "NOTE: $*"; } + +function einfo() { +# Afficher un message d'information + show_info || return + eval "$NULIB__DISABLE_SET_X" + __einfo "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} +function qinfo() { einfo "$*"; quietc_echo "INFO: $*"; } + +function eecho() { +# Afficher un message d'information sans préfixe + show_info || return + eval "$NULIB__DISABLE_SET_X" + __eecho "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} +function qecho() { eecho "$*"; quietc_echo "$*"; } + +function eecho_() { + show_info || return + eval "$NULIB__DISABLE_SET_X" + __eecho_ "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} + +function trace() { +# Afficher la commande $1..@, la lancer, puis afficher son code d'erreur si une +# erreur se produit + local r cmd="$(qvals "$@")" + show_info && { __eecho "\$ $cmd" 1>&2; } + "$@"; r=$? + if [ $r -ne 0 ]; then + if show_info; then + __eecho "^ [EC #$r]" 1>&2 + elif show_error; then + __eecho "^ $cmd [EC #$r]" 1>&2 + fi + fi + return $r +} + +function trace_error() { +# Lancer la commande $1..@, puis afficher son code d'erreur si une erreur se +# produit. La différence avec trace() est que la commande n'est affichée que si +# une erreur se produit. + local r + "$@"; r=$? + if [ $r -ne 0 ]; then + local cmd="$(qvals "$@")" + if show_error; then + __eecho "^ $cmd [EC #$r]" 1>&2 + fi + fi + return $r +} + +function edebug() { +# Afficher un message de debug + show_debug || return + eval "$NULIB__DISABLE_SET_X" + __edebug "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} + +# Afficher la description d'une opération. Cette fonction est particulièrement +# appropriée dans le contexte d'un etitle. +# Les variantes e (error), w (warning), n (note), i (info) permettent d'afficher +# des couleurs différentes, mais toutes sont du niveau info. +function estep() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estep "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepe() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepe "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepw() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepw "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepn() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepn "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepi() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepi "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estep_() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estep_ "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepe_() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepe_ "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepw_() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepw_ "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepn_() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepn_ "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepi_() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepi_ "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } + +function qstep() { estep "$*"; quietc_echo "* $*"; } + +function action() { +# commencer l'action $1 +# - si $2..$* est spécifié, c'est une commande qui est lancée dans le contexte +# de l'action, ensuite l'action est terminée en succès ou en échec suivant le +# code de retour. ne pas afficher la sortie de la commande comme avec quietc() +# - sinon il faut terminer le titre explicitement avec eend + eval "$NULIB__DISABLE_SET_X" + local action="$1"; shift + local r=0 + if [ $# -gt 0 ]; then + [ -z "$NULIB__TMPLOG" ] && ac_set_tmpfile NULIB__TMPLOG + [ -n "$action" ] && quietc_echo "$(__edate) ACTION: $action:" + "$@" >&"$NULIB__TMPLOG"; r=$? + [ -n "$NULIB_QUIETLOG" ] && cat "$NULIB__TMPLOG" >>"$NULIB_QUIETLOG" + if [ $r -ne 0 -o -n "$NULIB_DEBUG" ]; then + NULIB__ESTACK="$NULIB__ESTACK:a" + [ -n "$action" ] && action="$action:" + __action "$action" 1>&2 + cat "$NULIB__TMPLOG" + aresult $r + else + if [ $r -eq 0 ]; then + [ -n "$action" ] || action="succès" + __asuccess "$action" 1>&2 + else + [ -n "$action" ] || action="échec" + __afailure "$action" 1>&2 + fi + fi + else + NULIB__ESTACK="$NULIB__ESTACK:a" + [ -n "$action" ] && action="$action:" + __action "$action" 1>&2 + fi + eval "$NULIB__ENABLE_SET_X" + return $r +} + +function asuccess() { +# terminer l'action en cours avec le message de succès $* + [ "${NULIB__ESTACK%:a}" != "$NULIB__ESTACK" ] || return + eval "$NULIB__DISABLE_SET_X" + [ -n "$*" ] || set -- "succès" + NULIB__INDENT=" " __asuccess "$*" 1>&2 + NULIB__ESTACK="${NULIB__ESTACK%:a}" + eval "$NULIB__ENABLE_SET_X" + return 0 +} +function afailure() { +# terminer l'action en cours avec le message d'échec $* + [ "${NULIB__ESTACK%:a}" != "$NULIB__ESTACK" ] || return + eval "$NULIB__DISABLE_SET_X" + [ -n "$*" ] || set -- "échec" + NULIB__INDENT=" " __afailure "$*" 1>&2 + NULIB__ESTACK="${NULIB__ESTACK%:a}" + eval "$NULIB__ENABLE_SET_X" + return 1 +} +function aresult() { +# terminer l'action en cours avec un message de succès ou d'échec $2..* en +# fonction du code de retour $1 (0=succès, sinon échec) + [ "${NULIB__ESTACK%:a}" != "$NULIB__ESTACK" ] || return + eval "$NULIB__DISABLE_SET_X" + local r="${1:-0}"; shift + if [ "$r" == 0 ]; then + [ -n "$*" ] || set -- "succès" + NULIB__INDENT=" " __asuccess "$*" 1>&2 + else + [ -n "$*" ] || set -- "échec" + NULIB__INDENT=" " __afailure "$*" 1>&2 + fi + NULIB__ESTACK="${NULIB__ESTACK%:a}" + eval "$NULIB__ENABLE_SET_X" + return $r +} +function adone() { +# terminer l'action en cours avec le message neutre $* + [ "${NULIB__ESTACK%:a}" != "$NULIB__ESTACK" ] || return + eval "$NULIB__DISABLE_SET_X" + [ -n "$*" ] && NULIB__INDENT=" " __adone "$*" 1>&2 + NULIB__ESTACK="${NULIB__ESTACK%:a}" + eval "$NULIB__ENABLE_SET_X" + return 0 +} diff --git a/bash/src/base.path.sh b/bash/src/base.path.sh new file mode 100644 index 0000000..8df86b6 --- /dev/null +++ b/bash/src/base.path.sh @@ -0,0 +1,304 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.path "Fonctions de base: gestion des chemins et des fichiers" + +function: in_path "tester l'existence d'un programme dans le PATH" +function in_path() { + [ -n "$1" -a -x "$(which "$1" 2>/dev/null)" ] +} + +function: delpath "supprimer le chemin \$1 de \$2(=PATH)" +function delpath() { + local _qdir="${1//\//\\/}" + eval "export ${2:-PATH}; ${2:-PATH}"'="${'"${2:-PATH}"'#$1:}"; '"${2:-PATH}"'="${'"${2:-PATH}"'%:$1}"; '"${2:-PATH}"'="${'"${2:-PATH}"'//:$_qdir:/:}"; [ "$'"${2:-PATH}"'" == "$1" ] && '"${2:-PATH}"'=' +} + +function: addpath "Ajouter le chemin \$1 à la fin, dans \$2(=PATH), s'il n'y existe pas déjà" +function addpath() { + local _qdir="${1//\//\\/}" + eval "export ${2:-PATH}; "'[ "${'"${2:-PATH}"'#$1:}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'%:$1}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'//:$_qdir:/:}" == "$'"${2:-PATH}"'" -a "$'"${2:-PATH}"'" != "$1" ] && '"${2:-PATH}"'="${'"${2:-PATH}"':+$'"${2:-PATH}"':}$1"' +} + +function: inspathm "Ajouter le chemin \$1 au début, dans \$2(=PATH), s'il n'y existe pas déjà" +function inspathm() { + local _qdir="${1//\//\\/}" + eval "export ${2:-PATH}; "'[ "${'"${2:-PATH}"'#$1:}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'%:$1}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'//:$_qdir:/:}" == "$'"${2:-PATH}"'" -a "$'"${2:-PATH}"'" != "$1" ] && '"${2:-PATH}"'="$1${'"${2:-PATH}"':+:$'"${2:-PATH}"'}"' +} + +function: inspath "S'assurer que le chemin \$1 est au début de \$2(=PATH)" +function inspath() { + delpath "$@" + inspathm "$@" +} + +function: push_cwd "enregistrer le répertoire courant dans la variable \$2(=cwd) et se placer dans le répertoire \$1" +function push_cwd() { + eval "${2:-cwd}"'="$(pwd)"' + cd "$1" +} +function: pop_cwd "se placer dans le répertoire \${!\$2}(=\$cwd) puis retourner le code d'erreur \$1(=0)" +function pop_cwd() { + eval 'cd "$'"${2:-cwd}"'"' + return "${1:-0}" +} + +################################################################################ +## fichiers temporaires + +function: mktempf "générer un fichier temporaire et retourner son nom" +function mktempf() { + mktemp "${1:-"$TMPDIR/tmp.XXXXXX"}" +} + +function: mktempd "générer un répertoire temporaire et retourner son nom" +function mktempd() { + mktemp -d "${1:-"$TMPDIR/tmp.XXXXXX"}" +} + +function ac__forgetall() { NULIB__AC_FILES=(); } +ac__forgetall +function ac__trap() { + local file + for file in "${NULIB__AC_FILES[@]}"; do + [ -e "$file" ] && rm -rf "$file" 2>/dev/null + done + ac__forgetall +} +trap ac__trap 1 3 15 EXIT + +function: autoclean "\ +Ajouter les fichiers spécifiés à la liste des fichiers à supprimer à la fin du +programme" +function autoclean() { + local file + for file in "$@"; do + [ -n "$file" ] && NULIB__AC_FILES=("${NULIB__AC_FILES[@]}" "$file") + done +} + +function: ac_cleanall "\ +Supprimer *tous* les fichiers temporaires gérés par autoclean tout de suite." +function ac_cleanall() { + ac__trap +} + +function: ac_clean "\ +Supprimer les fichier temporaires \$1..@ si et seulement s'ils ont été générés +par ac_set_tmpfile() ou ac_set_tmpdir()" +function ac_clean() { + local file acfile found + local -a acfiles + for acfile in "${NULIB__AC_FILES[@]}"; do + found= + for file in "$@"; do + if [ "$file" == "$acfile" ]; then + found=1 + [ -e "$file" ] && rm -rf "$file" 2>/dev/null + break + fi + done + [ -z "$found" ] && acfiles=("${acfiles[@]}" "$acfile") + done + NULIB__AC_FILES=("${acfiles[@]}") +} + +function: ac_set_tmpfile "\ +Créer un fichier temporaire avec le motif \$2, l'ajouter à la liste des +fichiers à supprimer en fin de programme, et mettre sa valeur dans la +variable \$1 + +En mode debug, si (\$5 est vide ou \${!5} est une valeur vraie), et si \$3 n'est +pas vide, prendre ce fichier au lieu de générer un nouveau fichier temporaire. +Si \$4==keep, ne pas écraser le fichier \$3 s'il existe." +function ac_set_tmpfile() { + local se__d + if is_debug; then + if [ -n "$5" ]; then + is_yes "${!5}" && se__d=1 + else + se__d=1 + fi + fi + if [ -n "$se__d" -a -n "$3" ]; then + _setv "$1" "$3" + [ -f "$3" -a "$4" == keep ] || >"$3" + else + local se__t="$(mktempf "$2")" + autoclean "$se__t" + _setv "$1" "$se__t" + fi +} + +function: ac_set_tmpdir "\ +Créer un répertoire temporaire avec le motif \$2, l'ajouter à la liste des +fichiers à supprimer en fin de programme, et mettre sa valeur dans la +variable \$1 + +En mode debug, si (\$4 est vide ou \${!4} est une valeur vraie), et si \$3 n'est +pas vide, prendre ce nom de répertoire au lieu de créer un nouveau répertoire +temporaire" +function ac_set_tmpdir() { + local sr__d + if is_debug; then + if [ -n "$4" ]; then + is_yes "${!4}" && sr__d=1 + else + sr__d=1 + fi + fi + if [ -n "$sr__d" -a -n "$3" ]; then + _setv "$1" "$3" + mkdir -p "$3" + else + local sr__t="$(mktempd "$2")" + autoclean "$sr__t" + _setv "$1" "$sr__t" + fi +} + +################################################################################ +## manipulation de chemins + +#XXX repris tel quel depuis nutools, à migrer + +function normpath() { +# normaliser le chemin $1, qui est soit absolu, soit relatif à $2 (qui vaut +# $(pwd) par défaut) + local -a parts + local part ap + IFS=/ read -a parts <<<"$1" + if [ "${1#/}" != "$1" ]; then + ap=/ + elif [ -n "$2" ]; then + ap="$2" + else + ap="$(pwd)" + fi + for part in "${parts[@]}"; do + if [ "$part" == "." ]; then + continue + elif [ "$part" == ".." ]; then + ap="${ap%/*}" + [ -n "$ap" ] || ap=/ + else + [ "$ap" != "/" ] && ap="$ap/" + ap="$ap$part" + fi + done + echo "$ap" +} +function __normpath() { + # normaliser dans les cas simple le chemin absolu $1. sinon retourner 1. + # cette fonction est utilisée par abspath() + if [ -d "$1" ]; then + if [ -x "$1" ]; then + # le cas le plus simple: un répertoire dans lequel on peut entrer + (cd "$1"; pwd) + return 0 + fi + elif [ -f "$1" ]; then + local dn="$(dirname -- "$1")" bn="$(basename -- "$1")" + if [ -x "$dn" ]; then + # autre cas simple: un fichier situé dans un répertoire dans lequel + # on peut entrer + (cd "$dn"; echo "$(pwd)/$bn") + return 0 + fi + fi + return 1 +} +function abspath() { +# Retourner un chemin absolu vers $1. Si $2 est non nul et si $1 est un chemin +# relatif, alors $1 est exprimé par rapport à $2, sinon il est exprimé par +# rapport au répertoire courant. +# Si le chemin n'existe pas, il n'est PAS normalisé. Sinon, les meilleurs +# efforts sont faits pour normaliser le chemin. + local ap="$1" + if [ "${ap#/}" != "$ap" ]; then + # chemin absolu. on peut ignorer $2 + __normpath "$ap" && return + else + # chemin relatif. il faut exprimer le chemin par rapport à $2 + local cwd + if [ -n "$2" ]; then + cwd="$(abspath "$2")" + else + cwd="$(pwd)" + fi + ap="$cwd/$ap" + __normpath "$ap" && return + fi + # dans les cas spéciaux, il faut calculer "manuellement" le répertoire absolu + normpath "$ap" +} + +function ppath() { +# Dans un chemin *absolu*, remplacer "$HOME" par "~" et "$(pwd)/" par "", afin +# que le chemin soit plus facile à lire. Le répertoire courant est spécifié par +# $2 ou $(pwd) si $2 est vide + local path="$1" cwd="$2" + + path="$(abspath "$path")" # essayer de normaliser le chemin + [ -n "$cwd" ] || cwd="$(pwd)" + + [ "$path" == "$cwd" ] && path="." + [ "$cwd" != "/" -a "$cwd" != "$HOME" ] && path="${path#$cwd/}" + [ "${path#$HOME/}" != "$path" ] && path="~${path#$HOME}" + + echo "$path" +} +function ppath2() { +# Comme ppath() mais afficher '.' comme '. ($dirname)' pour la joliesse + local path="$1" cwd="$2" + + path="$(abspath "$path")" # essayer de normaliser le chemin + [ -n "$cwd" ] || cwd="$(pwd)" + + if [ "$path" == "$cwd" ]; then + path=". ($(basename -- "$path"))" + else + [ "$cwd" != "/" -a "$cwd" != "$HOME" ] && path="${path#$cwd/}" + [ "${path#$HOME/}" != "$path" ] && path="~${path#$HOME}" + fi + + echo "$path" +} + +function relpath() { +# Afficher le chemin relatif de $1 par rapport à $2. Si $2 n'est pas spécifié, +# on prend le répertoire courant. Si $1 ou $2 ne sont pas des chemins absolus, +# il sont transformés en chemins absolus par rapport à $3. Si $1==$2, retourner +# une chaine vide + local p="$(abspath "$1" "$3")" cwd="$2" + if [ -z "$cwd" ]; then + cwd="$(pwd)" + else + cwd="$(abspath "$cwd" "$3")" + fi + if [ "$p" == "$cwd" ]; then + echo "" + elif [ "${p#$cwd/}" != "$p" ]; then + echo "${p#$cwd/}" + else + local rp + while [ -n "$cwd" -a "${p#$cwd/}" == "$p" ]; do + rp="${rp:+$rp/}.." + cwd="${cwd%/*}" + done + rp="$rp/${p#$cwd/}" + # ${rp%//} traite le cas $1==/ + echo "${rp%//}" + fi +} +function relpathx() { +# Comme relpath, mais pour un chemin vers un exécutable qu'il faut lancer: +# s'assurer qu'il y a une spécification de chemin, e.g. ./script + local p="$(relpath "$@")" + if [ -z "$p" ]; then + echo . + elif [ "${p#../}" != "$p" -o "${p#./}" != "$p" ]; then + echo "$p" + else + echo "./$p" + fi +} diff --git a/bash/src/base.sh b/bash/src/base.sh new file mode 100644 index 0000000..cd88e49 --- /dev/null +++ b/bash/src/base.sh @@ -0,0 +1,22 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +# shim pour les fonctions de nulib.sh au cas où ce module n'est pas chargée +if [ -z "$NULIBDIR" -o "$NULIBDIR" != "$NULIBINIT" ]; then + function module:() { :; } + function function:() { :; } + function require:() { :; } +fi +##@include base.init.sh +##@include base.core.sh +##@include base.str.sh +##@include base.num.sh +##@include base.bool.sh +##@include base.array.sh +##@include base.split.sh +##@include base.path.sh +##@include base.args.sh +##@include base.tools.sh +##@include base.input.sh +##@include base.output.sh +module: base "Chargement de tous les modules base.*" +require: base.init base.core base.str base.num base.bool base.array base.split base.path base.args base.tools base.input base.output diff --git a/bash/src/base.split.sh b/bash/src/base.split.sh new file mode 100644 index 0000000..dac4eeb --- /dev/null +++ b/bash/src/base.split.sh @@ -0,0 +1,188 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.split "Fonctions de base: analyse et découpage de valeurs" + +function: splitfsep "\ +Découper \$1 de la forme first[SEPsecond] entre first, qui est placé dans la +variable \$3(=first) et second, qui est placée dans la variable \$4(=second). \$2 +est la valeur SEP. Le découpage est faite sur la *première* occurence de SEP." +function splitfsep() { + if [[ "$1" == *"$2"* ]]; then + setv "${3:-first}" "${1%%$2*}" + setv "${4:-second}" "${1#*$2}" + else + setv "${3:-first}" "$1" + setv "${4:-second}" + fi +} + +function: splitfsep2 "\ +Découper \$1 de la forme [firstSEP]second entre first, qui est placé dans la +variable \$3(=first) et second, qui est placée dans la variable \$4(=second). \$2 +est la valeur SEP. Le découpage est faite sur la *première* occurence de SEP." +function splitfsep2() { + if [[ "$1" == *"$2"* ]]; then + setv "${3:-first}" "${1%%$2*}" + setv "${4:-second}" "${1#*$2}" + else + setv "${3:-first}" + setv "${4:-second}" "$1" + fi +} + +function: splitlsep "\ +Découper \$1 de la forme first[SEPsecond] entre first, qui est placé dans la +variable \$3(=first) et second, qui est placée dans la variable \$4(=second). \$2 +est la valeur SEP. Le découpage est faite sur la *dernière* occurence de SEP." +function splitlsep() { + if [[ "$1" == *"$2"* ]]; then + setv "${3:-first}" "${1%$2*}" + setv "${4:-second}" "${1##*$2}" + else + setv "${3:-first}" "$1" + setv "${4:-second}" + fi +} + +function: splitlsep2 "\ +Découper \$1 de la forme [firstSEP]second entre first, qui est placé dans la +variable \$3(=first) et second, qui est placée dans la variable \$4(=second). \$2 +est la valeur SEP. Le découpage est faite sur la *dernière* occurence de SEP." +function splitlsep2() { + if [[ "$1" == *"$2"* ]]; then + setv "${3:-first}" "${1%$2*}" + setv "${4:-second}" "${1##*$2}" + else + setv "${3:-first}" + setv "${4:-second}" "$1" + fi +} + +function: splitvar "\ +Découper \$1 de la forme name[=value] entre le nom, qui est placé dans la +variable \$2(=name) et la valeur, qui est placée dans la variable \$3(=value)" +function splitvar() { + splitfsep "$1" = "${2:-name}" "${3:-value}" +} + +function: splitpath "\ +Découper \$1 de la forme [dir/]name entre le répertoire, qui est placé dans la +variable \$2(=dir), et le nom du fichier, qui est placé dans la variable +\$3(=name)" +function splitpath() { + splitlsep2 "$1" / "${2:-dir}" "${3:-name}" +} + +function: splitname "\ +Découper \$1 de la forme basename[.ext] entre le nom de base du fichier, qui +est placé dans la variable \$2(=basename) et l'extension, qui est placée dans +la variable \$3(=ext) + +Attention, si \$1 est un chemin, le résultat risque d'être faussé. Par exemple, +'splitname a.b/c' ne donne pas le résultat escompté." +function splitname() { + splitlsep "$1" . "${2:-basename}" "${3:-ext}" +} + +function: splithost "\ +Découper \$1 de la forme hostname[.domain] entre le nom d'hôte, qui est placé +dans la variable \$2(=hostname) et le domaine, qui est placée dans la variable +\$3(=domain)" +function splithost() { + splitfsep "$1" . "${2:-hostname}" "${3:-domain}" +} + +function: splituserhost "\ +Découper \$1 de la forme [user@]host entre le nom de l'utilisateur, qui est placé +dans la variable \$2(=user) et le nom d'hôte, qui est placée dans la variable +\$3(=host)" +function splituserhost() { + splitfsep2 "$1" @ "${2:-user}" "${3:-host}" +} + +function: splitpair "\ +Découper \$1 de la forme first[:second] entre la première valeur, qui est placé +dans la variable \$2(=src) et la deuxième valeur, qui est placée dans la variable +\$3(=dest)" +function splitpair() { + splitfsep "$1" : "${2:-src}" "${3:-dest}" +} + +function: splitproxy "\ +Découper \$1 de la forme http://[user:password@]host[:port]/ entre les valeurs +\$2(=host), \$3(=port), \$4(=user), \$5(=password) + +S'il n'est pas spécifié, port vaut 3128 par défaut" +function splitproxy() { + local sy__tmp sy__host sy__port sy__creds sy__user sy__password + + sy__tmp="${1#http://}" + if [[ "$sy__tmp" == *@* ]]; then + sy__creds="${sy__tmp%%@*}" + sy__tmp="${sy__tmp#${sy__creds}@}" + splitpair "$sy__creds" sy__user sy__password + fi + sy__tmp="${sy__tmp%%/*}" + splitpair "$sy__tmp" sy__host sy__port + [ -n "$sy__port" ] || sy__port=3128 + + setv "${2:-host}" "$sy__host" + setv "${3:-port}" "$sy__port" + setv "${4:-user}" "$sy__user" + setv "${5:-password}" "$sy__password" +} + +function: spliturl "\ +Découper \$1 de la forme scheme://[user:password@]host[:port]/path entre les +valeurs \$2(=scheme), \$3(=user), \$4(=password), \$5(=host), \$6(=port), \$7(=path) + +S'il n'est pas spécifié, port vaut 80 pour http, 443 pour https, 21 pour ftp" +function spliturl() { + local sl__tmp sl__scheme sl__creds sl__user sl__password sl__host sl__port sl__path + + sl__scheme="${1%%:*}" + sl__tmp="${1#${sl__scheme}://}" + if [[ "$sl__tmp" == */* ]]; then + sl__path="${sl__tmp#*/}" + sl__tmp="${sl__tmp%%/*}" + fi + if [[ "$sl__tmp" == *@* ]]; then + sl__creds="${sl__tmp%%@*}" + sl__tmp="${sl__tmp#${sl__creds}@}" + splitpair "$sl__creds" sl__user sl__password + fi + splitpair "$sl__tmp" sl__host sl__port + if [ -z "$sl__port" ]; then + [ "$sl__scheme" == "http" ] && sl__port=80 + [ "$sl__scheme" == "https" ] && sl__port=443 + [ "$sl__scheme" == "ftp" ] && sl__port=21 + fi + + setv "${2:-scheme}" "$sl__scheme" + setv "${3:-user}" "$sl__user" + setv "${4:-password}" "$sl__password" + setv "${5:-host}" "$sl__host" + setv "${6:-port}" "$sl__port" + setv "${7:-path}" "$sl__path" +} + +function: splitwcs "\ +Découper un nom de chemin \$1 entre la partie sans wildcards, qui est placée dans +la variables \$2(=basedir), et la partie avec wildcards, qui est placée dans la +variable \$3(=filespec)" +function splitwcs() { + local ss__p="$1" + local ss__dd="${2:-basedir}" ss__df="${3:-filespec}" ss__part ss__d ss__f + local -a ss__parts + array_split ss__parts "$ss__p" "/" + for ss__part in "${ss__parts[@]}"; do + if [[ "$ss__part" == *\** ]] || [[ "$ss__part" == *\?* ]] || [ -n "$ss__f" ]; then + ss__f="${ss__f:+$ss__f/}$ss__part" + else + ss__d="${ss__d:+$ss__d/}$ss__part" + fi + done + [ "${ss__p#/}" != "$ss__p" ] && ss__d="/$ss__d" + _setv "$ss__dd" "$ss__d" + _setv "$ss__df" "$ss__f" +} diff --git a/bash/src/base.str.sh b/bash/src/base.str.sh new file mode 100644 index 0000000..e83e714 --- /dev/null +++ b/bash/src/base.str.sh @@ -0,0 +1,140 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.str "Fonctions de base: gestion des valeurs chaines" + +function: strmid "Afficher la plage \$1 de la valeur \$2..* + +La plage peut être d'une des formes 'start', '[start]:length'. Si start est +négatif, le compte est effectué à partir de la fin de la chaine. Si length est +négatif, il est rajouté à la longueur de la chaine à partir de start" +function strmid() { + local range="$1"; shift + local str="$*" + if [[ "$range" == *:-* ]]; then + local max=${#str} + [ $max -eq 0 ] && return + local start="${range%%:*}" + [ -n "$start" ] || start=0 + while [ "$start" -lt 0 ]; do + start=$(($max$start)) + done + max=$(($max-$start)) + local length="${range#*:}" + while [ "$length" -lt 0 ]; do + length=$(($max$length)) + done + range="$start:$length" + fi + eval 'echo "${str:'" $range"'}"' +} + +function: strrepl "Remplacer dans la valeur \$3..* le motif \$1 par la chaine \$2 + +\$1 peut commencer par l'un des caractères /, #, % pour indiquer le type de recherche" +function strrepl() { + local pattern="$1"; shift + local repl="$1"; shift + local str="$*" + local cmd='echo "${str/' + if [ "${pattern#/}" != "$pattern" ]; then + pattern="${pattern#/}" + cmd="$cmd/" + elif [ "${pattern#\#}" != "$pattern" ]; then + pattern="${pattern#\#}" + cmd="$cmd#" + elif [ "${pattern#%}" != "$pattern" ]; then + pattern="${pattern#%}" + cmd="$cmd%" + fi + cmd="$cmd"'$pattern/$repl}"' + eval "$cmd" +} + +function: strlcomp "transformer dans le flux en entrée en UTF-8 certains caractères en leur équivalent transformable en latin1. + +si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée." +function strlcomp() { + if [ $# -gt 0 ]; then strlcomp <<<"$*" + else LANG=fr_FR.UTF-8 sed $' +s/[\xE2\x80\x90\xE2\x80\x91\xE2\x80\x92\xE2\x80\x93\xE2\x80\x94\xE2\x80\x95]/-/g +s/[‘’]/\x27/g +s/[«»“”]/"/g +s/[\xC2\xA0\xE2\x80\x87\xE2\x80\xAF\xE2\x81\xA0]/ /g +s/[\xE2\x80\xA6]/.../g +s/[œ]/oe/g +s/[Œ]/OE/g +s/[æ]/ae/g +s/[Æ]/AE/g +s/a\xCC\x80/à/g +s/e\xCC\x81/é/g; s/e\xCC\x80/è/g; s/e\xCC\x82/ê/g; s/e\xCC\x88/ë/g +s/i\xCC\x88/ï/g; s/i\xCC\x82/î/g +s/o\xCC\x82/ô/g; s/o\xCC\x88/ö/g +s/u\xCC\x88/ü/g; s/u\xCC\x82/û/g +s/c\xCC\xA7/ç/g +s/A\xCC\x80/À/g +s/E\xCC\x81/É/g; s/E\xCC\x80/È/g; s/E\xCC\x82/Ê/g; s/E\xCC\x88/Ë/g +s/I\xCC\x88/Ï/g; s/I\xCC\x82/Î/g +s/O\xCC\x82/Ô/g; s/O\xCC\x88/Ö/g +s/U\xCC\x88/Ü/g; s/U\xCC\x82/Û/g +s/C\xCC\xA7/Ç/g +' + fi +} + +function: strnacc "supprimer les accents dans le flux en entrée en UTF-8 + +si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée." +function strnacc() { + if [ $# -gt 0 ]; then strnacc <<<"$*" + else LANG=fr_FR.UTF-8 sed ' +s/[à]/a/g +s/[éèêë]/e/g +s/[ïî]/i/g +s/[ôö]/o/g +s/[üû]/u/g +s/[ç]/c/g +s/[À]/A/g +s/[ÉÈÊË]/E/g +s/[ÏÎ]/I/g +s/[ÔÖ]/O/g +s/[ÜÛ]/U/g +s/[Ç]/C/g +' + fi +} + +function: stripnl "Supprimer dans le flux en entrée les caractères de fin de ligne + +si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée." +function stripnl() { + if [ $# -gt 0 ]; then stripnl <<<"$*" + else tr -d '\r\n' + fi +} + +function: nl2lf "transformer dans le flux en entrée les fins de ligne en LF + +si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée." +function nl2lf() { + if [ $# -gt 0 ]; then nl2lf <<<"$*" + else lawk 'BEGIN {RS="\r|\r\n|\n"} {print}' + fi +} + +function: nl2crlf "transformer dans le flux en entrée les fins de ligne en CRLF + +si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée." +function nl2crlf() { + if [ $# -gt 0 ]; then nl2crlf <<<"$*" + else lawk 'BEGIN {RS="\r|\r\n|\n"} {print $0 "\r"}' + fi +} + +function: nl2cr "transformer dans le flux en entrée les fins de ligne en CR + +si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée." +function nl2cr() { + if [ $# -gt 0 ]; then nl2cr <<<"$*" + else lawk 'BEGIN {RS="\r|\r\n|\n"; ORS=""} {print $0 "\r"}' + fi +} diff --git a/bash/src/base.tools.sh b/bash/src/base.tools.sh new file mode 100644 index 0000000..1a7f800 --- /dev/null +++ b/bash/src/base.tools.sh @@ -0,0 +1,101 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.tools "Fonctions de base: outils divers" + +function: mkdirof 'Créer le répertoire correspondant au fichier $1' +function mkdirof() { + mkdir -p "$(dirname -- "$1")" +} + +function __la_cmd() { + [ $# -gt 0 ] || set '*' + local arg + local cmd="/bin/ls -1d" + for arg in "$@"; do + arg="$(qwc "$arg")" + cmd="$cmd $arg" + done + cmd="$cmd 2>/dev/null" + echo "$cmd" +} + +function: ls_all 'Lister les fichiers ou répertoires du répertoire $1, un par ligne +Les répertoires . et .. sont enlevés de la liste +$1=un répertoire dont le contenu doit être listé +$2..@=un ensemble de patterns pour le listage + +Seuls les noms des fichiers sont listés. Utiliser l'\''option -p pour inclure +les chemins' +function ls_all() { + local withp f b + if [ "$1" == -p ]; then withp=1; shift; fi + b="${1:-.}"; shift + + ( + cd "$b" || exit + eval "$(__la_cmd "$@")" | while read f; do + [ "$f" == "." -o "$f" == ".." ] && continue + recho "${withp:+$b/}$f" + done + ) +} + +function: ls_files 'Lister les fichiers du répertoire $1, un par ligne +$1=un répertoire dont le contenu doit être listé. +$2..@=un ensemble de patterns pour le listage + +Seuls les noms des fichiers sont listés. Utiliser l'\''option -p pour inclure +les chemins' +function ls_files() { + local withp f b + if [ "$1" == -p ]; then withp=1; shift; fi + b="${1:-.}"; shift + + ( + cd "$b" || exit + eval "$(__la_cmd "$@")" | while read f; do + [ -f "$f" ] && recho "${withp:+$b/}$f" + done + ) +} + +function: ls_dirs 'Lister les répertoires du répertoire $1, un par ligne +Les répertoires . et .. sont enlevés de la liste +$1=un répertoire dont le contenu doit être listé. +$2..@=un ensemble de patterns pour le listage + +Seuls les noms des répertoires sont listés. Utiliser l'\''option -p pour +inclure les chemins' +function ls_dirs() { + local withp f b + if [ "$1" == -p ]; then withp=1; shift; fi + b="${1:-.}"; shift + + ( + cd "$b" || exit + eval "$(__la_cmd "$@")" | while read f; do + [ "$f" == "." -o "$f" == ".." ] && continue + [ -d "$f" ] && recho "${withp:+$b/}$f" + done + ) +} + +function: quietgrep "tester la présence d'un pattern dans un fichier en mode silencieux" +function quietgrep() { grep -q "$@" 2>/dev/null; } + +function: testsame "tester si deux fichiers sont identiques en mode silencieux" +function testsame() { diff -q "$@" >&/dev/null; } + +function: testdiff "tester si deux fichiers sont différents en mode silencieux" +function testdiff() { ! diff -q "$@" >&/dev/null; } + +function: should_update "faut-il mettre à jour le \$1 qui est construit à partir de \$2..@" +function should_update() { + local dest="$1"; shift + local source + for source in "$@"; do + [ -f "$source" ] || continue + [ "$source" -nt "$dest" ] && return 0 + done + return 1 +} diff --git a/bash/src/donk.build.sh b/bash/src/donk.build.sh new file mode 100644 index 0000000..a88daa0 --- /dev/null +++ b/bash/src/donk.build.sh @@ -0,0 +1,4 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: donk.build "construire des images docker" +require: donk.common diff --git a/bash/src/donk.common.sh b/bash/src/donk.common.sh new file mode 100644 index 0000000..79a5efb --- /dev/null +++ b/bash/src/donk.common.sh @@ -0,0 +1,3 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: donk.common "fonctions communes" diff --git a/bash/src/donk.help.sh b/bash/src/donk.help.sh new file mode 100644 index 0000000..2e0a21b --- /dev/null +++ b/bash/src/donk.help.sh @@ -0,0 +1,41 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: donk.help "aide de donk" + +DONK_VALID_ACTIONS=( + dump:d + build:b + clean:k +) +dump_SUMMARY="afficher les valeurs des variables et ce que ferait l'action build" +build_SUMMARY="construire les images" +clean_SUMMARY="nettoyer le projet des fichiers créés avec 'copy gitignore=', en utilisant la commande 'git clean -dX'" + +DONK_HELP_SECTIONS=( + base:b + reference:ref:r +) + +function donk_help() { + : +} + +function _donk_show_actions() { + local action summary + echo " +ACTIONS" + for action in "${DONK_VALID_ACTIONS[@]}"; do + IFS=: read -a action <<<"$action"; action="${action[0]}" + summary="${action}_SUMMARY"; summary="${!summary}" + echo "\ + $action + $summary" + done +} + +function _donk_show_help() { + if [ -z "$value_" ]; then showhelp@ + else donk_help "$value_" + fi + exit $? +} diff --git a/bash/src/fndate.sh b/bash/src/fndate.sh new file mode 100644 index 0000000..7c94e2b --- /dev/null +++ b/bash/src/fndate.sh @@ -0,0 +1,45 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: fndate "gestion de fichiers dont le nom contient la date" + +function: fndate_verifix "\ +corriger le chemin \$1 pour ajouter le cas échéant une date au nom du fichier +le fichier n'existe peut-être pas au moment où cette fonction est appelée +\$2 est l'extension finale du fichier, à ignorer si elle est présente + (elle n'est cependant pas ajoutée si elle n'est pas présente) +\$3 est la date à sélectionner (par défaut c'est la date du jour) + +XXX à implémenter: +- gestion de la date +- ajout d'un suffixe .N le cas échéant (i.e YYMMDD.NN) +" +function fndate_verifix() { + local dir filename ext date + if [[ "$1" == */* ]]; then + dir="$(dirname -- "$1")" + filename="$(basename -- "$1")" + else + dir= + filename="$1" + fi + ext="$2" + if [ -n "$ext" ]; then + ext=".${2#.}" + if [ "${filename%$ext}" != "$filename" ]; then + filename="${filename%$ext}" + else + ext= + fi + fi + date="$3" + [ -n "$date" ] || date="$(date +%y%m%d)" + + case "$filename" in + ~~-*) filename="$date-${filename#~~-}";; + ~~*) filename="$date-${filename#~~}";; + *-~~) filename="${filename%-~~}-$date";; + *~~) filename="${filename%~~}-$date";; + esac + + echo "${dir:+$dir/}$filename$ext" +} diff --git a/bash/src/git.sh b/bash/src/git.sh new file mode 100644 index 0000000..b893864 --- /dev/null +++ b/bash/src/git.sh @@ -0,0 +1,226 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +##@require nulib.sh +##@require base +module: git "Fonctions pour faciliter l'utilisation de git" +require: nulib base + +function: git_geturl "" +function git_geturl() { + git config --get remote.origin.url +} + +function: git_have_annex "" +function git_have_annex() { + [ -n "$(git config --get annex.uuid)" ] +} + +NULIB_GIT_FUNCTIONS=( + git_check_gitvcs git_ensure_gitvcs + git_list_branches git_list_rbranches + git_have_branch git_have_rbranch + git_get_branch git_is_branch + git_have_remote git_track_branch + git_check_cleancheckout git_ensure_cleancheckout + git_is_ancestor git_should_ff git_should_push + git_is_merged +) +NULIB_GIT_FUNCTIONS_MAP=( + cg:git_check_gitvcs eg:git_ensure_gitvcs + lbs:git_list_branches rbs:git_list_rbranches + hlb:git_have_branch hrb:git_have_rbranch + gb:git_get_branch ib:git_is_branch + hr:git_have_remote tb:git_track_branch + cc:git_check_cleancheckout ec:git_ensure_cleancheckout + ia:git_is_ancestor sff:git_should_ff spu:git_should_push + im:git_is_merged +) + +function: git_get_toplevel "" +function git_get_toplevel() { + git rev-parse --show-toplevel 2>/dev/null +} + +function: git_check_gitvcs "" +function git_check_gitvcs() { + git rev-parse --show-toplevel >&/dev/null +} + +function: git_ensure_gitvcs "" +function git_ensure_gitvcs() { + git_check_gitvcs || die "$(ppath "$(pwd)" ~): ce répertoire n'est pas un dépôt git" || return +} + +function: git_list_branches "" +function git_list_branches() { + git for-each-ref refs/heads/ --format='%(refname:short)' | csort +} + +function: git_list_rbranches "" +function git_list_rbranches() { + git for-each-ref "refs/remotes/${1:-origin}/" --format='%(refname:short)' | csort +} + +function: git_list_pbranches "lister les branches locales et celles qui existent dans l'origine \$1(=origin) et qui pourraient devenir une branche locale avec la commande git checkout -b" +function git_list_pbranches() { + local prefix="${1:-origin}/" + { + git for-each-ref refs/heads/ --format='%(refname:short)' + git for-each-ref "refs/remotes/$prefix" --format='%(refname:short)' | grep -F "$prefix" | cut -c $((${#prefix} + 1))- + } | grep -vF HEAD | csort -u +} + +function: git_have_branch "" +function git_have_branch() { + git_list_branches | grep -qF "$1" +} + +function: git_have_rbranch "" +function git_have_rbranch() { + git_list_rbranches "${2:-origin}" | grep -qF "$1" +} + +function: git_get_branch "" +function git_get_branch() { + git rev-parse --abbrev-ref HEAD 2>/dev/null +} + +function: git_get_branch_remote "" +function git_get_branch_remote() { + local branch="$1" + [ -n "$branch" ] || branch="$(git_get_branch)" + [ -n "$branch" ] || return + git config --get "branch.$branch.remote" +} + +function: git_get_branch_merge "" +function git_get_branch_merge() { + local branch="$1" + [ -n "$branch" ] || branch="$(git_get_branch)" + [ -n "$branch" ] || return + git config --get "branch.$branch.merge" +} + +function: git_get_branch_rbranch "" +function git_get_branch_rbranch() { + local branch="$1" remote="$2" merge + [ -n "$branch" ] || branch="$(git_get_branch)" + [ -n "$branch" ] || return + [ -n "$remote" ] || remote="$(git_get_branch_remote "$branch")" + [ -n "$remote" ] || return + merge="$(git_get_branch_merge "$branch")" + [ -n "$merge" ] || return + echo "refs/remotes/$remote/${merge#refs/heads/}" +} + +function: git_is_branch "" +function git_is_branch() { + [ "$(git_get_branch)" == "${1:-master}" ] +} + +function: git_have_remote "" +function git_have_remote() { + [ -n "$(git config --get remote.${1:-origin}.url)" ] +} + +function: git_track_branch "" +function git_track_branch() { + local branch="$1" origin="${2:-origin}" + [ -n "$branch" ] || return + git_have_remote "$origin" || return + [ "$(git config --get branch.$branch.remote)" == "$origin" ] && return + if git_have_rbranch "$branch" "$origin"; then + if git_have_branch "$branch"; then + git branch -u "$origin/$branch" "$branch" + else + git branch -t "$branch" "$origin/$branch" + fi + elif git_have_branch "$branch"; then + git push -u "$origin" "$branch" || return + fi +} + +function: git_ensure_branch " +@return 0 si la branche a été créée, 1 si elle existait déjà, 2 en cas d'erreur" +function git_ensure_branch() { + local branch="$1" source="${2:-master}" origin="${3:-origin}" + [ -n "$branch" ] || return 2 + git_have_branch "$branch" && return 1 + if git_have_rbranch "$branch" "$origin"; then + # une branche du même nom existe dans l'origine. faire une copie de cette branche + git branch -t "$branch" "$origin/$branch" || return 2 + else + # créer une nouvelle branche du nom spécifié + git_have_branch "$source" || return 2 + git branch "$branch" "$source" || return 2 + if [ -z "$NULIB_GIT_OFFLINE" ]; then + git_have_remote "$origin" && git_track_branch "$branch" "$origin" + fi + fi + return 0 +} + +function: git_check_cleancheckout "vérifier qu'il n'y a pas de modification locales dans le dépôt correspondant au répertoire courant." +function git_check_cleancheckout() { + [ -z "$(git status --porcelain 2>/dev/null)" ] +} + +git_cleancheckout_VERBOSE=1 +git_cleancheckout_DIRTY="Vous avez des modifications locales. Enregistrez ces modifications avant de continuer" +function: git_ensure_cleancheckout "" +function git_ensure_cleancheckout() { + git_check_cleancheckout && return + [ -n "$git_cleancheckout_VERBOSE" ] && + git status --porcelain 2>/dev/null + die "$git_cleancheckout_DIRTY" || return +} + +function git__init_ff() { + o="${3:-origin}" + b="$1" s="${2:-refs/remotes/$o/$1}" + b="$(git rev-parse --verify --quiet "$b")" || return 1 + s="$(git rev-parse --verify --quiet "$s")" || return 1 + return 0 +} +function git__can_ff() { + [ "$1" == "$(git merge-base "$1" "$2")" ] +} + +function: git_is_ancestor "vérifier que la branche \$1 est un ancêtre direct de la branche \$2, qui vaut par défaut refs/remotes/\${3:-origin}/\$1 +note: cette fonction retourne vrai si \$1 et \$2 identifient le même commit" +function git_is_ancestor() { + local o b s; git__init_ff "$@" || return + git__can_ff "$b" "$s" +} + +function: git_should_ff "vérifier si la branche \$1 devrait être fast-forwardée à partir de la branche d'origine \$2, qui vaut par défaut refs/remotes/\${3:-origin}/\$1 +note: cette fonction est similaire à git_is_ancestor(), mais retourne false si \$1 et \$2 identifient le même commit" +function git_should_ff() { + local o b s; git__init_ff "$@" || return + [ "$b" != "$s" ] || return 1 + git__can_ff "$b" "$s" +} + +function: git_should_push "vérifier si la branche \$1 devrait être poussée vers la branche de même nom dans l'origine \$2(=origin), parce que l'origin peut-être fast-forwardée à partir de cette branche." +function git_should_push() { + git_should_ff "refs/remotes/${2:-origin}/$1" "$1" +} + +function: git_fast_forward "vérifier que la branche courante est bien \$1, puis tester s'il faut la fast-forwarder à partir de la branche d'origine \$2, puis le faire si c'est nécessaire. la branche d'origine \$2 vaut par défaut refs/remotes/origin/\$1" +function git_fast_forward() { + local o b s; git__init_ff "$@" || return + [ "$b" != "$s" ] || return 1 + local head="$(git rev-parse HEAD)" + [ "$head" == "$b" ] || return 1 + git__can_ff "$b" "$s" || return 1 + git merge --ff-only "$s" +} + +function: git_is_merged "vérifier que les branches \$1 et \$2 ont un ancêtre commun, et que la branche \$1 a été complètement fusionnée dans la branche destination \$2" +function git_is_merged() { + local b="$1" d="$2" + b="$(git rev-parse --verify --quiet "$b")" || return 1 + d="$(git rev-parse --verify --quiet "$d")" || return 1 + [ -n "$(git merge-base "$b" "$d")" ] || return 1 + [ -z "$(git rev-list "$d..$b")" ] +} diff --git a/bash/src/nulib.sh b/bash/src/nulib.sh new file mode 120000 index 0000000..562af8d --- /dev/null +++ b/bash/src/nulib.sh @@ -0,0 +1 @@ +../../load.sh \ No newline at end of file diff --git a/bash/src/pman.conf.sh b/bash/src/pman.conf.sh new file mode 100644 index 0000000..c33874c --- /dev/null +++ b/bash/src/pman.conf.sh @@ -0,0 +1,14 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +## configuration par défaut + +UPSTREAM= +DEVELOP=develop +FEATURE=wip/ +RELEASE=release- +MAIN=master +TAG_PREFIX= +TAG_SUFFIX= +HOTFIX=hotfix- +DIST= +NOAUTO= diff --git a/bash/src/pman.sh b/bash/src/pman.sh new file mode 100644 index 0000000..094b269 --- /dev/null +++ b/bash/src/pman.sh @@ -0,0 +1,473 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +# configuration par défaut +# doit être identique au contenu de pman.conf +# les branches sont mergées dans cet ordre: +# upstream --> develop --> [release -->] main --> dist +# feature _/ hotfix _/ + +# branche upstream +UPSTREAM= +# branches de développement +DEVELOP=develop +FEATURE=wip/ +# branche de préparation de release +RELEASE=release- +# branche de release +MAIN=master +TAG_PREFIX= +TAG_SUFFIX= +# branche de hotfix +HOTFIX=hotfix- +# branche de distribution +DIST= +# désactiver les releases automatiques? +NOAUTO= + +CONFIG_VARS=( + UPSTREAM DEVELOP FEATURE RELEASE MAIN TAG_PREFIX TAG_SUFFIX HOTFIX DIST NOAUTO +) + +function _init_changelog() { + setx date=date +%d/%m/%Y-%H:%M + ac_set_tmpfile changelog + echo >"$changelog" "\ +Vérifiez et complétez la liste des changements le cas échéant. +Un fichier vide annule l'opération +Ces lignes ne seront pas incluses dans le fichier destination +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +" +} + +function _filter_rel() { + # enlever les commits "techniques" générés par ce script + awk ' +BEGIN { tech = 0 } +tech == 0 && $0 ~ /^\+.*/ { tech = 1; next } +tech == 1 && $0 ~ /^\|/ { next } +tech == 1 && $0 ~ /^\+/ { tech = 0 } +$0 !~ // { print } +' +} + +function _filter_changes() { + # enlever les commits "inutiles" pour générer le fichier CHANGES.md + grep -vE '^([+|] )?[0-9a-f]+ modifs\.mineures sans commentaires$' | + grep -vE '^([+|] )?[0-9a-f]+ (cosmetic|typo|bug|fix|maj projet|maj deps)$' +} + +function _format_md() { + awk ' +$0 == "" || $0 ~ /^#/ { print; next } +$1 == "+" { + $1 = "*" + $2 = "`" $2 "`" + print; next +} +$1 == "|" { + $1 = " *" + $2 = "`" $2 "`" + print; next +} +{ + $1 = "* `" $1 "`" + print; next +} +' +} + +function _list_commits() { + local source="${1:-$SrcBranch}" dest="${2:-$DestBranch}" mergebase + setx mergebase=git merge-base "$dest" "$source" + git log --oneline --graph --no-decorate "$mergebase..$source" | + grep -vF '|\' | grep -vF '|/' | sed -r 's/^(\| )+\* +/| /; s/^\* +/+ /' | + _filter_rel +} + +function _show_diff() { + local source="${1:-$SrcBranch}" dest="${2:-$DestBranch}" mergebase + setx mergebase=git merge-base "$dest" "$source" + git diff ${_sd_COLOR:+--color=$_sd_COLOR} "$mergebase..$source" +} + +function _scripte() { + echo >>"$script" + echo "$comment$(qvals "$@")" >>"$script" +} + +function _scripta() { + [ $# -gt 0 ] && _scripte einfo "$*" + cat >>"$script" +} + +function _script_push_branches() { + [ ${#push_branches[*]} -gt 0 ] || return + [ -n "$Origin" ] || Origin=origin + git_have_remote "$Origin" || return + + local branch cmd remote rbranch + for branch in "${push_branches[@]}"; do + if [[ "$branch" == *:* ]]; then + cmd="$(qvals git push "$Origin" "$branch")" + else + setx remote=git_get_branch_remote "$branch" + if [ "$remote" == "$Origin" ]; then + setx rbranch=git_get_branch_merge "$branch" + if [ -n "$rbranch" ]; then + # pousser vers la branche distante existante + cmd="$(qvals git push "$Origin" "$branch:${rbranch#refs/heads/}")" + else + # pas de branche distante: pousser et traquer + cmd="$(qvals git push -u "$Origin" "$branch:$branch")" + fi + elif [ -n "$remote" ]; then + # pousser vers un remote différent + cmd="$(qvals git push "$Origin" "$branch:$branch")" + else + # pas de remote: pousser et traquer + cmd="$(qvals git push -u "$Origin" "$branch:$branch")" + fi + fi + _scripta <<<"$comment$cmd$or_die" + done +} + +function _script_push_tags() { + local origin tag + _scripte einfo "push tags" + for tag in "${push_tags[@]}"; do + origin="$Origin" + [ -n "$origin" ] || origin=origin + _scripta <"$config" 2>/dev/null + [ -s "$config" ] || die "$ConfigBranch: aucune configuration trouvée sur cette branche" || return + source "$config" + fi + elif [ -f .pman.conf ]; then + ConfigFile="$(pwd)/.pman.conf" + source "$ConfigFile" + elif [ -n "$1" -a -n "${MYNAME#$1}" ]; then + ConfigFile="$NULIBDIR/bash/src/pman${MYNAME#$1}.conf.sh" + source "$ConfigFile" + else + ConfigFile="$NULIBDIR/bash/src/pman.conf.sh" + fi + + # S'assurer que nulib est dans le PATH pour que les scripts utilisateurs + # puissent utiliser les outils fournis + export PATH="$NULIBDIR/bin:$PATH" +} + +################################################################################ +# Divers + +function resolve_should_push() { + local quiet="$1" + ShouldPush=1 + if ! git_have_remote "$Origin" && [ -n "$Push" ]; then + [ -n "$quiet" ] || enote "L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine" + ShouldPush= + fi + [ -z "$ShouldPush" ] && Push= +} + +function _push_branches() { + [ ${#push_branches[*]} -gt 0 ] || return + [ -n "$Origin" ] || Origin=origin + git_have_remote "$Origin" || return + + local -a cmds; local branch cmd remote rbranch + for branch in "${push_branches[@]}"; do + if [[ "$branch" == *:* ]]; then + cmds+=("$(qvals git push "$Origin" "$branch")") + else + setx remote=git_get_branch_remote "$branch" + if [ "$remote" == "$Origin" ]; then + setx rbranch=git_get_branch_merge "$branch" + if [ -n "$rbranch" ]; then + # pousser vers la branche distante existante + cmds+=("$(qvals git push "$Origin" "$branch:${rbranch#refs/heads/}")") + else + # pas de branche distante: pousser et traquer + cmds+=("$(qvals git push -u "$Origin" "$branch:$branch")") + fi + elif [ -n "$remote" ]; then + # pousser vers un remote différent + cmds+=("$(qvals git push "$Origin" "$branch:$branch")") + else + # pas de remote: pousser et traquer + cmds+=("$(qvals git push -u "$Origin" "$branch:$branch")") + fi + fi + done + [ -n "$Push" ] || enote "L'option --no-push étant utilisée, les opérations à effectuer sont simplement affichées" + for cmd in "${cmds[@]}"; do + einfo "$cmd" + if [ -n "$Push" ]; then + if ! eval "$cmd"; then + ewarn "Une erreur s'est produite, les opérations seront simplement affichées" + Push= + fi + fi + done +} + +################################################################################ +# Merge + +function _mscript_start() { + >"$script" + _scripta <"$script" + _scripta <>"$changelog" "\ +## Release $Tag du $date +" + _list_commits | _filter_changes | _format_md >>"$changelog" + if [ -s CHANGES.md ]; then + echo >>"$changelog" + cat CHANGES.md >>"$changelog" + fi + "${EDITOR:-nano}" +7 "$changelog" + [ -s "$changelog" ] || exit_with ewarn "Création de la release annulée" + + # créer la branche de release et basculer dessus + _scripta "create branch $ReleaseBranch" <CHANGES.md +git add CHANGES.md +EOF + + # mettre à jour la version + _scripta "update VERSION.txt" <VERSION.txt +git add VERSION.txt +EOF + + # Enregistrer les changements + _scripta "commit" <Init changelog & version $Tag") +EOF +} + +function _rscript_merge_release_branch() { + local dest="$1" tag="$2" + + # basculer sur la branche + _scripta "switch to branch $dest" <Intégration de la branche $ReleaseBranch" --no-ff)$or_die +EOF + array_addu push_branches "$dest" + + # tagger la release + if [ -n "$tag" ]; then + _scripta "create tag $tag" <"$awkscript" + echo '@include "base.tools.awk"' + echo 'BEGIN {' + for list in "${template_lists[@]}"; do + items="${!list}"; read -a items <<<"${items// +/ }" + let i=0 + echo " $list[0] = 0; delete $list" + for item in "${items[@]}"; do + item="${item//\\/\\\\}" + item="${item//\"/\\\"}" + echo " $list[$i] = \"$item\"" + let i=i+1 + done + done + echo '}' + echo '{ if (should_generate_password()) { generate_password() } }' + for list in "${template_lists[@]}"; do + items="${!list}"; read -a items <<<"${items// +/ }" + echo "/@@FOR:$list@@/ {" + if [ ${#items[*]} -gt 0 ]; then + if [ "${list%S}" != "$list" ]; then item="${list%S}" + elif [ "${list%s}" != "$list" ]; then item="${list%s}" + else item="$list" + fi + echo " sub(/@@FOR:$list@@/, \"\")" + echo " for (i in $list) {" + echo " print gensub(/@@$item@@/, $list[i], \"g\")" + echo " }" + fi + echo " next" + echo "}" + done + echo '{ print }' + + # if, ul, var + exec >"$sedscript" + for var in "${template_vars[@]}"; do + value="${!var}" + value="${value//\//\\\/}" + value="${value//[/\\[}" + value="${value//\*/\\\*}" + value="${value//$NL/\\n}" + if [ -n "$value" ]; then + echo "s/#@@IF:${var}@@//g" + echo "s/#@@if:${var}@@//g" + echo "/#@@UL:${var}@@/d" + echo "s/#@@ul:${var}@@/#/g" + echo "s/@@${var}:-([^@]*)@@/${value}/g" + echo "s/@@${var}:+([^@]*)@@/\\1/g" + else + echo "/#@@IF:${var}@@/d" + echo "s/#@@if:${var}@@/#/g" + echo "s/#@@UL:${var}@@//g" + echo "s/#@@ul:${var}@@//g" + echo "s/@@${var}:-([^@]*)@@/\\1/g" + echo "s/@@${var}:+([^@]*)@@//g" + fi + echo "s/@@${var}@@/${value}/g" + done + ) + #etitle "awkscript" cat "$awkscript" + #etitle "sedscript" cat "$sedscript" +} + +function: _template_can_process "\ +Indiquer si \$1 est un fichier texte, qui peut être traité par +template_process_userfiles" +function _template_can_process() { + case "$1" in + *.png|*.jpg|*.gif|*.bmp) return 1;; + *.zip|*.jar|*.war|*.ear) return 1;; + *.tar|*.gz|*.tgz|*.bz2|*.tbz2) return 1;; + *) return 0;; + esac +} + +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 + _template_can_process "$userfile" || continue + 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-interaction.sh b/bash/tests/test-interaction.sh new file mode 100755 index 0000000..db35861 --- /dev/null +++ b/bash/tests/test-interaction.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 +#NULIB_NO_DISABLE_SET_X=1 + +args=( + "tester diverses fonctions de saisie" +) +parse_args "$@"; set -- "${args[@]}" + +estep "inter non auto non" +ask_yesno "oui ou non?" && echo oui || echo non +estep "inter oui auto oui" +ask_yesno "oui ou non?" O && echo oui || echo non +estep "inter non auto non" +ask_yesno "oui ou non?" N && echo oui || echo non +estep "inter non auto oui" +ask_yesno "oui ou non?" C && echo oui || echo non +estep "inter oui auto non" +ask_yesno "oui ou non?" X && echo oui || echo non + +estep "valeur par défaut vide" +read_value "valeur" empty "" N; echo "valeur=$empty" + +estep "valeur par défaut non vide" +read_value "valeur" default default N; echo "valeur=$default" + +estep "valeur requise" +read_value "valeur" required; echo "valeur=$required" diff --git a/bash/tests/test-output.sh b/bash/tests/test-output.sh new file mode 100755 index 0000000..3b369d7 --- /dev/null +++ b/bash/tests/test-output.sh @@ -0,0 +1,168 @@ +#!/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,--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/bash/tests/test-verbosity.sh b/bash/tests/test-verbosity.sh new file mode 100755 index 0000000..4b53d8f --- /dev/null +++ b/bash/tests/test-verbosity.sh @@ -0,0 +1,24 @@ +#!/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=( + "afficher divers messages avec les fonctions e*" +) +parse_args "$@"; set -- "${args[@]}" + +eimportant "important (q)" +eattention "attention (q)" +eerror "error (q)" +ewarn "warn (q)" +enote "note (qv)" +einfo "info (qv)" +eecho "echo (qv)" +edebug "debug (D)" + +estep "step (qv)" +estepe "stepe (qv)" +estepw "stepw (qv)" +estepn "stepn (qv)" +estepi "stepi (qv)" diff --git a/bin/_merge82 b/bin/_merge82 new file mode 100755 index 0000000..e372fb8 --- /dev/null +++ b/bin/_merge82 @@ -0,0 +1,4 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +exec "$(dirname -- "$0")/pmer" --tech-merge -Bdev82 dev74 -a "git checkout dev74" "$@" diff --git a/bin/_pman-composer_local_deps.php b/bin/_pman-composer_local_deps.php new file mode 100755 index 0000000..92aeda8 --- /dev/null +++ b/bin/_pman-composer_local_deps.php @@ -0,0 +1,14 @@ +#!/usr/bin/php +getLocalDeps(); +foreach ($deps as $dep => $path) { + echo "$path\n"; +} diff --git a/bin/_pman-composer_select_profile.php b/bin/_pman-composer_select_profile.php new file mode 100755 index 0000000..75cb8d9 --- /dev/null +++ b/bin/_pman-composer_select_profile.php @@ -0,0 +1,22 @@ +#!/usr/bin/php +selectProfile($profile, $config); +if (getenv("PMAN_COMPOSER_DEBUG")) { + $composer->print(); +} else { + $composer->write(); +} diff --git a/bin/_runphp_build-all b/bin/_runphp_build-all new file mode 100755 index 0000000..c6d0f4f --- /dev/null +++ b/bin/_runphp_build-all @@ -0,0 +1,30 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 + +force= +args=( + "Construire toutes les images supportées de runphp" + #"usage" + -f,--force force=1 "Créer les images même si elles existent déjà" +) +parse_args "$@"; set -- "${args[@]}" + +export RUNPHP_STANDALONE="$NULIBDIR" +export RUNPHP_PROJDIR= +export RUNPHP_REGISTRY= +export RUNPHP_DIST= +export RUNPHP_BUILD_FLAVOUR= + +runphp=("$MYDIR/../runphp/runphp" --bs) +[ -z "$force" ] && runphp+=(--ue) + +for RUNPHP_DIST in d12 d11; do + for RUNPHP_BUILD_FLAVOUR in +ic none; do + flavour="$RUNPHP_BUILD_FLAVOUR" + [ "$flavour" == none ] && flavour= + etitle "$RUNPHP_DIST$flavour" + "${runphp[@]}" + eend + done +done diff --git a/bin/composer b/bin/composer new file mode 120000 index 0000000..42fbf67 --- /dev/null +++ b/bin/composer @@ -0,0 +1 @@ +runphp \ No newline at end of file diff --git a/bin/nlman b/bin/nlman new file mode 100755 index 0000000..a6845bd --- /dev/null +++ b/bin/nlman @@ -0,0 +1,69 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +NULIB_NO_IMPORT_DEFAULTS=1 +source "$(dirname -- "$0")/../load.sh" || exit 1 + +LIST_FUNCS= +SHOW_MODULE= +SHOW_FUNCS=() +function nulib__define_functions() { + function nulib_check_loaded() { + local module + for module in "${NULIB_LOADED_MODULES[@]}"; do + [ "$module" == "$1" ] && return 0 + done + return 1 + } + function module:() { + if [ -n "$LIST_FUNCS" ]; then + esection "@$1: $2" + fi + local module + SHOW_MODULE= + for module in "${SHOW_FUNCS[@]}"; do + if [ "$module" == "@$1" ]; then + SHOW_MODULE=1 + fi + done + NULIB_MODULE="$1" + if ! nulib_check_loaded "$1"; then + NULIB_LOADED_MODULES+=("$1") + fi + } + function function:() { + if [ -n "$LIST_FUNCS" ]; then + eecho "$1" + elif [ -n "$SHOW_MODULE" ]; then + eecho "$COULEUR_BLEUE>>> $1 <<<$COULEUR_NORMALE" + eecho "$2" + else + local func + for func in "${SHOW_FUNCS[@]}"; do + if [ "$func" == "$1" ]; then + esection "$1" + eecho "$2" + fi + done + fi + } +} +require: DEFAULTS + +modules=() +args=( + "afficher l'aide d'une fonction nulib" + "FUNCTIONS|@MODULES..." + -m:,--module: modules "charger des modules supplémentaires" + -l,--list LIST_FUNCS=1 "lister les fonctions pour lesquelles une aide existe" +) +parse_args "$@"; set -- "${args[@]}" + +for func in "$@"; do + SHOW_FUNCS+=("$func") +done + +NULIB_LOADED_MODULES=(nulib) +require: DEFAULTS +for module in "${modules[@]}"; do + require: "$module" +done diff --git a/bin/nlshell b/bin/nlshell new file mode 100755 index 0000000..f5f64f6 --- /dev/null +++ b/bin/nlshell @@ -0,0 +1,38 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 + +force_reload= +args=( + "lancer un shell avec les fonctions de nulib préchargées" + -r,--force-reload force_reload=1 "forcer le rechargement des modules" +) +parse_args "$@"; set -- "${args[@]}" + +ac_set_tmpfile bashrc +echo >"$bashrc" "\ +if ! grep -q '/etc/bash.bashrc' /etc/profile; then + [ -f /etc/bash.bashrc ] && source /etc/bash.bashrc +fi +if ! grep -q '~/.bashrc' ~/.bash_profile; then + [ -f ~/.bashrc ] && source ~/.bashrc +fi +[ -f /etc/profile ] && source /etc/profile +[ -f ~/.bash_profile ] && source ~/.bash_profile + +# Modifier le PATH. Ajouter aussi le chemin vers les uapps python +PATH=$(qval "$NULIBDIR/bin:$PATH") + +if [ -n '$DEFAULT_PS1' ]; then + DEFAULT_PS1=$(qval "[nlshell] $DEFAULT_PS1") +else + if [ -z '$PS1' ]; then + PS1='\\u@\\h \\w \\$ ' + fi + PS1=\"[nlshell] \$PS1\" +fi + +$(qvals source "$NULIBDIR/load.sh") +NULIB_FORCE_RELOAD=$(qval "$force_reload")" + +"$SHELL" --rcfile "$bashrc" -i -- "$@" diff --git a/bin/p b/bin/p new file mode 100755 index 0000000..1c52283 --- /dev/null +++ b/bin/p @@ -0,0 +1,95 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 +require: git + +function git_status() { + local status r cwd + status="$(git -c color.status=always status "$@" 2>&1)"; r=$? + if [ -n "$status" ]; then + cwd="$Cwd" + [ -n "$cwd" ] || cwd="$(pwd)" + setx cwd=ppath2 "$cwd" "$OrigCwd" + etitle "$cwd" + if [ $r -eq 0 ]; then + echo "$status" + else + eerror "$status" + fi + eend + fi +} + +function git_statuses() { + local cwd="$(pwd)" dir + for dir in "$@"; do + cd "$dir" || die + git_status --porcelain + cd "$cwd" + done +} + +# sans arguments, il y a un comportement spécial +[ $# -eq 0 ] && NoArgs=1 || NoArgs= + +chdir= +all= +composer= +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" + -r,--composer composer=1 "faire l'opération sur tous les projets composer dépendants" +) +parse_args "$@"; set -- "${args[@]}" + +if [ -n "$NoArgs" ]; then + # si aucun argument n'est spécifié et si on n'est pas dans un projet git, + # afficher le status de tous les sous répertoires + setx toplevel=git_get_toplevel + [ -z "$toplevel" ] && all=1 +fi + +setx OrigCwd=pwd +if [ -n "$chdir" ]; then + cd "$chdir" || die +fi + +Cwd= +if [ -n "$all" ]; then + # liste de sous répertoires + if [ $# -gt 0 ]; then + # si on a une liste de patterns, l'utiliser + setx -a dirs=ls_dirs . "$@" + else + dirs=() + for dir in */.git; do + [ -d "$dir" ] || continue + dirs+=("${dir%/.git}") + done + fi + git_statuses "${dirs[@]}" + +elif [ -n "$composer" ]; then + # projets dépendants + git_ensure_gitvcs + setx toplevel=git_get_toplevel + cd "$toplevel" || die + setx cwd=ppath2 . "$OrigCwd" + [ -f composer.json ] || die "$cwd: ce n'est pas un projet composer" + + setx -a dirs="$MYDIR/_pman-composer_local_deps.php" + git_statuses "${dirs[@]}" + +else + # répertoire courant uniquement + git_ensure_gitvcs + Cwd="$(git_get_toplevel)" + + args=() + isatty || args+=(--porcelain) + git_status "${args[@]}" +fi diff --git a/bin/pman b/bin/pman new file mode 100755 index 0000000..123f193 --- /dev/null +++ b/bin/pman @@ -0,0 +1,371 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 +require: git pman pman.conf + +################################################################################ +# Informations +################################################################################ + +SHOW_VARS=( + --Configuration + "${CONFIG_VARS[@]}" + --Paramètres + CurrentBranch + CurrentType=SrcType +) + +function show_action() { + local var src + echo_setv ConfigBranch="$ConfigBranch" + echo_setv ConfigFile="$(ppath "$ConfigFile")" + for var in "${SHOW_VARS[@]}"; do + if [ "${var#--}" != "$var" ]; then + estep "${var#--}" + else + splitfsep "$var" = var src + [ -n "$src" ] || src="$var" + echo_setv "$var=${!src}" + fi + done +} + +################################################################################ +# Initialisation +################################################################################ + +function _init_config() { + if [ ! -f .pman.conf -o -n "$ForceCreate" ]; then + ac_set_tmpfile config + cp "$ConfigFile" "$config" + "${EDITOR:-nano}" "$config" + [ -s "$config" ] || return 1 + + cp "$config" .pman.conf + if testdiff .pman.conf "$ConfigFile"; then + ConfigFile="$(pwd)/.pman.conf" + load_config + load_branches current "$SrcBranch" + fi + git add .pman.conf + fi + if [ ! -f ".gitignore" ]; then + echo >.gitignore "\ +.~lock*# +.*.swp" + git add .gitignore + fi + return 0 +} + +function init_repo_action() { + local -a push_branches; local config + + [ ${#LocalBranches[*]} -eq 0 ] || die "Ce dépôt a déjà été initialisé" + + _init_config || exit_with ewarn "Initialisation du dépôt annulée" + + einfo "Création de la branche $MAIN" + git symbolic-ref HEAD "refs/heads/$MAIN" + git commit -m "commit initial" + push_branches+=("$MAIN") + + einfo "Création de la branche $DEVELOP" + git checkout -b "$DEVELOP" + push_branches+=("$DEVELOP") + + _push_branches +} + +function init_config_action() { + local -a push_branches; local config + + [ -f .pman.conf -a -z "$ForceCreate" ] && die "La configuration pman a déjà été initialisée" + + resolve_should_push + + _init_config || exit_with ewarn "Initialisation de la configuration annulée" + git commit -m "configuration pman" + push_branches+=("$CurrentBranch") + + _push_branches +} + +function _init_composer() { + if [ ! -f .composer.pman.yml -o -n "$ForceCreate" ]; then + ac_set_tmpfile config + cat >"$config" <Intégration initiale de la branche $UPSTREAM" \ + -srecursive -Xours --allow-unrelated-histories \ + "$UPSTREAM" + push_branches+=("$DEVELOP") + + _push_branches + fi + git checkout -q "$UPSTREAM" +} + +function _ensure_dist_branch() { + [ -n "$DIST" ] || die "La branche DIST n'a pas été définie" + [ "$1" == init -o -n "$DistBranch" ] || die "$DIST: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" +} + +function init_dist_action() { + local -a push_branches + + if [ -z "$DistBranch" ]; then + array_contains AllBranches "$DIST" && exit_with enote "\ +$DIST: une branche du même nom existe dans l'origine + git checkout $DIST" + _ensure_main_branch + _ensure_dist_branch init + + resolve_should_push + + enote "Vous allez créer la branche ${COULEUR_VERTE}$DIST${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$MAIN${COULEUR_NORMALE}" + ask_yesno "Voulez-vous continuer?" O || die + + einfo "Création de la branche $DIST" + git checkout -b "$DIST" "$MAIN" || die + push_branches+=("$DIST") + + _push_branches + fi + git checkout -q "$DIST" +} + +function init_feature_action() { + local -a push_branches; local branch + + [ -n "$FEATURE" ] || die "La branche FEATURE n'a pas été définie" + branch="${1#$FEATURE}" + [ -n "$branch" ] || die "Vous devez spécifier le nom de la branche" + branch="$FEATURE$branch" + + if ! array_contains LocalBranches "$branch"; then + array_contains AllBranches "$branch" && exit_with enote "\ +$branch: une branche du même nom existe dans l'origine + git checkout $branch" + _ensure_develop_branch + + resolve_should_push + + enote "Vous allez créer la branche ${COULEUR_VERTE}$branch${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$DEVELOP${COULEUR_NORMALE}" + ask_yesno "Voulez-vous continuer?" O || die + + einfo "Création de la branche $branch" + git checkout -b "$branch" "$DEVELOP" || die + push_branches+=("$branch") + + _push_branches + fi + git checkout -q "$branch" +} + +function init_action() { + local what="${1:-develop}"; shift + case "$what" in + init|repo|r) init_repo_action "$@";; + config) init_config_action "$@";; + composer) init_composer_action "$@";; + main|m) checkout_main_action;; + develop|dev|d) init_develop_action "$@";; + upstream|up|u) init_upstream_action "$@";; + dist|x) init_dist_action "$@";; + *) init_feature_action "$what" "$@";; + esac +} + +################################################################################ +# Programme principal +################################################################################ + +chdir= +ConfigBranch= +ConfigFile= +action=init +Origin= +[ -z "$PMAN_NO_PUSH" ] && Push=1 || Push= +ForceCreate= +args=( + "gérer un projet git" + "repo|config|composer +develop|upstream|dist + +INITIALISATION + +Par défaut, le script agit en mode initialisation qui permet de créer et/ou +configurer certaines branches du dépôt si elles n'existent pas déjà + + repo + initialiser un dépôt vide et créer les branches $MAIN et $DEVELOP + + develop + créer la branche $DEVELOP + upstream + créer la branche ${UPSTREAM:-UPSTREAM} en tant que source de la branche $DEVELOP + dist + créer la branche ${DIST:-DIST} en tant que destination de la branche $MAIN + anything + créer la branche ${FEATURE}anything à partir de la branche $DEVELOP" + -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations" + -B:,--config-branch ConfigBranch= "++\ +branche à partir de laquelle charger la configuration" + -c:,--config-file:CONFIG ConfigFile= "++\ +fichier de configuration des branches. cette option est prioritaire sur --config-branch +par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe" + -w,--show-config action=show "++\ +afficher la configuration chargée" + --composer-select-profile action=composer_select_profile "\ +sélectionner le profil composer spécifié en argument" + -O:,--origin Origin= "++\ +origine vers laquelle pousser les branches" + -n,--no-push Push= "\ +ne pas pousser les branches vers leur origine après leur création" + --push Push=1 "++\ +pousser les branches vers leur origine après leur création. +c'est l'option par défaut" + -f,--force-create ForceCreate=1 "\ +Avec config, forcer la (re)création du fichier .pman.conf" +) +parse_args "$@"; set -- "${args[@]}" + +# charger la configuration +ensure_gitdir "$chdir" +load_branches all +load_config "$MYNAME" +load_branches current + +# puis faire l'action que l'on nous demande +case "$action" in +show) + show_action "$@" + ;; +init) + git_ensure_cleancheckout + init_action "$@" + ;; +composer_select_profile) + exec "$MYDIR/_pman-$action.php" "$@" + ;; +*) + die "$action: action non implémentée" + ;; +esac diff --git a/bin/pmer b/bin/pmer new file mode 100755 index 0000000..9a7daf8 --- /dev/null +++ b/bin/pmer @@ -0,0 +1,262 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 +require: git pman pman.conf + +git_cleancheckout_DIRTY="\ +Vous avez des modifications locales. +Enregistrez ces modifications avant de fusionner la branche" + +function show_action() { + local commits + setx commits=_list_commits + if [ -n "$commits" ]; then + if [ $ShowLevel -ge 2 ]; then + { + echo "\ +# Commits à fusionner $SrcBranch --> $DestBranch + +$commits +" + _sd_COLOR=always _show_diff + } | less -eRF + else + einfo "Commits à fusionner $SrcBranch --> $DestBranch" + eecho "$commits" + fi + fi +} + +function ensure_branches() { + [ -n "$SrcBranch" -a -n "$DestBranch" ] || + die "$SrcBranch: Aucune configuration de fusion trouvée pour cette branche" + + array_contains LocalBranches "$SrcBranch" || die "$SrcBranch: branche source introuvable" + array_contains LocalBranches "$DestBranch" || die "$DestBranch: branche destination introuvable" +} + +function merge_action() { + [ -z "$ShouldPush" ] && enote "\ +L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine" + + enote "\ +Ce script va +- fusionner la branche ${COULEUR_BLEUE}$SrcBranch${COULEUR_NORMALE} dans ${COULEUR_ROUGE}$DestBranch${COULEUR_NORMALE}${Push:+ +- pousser les branches modifiées}" + ask_yesno "Voulez-vous continuer?" O || die + + local script=".git/pman-merge.sh" + local -a push_branches delete_branches + local hook + local comment= + local or_die=" || exit 1" + + _mscript_start + _scripta < + AFTER_MERGE_ + AFTER_DELETE_ + BEFORE_PUSH_ + AFTER_PUSH_ +srcType et destType pouvant valoir UPSTREAM, DEVELOP, FEATURE, RELEASE, MAIN, HOTFIX, DIST" + -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations" + -O:,--origin Origin= "++\ +origine à partir de laquelle les branches distantes sont considérées" + -B:,--config-branch ConfigBranch= "++\ +branche à partir de laquelle charger la configuration" + -c:,--config-file:CONFIG ConfigFile= "++\ +fichier de configuration des branches. cette option est prioritaire sur --config-branch +par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe" + --fake _Fake=1 "++option non documentée" + --keep-script _KeepScript=1 "++option non documentée" + -w,--show '$action=show; inc@ ShowLevel' "\ +lister les modifications qui seraient fusionnées dans la branche destination" + -b,--rebase action=rebase "\ +lancer git rebase -i sur la branche source. cela permet de réordonner les +commits pour nettoyer l'historique avant la fusion" + --merge action=merge "++\ +fusionner la branche source dans la branche destination correspondante. +c'est l'action par défaut" + --tech-merge TechMerge=1 "++option non documentée" + -s:,--squash:COMMIT_MSG SquashMsg= "\ +fusionner les modifications de la branche comme un seul commit. +cette option ne devrait pas être utilisée avec --no-delete" + -n,--no-push Push= "\ +ne pas pousser les branches vers leur origine après la fusion" + --push Push=1 "++\ +pousser les branches vers leur origine après la fusion. +c'est l'option par défaut" + -k,--no-delete Delete= "\ +ne pas supprimer la branche après la fusion dans la destination" + --delete Delete=1 "++\ +supprimer la branche après la fusion dans la destination. +c'est l'option par défaut" + -f,--force-merge ForceMerge=1 "++\ +forcer la fusion pour une branche qui devrait être traitée par prel" + -a:,--after-merge AfterMerge= "\ +évaluer le script spécifié après une fusion *réussie*" +) +parse_args "$@"; set -- "${args[@]}" + +# charger la configuration +ensure_gitdir "$chdir" +load_branches all +load_config "$MYNAME" +load_branches current "$1" + +resolve_should_push quiet + +# puis faire l'action que l'on nous demande +case "$action" in +show) + git_check_cleancheckout || ewarn "$git_cleancheckout_DIRTY" + ensure_branches + show_action "$@" + ;; +merge) + ShouldDelete=1 + no_merge_msg="$SrcBranch: cette branche doit être fusionnée dans $DestBranch avec prel" + if [ "$SrcType" == develop ]; then + [ -z "$ForceMerge" ] && die "$no_merge_msg" + [ -n "$AfterMerge" ] || setx AfterMerge=qvals git checkout -q "$SrcBranch" + elif [ "$SrcType" == release -o "$SrcType" == hotfix ]; then + die "$no_merge_msg" + fi + # n'autoriser la suppression que pour feature + [ "$SrcType" == feature ] || ShouldDelete= + [ -z "$ShouldDelete" ] && Delete= + [ -z "$_Fake" ] && git_ensure_cleancheckout + if array_contains LocalBranches "$SrcBranch"; then + ensure_branches + merge_action "$@" + elif array_contains AllBranches "$SrcBranch"; then + enote "$SrcBranch: une branche du même nom existe dans l'origine" + die "$SrcBranch: branche locale introuvable" + else + die "$SrcBranch: branche introuvable" + fi + ;; +*) + die "$action: action non implémentée" + ;; +esac diff --git a/bin/prel b/bin/prel new file mode 100755 index 0000000..2ec3a31 --- /dev/null +++ b/bin/prel @@ -0,0 +1,292 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 +require: git pman pman.conf + +git_cleancheckout_DIRTY="\ +Vous avez des modifications locales. +Enregistrez ces modifications avant de créer une release" + +function show_action() { + local commits + setx commits=_list_commits + if [ -n "$commits" ]; then + if [ $ShowLevel -ge 2 ]; then + { + echo "\ +# Commits à fusionner $SrcBranch --> $DestBranch + +$commits +" + _sd_COLOR=always _show_diff + } | less -eRF + else + einfo "Commits à fusionner $SrcBranch --> $DestBranch" + eecho "$commits" + fi + fi +} + +function ensure_branches() { + [ -n "$SrcBranch" -a -n "$DestBranch" ] || + die "$SrcBranch: Aucune configuration de fusion trouvée pour cette branche" + + array_contains LocalBranches "$SrcBranch" || die "$SrcBranch: branche source introuvable" + array_contains LocalBranches "$DestBranch" || die "$DestBranch: branche destination introuvable" + + Tag="$TAG_PREFIX$Version$TAG_SUFFIX" + local -a tags + setx -a tags=git tag -l "${TAG_PREFIX}*${TAG_SUFFIX}" + if [ -z "$ForceCreate" ]; then + array_contains tags "$Tag" && die "$Tag: le tag correspondant à la version existe déjà" + fi +} + +function create_release_action() { + if [ -n "$ReleaseBranch" ]; then + Version="${ReleaseBranch#$RELEASE}" + Tag="$TAG_PREFIX$Version$TAG_SUFFIX" + merge_release_action "$@"; return $? + elif [ -n "$HotfixBranch" ]; then + Version="${HotfixBranch#$HOTFIX}" + Tag="$TAG_PREFIX$Version$TAG_SUFFIX" + merge_hotfix_action "$@"; return $? + fi + + [ -n "$ManualRelease" ] && ewarn "\ +L'option --no-merge a été forcée puisque ce dépôt ne supporte pas les releases automatiques" + [ -z "$ShouldPush" ] && enote "\ +L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine" + + if [ -z "$Version" -a -n "$CurrentVersion" -a -f VERSION.txt ]; then + Version="$(" + -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations" + -O:,--origin Origin= "++\ +origine à partir de laquelle les branches distantes sont considérées" + -B:,--config-branch ConfigBranch= "++\ +branche à partir de laquelle charger la configuration" + -c:,--config-file:CONFIG ConfigFile= "++\ +fichier de configuration des branches. cette option est prioritaire sur --config-branch +par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe" + -n,--no-push Push= "\ +ne pas pousser les branches vers leur origine après la fusion" + --push Push=1 "++\ +pousser les branches vers leur origine après la fusion. +c'est l'option par défaut" +) +parse_args "$@"; set -- "${args[@]}" + +# charger la configuration +ensure_gitdir "$chdir" +load_branches all +load_config "$MYNAME" +load_branches current + +branch="$1" +if [ -z "$branch" -a ${#FeatureBranches[*]} -eq 1 ]; then + branch="${FeatureBranches[0]}" +fi +[ -n "$branch" ] || die "Vous devez spécifier la branche à créer" +branch="$FEATURE${branch#$FEATURE}" + +resolve_should_push +git_ensure_cleancheckout + +if array_contains AllBranches "$branch"; then + git checkout -q "$branch" +else + # si la branche source n'existe pas, la créer + args=(--origin "$Origin") + if [ -n "$ConfigFile" ]; then args+=(--config-file "$ConfigFile") + elif [ -n "$ConfigBranch" ]; then args+=(--config-branch "$ConfigBranch") + fi + [ -z "$Push" ] && args+=(--no-push) + exec "$MYDIR/pman" "${args[@]}" "$branch" +fi diff --git a/bin/runphp b/bin/runphp new file mode 100755 index 0000000..81a4f81 --- /dev/null +++ b/bin/runphp @@ -0,0 +1,51 @@ +#!/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 + +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 + if [ "$cwd" == "$HOME" -o "$cwd" == / ]; then + cd "$owd" + break + fi + cd .. +done + +if [ -z "$PROJDIR" ]; then + # s'il n'y a pas de projet, --bs est l'action par défaut + [ $# -gt 0 ] || set -- --bs --ue +elif [ "$MYNAME" == composer ]; then + set -- composer "$@" +else + case "$1" in + *.php|*.phar) set -- php "$@";; + esac +fi + +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/composer.json b/composer.json new file mode 100644 index 0000000..5a6a8da --- /dev/null +++ b/composer.json @@ -0,0 +1,55 @@ +{ + "name": "nulib/base", + "type": "library", + "description": "fonctions et classes essentielles", + "repositories": [ + { + "type": "composer", + "url": "https://repos.univ-reunion.fr/composer" + } + ], + "extra": { + "branch-alias": { + "dev-dev74": "7.4.x-dev", + "dev-dev82": "8.2.x-dev" + } + }, + "replace": { + "nulib/php": "<0.6.0" + }, + "require": { + "symfony/yaml": "^5.0", + "ext-json": "*", + "php": "^7.4" + }, + "require-dev": { + "nulib/tests": "^7.4", + "ext-posix": "*", + "ext-pcntl": "*", + "ext-curl": "*", + "ext-pdo": "*", + "ext-pgsql": "*", + "ext-sqlite3": "*" + }, + "autoload": { + "psr-4": { + "nulib\\": "php/src" + } + }, + "autoload-dev": { + "psr-4": { + "nulib\\": "php/tests" + } + }, + "config": { + "vendor-dir": "php/vendor" + }, + "scripts": { + }, + "authors": [ + { + "name": "Jephte Clain", + "email": "Jephte.Clain@univ-reunion.fr" + } + ] +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..23b756f --- /dev/null +++ b/composer.lock @@ -0,0 +1,2038 @@ +{ + "_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": "2d630ab5ff0ffe6139447cf93c362ed7", + "packages": [ + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/yaml", + "version": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "a454d47278cc16a5db371fe73ae66a78a633371e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/a454d47278cc16a5db371fe73ae66a78a633371e", + "reference": "a454d47278cc16a5db371fe73ae66a78a633371e", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<5.3" + }, + "require-dev": { + "symfony/console": "^5.3|^6.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:15:36+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "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.13.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-04-29T12:36:36+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.4.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, + { + "name": "nulib/tests", + "version": "7.4", + "source": { + "type": "git", + "url": "https://git.univ-reunion.fr/sda-php/nulib-tests.git", + "reference": "8f641d9a7cf6aba1453cb42ebd15951aa7002e1b" + }, + "require": { + "php": "^7.3 || 8.0.*", + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-pu9": "7.3.x-dev", + "dev-pu10": "8.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "nulib\\tests\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "nulib\\tests\\": "tests" + } + }, + "authors": [ + { + "name": "Jephte Clain", + "email": "Jephte.Clain@univ-reunion.fr" + } + ], + "description": "fonctions et classes pour les tests", + "time": "2025-02-28T17:12:35+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.23", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.6", + "sebastian/global-state": "^5.0.7", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.23" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-05-02T06:40:34+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:33:00+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:35:11+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "ext-json": "*", + "php": "^7.4" + }, + "platform-dev": { + "ext-posix": "*", + "ext-pcntl": "*", + "ext-curl": "*", + "ext-pdo": "*", + "ext-pgsql": "*", + "ext-sqlite3": "*" + }, + "plugin-api-version": "2.2.0" +} diff --git a/dockerfiles/Dockerfile.adminer b/dockerfiles/Dockerfile.adminer new file mode 100644 index 0000000..688f199 --- /dev/null +++ b/dockerfiles/Dockerfile.adminer @@ -0,0 +1,31 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr/dist + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=php /g/ /g/ +RUN /g/build -a @adminer + +EXPOSE 80 +ENTRYPOINT ["/g/entrypoint"] diff --git a/dockerfiles/Dockerfile.adminer+ic b/dockerfiles/Dockerfile.adminer+ic new file mode 100644 index 0000000..13d4fe0 --- /dev/null +++ b/dockerfiles/Dockerfile.adminer+ic @@ -0,0 +1,40 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr/dist + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/instantclient AS instantclient +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +COPY --from=instantclient /g/ /g/ +COPY --from=instantclient /src/ /src/ +RUN /g/build _instantclient_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=php /g/ /g/ +RUN /g/build -a @adminer + +COPY --from=instantclient /g/ /g/ +COPY --from=builder /opt/oracle/ /opt/oracle/ +RUN /g/build instantclient + +EXPOSE 80 +ENTRYPOINT ["/g/entrypoint"] diff --git a/dockerfiles/Dockerfile.mariadb10 b/dockerfiles/Dockerfile.mariadb10 new file mode 100644 index 0000000..f77d113 --- /dev/null +++ b/dockerfiles/Dockerfile.mariadb10 @@ -0,0 +1,19 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG REGISTRY=pubdocker.univ-reunion.fr/dist +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/mariadb AS mariadb +FROM $REGISTRY/src/legacytools AS legacytools + +FROM mariadb:10 +ARG APT_PROXY TIMEZONE +ENV APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=mariadb /g/ /g/ +RUN /g/build -a @base @mariadb + +COPY --from=legacytools /g/ /g/ +RUN /g/build nutools servertools + +EXPOSE 3306 +ENTRYPOINT ["/g/entrypoint"] diff --git a/dockerfiles/Dockerfile.php-apache b/dockerfiles/Dockerfile.php-apache new file mode 100644 index 0000000..641de40 --- /dev/null +++ b/dockerfiles/Dockerfile.php-apache @@ -0,0 +1,31 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr/dist + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=php /g/ /g/ +RUN /g/build -a @php-apache-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..8907e78 --- /dev/null +++ b/dockerfiles/Dockerfile.php-apache+ic @@ -0,0 +1,44 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr/dist + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/legacytools AS legacytools +FROM $REGISTRY/src/instantclient AS instantclient +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +COPY --from=instantclient /g/ /g/ +COPY --from=instantclient /src/ /src/ +RUN /g/build _instantclient_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=legacytools /g/ /g/ +RUN /g/build nutools + +COPY --from=php /g/ /g/ +RUN /g/build -a @php-apache-cas php-utils + +COPY --from=instantclient /g/ /g/ +COPY --from=builder /opt/oracle/ /opt/oracle/ +RUN /g/build instantclient + +EXPOSE 80 443 +ENTRYPOINT ["/g/entrypoint"] diff --git a/dockerfiles/Dockerfile.php-cli b/dockerfiles/Dockerfile.php-cli new file mode 100644 index 0000000..f6bcfbc --- /dev/null +++ b/dockerfiles/Dockerfile.php-cli @@ -0,0 +1,30 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr/dist + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=php /g/ /g/ +RUN /g/build @php-cli php-utils + +ENTRYPOINT ["/g/entrypoint"] diff --git a/dockerfiles/Dockerfile.php-cli+ic b/dockerfiles/Dockerfile.php-cli+ic new file mode 100644 index 0000000..48f376b --- /dev/null +++ b/dockerfiles/Dockerfile.php-cli+ic @@ -0,0 +1,43 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr/dist + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/legacytools AS legacytools +FROM $REGISTRY/src/instantclient AS instantclient +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +COPY --from=instantclient /g/ /g/ +COPY --from=instantclient /src/ /src/ +RUN /g/build _instantclient_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=legacytools /g/ /g/ +RUN /g/build nutools + +COPY --from=php /g/ /g/ +RUN /g/build @php-cli php-utils + +COPY --from=instantclient /g/ /g/ +COPY --from=builder /opt/oracle/ /opt/oracle/ +RUN /g/build instantclient + +ENTRYPOINT ["/g/entrypoint"] diff --git a/dockerfiles/Dockerfile.postgres15 b/dockerfiles/Dockerfile.postgres15 new file mode 100644 index 0000000..4edb5c5 --- /dev/null +++ b/dockerfiles/Dockerfile.postgres15 @@ -0,0 +1,17 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG REGISTRY=pubdocker.univ-reunion.fr/dist + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/postgres AS postgres + +FROM postgres:15-bookworm +ARG APT_PROXY TIMEZONE +ENV APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=postgres /g/ /g/ +RUN /g/build -a @base @postgres +RUN /g/pkg i @ssl @git + +EXPOSE 5432 +ENTRYPOINT ["/g/entrypoint"] diff --git a/lib/completion.d/pman b/lib/completion.d/pman new file mode 100644 index 0000000..787e28b --- /dev/null +++ b/lib/completion.d/pman @@ -0,0 +1,37 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +function __pman_pmer_branches() { + local toplevel="$(git rev-parse --show-toplevel 2>/dev/null)" + [ -n "$toplevel" ] || return 0 + + ( + # cf pman.conf.sh + UPSTREAM= + DEVELOP=develop + FEATURE=wip/ + RELEASE=release- + MAIN=master + TAG_PREFIX= + TAG_SUFFIX= + HOTFIX=hotfix- + DIST= + [ -f "$toplevel/.pman.conf" ] && source "$toplevel/.pman.conf" + # lister les branches + branches="$DEVELOP|$FEATURE.*" + [ -n "$UPSTREAM" ] && branches="$branches|$UPSTREAM" + remote=origin/ + { + git for-each-ref refs/heads/ --format='%(refname:short)' 2>/dev/null + git for-each-ref "refs/remotes/$remote" --format='%(refname:short)' 2>/dev/null | + grep -F "$remote" | + cut -c $((${#remote} + 1))- + } | LANG=C sort -u | grep -E "^($branches)\$" + ) +} + +function __pmer_completion() { + local cur + _get_comp_words_by_ref cur + COMPREPLY=($(compgen -W "$(__pman_pmer_branches)" "$cur")) +} +complete -F __pmer_completion pmer 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..8ed9698 --- /dev/null +++ b/lib/uinst/conf @@ -0,0 +1,20 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +source "$@" || exit 1 +cd "$srcdir" + +# supprimer les fichiers de VCS +rm -rf .git + +# completion +fromdir=lib/completion.d +todir="$HOME/etc/completion.d" +mkdir -p "$todir" +for file in pman; do + from="$fromdir/$file" + to="$todir/$file" + if [ -f "$to" ]; then + diff -q "$from" "$to" && continue + fi + cp "$from" "$to" +done diff --git a/lib/uinst/rootconf b/lib/uinst/rootconf new file mode 100644 index 0000000..bdc0ed3 --- /dev/null +++ b/lib/uinst/rootconf @@ -0,0 +1,5 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +source "$@" || exit 1 + +"$srcdir/lib/setup.sh" "$dest" diff --git a/load.sh b/load.sh new file mode 100644 index 0000000..fa544c9 --- /dev/null +++ b/load.sh @@ -0,0 +1,182 @@ +##@cooked comments # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +## Charger nulib et rendre disponible les modules bash, awk, php et python +##@cooked nocomments +# Ce fichier doit être sourcé en premier. Si ce fichier n'est pas sourcé, alors +# le répertoire nulib doit être disponible dans le répertoire du script qui +# inclue ce fichier. +# Une fois ce fichier sourcé, les autres modules peuvent être importés avec +# require:() e.g. +# source /etc/nulib.sh || exit 1 +# require: other_modules +# ou pour une copie locale de nulib: +# source "$(dirname "$0")/nulib/load.sh" || exit 1 +# require: other_modules + +# vérifier version minimum de bash +if [ "x$BASH" = "x" ]; then + echo "ERROR: nulib: this script requires bash" + exit 1 +fi + +function eerror() { echo "ERROR: $*" 1>&2; } +function die() { [ $# -gt 0 ] && eerror "$*"; exit 1; } +function edie() { [ $# -gt 0 ] && eerror "$*"; return 1; } +function delpath() { local _qdir="${1//\//\\/}"; eval "export ${2:-PATH}; ${2:-PATH}"'="${'"${2:-PATH}"'#$1:}"; '"${2:-PATH}"'="${'"${2:-PATH}"'%:$1}"; '"${2:-PATH}"'="${'"${2:-PATH}"'//:$_qdir:/:}"; [ "$'"${2:-PATH}"'" == "$1" ] && '"${2:-PATH}"'='; } +function addpath() { local _qdir="${1//\//\\/}"; eval "export ${2:-PATH}; "'[ "${'"${2:-PATH}"'#$1:}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'%:$1}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'//:$_qdir:/:}" == "$'"${2:-PATH}"'" -a "$'"${2:-PATH}"'" != "$1" ] && '"${2:-PATH}"'="${'"${2:-PATH}"':+$'"${2:-PATH}"':}$1"'; } +function inspathm() { local _qdir="${1//\//\\/}"; eval "export ${2:-PATH}; "'[ "${'"${2:-PATH}"'#$1:}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'%:$1}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'//:$_qdir:/:}" == "$'"${2:-PATH}"'" -a "$'"${2:-PATH}"'" != "$1" ] && '"${2:-PATH}"'="$1${'"${2:-PATH}"':+:$'"${2:-PATH}"'}"'; } +function inspath() { delpath "$@"; inspathm "$@"; } + +if [ ${BASH_VERSINFO[0]} -ge 5 -o \( ${BASH_VERSINFO[0]} -eq 4 -a ${BASH_VERSINFO[1]} -ge 1 \) ]; then : +elif [ -n "$NULIB_IGNORE_BASH_VERSION" ]; then : +else die "nulib: bash 4.1+ is required" +fi + +# Calculer emplacement de nulib +NULIBDIR="@@dest@@" +if [ "$NULIBDIR" = "@@""dest""@@" ]; then + # La valeur "@@"dest"@@" n'est remplacée que dans la copie de ce script + # faite dans /etc. Sinon, il faut toujours faire le calcul. Cela permet de + # déplacer la librairie n'importe où sur le disque, ce qui est + # particulièrement intéressant quand on fait du déploiement. + NULIBDIR="${BASH_SOURCE[0]}" + if [ -f "$NULIBDIR" -a "$(basename -- "$NULIBDIR")" == load.sh ]; then + # Fichier sourcé depuis nulib/ + NULIB_SOURCED=1 + NULIBDIR="$(dirname -- "$NULIBDIR")" + elif [ -f "$NULIBDIR" -a "$(basename -- "$NULIBDIR")" == nulib.sh ]; then + # Fichier sourcé depuis nulib/bash/src + NULIB_SOURCED=1 + NULIBDIR="$(dirname -- "$NULIBDIR")/../.." + else + # Fichier non sourcé. Tout exprimer par rapport au script courant + NULIB_SOURCED= + NULIBDIR="$(dirname -- "$0")" + if [ -d "$NULIBDIR/nulib" ]; then + NULIBDIR="$NULIBDIR/nulib" + elif [ -d "$NULIBDIR/lib/nulib" ]; then + NULIBDIR="$NULIBDIR/lib/nulib" + fi + fi +elif [ "${BASH_SOURCE[0]}" = /etc/nulib.sh ]; then + # Fichier chargé depuis /etc/nulib.sh + NULIB_SOURCED=1 +fi +NULIBDIR="$(cd "$NULIBDIR" 2>/dev/null; pwd)" +NULIBDIRS=("$NULIBDIR/bash/src") + +# marqueur pour vérifier que nulib a réellement été chargé. il faut avoir $NULIBINIT == $NULIBDIR +# utilisé par le module base qui doit pouvoir être inclus indépendamment +NULIBINIT="$NULIBDIR" + +## Modules bash +NULIB_LOADED_MODULES=(nulib) +NULIB_DEFAULT_MODULES=(base pretty sysinfos) + +# Si cette variable est non vide, require: recharge toujours le module, même +# s'il a déjà été chargé. Cette valeur n'est pas transitive: il faut toujours +# recharger explicitement tous les modules désirés +NULIB_FORCE_RELOAD= + +function nulib__define_functions() { + function nulib_check_loaded() { + local module + for module in "${NULIB_LOADED_MODULES[@]}"; do + [ "$module" == "$1" ] && return 0 + done + return 1 + } + function module:() { + NULIB_MODULE="$1" + if ! nulib_check_loaded "$1"; then + NULIB_LOADED_MODULES+=("$1") + fi + } + function function:() { + : + } +} + +function nulib__load:() { + local nl__module nl__nulibdir nl__found + [ $# -gt 0 ] || set DEFAULTS + + for nl__module in "$@"; do + nl__found= + for nl__nulibdir in "${NULIBDIRS[@]}"; do + if [ -f "$nl__nulibdir/$nl__module.sh" ]; then + source "$nl__nulibdir/$nl__module.sh" || die + nl__found=1 + break + fi + done + [ -n "$nl__found" ] || die "nulib: unable to find module $nl__module in (${NULIBDIRS[*]})" + done +} +function nulib__require:() { + local nr__module nr__nulibdir nr__found + [ $# -gt 0 ] || set DEFAULTS + + # sauvegarder valeurs globales + local nr__orig_module="$NULIB_MODULE" + NULIB_MODULE= + + # garder une copie de la valeur originale et casser la transitivité + local nr__force_reload="$NULIB_FORCE_RELOAD" + local NULIB_FORCE_RELOAD + + for nr__module in "$@"; do + nr__found= + for nr__nulibdir in "${NULIBDIRS[@]}"; do + if [ -f "$nr__nulibdir/$nr__module.sh" ]; then + nr__found=1 + if [ -n "$nr__force_reload" ] || ! nulib_check_loaded "$nr__module"; then + NULIB_LOADED_MODULES+=("$nr__module") + source "$nr__nulibdir/$nr__module.sh" || die + fi + break + fi + done + if [ -z "$nr__found" -a "$nr__module" == DEFAULTS ]; then + for nr__module in "${NULIB_DEFAULT_MODULES[@]}"; do + if [ -f "$nr__nulibdir/$nr__module.sh" ]; then + nr__found=1 + if [ -n "$nr__force_reload" ] || ! nulib_check_loaded "$nr__module"; then + NULIB_LOADED_MODULES+=("$nr__module") + source "$nr__nulibdir/$nr__module.sh" || die + fi + else + break + fi + done + fi + [ -n "$nr__found" ] || die "nulib: unable to find module $nr__module in (${NULIBDIRS[*]})" + done + + # restaurer valeurs globales + NULIB_MODULE="$nr__orig_module" +} + +# désactiver set -x +NULIB__DISABLE_SET_X='local NULIB__SET_X; [ -z "$NULIB_NO_DISABLE_SET_X" ] && [[ $- == *x* ]] && { set +x; NULIB__SET_X=1; }' +NULIB__ENABLE_SET_X='[ -n "$NULIB__SET_X" ] && set -x' +# désactiver set -x de manière réentrante +NULIB__RDISABLE_SET_X='[ -z "$NULIB_NO_DISABLE_SET_X" ] && [[ $- == *x* ]] && { set +x; local NULIB_REQUIRE_SET_X=1; }; if [ -n "$NULIB_REQUIRE_SET_X" ]; then [ -n "$NULIB_REQUIRE_SET_X_RL1" ] || local NULIB_REQUIRE_SET_X_RL1; local NULIB_REQUIRE_SET_X_RL2=$RANDOM; [ -n "$NULIB_REQUIRE_SET_X_RL1" ] || NULIB_REQUIRE_SET_X_RL1=$NULIB_REQUIRE_SET_X_RL2; fi' +NULIB__RENABLE_SET_X='[ -n "$NULIB_REQUIRE_SET_X" -a "$NULIB_REQUIRE_SET_X_RL1" == "$NULIB_REQUIRE_SET_X_RL2" ] && set -x' + +function require:() { + eval "$NULIB__RDISABLE_SET_X" + nulib__define_functions + nulib__require: "$@" + eval "$NULIB__RENABLE_SET_X" + return 0 +} + +## Autres modules +[ -d "$NULIBDIR/awk/src" ] && inspath "$NULIBDIR/awk/src" AWKPATH; export AWKPATH +[ -d "$NULIBDIR/python3/src" ] && inspath "$NULIBDIR/python3/src" PYTHONPATH; export PYTHONPATH + +## Auto import DEFAULTS +nulib__define_functions +if [ -n "$NULIB_SOURCED" -a -z "$NULIB_NO_IMPORT_DEFAULTS" ]; then + require: DEFAULTS +fi diff --git a/php/run-tests b/php/run-tests new file mode 100755 index 0000000..1b0c5a1 --- /dev/null +++ b/php/run-tests @@ -0,0 +1,5 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +MYDIR="$(dirname -- "$0")" +VENDOR="$MYDIR/../vendor" +"$VENDOR/bin/phpunit" --bootstrap "$VENDOR/autoload.php" "$@" "$MYDIR/tests" diff --git a/php/src/A.php b/php/src/A.php new file mode 100644 index 0000000..75f0284 --- /dev/null +++ b/php/src/A.php @@ -0,0 +1,351 @@ +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 shift(?array &$dest, int $count=1, $default=null) { + if ($dest === null) return null; + $values = array_slice($dest, 0, $count); + $dest = array_slice($dest, $count); + if ($values === []) return $default; + else return $count == 1? $values[0]: $values; + } + + 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)); + } + + ############################################################################# + + /** + * s'assurer que $array est un tableau associatif, en remplaçant toutes les + * clés numériques par la clé correspondante dans $key + * + * $array = ["first", "second"] + * A::ensure_assoc($array, ["a", "b"]); + * // returns ["a" => "first", "b" => "second"] + * + */ + static final function ensure_assoc(?array &$array, array $keys, ?array $params=null): void { + $prefix = $params["key_prefix"] ?? null; + $suffix = $params["key_suffix"] ?? null; + $index = 0; + foreach ($keys as $key) { + if ($prefix !== null || $suffix !== null) { + $destKey = "$prefix$key$suffix"; + } else { + # préserver les clés numériques + $destKey = $key; + } + if ($array !== null && array_key_exists($destKey, $array)) continue; + while (in_array($index, $keys, true)) { + $index++; + } + if ($array !== null && array_key_exists($index, $array)) { + $array[$destKey] = $array[$index]; + unset($array[$index]); + $index++; + } + } + } + + /** + * s'assurer que $array contient toutes les clés de $defaults, avec la valeur + * par défaut le cas échéant + * + * $missings est un tableau indiquant des valeurs qui si elles sont dans + * $array, signifie que la clé correspondante doit être considérée comme + * inexistante (et donc remplacée par la valeur de $defaults) + */ + static final function ensure_keys(?array &$array, array $defaults, ?array $missings=null, ?array $params=null): void { + $keys = array_keys($defaults); + $prefix = $params["key_prefix"] ?? null; + $suffix = $params["key_suffix"] ?? null; + foreach ($keys as $key) { + $destKey = "$prefix$key$suffix"; + $haveMissing = $missings !== null && array_key_exists($key, $missings); + if ($array === null || !array_key_exists($destKey, $array)) { + $array[$destKey] = $defaults[$key]; + } elseif ($haveMissing && $array[$destKey] === $missings[$key]) { + $array[$destKey] = $defaults[$key]; + } + } + } + + /** + * supprimer dans $array les clés dont les valeurs correspondent au tableau + * $missings + */ + static final function delete_missings(?array &$array, array $missings, ?array $params=null): void { + $prefix = $params["key_prefix"] ?? null; + $suffix = $params["key_suffix"] ?? null; + foreach ($missings as $key => $missing) { + $destKey = "$prefix$key$suffix"; + if (array_key_exists($destKey, $array) && $array[$destKey] === $missing) { + unset($array[$destKey]); + } + } + } + + /** + * s'assurer que les clés dans $array sont dans le même ordre que dans $keys + * + * les clés supplémentaires sont poussées à la fin du tableau + */ + static final function ensure_order(?array &$array, array $keys, ?array $params=null): void { + if ($array === null) return; + + $prefix = $params["key_prefix"] ?? null; + $suffix = $params["key_suffix"] ?? null; + if ($prefix !== null || $suffix !== null) { + foreach ($keys as &$key) { + $key = "$prefix$key$suffix"; + }; unset($key); + } + + $destKeys = array_keys($array); + $keyCount = count($keys); + if (array_slice($destKeys, 0, $keyCount) === $keys) { + # si le tableau a déjà les bonnes clés dans le bon ordre, rien à faire + return; + } + + $ordered = []; + foreach ($keys as $key) { + if (array_key_exists($key, $array)) { + $ordered[$key] = $array[$key]; + unset($array[$key]); + } + } + $preserveKeys = $params["preserve_keys"] ?? false; + if ($preserveKeys) $array = cl::merge2($ordered, $array); + else $array = array_merge($ordered, $array); + } +} diff --git a/php/src/AccessException.php b/php/src/AccessException.php new file mode 100644 index 0000000..6996667 --- /dev/null +++ b/php/src/AccessException.php @@ -0,0 +1,36 @@ + $file, + "line" => $line, + "class" => $class, + "object" => null, + "type" => $type, + "function" => $function, + "args" => [], + ]; + } + return $frames; + } + + function __construct(Throwable $exception) { + $this->class = get_class($exception); + $this->message = $exception->getMessage(); + $this->code = $exception->getCode(); + $this->file = $exception->getFile(); + $this->line = $exception->getLine(); + $this->trace = self::extract_trace($exception->getTrace()); + $previous = $exception->getPrevious(); + if ($previous !== null) $this->previous = new static($previous); + } + + /** @var string */ + protected $class; + + function getClass(): string { + return $this->class; + } + + /** @var string */ + protected $message; + + function getMessage(): string { + return $this->message; + } + + /** @var mixed */ + protected $code; + + function getCode() { + return $this->code; + } + + /** @var string */ + protected $file; + + function getFile(): string { + return $this->file; + } + + /** @var int */ + protected $line; + + function getLine(): int { + return $this->line; + } + + /** @var array */ + protected $trace; + + function getTrace(): array { + return $this->trace; + } + + function getTraceAsString(): string { + $lines = []; + foreach ($this->trace as $index => $frame) { + $lines[] = "#$index $frame[file]($frame[line]): $frame[class]$frame[type]$frame[function]()"; + } + $index++; + $lines[] = "#$index {main}"; + return implode("\n", $lines); + } + + /** @var ExceptionShadow */ + protected $previous; + + function getPrevious(): ?ExceptionShadow { + return $this->previous; + } +} diff --git a/php/src/ExitError.php b/php/src/ExitError.php new file mode 100644 index 0000000..a14c3a8 --- /dev/null +++ b/php/src/ExitError.php @@ -0,0 +1,31 @@ +userMessage = $userMessage; + } + + function isError(): bool { + return $this->getCode() !== 0; + } + + /** @var ?string */ + protected $userMessage; + + function haveUserMessage(): bool { + return $this->userMessage !== null; + } + + function getUserMessage(): ?string { + return $this->userMessage; + } +} diff --git a/php/src/IArrayWrapper.php b/php/src/IArrayWrapper.php new file mode 100644 index 0000000..1509129 --- /dev/null +++ b/php/src/IArrayWrapper.php @@ -0,0 +1,11 @@ +getUserMessage(); + else return null; + } + + /** @param Throwable|ExceptionShadow $e */ + static final function get_user_summary($e): string { + $parts = []; + $first = true; + while ($e !== null) { + $message = self::get_user_message($e); + if (!$message) $message = "(no message)"; + if ($first) $first = false; + else $parts[] = "caused by "; + $parts[] = get_class($e) . ": " . $message; + $e = $e->getPrevious(); + } + return implode(", ", $parts); + } + + /** @param Throwable|ExceptionShadow $e */ + static function get_message($e): ?string { + $message = $e->getMessage(); + if (!$message && $e instanceof self) $message = $e->getUserMessage(); + return $message; + } + + /** @param Throwable|ExceptionShadow $e */ + static final function get_summary($e): string { + $parts = []; + $first = true; + while ($e !== null) { + $message = self::get_message($e); + if (!$message) $message = "(no message)"; + if ($first) $first = false; + else $parts[] = "caused by "; + if ($e instanceof ExceptionShadow) $class = $e->getClass(); + else $class = get_class($e); + $parts[] = "$class: $message"; + $e = $e->getPrevious(); + } + return implode(", ", $parts); + } + + /** @param Throwable|ExceptionShadow $e */ + static final function get_traceback($e): string { + $tbs = []; + $previous = false; + while ($e !== null) { + if (!$previous) { + $efile = $e->getFile(); + $eline = $e->getLine(); + $tbs[] = "at $efile($eline)"; + } else { + $tbs[] = "~~ caused by: " . self::get_summary($e); + } + $tbs[] = $e->getTraceAsString(); + $e = $e->getPrevious(); + $previous = true; + #XXX il faudrait ne pas réinclure les lignes communes aux exceptions qui + # ont déjà été affichées + } + return implode("\n", $tbs); + } + + function __construct($userMessage, $techMessage=null, $code=0, ?Throwable $previous=null) { + $this->userMessage = $userMessage; + if ($techMessage === null) $techMessage = $userMessage; + parent::__construct($techMessage, $code, $previous); + } + + /** @var ?string */ + protected $userMessage; + + function getUserMessage(): ?string { + return $this->userMessage; + } +} diff --git a/php/src/ValueException.php b/php/src/ValueException.php new file mode 100644 index 0000000..12813d2 --- /dev/null +++ b/php/src/ValueException.php @@ -0,0 +1,76 @@ +"; + } elseif (is_array($value)) { + $values = $value; + $parts = []; + $index = 0; + foreach ($values as $key => $value) { + if ($key === $index) { + $index++; + $parts[] = self::value($value); + } else { + $parts[] = "$key=>".self::value($value); + } + } + return "[" . implode(", ", $parts) . "]"; + } elseif (is_string($value)) { + return $value; + } else { + return var_export($value, true); + } + } + + private static function message($value, ?string $message, ?string $kind, ?string $prefix, ?string $suffix): string { + if ($kind === null) $kind = "value"; + if ($message === null) $message = "$kind$suffix"; + if ($value !== null) { + $value = self::value($value); + if ($prefix) $prefix = "$prefix: $value"; + else $prefix = $value; + } + if ($prefix) $prefix = "$prefix: "; + return $prefix.$message; + } + + static final function null(?string $kind=null, ?string $prefix=null, ?string $message=null): self { + return new static(self::message(null, $message, $kind, $prefix, " should not be null")); + } + + static final function check_null($value, ?string $kind=null, ?string $prefix=null, ?string $message=null) { + if ($value === null) throw static::null($kind, $prefix, $message); + return $value; + } + + static final function invalid_kind($value=null, ?string $kind=null, ?string $prefix=null, ?string $message=null): self { + return new static(self::message($value, $message, $kind, $prefix, " is invalid")); + } + + static final function invalid_key($value, ?string $prefix=null, ?string $message=null): self { + return self::invalid_kind($value, "key", $prefix, $message); + } + + static final function invalid_value($value, ?string $prefix=null, ?string $message=null): self { + return self::invalid_kind($value, "value", $prefix, $message); + } + + static final function invalid_type($value, string $expected_type): self { + return new static(self::message($value, null, "type", null, " is invalid, expected $expected_type")); + } + + static final function invalid_class($class, string $expected_class): self { + if (is_object($class)) $class = get_class($class); + return new static(self::message($class, null, "class", null, " is invalid, expected $expected_class")); + } + + static final function forbidden($value=null, ?string $kind=null, ?string $prefix=null, ?string $message=null): self { + return new static(self::message($value, $message, $kind, $prefix, " is forbidden")); + } +} diff --git a/php/src/app/LockFile.php b/php/src/app/LockFile.php new file mode 100644 index 0000000..d32bd2d --- /dev/null +++ b/php/src/app/LockFile.php @@ -0,0 +1,89 @@ +file = new SharedFile($file); + $this->name = $name ?? static::NAME; + $this->title = $title ?? static::TITLE; + } + + /** @var SharedFile */ + protected $file; + + /** @var ?string */ + protected $name; + + /** @var ?string */ + protected $title; + + protected function initData(): array { + return [ + "name" => $this->name, + "title" => $this->title, + "locked" => false, + "date_lock" => null, + "date_release" => null, + ]; + } + + function read(bool $close=true): array { + $data = $this->file->unserialize(null, $close); + if (!is_array($data)) $data = $this->initData(); + return $data; + } + + function isLocked(?array &$data=null): bool { + $data = $this->read(); + return $data["locked"]; + } + + function warnIfLocked(?array $data=null): bool { + if ($data === null) $data = $this->read(); + if ($data["locked"]) { + msg::warning("$data[name]: possède le verrou depuis $data[date_lock] -- $data[title]"); + return true; + } + return false; + } + + function lock(?array &$data=null): bool { + $file = $this->file; + $data = $this->read(false); + if ($data["locked"]) { + $file->close(); + return false; + } else { + $file->ftruncate(); + $file->serialize(cl::merge($data, [ + "locked" => true, + "date_lock" => new DateTime(), + "date_release" => null, + ])); + return true; + } + } + + function release(?array &$data=null): void { + $file = $this->file; + $data = $this->read(false); + $file->ftruncate(); + $file->serialize(cl::merge($data, [ + "locked" => false, + "date_release" => new DateTime(), + ])); + } +} diff --git a/php/src/app/RunFile.php b/php/src/app/RunFile.php new file mode 100644 index 0000000..bd34357 --- /dev/null +++ b/php/src/app/RunFile.php @@ -0,0 +1,497 @@ +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 $sinceStart le $dateStart", + "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", + "stopped" => "Arrêtée $sinceStop le $dateStop", + "duration" => $duration, + "result" => $result, + ]; + } else { + $type = "warning"; + $haveLog = true; + $exitcode = null; + $message = [ + "status" => "$desc: ETAT INCONNU", + "started" => "Commencée le $dateStart", + "stopped" => $dateStop? "Arrêtée le $dateStop": null, + "exitcode" => $result !== null? "Code de retour $result": null, + ]; + } + return [ + "type" => $type, + "have_log" => $haveLog, + "exitcode" => $exitcode, + "message" => array_filter($message), + ]; + } +} diff --git a/php/src/app/args.php b/php/src/app/args.php new file mode 100644 index 0000000..90e24c7 --- /dev/null +++ b/php/src/app/args.php @@ -0,0 +1,39 @@ + $value] devient ["--my-arg", "$value"] + * - ["myOpt" => true] devient ["--my-opt"] + * - ["myOpt" => false] est omis + * - les autres valeurs sont prises telles quelles + */ + static function from_array(?array $array): array { + $args = []; + if ($array === null) return $args; + $index = 0; + foreach ($array as $arg => $value) { + if ($value === false) continue; + if ($arg === $index) { + $index++; + } else { + $arg = str::us2camel($arg); + $arg = str::camel2us($arg, false, "-"); + $arg = str_replace("_", "-", $arg); + $args[] = "--$arg"; + if (is_array($value)) $value[] = "--"; + elseif ($value === true) $value = null; + } + if (is_array($value)) { + A::merge($args, array_map("strval", $value)); + } elseif ($value !== null) { + $args[] = "$value"; + } + } + return $args; + } +} diff --git a/php/src/app/cli/include-launcher.php b/php/src/app/cli/include-launcher.php new file mode 100644 index 0000000..0958cb7 --- /dev/null +++ b/php/src/app/cli/include-launcher.php @@ -0,0 +1,29 @@ + $name, + ]); + require $app; +} diff --git a/php/src/cl.php b/php/src/cl.php new file mode 100644 index 0000000..2c2bac1 --- /dev/null +++ b/php/src/cl.php @@ -0,0 +1,926 @@ + $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 valeur à l'index $index, ou $default si le tableau est null + * ou vide, ou si l'index n'existe pas + * + * ici, l'index est le rang de la clé: 0 pour la première clé du tableau, 1 + * pour la deuxième, etc. + * + * si $index est négatif, il est compté à partir de la fin du tableau + */ + static final function nth(?iterable $iterable, int $index, $default=null) { + if ($iterable === null) return $default; + if ($index < 0 && !is_array($iterable)) { + $iterable = iterator_to_array($iterable, false); + } + if (is_array($iterable)) { + $keys = array_keys($iterable); + $count = count($keys); + while ($index < 0) $index += $count; + $key = $keys[$index] ?? null; + if ($key === null) return $default; + return $iterable[$key]; + } + foreach ($iterable as $value) { + if ($index === 0) return $value; + $index--; + } + return $default; + } + + /** + * retourner la dernière valeur de $array ou $default si le tableau est null + * ou vide + */ + static final function last(?iterable $iterable, $default=null) { + if (is_array($iterable)) { + $key = array_key_last($iterable); + if ($key === null) return $default; + return $iterable[$key]; + } + $value = $default; + if (is_iterable($iterable)) { + foreach ($iterable as $value) { + # parcourir tout l'iterateur pour avoir le dernier élément + } + } + return $value; + } + + /** retourner un array non null à partir de $array */ + static final function with($array): array { + if ($array instanceof IArrayWrapper) $array = $array->wrappedArray(); + if (is_array($array)) return $array; + elseif ($array === null || $array === false) return []; + elseif ($array instanceof Traversable) return self::all($array); + else return [$array]; + } + + /** retourner un array à partir de $array, ou null */ + static final function withn($array): ?array { + if ($array instanceof IArrayWrapper) $array = $array->wrappedArray(); + if (is_array($array)) return $array; + elseif ($array === null || $array === false) return null; + elseif ($array instanceof Traversable) return self::all($array); + else return [$array]; + } + + /** tester si $array a au moins une clé numérique */ + static final function have_num_keys(?array $array): bool { + if ($array === null) return false; + foreach ($array as $key => $value) { + if (is_int($key)) return true; + } + return false; + } + + /** + * tester si $array est une liste, c'est à dire un tableau non null avec + * uniquement des clés numériques séquentielles commençant à zéro + * + * NB: is_list(null) === false + * et is_list([]) === true + */ + static final function is_list(?array $array): bool { + if ($array === null) return false; + $index = -1; + foreach ($array as $key => $value) { + ++$index; + if ($key !== $index) return false; + } + return true; + } + + /** + * tester si $array contient la clé $key + * + * @param array|ArrayAccess $array + */ + static final function has($array, $key): bool { + if (is_array($array)) { + return array_key_exists($key, $array); + } elseif ($array instanceof ArrayAccess) { + return $array->offsetExists($key); + } + return false; + } + + /** + * retourner $array[$key] ou $default si la clé n'existe pas + * + * @param array|ArrayAccess $array + */ + static final function get($array, $key, $default=null) { + if (is_array($array)) { + if (array_key_exists($key, $array)) return $array[$key]; + } elseif ($array instanceof ArrayAccess) { + if ($array->offsetExists($key)) return $array->offsetGet($key); + } + return $default; + } + + /** + * retourner un tableau construit à partir des clés de $keys + * - [$to => $from] --> $dest[$to] = self::get($array, $from) + * - [$to => null] --> $dest[$to] = null + * - [$to => false] --> NOP + * - [$to] --> $dest[$to] = self::get($array, $to) + * - [null] --> $dest[] = null + * - [false] --> NOP + * + * Si $inverse===true, le mapping est inversé: + * - [$to => $from] --> $dest[$from] = self::get($array, $to) + * - [$to => null] --> $dest[$to] = self::get($array, $to) + * - [$to => false] --> NOP + * - [$to] --> $dest[$to] = self::get($array, $to) + * - [null] --> NOP (XXX que faire dans ce cas?) + * - [false] --> NOP + * + * notez que l'ordre est inversé par rapport à {@link self::rekey()} qui + * attend des mappings [$from => $to], alors que cette méthode attend des + * mappings [$to => $from] + */ + static final function select($array, ?array $mappings, bool $inverse=false): array { + $dest = []; + $index = 0; + if (!$inverse) { + foreach ($mappings as $to => $from) { + if ($to === $index) { + $index++; + $to = $from; + if ($to === false) continue; + elseif ($to === null) $dest[] = null; + else $dest[$to] = self::get($array, $to); + } elseif ($from === false) { + continue; + } elseif ($from === null) { + $dest[$to] = null; + } else { + $dest[$to] = self::get($array, $from); + } + } + } else { + foreach ($mappings as $to => $from) { + if ($to === $index) { + $index++; + $to = $from; + if ($to === false) continue; + elseif ($to === null) continue; + else $dest[$to] = self::get($array, $to); + } elseif ($from === false) { + continue; + } elseif ($from === null) { + $dest[$to] = self::get($array, $to); + } else { + $dest[$from] = self::get($array, $to); + } + } + } + return $dest; + } + + /** + * obtenir la liste des clés qui seraient 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, c'est comme si toutes les clés étaient incluses + * + */ + static final function xselect($array, ?array $includes, ?array $excludes=null): ?array { + if ($array === null) return null; + $array = self::withn($array); + if ($includes === null && $excludes === null) return $array; + if ($includes === null) $includes = array_keys($array); + if ($excludes === null) $excludes = []; + $result = []; + foreach ($array as $key => $value) { + if (!in_array($key, $includes)) continue; + if (in_array($key, $excludes)) continue; + $result[$key] = $value; + } + return $result; + } + + /** + * si $array est un array ou une instance de ArrayAccess, créer ou modifier + * l'élément dont la clé est $key + * + * @param array|ArrayAccess $array + */ + static final function set(&$array, $key, $value): void { + if (is_array($array) || $array === null) { + if ($key === null) $array[] = $value; + else $array[$key] = $value; + } elseif ($array instanceof ArrayAccess) { + $array->offsetSet($key, $value); + } + } + + /** + * si $array est un array ou une instance de ArrayAccess, supprimer l'élément + * dont la clé est $key + * + * @param array|ArrayAccess $array + */ + static final function del(&$array, $key): void { + if (is_array($array)) { + unset($array[$key]); + } elseif ($array instanceof ArrayAccess) { + $array->offsetUnset($key); + } + } + + /** + * si $array est un array ou une instance de ArrayAccess&Traversable, + * supprimer le premier élément dont la valeur est $value + * + * @param array|ArrayAccess $array + */ + static final function delv(&$array, $value, bool $strict=false): void { + if (is_array($array)) { + $key = array_search($value, $array, $strict); + if ($key !== false) unset($array[$key]); + } elseif ($array instanceof ArrayAccess && $array instanceof Traversable) { + $found = false; + foreach ($array as $key => $val) { + if ($strict) $found = $val === $value; + else $found = $val == $value; + if ($found) break; + } + if ($found) $array->offsetUnset($key); + } + } + + /** retourner le nombre d'éléments de $array */ + static final function count(?array $array): int { + return $array !== null? count($array): 0; + } + + /** retourner la liste des clés de $array */ + static final function keys(?array $array): array { + return $array !== null? array_keys($array): []; + } + + ############################################################################# + + /** + * Fusionner tous les tableaux spécifiés. Les valeurs null sont ignorées. + * IMPORTANT: les clés numériques sont réordonnées. + * Retourner null si aucun tableau n'est fourni ou s'ils étaient tous null. + */ + static final function merge(...$arrays): ?array { + $merges = []; + foreach ($arrays as $array) { + A::ensure_narray($array); + if ($array !== null) $merges[] = $array; + } + return $merges? array_merge(...$merges): null; + } + + /** + * Fusionner tous les tableaux spécifiés. Les valeurs null sont ignorées. + * IMPORTANT: les clés numériques NE SONT PAS réordonnées. + * Retourner null si aucun tableau n'est fourni ou s'ils étaient tous null. + */ + static final function merge2(...$arrays): ?array { + $merged = null; + foreach ($arrays as $array) { + $array = self::withn($array); + if ($array === null) continue; + $merged ??= []; + foreach ($array as $key => $value) { + $merged[$key] = $value; + } + } + return $merged; + } + + ############################################################################# + + /** + * tester si $array satisfait les conditions de $filter + * - $filter est un scalaire, le transformer en [$filter] + * - sinon $filter doit être un tableau de scalaires + * + * les règles des conditions sont les suivantes: + * - une valeur séquentielle $key est équivalente à la valeur associative + * $key => true + * - une valeur associative $key => bool indique que la clé correspondante ne + * doit pas (resp. doit) exister selon que bool vaut false (resp. true) + * - une valeur associative $key => $value indique que la clé correspondante + * doit exiter avec la valeur spécifiée + */ + static final function filter(?array $array, $filter): bool { + if ($filter === null) return false; + if (!is_array($filter)) $filter = [$filter]; + if (!$filter) return false; + + $index = 0; + foreach ($filter as $key => $value) { + if ($key === $index) { + $index++; + if ($array === null) return false; + if (!array_key_exists($value, $array)) return false; + } elseif (is_bool($value)) { + if ($value) { + if ($array === null || !array_key_exists($key, $array)) return false; + } else { + if ($array !== null && array_key_exists($key, $array)) return false; + } + } else { + if ($array === null) return false; + if (!array_key_exists($key, $array)) return false; + if ($array[$key] !== $value) return false; + } + } + return true; + } + + /** + * mapper le tableau source $array selon les règles suivantes illustrées dans + * l'exemple suivant: + * si + * $map = ["a", "b" => "x", "c" => function() { return "y"; }, "d" => null] + * alors retourner le tableau + * ["a" => $array["a"], "b" => $array["x"], "c" => "y", "d" => null] + * + * si une fonction est utilisée, sa signature est + * function(mixed $value, string|int $key, ?array $array) + */ + static function map(?array $array, ?array $map): ?array { + if ($map === null) return $array; + $index = 0; + $mapped = []; + foreach ($map as $key => $value) { + if ($key === $index) { + $index++; + if ($value === null) $mapped[] = null; + else $mapped[$value] = cl::get($array, $value); + } elseif (is_callable($value)) { + $func = func::with($value); + $value = cl::get($array, $key); + $mapped[$key] = $func->invoke([$value, $key, $array]); + } else { + if ($value === null) $mapped[$key] = null; + else $mapped[$key] = cl::get($array, $value); + } + } + return $mapped; + } + + static final function mapf(?iterable $items, $func): array { + $mapped = []; + if ($items !== null) { + $func = func::with($func); + foreach ($items as $key => $item) { + $mapped[$key] = $func->invoke([$item, $key, $items]); + } + } + return $mapped; + } + + ############################################################################# + + /** + * vérifier que le chemin $keys existe dans le tableau $array + * + * si $pkey est vide ou null, retourner true + */ + static final function phas($array, $pkey): bool { + # optimisations + if ($pkey === null || $pkey === []) { + return true; + } elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) { + return self::has($array, $pkey); + } elseif (!is_array($pkey)) { + $pkey = explode(".", strval($pkey)); + } + # phas + $first = true; + foreach($pkey as $key) { + if ($key === "" && $first) { + # une chaine vide en première position est ignorée + continue; + } elseif (is_array($array)) { + if (!array_key_exists($key, $array)) return false; + $array = $array[$key]; + } elseif ($array instanceof ArrayAccess) { + if (!$array->offsetExists($key)) return false; + $array = $array->offsetGet($key); + } else { + return false; + } + $first = false; + } + return true; + } + + static final function each_phas($array, ?array $pkeys): array { + $result = []; + if ($pkeys !== null) { + foreach ($pkeys as $pkey) { + $result[] = self::phas($array, $pkey); + } + } + return $result; + } + + /** + * obtenir la valeur correspondant au chemin $keys dans $array + * + * si $pkey est vide ou null, retourner $default + */ + static final function pget($array, $pkey, $default=null) { + # optimisations + if ($pkey === null || $pkey === []) return $default; + elseif ($pkey === "") return $array; + elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) { + return self::get($array, $pkey, $default); + } elseif (!is_array($pkey)) { + $pkey = explode(".", strval($pkey)); + } + # pget + $value = $array; + $first = true; + foreach($pkey as $key) { + if ($key === "" && $first) { + # une chaine vide en première position est ignorée + continue; + } elseif (is_array($value)) { + if (!array_key_exists($key, $value)) return $default; + $value = $value[$key]; + } elseif ($value instanceof ArrayAccess) { + if (!$value->offsetExists($key)) return $default; + $value = $value->offsetGet($key); + } else { + return $default; + } + $first = false; + } + return $value; + } + + static final function each_pget($array, ?array $pkeys): array { + $result = []; + if ($pkeys !== null) { + foreach ($pkeys as $key => $pkey) { + $result[$key] = self::pget($array, $pkey); + } + } + return $result; + } + + /** + * retourner un tableau construit à partir des chemins de clé de $pkeys + * ces chemins peuvent être exprimés de plusieurs façon: + * - [$key => $pkey] --> $dest[$key] = self::pget($array, $pkey) + * - [$key => null] --> $dest[$key] = null + * - [$pkey] --> $dest[$key] = self::pget($array, $pkey) + * avec $key = implode("__", $pkey)) + * - [null] --> $dest[] = null + * - [false] --> NOP + */ + static final function pselect($array, ?array $pkeys): array { + $dest = []; + $index = 0; + foreach ($pkeys as $key => $pkey) { + if ($key === $index) { + $index++; + if ($pkey === null) continue; + $value = self::pget($array, $pkey); + if (!is_array($pkey)) $pkey = explode(".", strval($pkey)); + $key = implode("__", $pkey); + } elseif ($pkey === null) { + $value = null; + } else { + $value = self::pget($array, $pkey); + } + $dest[$key] = $value; + } + return $dest; + } + + /** + * méthode de convenance qui sélectionne certaines clés de $array avec + * {@link self::pselect()} puis merge le tableau $merge au résultat. + */ + static final function pselectm($array, ?array $pkeys, ?array $merge=null): array { + return cl::merge(self::pselect($array, $pkeys), $merge); + } + + /** + * méthode de convenance qui merge $merge dans $array puis sélectionne + * certaines clés avec {@link self::pselect()} + */ + static final function mpselect($array, ?array $merge, ?array $mappings): array { + return self::pselect(cl::merge($array, $merge), $mappings); + } + + /** + * modifier la valeur au chemin de clé $keys dans le tableau $array + * + * utiliser la clé "" (chaine vide) en dernière position pour rajouter à la fin, e.g + * - pset($array, [""], $value) est équivalent à $array[] = $value + * - pset($array, ["a", "b", ""], $value) est équivalent à $array["a"]["b"][] = $value + * la clé "" n'a pas de propriété particulière quand elle n'est pas en dernière position + * + * si $pkey est vide ou null, $array est remplacé par $value + */ + static final function pset(&$array, $pkey, $value): void { + # optimisations + if ($pkey === null || $pkey === []) { + $array = $value; + return; + } elseif ($pkey === "") { + $array[] = $value; + return; + } elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) { + self::set($array, $pkey, $value); + return; + } elseif (!is_array($pkey)) { + $pkey = explode(".", strval($pkey)); + } + # pset + A::ensure_array($array); + $current =& $array; + $key = null; + $last = count($pkey) - 1; + $i = 0; + foreach ($pkey as $key) { + if ($i == $last) break; + if ($current instanceof ArrayAccess) { + if (!$current->offsetExists($key)) $current->offsetSet($key, []); + $current =& $current->offsetGet($key); + if ($current === null) { + $current = []; + } elseif (!is_array($current) && !($current instanceof ArrayAccess)) { + $current = [$current]; + } + } else { + A::ensure_array($current[$key]); + $current =& $current[$key]; + } + $i++; + } + if ($key === "") $current[] = $value; + else $current[$key] = $value; + } + + static final function each_pset(&$array, ?array $values): void { + if ($values !== null) { + foreach ($values as $pkey => $value) { + self::pset($array, $pkey, $value); + } + } + } + + /** + * supprimer la valeur au chemin de clé $keys dans $array + * + * si $array vaut null ou false, sa valeur est inchangée. + * si $pkey est vide ou null, $array devient null + */ + static final function pdel(&$array, $pkey): void { + # optimisations + if ($array === null || $array === false) { + return; + } elseif ($pkey === null || $pkey === []) { + $array = null; + return; + } elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) { + self::del($array, $pkey); + return; + } elseif (!is_array($pkey)) { + $pkey = explode(".", strval($pkey)); + } + # pdel + A::ensure_array($array); + $current =& $array; + $key = null; + $last = count($pkey) - 1; + $i = 0; + foreach ($pkey as $key) { + if ($i == $last) break; + if ($current instanceof ArrayAccess) { + if (!$current->offsetExists($key)) break; + } elseif (is_array($current)) { + if (!array_key_exists($key, $current)) break; + } else { + break; + } + $current =& $current[$key]; + $i++; + } + if ($i == $last) { + if ($current instanceof ArrayAccess) { + $current->offsetUnset($key); + } elseif (is_array($current)) { + unset($current[$key]); + } + } + } + + static final function each_pdel(&$array, ?array $pkeys): void { + if ($pkeys !== null) { + foreach ($pkeys as $pkey) { + self::pdel($array, $pkey); + } + } + } + + ############################################################################# + + /** + * tester si $array a en début de tableau les mêmes clés que $ref, et dans le + * même ordre. $array peut avoir d'autres clés, ça n'influe pas sur le résultat + * + * $keys obtient la liste des clés de $ref trouvées, dans l'ordre de $array + * $remainKeys obtient la liste des clés de $array qui ne sont pas dans $ref + * $missingKeys obtient la liste des clés de $ref qui ne sont pas dans $array + */ + static function same_keys(?array $array, ?array $ref, ?array &$keys=null, ?array &$remainKeys=null, ?array &$missingKeys=null): bool { + $keys = []; + $remainKeys = []; + $missingKeys = []; + if ($array === null || $array === []) { + if ($ref === null || $ref === []) return true; + $missingKeys = array_keys($ref); + return false; + } elseif ($ref === null || $ref === []) { + $remainKeys = array_keys($array); + return true; + } + $refKeys = array_keys($ref); + $refCount = count($ref); + $index = 0; + $sameKeys = true; + foreach (array_keys($array) as $key) { + if ($index < $refCount) { + if ($key !== $refKeys[$index]) $sameKeys = false; + $index++; + } + if (array_key_exists($key, $ref)) $keys[] = $key; + else $remainKeys[] = $key; + } + $missingKeys = array_values(array_diff($refKeys, $keys)); + return $sameKeys && $index == $refCount; + } + + /** + * retourner le tableau $array en "renommant" les clés selon le tableau + * $mappings qui contient des associations de la forme [$from => $to] + * + * Si $inverse===true, renommer dans le sens $to => $from + */ + static function rekey(?array $array, ?array $mappings, bool $inverse=false): ?array { + if ($array === null || $mappings === null) return $array; + if ($inverse) $mappings = array_flip($mappings); + $mapped = []; + foreach ($array as $key => $value) { + if (array_key_exists($key, $mappings)) $key = $mappings[$key]; + $mapped[$key] = $value; + } + return $mapped; + } + + /** + * indiquer si {@link self::rekey()} modifierai le tableau indiqué (s'il y a + * des modifications à faire) + */ + static function would_rekey(?array $array, ?array $mappings, bool $inverse=false): bool { + if ($array === null || $mappings === null) return false; + if ($inverse) $mappings = array_flip($mappings); + foreach ($array as $key => $value) { + if (array_key_exists($key, $mappings)) return true; + } + return false; + } + + ############################################################################# + + /** tester si tous les éléments du tableau satisfont la condition */ + static final function all_if(?array $array, callable $cond): bool { + if ($array !== null) { + foreach ($array as $value) { + if (!$cond($value)) return false; + } + } + return true; + } + + static final function all_z(?array $array): bool { return self::all_if($array, [cv::class, "z"]);} + static final function all_nz(?array $array): bool { return self::all_if($array, [cv::class, "nz"]);} + static final function all_n(?array $array): bool { return self::all_if($array, [cv::class, "n"]);} + static final function all_nn(?array $array): bool { return self::all_if($array, [cv::class, "nn"]);} + static final function all_t(?array $array): bool { return self::all_if($array, [cv::class, "t"]);} + static final function all_f(?array $array): bool { return self::all_if($array, [cv::class, "f"]);} + static final function all_pt(?array $array): bool { return self::all_if($array, [cv::class, "pt"]);} + static final function all_pf(?array $array): bool { return self::all_if($array, [cv::class, "pf"]);} + static final function all_equals(?array $array, $value): bool { return self::all_if($array, cv::equals($value)); } + static final function all_not_equals(?array $array, $value): bool { return self::all_if($array, cv::not_equals($value)); } + static final function all_same(?array $array, $value): bool { return self::all_if($array, cv::same($value)); } + static final function all_not_same(?array $array, $value): bool { return self::all_if($array, cv::not_same($value)); } + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /** tester si au moins un élément du tableau satisfait la condition */ + static final function any_if(?array $array, callable $cond): bool { + if ($array !== null) { + foreach ($array as $value) { + if ($cond($value)) return true; + } + } + return false; + } + + static final function any_z(?array $array): bool { return self::any_if($array, [cv::class, "z"]);} + static final function any_nz(?array $array): bool { return self::any_if($array, [cv::class, "nz"]);} + static final function any_n(?array $array): bool { return self::any_if($array, [cv::class, "n"]);} + static final function any_nn(?array $array): bool { return self::any_if($array, [cv::class, "nn"]);} + static final function any_t(?array $array): bool { return self::any_if($array, [cv::class, "t"]);} + static final function any_f(?array $array): bool { return self::any_if($array, [cv::class, "f"]);} + static final function any_pt(?array $array): bool { return self::any_if($array, [cv::class, "pt"]);} + static final function any_pf(?array $array): bool { return self::any_if($array, [cv::class, "pf"]);} + static final function any_equals(?array $array, $value): bool { return self::any_if($array, cv::equals($value)); } + static final function any_not_equals(?array $array, $value): bool { return self::any_if($array, cv::not_equals($value)); } + static final function any_same(?array $array, $value): bool { return self::any_if($array, cv::same($value)); } + static final function any_not_same(?array $array, $value): bool { return self::any_if($array, cv::not_same($value)); } + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + static final function filter_if(?array $array, callable $cond): ?array { + if ($array === null) return null; + $filtered = []; + $index = 0; + foreach ($array as $key => $value) { + if (!$cond($value)) { + if ($key === $index) { + $index++; + $filtered[] = $value; + } else { + $filtered[$key] = $value; + } + } elseif ($key === $index) { + $index++; + } + } + return $filtered; + } + + static final function filter_z(?array $array): ?array { return self::filter_if($array, [cv::class, "z"]);} + static final function filter_nz(?array $array): ?array { return self::filter_if($array, [cv::class, "nz"]);} + static final function filter_n(?array $array): ?array { return self::filter_if($array, [cv::class, "n"]);} + static final function filter_nn(?array $array): ?array { return self::filter_if($array, [cv::class, "nn"]);} + static final function filter_t(?array $array): ?array { return self::filter_if($array, [cv::class, "t"]);} + static final function filter_f(?array $array): ?array { return self::filter_if($array, [cv::class, "f"]);} + static final function filter_pt(?array $array): ?array { return self::filter_if($array, [cv::class, "pt"]);} + static final function filter_pf(?array $array): ?array { return self::filter_if($array, [cv::class, "pf"]);} + static final function filter_equals(?array $array, $value): ?array { return self::filter_if($array, cv::equals($value)); } + static final function filter_not_equals(?array $array, $value): ?array { return self::filter_if($array, cv::not_equals($value)); } + static final function filter_same(?array $array, $value): ?array { return self::filter_if($array, cv::same($value)); } + static final function filter_not_same(?array $array, $value): ?array { return self::filter_if($array, cv::not_same($value)); } + + ############################################################################# + + static final function sorted(?array $array, int $flags=SORT_REGULAR, bool $assoc=false): ?array { + A::sort($array, $flags, $assoc); + return $array; + } + + static final function ksorted(?array $array, int $flags=SORT_REGULAR): ?array { + A::ksort($array, $flags); + return $array; + } + + /** + * retourner une fonction permettant de trier un tableau sur les clés + * spécifiées. + * + * - les clés ayant le préfixe '+' ou le suffixe '|asc' indiquent un tri + * ascendant + * - les clés ayant le préfixe '-' ou le suffixe '|desc' indiquent un tri + * descendant + * - sinon, par défaut, le tri est ascendant + */ + static final function compare(array $keys): callable { + return function ($a, $b) use ($keys) { + foreach ($keys as $key) { + if (str::del_prefix($key, "+")) $w = 1; + elseif (str::del_prefix($key, "-")) $w = -1; + elseif (str::del_suffix($key, "|asc")) $w = 1; + elseif (str::del_suffix($key, "|desc")) $w = -1; + else $w = 1; + if ($c = $w * cv::compare(cl::get($a, $key), cl::get($b, $key))) { + return $c; + } + } + return 0; + }; + } + + static final function usorted(?array $array, array $keys, bool $assoc=false): ?array { + A::usort($array, $keys, $assoc); + return $array; + } +} diff --git a/php/src/cv.php b/php/src/cv.php new file mode 100644 index 0000000..fd61a8e --- /dev/null +++ b/php/src/cv.php @@ -0,0 +1,250 @@ + $b) return 1; + return 0; + } + + /** comparer la longueur de $a et $b */ + static final function complen($a, $b): int { + if (is_array($a)) $la = count($a); + else $la = strlen(strval($a)); + if (is_array($b)) $lb = count($b); + else $lb = strlen(strval($b)); + return self::compare($la, $lb); + } +} diff --git a/php/src/db/Capacitor.php b/php/src/db/Capacitor.php new file mode 100644 index 0000000..5551f1b --- /dev/null +++ b/php/src/db/Capacitor.php @@ -0,0 +1,182 @@ +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(); + } + + function getCreateSql(): string { + $channel = $this->channel; + return $this->storage->_getMigration($channel)->getSql(get_class($channel), $this->db()); + } + + /** @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 { + 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 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 dbUpdate(array $update) { + return $this->storage->db()->exec(cl::merge([ + "update", + "table" => $this->getTableName(), + ], $update)); + } + + 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..4349e4b --- /dev/null +++ b/php/src/db/CapacitorChannel.php @@ -0,0 +1,492 @@ +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 = $this->COLUMN_DEFINITIONS(); + $primaryKeys = cl::withn(static::PRIMARY_KEYS); + $migration = cl::withn(static::MIGRATION); + $lastMkey = 1; + if ($columnDefinitions !== null) { + # mettre à jour la liste des clés primaires et des migrations + $index = 0; + foreach ($columnDefinitions as $col => $def) { + if ($col === $index) { + $index++; + if (is_array($def)) { + # tableau: c'est une migration + $mkey = null; + $mvalues = null; + $mdefs = $def; + $mindex = 0; + foreach ($mdefs as $mcol => $mdef) { + if ($mindex === 0 && $mcol === 0) { + $mindex++; + $mkey = $mdef; + } elseif ($mcol === $mindex) { + # si définition séquentielle, prendre la migration telle quelle + $mindex++; + $mvalues[] = $mdef; + } elseif ($mdef) { + # mise à jour d'une colonne + $mvalues[] = "alter table $tableName add column $mcol $mdef"; + } else { + # suppression d'une colonne + $mvalues[] = "alter table $tableName drop column $mcol"; + } + } + if ($mvalues !== null) { + if ($mkey === null) $mkey = $lastMkey++; + $migration[$mkey] = $mvalues; + } + } else { + # si définition séquentielle, seules les définitions de clé + # primaires sont supportées + if (preg_match('/\bprimary\s+key\s+\((.+)\)/i', $def, $ms)) { + $primaryKeys = preg_split('/\s*,\s*/', trim($ms[1])); + } + } + } else { + # chaine: c'est une définition + $def = strval($def); + if (preg_match('/\bprimary\s+key\b/i', $def)) { + $primaryKeys[] = $col; + } + } + } + } + $this->columnDefinitions = $columnDefinitions; + $this->primaryKeys = $primaryKeys; + $this->migration = $migration; + } + + 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 $migration; + + function getMigration(): ?array { + return $this->migration; + } + + 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; + } + + 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 willUpdate(...$transactors): ITransactor { + return $this->capacitor->willUpdate(...$transactors); + } + + function inTransaction(): bool { + return $this->capacitor->inTransaction(); + } + + function beginTransaction(?callable $func=null, bool $commit=true): void { + $this->capacitor->beginTransaction($func, $commit); + } + + function commit(): void { + $this->capacitor->commit(); + } + + function rollback(): void { + $this->capacitor->rollback(); + } + + function db(): IDatabase { + return $this->capacitor->getStorage()->db(); + } + + function exists(): bool { + return $this->capacitor->exists(); + } + + function ensureExists(): void { + $this->capacitor->ensureExists(); + } + + function reset(bool $recreate=false): void { + $this->capacitor->reset($recreate); + } + + 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); + } + + function dbUpdate(array $update) { + return $this->capacitor->dbUpdate($update); + } + + function close(): void { + $this->capacitor->close(); + } +} diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php new file mode 100644 index 0000000..9d4cdb1 --- /dev/null +++ b/php/src/db/CapacitorStorage.php @@ -0,0 +1,728 @@ +_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 SERDATA_DEFINITION = "mediumtext"; + const SERSUM_DEFINITION = "varchar(40)"; + const SERTS_DEFINITION = "datetime"; + + protected static function sercol($def): string { + if (!is_string($def)) $def = strval($def); + switch ($def) { + case "serdata": $def = static::SERDATA_DEFINITION; break; + case "sersum": $def = static::SERSUM_DEFINITION; break; + case "serts": $def = static::SERTS_DEFINITION; break; + } + return $def; + } + + const COLUMN_DEFINITIONS = [ + "item__" => "serdata", + "item__sum_" => "sersum", + "created_" => "serts", + "modified_" => "serts", + ]; + + protected function ColumnDefinitions(CapacitorChannel $channel, bool $ignoreMigrations=false): 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++; + if (is_array($def)) { + if (!$ignoreMigrations) { + $mdefs = $def; + $mindex = 0; + foreach ($mdefs as $mcol => $mdef) { + if ($mcol === $mindex) { + $mindex++; + } else { + if ($mdef) { + $definitions[$mcol] = self::sercol($mdef); + } else { + unset($definitions[$mcol]); + } + } + } + } + } else { + $constraints[] = $def; + } + } else { + $definitions[$col] = self::sercol($def); + } + } + return cl::merge($definitions, $constraints); + } + + protected function getMigration(CapacitorChannel $channel): ?array { + return $channel->getMigration(); + } + + /** 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 { + return [ + "create table if not exists", + "table" => $channel->getTableName(), + "cols" => $this->ColumnDefinitions($channel, true), + ]; + } + + abstract protected function tableExists(string $tableName): bool; + + const METADATA_TABLE = "_metadata"; + const METADATA_COLS = [ + "name" => "varchar not null primary key", + "value" => "varchar", + ]; + + protected function _prepareMetadata(): void { + if (!$this->tableExists(static::METADATA_TABLE)) { + $db = $this->db(); + $db->exec([ + "drop table if exists", + "table" => self::CHANNELS_TABLE, + ]); + $db->exec([ + "drop table if exists", + "table" => _migration::MIGRATION_TABLE, + ]); + $db->exec([ + "create table", + "table" => static::METADATA_TABLE, + "cols" => static::METADATA_COLS, + ]); + $db->exec([ + "insert", + "into" => static::METADATA_TABLE, + "values" => [ + "name" => "version", + "value" => "1", + ], + ]); + } + } + + abstract function _getMigration(CapacitorChannel $channel): _migration; + + const CHANNELS_TABLE = "_channels"; + const CHANNELS_COLS = [ + "name" => "varchar not null primary key", + "table_name" => "varchar", + "class_name" => "varchar", + ]; + + protected function _createChannelsSql(): array { + return [ + "create table if not exists", + "table" => static::CHANNELS_TABLE, + "cols" => static::CHANNELS_COLS, + ]; + } + + protected function _addToChannelsSql(CapacitorChannel $channel): array { + return [ + "insert", + "into" => static::CHANNELS_TABLE, + "values" => [ + "name" => $channel->getName(), + "table_name" => $channel->getTableName(), + "class_name" => get_class($channel), + ], + ]; + } + + protected function _afterCreate(CapacitorChannel $channel): void { + $db = $this->db(); + $db->exec($this->_createChannelsSql()); + $db->exec($this->_addToChannelsSql($channel)); + } + + protected function _create(CapacitorChannel $channel): void { + $channel->ensureSetup(); + if (!$channel->isCreated()) { + $this->_prepareMetadata(); + $this->_getMigration($channel)->migrate($this->db()); + $this->_afterCreate($channel); + $channel->setCreated(); + } + } + + /** tester si le canal spécifié existe */ + function _exists(CapacitorChannel $channel): bool { + return $this->tableExists($channel->getTableName()); + } + + 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 { + $db = $this->db; + $name = $channel->getName(); + $db->exec([ + "delete", + "from" => _migration::MIGRATION_TABLE, + "where" => [ + "channel" => $name, + ], + ]); + $db->exec([ + "delete", + "from" => static::CHANNELS_TABLE, + "where" => [ + "name" => $name, + ], + ]); + } + + /** 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 ??= []; + + $values = func::call([$channel, "getItemValues"], $item, ...$args); + 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 = func::with([$channel, "onCreate"], $args); + $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 = func::with([$channel, "onUpdate"], $args); + $values = $this->unserialize($channel, $row); + $pvalues = $this->unserialize($channel, $prow); + } + + $updates = $initFunc->prependArgs([$item, $values, $pvalues])->invoke(); + 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) { + $updates = func::with($func) + ->prependArgs([$item, $values, $pvalues]) + ->bind($channel) + ->invoke(); + 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; + $onEach = func::with($func)->bind($channel); + $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 = $onEach->invoke([$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; + $onDelete = func::with($func)->bind($channel); + $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); + $shouldDelete = boolval($onDelete->invoke([$values["item"], $values, ...$args])); + if ($shouldDelete) { + $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..1383c9a --- /dev/null +++ b/php/src/db/IDatabase.php @@ -0,0 +1,26 @@ +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/Tvalues.php b/php/src/db/_private/Tvalues.php new file mode 100644 index 0000000..ad93f5c --- /dev/null +++ b/php/src/db/_private/Tvalues.php @@ -0,0 +1,35 @@ + "create", "type" => "ddl"]; + } elseif (_select::isa($prefix)) { + $sql = _select::parse($sql, $bindings); + $meta = ["isa" => "select", "type" => "dql"]; + } elseif (_insert::isa($prefix)) { + $sql = _insert::parse($sql, $bindings); + $meta = ["isa" => "insert", "type" => "dml"]; + } elseif (_update::isa($prefix)) { + $sql = _update::parse($sql, $bindings); + $meta = ["isa" => "update", "type" => "dml"]; + } elseif (_delete::isa($prefix)) { + $sql = _delete::parse($sql, $bindings); + $meta = ["isa" => "delete", "type" => "dml"]; + } elseif (_generic::isa($prefix)) { + $sql = _generic::parse($sql, $bindings); + $meta = ["isa" => "generic", "type" => null]; + } else { + throw ValueException::invalid_kind($sql, "query"); + } + } else { + if (!is_string($sql)) $sql = strval($sql); + if (_create::isa($sql)) { + $meta = ["isa" => "create", "type" => "ddl"]; + } elseif (_select::isa($sql)) { + $meta = ["isa" => "select", "type" => "dql"]; + } elseif (_insert::isa($sql)) { + $meta = ["isa" => "insert", "type" => "dml"]; + } elseif (_update::isa($sql)) { + $meta = ["isa" => "update", "type" => "dml"]; + } elseif (_delete::isa($sql)) { + $meta = ["isa" => "delete", "type" => "dml"]; + } elseif (_generic::isa($sql)) { + $meta = ["isa" => "generic", "type" => null]; + } else { + $meta = ["isa" => "generic", "type" => null]; + } + } + } + + static function with($sql, ?array $params=null): array { + static::verifix($sql, $params); + return [$sql, $params]; + } + + 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/_common.php b/php/src/db/_private/_common.php new file mode 100644 index 0000000..575a53b --- /dev/null +++ b/php/src/db/_private/_common.php @@ -0,0 +1,255 @@ + $value) { + if ($key === $index) { + $index++; + if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) { + $sql .= " "; + } + $sql .= $value; + } + } + return $sql; + } + + protected static function is_sep(&$cond): bool { + if (!is_string($cond)) return false; + if (!preg_match('/^\s*(and|or|not)\s*$/i', $cond, $ms)) return false; + $cond = $ms[1]; + return true; + } + + static function parse_conds(?array $conds, ?array &$sql, ?array &$bindings): void { + if (!$conds) return; + $sep = null; + $index = 0; + $condsql = []; + foreach ($conds as $key => $cond) { + if ($key === $index) { + ## séquentiel + if ($index === 0 && self::is_sep($cond)) { + $sep = $cond; + } elseif (is_bool($cond)) { + # ignorer les valeurs true et false + } elseif (is_array($cond)) { + # condition récursive + self::parse_conds($cond, $condsql, $bindings); + } else { + # condition litérale + $condsql[] = strval($cond); + } + $index++; + } elseif ($cond === false) { + ## associatif + # condition litérale ignorée car condition false + } elseif ($cond === true) { + # condition litérale sélectionnée car condition true + $condsql[] = strval($key); + } else { + ## associatif + # paramètre + $param0 = preg_replace('/^.+\./', "", $key); + $i = false; + if ($bindings !== null && array_key_exists($param0, $bindings)) { + $i = 2; + while (array_key_exists("$param0$i", $bindings)) { + $i++; + } + } + # value ou [operator, value] + $condprefix = $condsep = $condsuffix = null; + if (is_array($cond)) { + $condkey = 0; + $condkeys = array_keys($cond); + $op = null; + if (array_key_exists("op", $cond)) { + $op = $cond["op"]; + } elseif (array_key_exists($condkey, $condkeys)) { + $op = $cond[$condkeys[$condkey]]; + $condkey++; + } + $op = strtolower($op); + $condvalues = null; + switch ($op) { + case "between": + # ["between", $upper, $lower] + $condsep = " and "; + if (array_key_exists("lower", $cond)) { + $condvalues[] = $cond["lower"]; + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues[] = $cond[$condkeys[$condkey]]; + $condkey++; + } + if (array_key_exists("upper", $cond)) { + $condvalues[] = $cond["upper"]; + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues[] = $cond[$condkeys[$condkey]]; + $condkey++; + } + break; + case "any": + case "all": + case "not any": + case "not all": + # ["list", $values] + if ($op === "any" || $op === "all") { + $condprefix = $op; + $op = "="; + } elseif ($op === "not any" || $op === "not all") { + $condprefix = substr($op, strlen("not ")); + $op = "<>"; + } + $condprefix .= "(array["; + $condsep = ", "; + $condsuffix = "])"; + $condvalues = null; + if (array_key_exists("values", $cond)) { + $condvalues = cl::with($cond["values"]); + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues = cl::with($cond[$condkeys[$condkey]]); + $condkey++; + } + break; + case "in": + # ["in", $values] + $condprefix = "("; + $condsep = ", "; + $condsuffix = ")"; + $condvalues = null; + if (array_key_exists("values", $cond)) { + $condvalues = cl::with($cond["values"]); + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues = cl::with($cond[$condkeys[$condkey]]); + $condkey++; + } + break; + case "null": + case "is null": + $op = "is null"; + break; + case "not null": + case "is not null": + $op = "is not null"; + break; + default: + if (array_key_exists("value", $cond)) { + $condvalues = [$cond["value"]]; + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues = [$cond[$condkeys[$condkey]]]; + $condkey++; + } + } + } elseif ($cond !== null) { + $op = "="; + $condvalues = [$cond]; + } else { + $op = "is null"; + $condvalues = null; + } + $cond = [$key, $op]; + if ($condvalues !== null) { + $parts = []; + foreach ($condvalues as $condvalue) { + if (is_array($condvalue)) { + $first = true; + foreach ($condvalue as $value) { + if ($first) { + $first = false; + } else { + if ($sep === null) $sep = "and"; + $parts[] = " $sep "; + $parts[] = $key; + $parts[] = " $op "; + } + $param = "$param0$i"; + $parts[] = ":$param"; + $bindings[$param] = $value; + if ($i === false) $i = 2; + else $i++; + } + } else { + $param = "$param0$i"; + $parts[] = ":$param"; + $bindings[$param] = $condvalue; + if ($i === false) $i = 2; + else $i++; + } + } + $cond[] = $condprefix.implode($condsep, $parts).$condsuffix; + } + $condsql[] = implode(" ", $cond); + } + } + if ($sep === null) $sep = "and"; + $count = count($condsql); + if ($count > 1) { + $sql[] = "(" . implode(" $sep ", $condsql) . ")"; + } elseif ($count == 1) { + $sql[] = $condsql[0]; + } + } + + static function parse_set_values(?array $values, ?array &$sql, ?array &$bindings): void { + if (!$values) return; + $index = 0; + $parts = []; + foreach ($values as $key => $part) { + if ($key === $index) { + ## séquentiel + if (is_array($part)) { + # paramètres récursifs + self::parse_set_values($part, $parts, $bindings); + } else { + # paramètre litéral + $parts[] = strval($part); + } + $index++; + } else { + ## associatif + # paramètre + $param = $param0 = preg_replace('/^.+\./', "", $key); + if ($bindings !== null && array_key_exists($param0, $bindings)) { + $i = 2; + while (array_key_exists("$param0$i", $bindings)) { + $i++; + } + $param = "$param0$i"; + } + # value + $value = $part; + $part = [$key, "="]; + if ($value === null) { + $part[] = "null"; + } else { + $part[] = ":$param"; + $bindings[$param] = $value; + } + $parts[] = implode(" ", $part); + } + } + $sql = cl::merge($sql, $parts); + } + + protected static function check_eof(string $tmpsql, string $usersql): void { + self::consume(';\s*', $tmpsql); + if ($tmpsql) { + throw new ValueException("unexpected value at end: $usersql"); + } + } +} diff --git a/php/src/db/_private/_config.php b/php/src/db/_private/_config.php new file mode 100644 index 0000000..4a9ab06 --- /dev/null +++ b/php/src/db/_private/_config.php @@ -0,0 +1,36 @@ +configs = $configs; + } + + /** @var array */ + protected $configs; + + function configure(IDatabase $db): void { + foreach ($this->configs as $key => $config) { + if (is_string($config) && !func::is_method($config)) { + $db->exec($config); + } else { + func::with($config)->bind($this)->invoke([$db, $key]); + } + } + } +} diff --git a/php/src/db/_private/_create.php b/php/src/db/_private/_create.php new file mode 100644 index 0000000..afb90c5 --- /dev/null +++ b/php/src/db/_private/_create.php @@ -0,0 +1,54 @@ + "?string", + "table" => "string", + "schema" => "?array", + "cols" => "?array", + "suffix" => "?string", + ]; + + static function isa(string $sql): bool { + #XXX implémentation minimale + return preg_match("/^create(?:\s+table)?\b/i", $sql); + } + + static function parse(array $query, ?array &$bindings=null): string { + #XXX implémentation minimale + $tmpsql = self::merge_seq($query); + self::consume('create(?:\s+table)?\b', $tmpsql); + $sql = ["create table"]; + if ($tmpsql) $sql[] = $tmpsql; + + ## préfixe + $prefix = $query["prefix"] ?? null; + if ($prefix !== null) $sql[] = $prefix; + + ## table + $table = $query["table"] ?? null; + if ($table !== null) $sql[] = $table; + + ## columns + $cols = $query["cols"] ?? null; + if ($cols !== null) { + $index = 0; + foreach ($cols as $col => &$definition) { + if ($col === $index) { + $index++; + } else { + $definition = "$col $definition"; + } + }; unset($definition); + $sql[] = "(\n ".implode("\n, ", $cols)."\n)"; + } + + ## suffixe + $suffix = $query["suffix"] ?? null; + if ($suffix !== null) $sql[] = $suffix; + + ## fin de la requête + return implode(" ", $sql); + } +} diff --git a/php/src/db/_private/_delete.php b/php/src/db/_private/_delete.php new file mode 100644 index 0000000..55409d9 --- /dev/null +++ b/php/src/db/_private/_delete.php @@ -0,0 +1,48 @@ + "?string", + "from" => "?string", + "where" => "?array", + "suffix" => "?string", + ]; + + static function isa(string $sql): bool { + return preg_match("/^delete(?:\s+from)?\b/i", $sql); + } + + static function parse(array $query, ?array &$bindings=null): string { + #XXX implémentation minimale + $tmpsql = self::merge_seq($query); + self::consume('delete(?:\s+from)?\b', $tmpsql); + $sql = ["delete from"]; + if ($tmpsql) $sql[] = $tmpsql; + + ## préfixe + $prefix = $query["prefix"] ?? null; + if ($prefix !== null) $sql[] = $prefix; + + ## table + $from = $query["from"] ?? null; + if ($from !== null) $sql[] = $from; + + ## where + $where = $query["where"] ?? null; + if ($where !== null) { + self::parse_conds($where, $wheresql, $bindings); + if ($wheresql) { + $sql[] = "where"; + $sql[] = implode(" and ", $wheresql); + } + } + + ## suffixe + $suffix = $query["suffix"] ?? null; + if ($suffix !== null) $sql[] = $suffix; + + ## fin de la requête + return implode(" ", $sql); + } +} diff --git a/php/src/db/_private/_generic.php b/php/src/db/_private/_generic.php new file mode 100644 index 0000000..d71e157 --- /dev/null +++ b/php/src/db/_private/_generic.php @@ -0,0 +1,24 @@ + "?string", + "into" => "?string", + "schema" => "?array", + "cols" => "?array", + "values" => "?array", + "suffix" => "?string", + ]; + + static function isa(string $sql): bool { + return preg_match("/^insert\b/i", $sql); + } + + /** + * parser une chaine de la forme + * "insert [into] [TABLE] [(COLS)] [values (VALUES)]" + */ + static function parse(array $query, ?array &$bindings=null): string { + # fusionner d'abord toutes les parties séquentielles + $usersql = $tmpsql = self::merge_seq($query); + + ### vérifier la présence des parties nécessaires + $sql = []; + if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + + ## insert + self::consume('(insert(?:\s+or\s+(?:ignore|replace))?)\s*', $tmpsql, $ms); + $sql[] = $ms[1]; + + ## into + self::consume('into\s*', $tmpsql); + $sql[] = "into"; + $into = $query["into"] ?? null; + if (self::consume('([a-z_][a-z0-9_]*)\s*', $tmpsql, $ms)) { + if ($into === null) $into = $ms[1]; + $sql[] = $into; + } elseif ($into !== null) { + $sql[] = $into; + } else { + throw new ValueException("expected table name: $usersql"); + } + + ## cols & values + $usercols = []; + $uservalues = []; + if (self::consume('\(([^)]*)\)\s*', $tmpsql, $ms)) { + $usercols = array_merge($usercols, preg_split("/\s*,\s*/", $ms[1])); + } + $cols = cl::withn($query["cols"] ?? null); + $values = cl::withn($query["values"] ?? null); + $schema = $query["schema"] ?? null; + if ($cols === null) { + if ($usercols) { + $cols = $usercols; + } elseif ($values) { + $cols = array_keys($values); + $usercols = array_merge($usercols, $cols); + } elseif ($schema && is_array($schema)) { + #XXX implémenter support AssocSchema + $cols = array_keys($schema); + $usercols = array_merge($usercols, $cols); + } + } + if (self::consume('values\s+\(\s*(.*)\s*\)\s*', $tmpsql, $ms)) { + if ($ms[1]) $uservalues[] = $ms[1]; + } + if ($cols !== null && !$uservalues) { + if (!$usercols) $usercols = $cols; + foreach ($cols as $col) { + $uservalues[] = ":$col"; + $bindings[$col] = $values[$col] ?? null; + } + } + $sql[] = "(" . implode(", ", $usercols) . ")"; + $sql[] = "values (" . implode(", ", $uservalues) . ")"; + + ## 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/_migration.php b/php/src/db/_private/_migration.php new file mode 100644 index 0000000..79d8686 --- /dev/null +++ b/php/src/db/_private/_migration.php @@ -0,0 +1,99 @@ +db = $db; + $this->channel = $channel; + if ($migrations === null) $migrations = static::MIGRATION; + 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; + } + + protected ?IDatabase $db; + + protected string $channel; + + const MIGRATION_TABLE = "_migration"; + const MIGRATION_COLS = [ + "channel" => "varchar not null", + "name" => "varchar not null", + "done" => "integer not null default 0", + "primary key (channel, name)", + ]; + + protected function ensureTable(): void { + $this->db->exec([ + "create table if not exists", + "table" => static::MIGRATION_TABLE, + "cols" => static::MIGRATION_COLS, + ]); + } + + protected function isMigrated(string $name): bool { + return boolval($this->db->get([ + "select 1", + "from" => static::MIGRATION_TABLE, + "where" => [ + "channel" => $this->channel, + "name" => $name, + "done" => 1, + ], + ])); + } + + abstract protected function setMigrated(string $name, bool $done): void; + + /** @var callable[]|string[] */ + protected $migrations; + + function migrate(?IDatabase $db=null): void { + $db = ($this->db ??= $db); + $this->ensureTable(); + foreach ($this->migrations as $name => $migration) { + if ($this->isMigrated($name)) continue; + $this->setMigrated($name, false); + if (func::is_callable($migration)) { + func::with($migration)->bind($this)->invoke([$db, $name]); + } else { + foreach (cl::with($migration) as $query) { + $db->exec($query); + } + } + $this->setMigrated($name, true); + } + } + + protected static function sql_prefix(?string $source=null): string { + $prefix = "-- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8\n"; + if ($source !== null) $prefix .= "-- autogénéré à partir de $source\n"; + return $prefix; + } + + function getSql(?string $source=null, ?IDatabase $db=null): string { + $db = ($this->db ??= $db); + $lines = [self::sql_prefix($source)]; + foreach ($this->migrations as $name => $migration) { + $lines[] = "-- $name"; + if (func::is_callable($migration)) { + $lines[] = "-- "; + } else { + foreach (cl::with($migration) as $query) { + $sql = $db->getSql($query); + $lines[] = "$sql;"; + } + } + $lines[] = ""; + } + return implode("\n", $lines); + } +} diff --git a/php/src/db/_private/_select.php b/php/src/db/_private/_select.php new file mode 100644 index 0000000..1e7f1d0 --- /dev/null +++ b/php/src/db/_private/_select.php @@ -0,0 +1,182 @@ + "?string", + "schema" => "?array", + "cols" => "?array", + "col_prefix" => "?string", + "from" => "?string", + "where" => "?array", + "order by" => "?array", + "group by" => "?array", + "having" => "?array", + "suffix" => "?string", + ]; + + static function isa(string $sql): bool { + return preg_match("/^select\b/i", $sql); + } + + private static function add_prefix(?string $col, ?string $prefix): string { + $col ??= "null"; + if ($prefix === null) return $col; + if (strpos($col, ".") !== false) return $col; + return "$prefix$col"; + } + + /** + * parser une chaine de la forme + * "select [COLS] [from TABLE] [where CONDS] [order by ORDERS] [group by GROUPS] [having CONDS]" + */ + static function parse(array $query, ?array &$bindings=null): string { + # fusionner d'abord toutes les parties séquentielles + $usersql = $tmpsql = self::merge_seq($query); + + ### vérifier la présence des parties nécessaires + $sql = []; + + ## préfixe + if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + + ## select + self::consume('(select(?:\s*distinct)?)\s*', $tmpsql, $ms); + $sql[] = $ms[1]; + + ## cols + $usercols = []; + if (self::consume('(.*?)\s*(?=$|\bfrom\b)', $tmpsql, $ms)) { + if ($ms[1]) $usercols[] = $ms[1]; + } + $colPrefix = $query["col_prefix"] ?? null; + if ($colPrefix !== null) str::add_suffix($colPrefix, "."); + $tmpcols = cl::withn($query["cols"] ?? null); + $schema = $query["schema"] ?? null; + if ($tmpcols !== null) { + $cols = []; + $index = 0; + foreach ($tmpcols as $key => $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/_update.php b/php/src/db/_private/_update.php new file mode 100644 index 0000000..7b297c4 --- /dev/null +++ b/php/src/db/_private/_update.php @@ -0,0 +1,53 @@ + "?string", + "table" => "?string", + "schema" => "?array", + "cols" => "?array", + "values" => "?array", + "where" => "?array", + "suffix" => "?string", + ]; + + static function isa(string $sql): bool { + return preg_match("/^update\b/i", $sql); + } + + static function parse(array $query, ?array &$bindings=null): string { + #XXX implémentation minimale + $sql = [self::merge_seq($query)]; + + ## préfixe + $prefix = $query["prefix"] ?? null; + if ($prefix !== null) $sql[] = $prefix; + + ## table + $table = $query["table"] ?? null; + if ($table !== null) $sql[] = $table; + + ## set + self::parse_set_values($query["values"], $setsql, $bindings); + $sql[] = "set"; + $sql[] = implode(", ", $setsql); + + ## where + $where = $query["where"] ?? null; + if ($where !== null) { + self::parse_conds($where, $wheresql, $bindings); + if ($wheresql) { + $sql[] = "where"; + $sql[] = implode(" and ", $wheresql); + } + } + + ## suffixe + $suffix = $query["suffix"] ?? null; + if ($suffix !== null) $sql[] = $suffix; + + ## fin de la requête + return implode(" ", $sql); + } +} diff --git a/php/src/db/conds.php b/php/src/db/conds.php new file mode 100644 index 0000000..f0c3dfd --- /dev/null +++ b/php/src/db/conds.php @@ -0,0 +1,48 @@ +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..ace8beb --- /dev/null +++ b/php/src/db/mysql/MysqlStorage.php @@ -0,0 +1,65 @@ +db = Mysql::with($mysql); + } + + protected Mysql $db; + + function db(): Mysql { + return $this->db; + } + + const PRIMARY_KEY_DEFINITION = [ + "id_" => "integer primary key auto_increment", + ]; + + protected function tableExists(string $tableName): bool { + $db = $this->db; + $found = $db->get([ + "select table_name from information_schema.tables", + "where" => [ + "table_schema" => $db->getDbname(), + "table_name" => $tableName, + ], + ]); + return $found !== null; + } + + const METADATA_COLS = [ + "name" => "varchar(64) not null primary key", + "value" => "varchar(255)", + ]; + + function _getMigration(CapacitorChannel $channel): _mysqlMigration { + $migrations = cl::merge([ + "0init" => [$this->_createSql($channel)], + ], $channel->getMigration()); + return new _mysqlMigration($migrations, $channel->getName()); + } + + const CHANNELS_COLS = [ + "name" => "varchar(255) not null primary key", + "table_name" => "varchar(64)", + "class_name" => "varchar(255)", + ]; + + protected function _addToChannelsSql(CapacitorChannel $channel): array { + return cl::merge(parent::_addToChannelsSql($channel), [ + "suffix" => "on duplicate key update name = name", + ]); + } + + function close(): void { + $this->db->close(); + } +} diff --git a/php/src/db/mysql/_mysqlMigration.php b/php/src/db/mysql/_mysqlMigration.php new file mode 100644 index 0000000..2d63db1 --- /dev/null +++ b/php/src/db/mysql/_mysqlMigration.php @@ -0,0 +1,31 @@ + "varchar(64) not null", + "name" => "varchar(64) not null", + "done" => "integer not null default 0", + "primary key (channel, name)", + ]; + + protected function setMigrated(string $name, bool $done): void { + $this->db->exec([ + "insert", + "into" => static::MIGRATION_TABLE, + "values" => [ + "channel" => $this->channel, + "name" => $name, + "done" => $done? 1: 0, + ], + "suffix" => "on duplicate key update done = :done", + ]); + } +} diff --git a/php/src/db/mysql/_mysqlQuery.php b/php/src/db/mysql/_mysqlQuery.php new file mode 100644 index 0000000..b207161 --- /dev/null +++ b/php/src/db/mysql/_mysqlQuery.php @@ -0,0 +1,8 @@ + $pdo->dbconn, + "options" => $pdo->options, + "config" => $pdo->config, + "migration" => $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 MIGRATION = 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"], + "migration" => ["?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["migration"] ?? static::MIGRATION; + # + $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; + if ($params["auto_open"] ?? $defaultAutoOpen) { + $this->open(); + } + } + + protected ?array $dbconn; + + /** @var array|callable */ + protected $options; + + /** @var array|string|callable */ + protected $config; + + /** @var array|string|callable */ + protected $migration; + + protected ?\PDO $db = null; + + function getSql($query, ?array $params=null): string { + $query = new _pdoQuery($query, $params); + return $query->getSql(); + } + + function open(): self { + if ($this->db === null) { + $dbconn = $this->dbconn; + $options = $this->options; + if (is_callable($options)) { + $options = func::with($options)->bind($this)->invoke(); + } + $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); + } + + function exec($query, ?array $params=null) { + $db = $this->db(); + $query = new _pdoQuery($query, $params); + if ($query->_use_stmt($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 ($query->isInsert()) 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 { + 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 _pdoQuery($query, $params); + $stmt = null; + try { + /** @var \PDOStatement $stmt */ + if ($query->_use_stmt($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); + } + + function all($query, ?array $params=null, $primaryKeys=null): iterable { + $db = $this->db(); + $query = new _pdoQuery($query, $params); + $stmt = null; + try { + /** @var \PDOStatement $stmt */ + if ($query->_use_stmt($db, $stmt, $sql)) { + if ($stmt->execute() === false) return; + } else { + $stmt = $db->query($sql); + } + $primaryKeys = cl::withn($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/_pdoQuery.php b/php/src/db/pdo/_pdoQuery.php new file mode 100644 index 0000000..c1161fa --- /dev/null +++ b/php/src/db/pdo/_pdoQuery.php @@ -0,0 +1,30 @@ +sql); + //msg::info(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/pgsql/Pgsql.php b/php/src/db/pgsql/Pgsql.php new file mode 100644 index 0000000..f765666 --- /dev/null +++ b/php/src/db/pgsql/Pgsql.php @@ -0,0 +1,309 @@ + $pgsql->dbconn, + "options" => $pgsql->options, + "config" => $pgsql->config, + "migration" => $pgsql->migration, + ], $params)); + } else { + return new static($pgsql, $params); + } + } + + + protected const OPTIONS = [ + # XXX désactiver les connexions persistantes par défaut + # pour réactiver par défaut, il faudrait vérifier la connexion à chaque fois + # qu'elle est ouverte avec un "select 1". en effet, l'expérience jusqu'ici + # est que la première connexion après un long timeout échoue + "persistent" => false, + "force_new" => false, + "serial_support" => true, + ]; + + const CONFIG = null; + + const MIGRATION = null; + + const params_SCHEMA = [ + "dbconn" => ["array"], + "options" => ["?array|callable"], + "replace_config" => ["?array|callable"], + "config" => ["?array|callable"], + "migration" => ["?array|string|callable"], + "auto_open" => ["bool", true], + ]; + + const dbconn_SCHEMA = [ + "" => "?string", + "host" => "string", + "hostaddr" => "?string", + "port" => "?int", + "dbname" => "string", + "user" => "string", + "password" => "string", + "connect_timeout" => "?int", + "options" => "?string", + "sslmode" => "?string", + "service" => "?string", + ]; + + protected const dbconn_MAP = [ + "name" => "dbname", + "pass" => "password", + ]; + + const options_SCHEMA = [ + "persistent" => ["bool", self::OPTIONS["persistent"]], + "force_new" => ["bool", self::OPTIONS["force_new"]], + ]; + + function __construct($dbconn=null, ?array $params=null) { + if ($dbconn !== null) { + if (!is_array($dbconn)) { + $dbconn = ["" => $dbconn]; + #XXX à terme, il faudra interroger config + #$tmp = config::db($dbconn); + #if ($tmp !== null) $dbconn = $tmp; + #else $dbconn = ["" => $dbconn]; + } + unset($dbconn["type"]); + $name = $dbconn["name"] ?? null; + if ($name !== null) { + $dbconn[""] = $name; + unset($dbconn["name"]); + } + $params["dbconn"] = $dbconn; + } + # dbconn + $this->dbconn = $params["dbconn"] ?? 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]; + } + $this->config = $config; + # migrations + $this->migration = $params["migration"] ?? static::MIGRATION; + # + $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; + if ($params["auto_open"] ?? $defaultAutoOpen) { + $this->open(); + } + } + + protected ?array $dbconn; + + /** @var array|callable|null */ + protected $options; + + /** @var array|string|callable */ + protected $config; + + /** @var array|string|callable */ + protected $migration; + + /** @var resource */ + protected $db = null; + + function getSql($query, ?array $params=null): string { + $query = new _pgsqlQuery($query, $params); + return $query->getSql(); + } + + function open(): self { + if ($this->db === null) { + $dbconn = $this->dbconn; + $connection_string = [$dbconn[""] ?? null]; + unset($dbconn[""]); + foreach ($dbconn as $key => $value) { + if ($value === null) continue; + $value = strval($value); + if ($value === "" || preg_match("/[ '\\\\]/", $value)) { + $value = str_replace("\\", "\\\\", $value); + $value = str_replace("'", "\\'", $value); + $value = "'$value'"; + } + $key = cl::get(self::dbconn_MAP, $key, $key); + $connection_string[] = "$key=$value"; + } + $connection_string = implode(" ", array_filter($connection_string)); + $options = $this->options; + if (is_callable($options)) { + $options = func::with($options)->bind($this)->invoke(); + } + $forceNew = $options["force_new"] ?? false; + $flags = $forceNew? PGSQL_CONNECT_FORCE_NEW: 0; + + if ($options["persistent"] ?? true) $db = pg_pconnect($connection_string, $flags); + else $db = pg_connect($connection_string, $flags); + if ($db === false) throw new PgsqlException("unable to connect"); + $this->db = $db; + + _config::with($this->config)->configure($this); + //_migration::with($this->migration)->migrate($this); + } + return $this; + } + + function close(): self { + if ($this->db !== null) { + pg_close($this->db); + $this->db = null; + } + return $this; + } + + protected function db() { + $this->open(); + return $this->db; + } + + function _exec(string $query): bool { + $result = pg_query($this->db(), $query); + if ($result === false) return false; + pg_free_result($result); + return true; + } + + function getLastSerial() { + $db = $this->db(); + $result = @pg_query($db, "select lastval()"); + if ($result === false) return false; + $lastSerial = pg_fetch_row($result)[0]; + pg_free_result($result); + return $lastSerial; + } + + function exec($query, ?array $params=null) { + $db = $this->db(); + $query = new _pgsqlQuery($query, $params); + $result = $query->_exec($db); + $serialSupport = $this->options["serial_support"] ?? true; + if ($serialSupport && $query->isInsert()) return $this->getLastSerial(); + $affected_rows = pg_affected_rows($result); + pg_free_result($result); + return $affected_rows; + } + + /** @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 &$inerror=null): bool { + $status = pg_transaction_status($this->db()); + if ($status === PGSQL_TRANSACTION_ACTIVE || $status === PGSQL_TRANSACTION_INTRANS) { + $inerror = false; + return true; + } elseif ($status === PGSQL_TRANSACTION_INERROR) { + $inerror = true; + return true; + } else { + return false; + } + } + + function beginTransaction(?callable $func=null, bool $commit=true): void { + $this->_exec("begin"); + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->beginTransaction(); + } + } + if ($func !== null) { + $commited = false; + try { + func::call($func, $this); + if ($commit) { + $this->commit(); + $commited = true; + } + } finally { + if ($commit && !$commited) $this->rollback(); + } + } + } + + function commit(): void { + $this->_exec("commit"); + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->commit(); + } + } + } + + function rollback(): void { + $this->_exec("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 _pgsqlQuery($query, $params); + $result = $query->_exec($db); + $row = pg_fetch_assoc($result); + pg_free_result($result); + if ($row === false) return null; + $this->verifixRow($row); + if ($entireRow) return $row; + else return cl::first($row); + } + + function one($query, ?array $params=null): ?array { + return $this->get($query, $params, true); + } + + function all($query, ?array $params=null, $primaryKeys=null): iterable { + $db = $this->db(); + $query = new _pgsqlQuery($query, $params); + $result = $query->_exec($db); + $primaryKeys = cl::withn($primaryKeys); + while (($row = pg_fetch_assoc($result)) !== false) { + $this->verifixRow($row); + if ($primaryKeys !== null) { + $key = implode("-", cl::select($row, $primaryKeys)); + yield $key => $row; + } else { + yield $row; + } + } + pg_free_result($result); + } +} diff --git a/php/src/db/pgsql/PgsqlException.php b/php/src/db/pgsql/PgsqlException.php new file mode 100644 index 0000000..f9a500e --- /dev/null +++ b/php/src/db/pgsql/PgsqlException.php @@ -0,0 +1,15 @@ +getMessage(), $e->getCode(), $e); + } +} diff --git a/php/src/db/pgsql/PgsqlStorage.php b/php/src/db/pgsql/PgsqlStorage.php new file mode 100644 index 0000000..dd89e2a --- /dev/null +++ b/php/src/db/pgsql/PgsqlStorage.php @@ -0,0 +1,60 @@ +db = Pgsql::with($pgsql); + } + + protected Pgsql $db; + + function db(): Pgsql { + return $this->db; + } + + const PRIMARY_KEY_DEFINITION = [ + "id_" => "serial primary key", + ]; + + protected function tableExists(string $tableName): bool { + if (($index = strpos($tableName, ".")) !== false) { + $schemaName = substr($tableName, 0, $index); + $tableName = substr($tableName, $index + 1); + } else { + $schemaName = "public"; + } + $found = $this->db->get([ + "select tablename from pg_tables", + "where" => [ + "schemaname" => $schemaName, + "tablename" => $tableName, + ], + ]); + return $found !== null; + } + + function _getMigration(CapacitorChannel $channel): _pgsqlMigration { + $migrations = cl::merge([ + "0init" => [$this->_createSql($channel)], + ], $channel->getMigration()); + return new _pgsqlMigration($migrations, $channel->getName()); + } + + protected function _addToChannelsSql(CapacitorChannel $channel): array { + return cl::merge(parent::_addToChannelsSql($channel), [ + "suffix" => "on conflict (name) do nothing", + ]); + } + + function close(): void { + $this->db->close(); + } +} diff --git a/php/src/db/pgsql/_pgsqlMigration.php b/php/src/db/pgsql/_pgsqlMigration.php new file mode 100644 index 0000000..2a37c4b --- /dev/null +++ b/php/src/db/pgsql/_pgsqlMigration.php @@ -0,0 +1,24 @@ +db->exec([ + "insert", + "into" => static::MIGRATION_TABLE, + "values" => [ + "channel" => $this->channel, + "name" => $name, + "done" => $done? 1: 0, + ], + "suffix" => "on conflict (channel, name) do update set done = :done", + ]); + } +} diff --git a/php/src/db/pgsql/_pgsqlQuery.php b/php/src/db/pgsql/_pgsqlQuery.php new file mode 100644 index 0000000..34c7ed1 --- /dev/null +++ b/php/src/db/pgsql/_pgsqlQuery.php @@ -0,0 +1,44 @@ +sql; + $bindings = $this->bindings; + if (static::DEBUG_QUERIES) {#XXX + msg::info($sql); + //msg::info(var_export($bindings, true)); + } + if ($bindings !== null) { + # trier d'abord les champ par ordre de longueur, pour éviter les overlaps + $names = array_keys($bindings); + usort($names, function ($a, $b) { + return -cv::compare(strlen(strval($a)), strlen(strval($b))); + }); + $bparams = []; + $number = 1; + foreach ($names as $name) { + $sql = str_replace(":$name", "\$$number", $sql); + $bparams[] = $bindings[$name]; + $number++; + } + $result = pg_query_params($db, $sql, $bparams); + } else { + $result = pg_query($db, $sql); + } + if ($result === false) throw PgsqlException::last_error($db); + return $result; + } +} diff --git a/php/src/db/sqlite/Sqlite.php b/php/src/db/sqlite/Sqlite.php new file mode 100644 index 0000000..ae4ea99 --- /dev/null +++ b/php/src/db/sqlite/Sqlite.php @@ -0,0 +1,332 @@ + $sqlite->file, + "flags" => $sqlite->flags, + "encryption_key" => $sqlite->encryptionKey, + "allow_wal" => $sqlite->allowWal, + "config" => $sqlite->config, + "migration" => $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 MIGRATION = 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"], + "migration" => ["?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["migration"] ?? static::MIGRATION; + # + $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 getSql($query, ?array $params=null): string { + $query = new _sqliteQuery($query, $params); + return $query->getSql(); + } + + function open(): self { + if ($this->db === null) { + $this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey); + _config::with($this->config)->configure($this); + _sqliteMigration::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); + } + + function exec($query, ?array $params=null) { + $db = $this->db(); + $query = new _sqliteQuery($query, $params); + if ($query->_use_stmt($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 ($query->isInsert()) 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 { + 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 _sqliteQuery($query, $params); + if ($query->_use_stmt($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 { + $primaryKeys = cl::withn($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(); + } + } + + function all($query, ?array $params=null, $primaryKeys=null): iterable { + $db = $this->db(); + $query = new _sqliteQuery($query, $params); + if ($query->_use_stmt($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..7495b46 --- /dev/null +++ b/php/src/db/sqlite/SqliteStorage.php @@ -0,0 +1,82 @@ +db = Sqlite::with($sqlite); + } + + protected Sqlite $db; + + function db(): Sqlite { + return $this->db; + } + + const PRIMARY_KEY_DEFINITION = [ + "id_" => "integer primary key autoincrement", + ]; + + protected function tableExists(string $tableName): bool { + $found = $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 $found !== null; + } + + function _getMigration(CapacitorChannel $channel): _sqliteMigration { + $migrations = cl::merge([ + "0init" => [$this->_createSql($channel)], + ], $channel->getMigration()); + return new _sqliteMigration($migrations, $channel->getName()); + } + + function channelExists(string $name, ?array &$row=null): bool { + $row = $this->db->one([ + "select", + "from" => static::CHANNELS_TABLE, + "where" => ["name" => $name], + ]); + return $row !== null; + } + + function getChannels(): iterable { + return $this->db->all([ + "select", + "from" => static::CHANNELS_TABLE, + ]); + } + + protected function _addToChannelsSql(CapacitorChannel $channel): array { + $sql = parent::_addToChannelsSql($channel); + $sql[0] = "insert or ignore"; + return $sql; + } + + protected function _afterCreate(CapacitorChannel $channel): void { + $db = $this->db; + if (!$this->tableExists(static::CHANNELS_TABLE)) { + # ne pas créer si la table existe déjà, pour éviter d'avoir besoin d'un + # verrou en écriture + $db->exec($this->_createChannelsSql()); + } + 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($this->_addToChannelsSql($channel)); + } + } + + function close(): void { + $this->db->close(); + } +} diff --git a/php/src/db/sqlite/_sqliteMigration.php b/php/src/db/sqlite/_sqliteMigration.php new file mode 100644 index 0000000..182c71c --- /dev/null +++ b/php/src/db/sqlite/_sqliteMigration.php @@ -0,0 +1,23 @@ +db->exec([ + "insert or replace", + "into" => static::MIGRATION_TABLE, + "values" => [ + "channel" => $this->channel, + "name" => $name, + "done" => $done? 1: 0, + ], + ]); + } +} diff --git a/php/src/db/sqlite/_sqliteQuery.php b/php/src/db/sqlite/_sqliteQuery.php new file mode 100644 index 0000000..04e6f1c --- /dev/null +++ b/php/src/db/sqlite/_sqliteQuery.php @@ -0,0 +1,39 @@ +sql); + //msg::info(var_export($this->bindings, true)); + } + 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/ext/JsonException.php b/php/src/ext/JsonException.php new file mode 100644 index 0000000..75feb16 --- /dev/null +++ b/php/src/ext/JsonException.php @@ -0,0 +1,20 @@ +getContents(); + return JsonException::ensure_json_value(self::decode($contents)); + } + + /** obtenir la valeur JSON correspondant au corps de la requête POST */ + static final function post_data() { + $content = file_get_contents("php://input"); + return JsonException::ensure_json_value(self::decode($content)); + } + + /** envoyer $data au format JSON */ + static final function send($data, bool $exit=true): void { + header("Content-Type: application/json"); + print self::encode($data); + if ($exit) exit; + } + + const INDENT_TABS = "\t"; + + static final function with($data, ?string $indent=null): string { + $json = self::encode($data, JSON_PRETTY_PRINT); + if ($indent !== null) { + $json = preg_replace_callback('/^(?: {4})+/m', function (array $ms) use ($indent) { + return str_repeat($indent, strlen($ms[0]) / 4); + }, $json); + } + return $json; + } + + static final function dump($data, $output=null): void { + file::writer($output)->putContents(self::with($data)); + } +} diff --git a/php/src/ext/yaml.php b/php/src/ext/yaml.php new file mode 100644 index 0000000..997d66a --- /dev/null +++ b/php/src/ext/yaml.php @@ -0,0 +1,35 @@ +getContents(); + try { + return cl::with(SymfonyYaml::parse($contents)); + } catch (ParseException $e) { + $message = "parse error"; + if (is_string($input)) $message .= " while loading $input"; + throw new IOException($message, 0, $e); + } + } + + static final function with($data, int $indent=2, int $flags=0): string { + return SymfonyYaml::dump($data, PHP_INT_MAX, $indent, $flags); + } + + static final function dump($data, $output=null, int $indent=2, int $flags=0): void { + file::writer($output)->putContents(self::with($data, $indent, $flags)); + } +} diff --git a/php/src/file.php b/php/src/file.php new file mode 100644 index 0000000..feaa12c --- /dev/null +++ b/php/src/file.php @@ -0,0 +1,91 @@ +close(); + } + } + return $file; + } + + static function writer($output, ?string $mode="w+b", ?callable $func=null): FileWriter { + $file = new FileWriter(self::fix_dash($output), $mode); + if ($func !== null) { + try { + $func($file); + } finally { + $file->close(); + } + } + return $file; + } + + static function shared($file, ?callable $func=null): SharedFile { + $file = new SharedFile($file); + if ($func !== null) { + try { + $func($file); + } finally { + $file ->close(); + } + } + return $file; + } + + static function tmpwriter($destdir=null, ?callable $func=null): TmpfileWriter { + $file = new TmpfileWriter($destdir); + if ($func !== null) { + try { + $func($file); + } finally { + $file->close(); + } + } + return $file; + } + + static function memory(?callable $func=null): MemoryStream { + $file = new MemoryStream(); + if ($func !== null) { + try { + $func($file); + } finally { + $file->close(); + } + } + return $file; + } + + static function temp(?callable $func=null): TempStream { + $file = new TempStream(); + if ($func !== null) { + try { + $func($file); + } finally { + $file->close(); + } + } + return $file; + } +} diff --git a/php/src/file/FileReader.php b/php/src/file/FileReader.php new file mode 100644 index 0000000..d663296 --- /dev/null +++ b/php/src/file/FileReader.php @@ -0,0 +1,51 @@ +ignoreBom = $ignoreBom; + if ($input === null) { + $fd = STDIN; + $close = false; + } elseif (is_resource($input)) { + $fd = $input; + $close = false; + } else { + $file = $input; + if ($mode === null) $mode = static::DEFAULT_MODE; + $this->file = $file; + $this->mode = $mode; + $fd = $this->open(); + $close = true; + } + parent::__construct($fd, $close, $throwOnError, $allowLocking); + } + + /** @return resource */ + protected function open() { + $fd = parent::open(); + $this->haveBom = false; + if ($this->ignoreBom) { + $bom = fread($fd, 3); + if ($bom === "\xEF\xBB\xBF") $this->seekOffset = 3; + else rewind($fd); + } + return $fd; + } + + function haveBom(): bool { + return $this->seekOffset !== 0; + } +} diff --git a/php/src/file/FileWriter.php b/php/src/file/FileWriter.php new file mode 100644 index 0000000..b3fdfc9 --- /dev/null +++ b/php/src/file/FileWriter.php @@ -0,0 +1,31 @@ +file = $file; + $this->mode = $mode; + $fd = $this->open(); + $close = true; + } + parent::__construct($fd, $close, $throwOnError, $allowLocking); + } +} diff --git a/php/src/file/IReader.php b/php/src/file/IReader.php new file mode 100644 index 0000000..fa96f7d --- /dev/null +++ b/php/src/file/IReader.php @@ -0,0 +1,45 @@ +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 && !$alreadyLocked; + if ($useLocking) $this->lock(LOCK_SH); + try { + return IOException::ensure_valid(stream_get_contents($this->fd), $this->throwOnError); + } finally { + if ($useLocking) $this->unlock($close); + elseif ($close) $this->close(); + } + } + + /** @var bool faut-il faire un mapping pour la compatibilité avec nur/sery */ + const NURSERY_COMPAT_ENABLED = true; + + /** + * @var string[] mappings pour la compatibilité avec des fichiers générés par + * nur/sery + */ + const NURSERY_COMPAT_MAPPING = [ + 'O:22:"nur\sery\php\time\Date":' => 'O:19:"nulib\php\time\Date":', + 'O:26:"nur\sery\php\time\DateTime":' => 'O:23:"nulib\php\time\DateTime":', + 'O:23:"nur\sery\php\time\Delay":' => 'O:20:"nulib\php\time\Delay":', + ]; + + static function nursery_compat_verifix(string $contents): string { + if (static::NURSERY_COMPAT_ENABLED) { + foreach (self::NURSERY_COMPAT_MAPPING as $from => $to) { + $contents = str_replace($from, $to, $contents); + } + } + return $contents; + } + + function unserialize(?array $options=null, bool $close=true, bool $alreadyLocked=false) { + $contents = $this->getContents($close, $alreadyLocked); + $contents = self::nursery_compat_verifix($contents); + $args = [$contents]; + if ($options !== null) $args[] = $options; + return unserialize(...$args); + } + + function decodeJson(bool $close=true, bool $alreadyLocked=false) { + $contents = $this->getContents($close, $alreadyLocked); + return json_decode($contents, true, 512, JSON_THROW_ON_ERROR); + } + + ############################################################################# + # Iterator + + protected function iter_setup(): void { + } + + protected function iter_next(&$key) { + try { + return $this->fgets(); + } catch (EOFException $e) { + throw new NoMoreDataException(); + } + } + + private function _rewindFd(): void { + self::probe_fd($this->fd, $seekable); + if ($seekable) $this->fseek(0); + } + + protected function iter_teardown(): void { + $this->_rewindFd(); + } + + function rewind(): void { + # il faut toujours faire un rewind sur la resource, que l'itérateur aie été + # initialisé ou non + if ($this->_hasIteratorBeenSetup()) parent::rewind(); + else $this->_rewindFd(); + } + + ############################################################################# + # Writer + + /** @throws IOException */ + function ftruncate(int $size=0, bool $rewind=true): self { + $fd = $this->getResource(); + $r = ftruncate($fd, $size); + $this->stat = null; + IOException::ensure_valid($r, $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); + $this->stat = null; + 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); + $r = fwrite($fd, "$line\n"); + $this->stat = null; + IOException::ensure_valid($r, $this->throwOnError); + } else { + $r = fputcsv($fd, $row, $params[0], $params[1], $params[2]); + $this->stat = null; + IOException::ensure_valid($r, $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 && !$alreadyLocked; + if ($useLocking) $this->lock(LOCK_EX); + try { + $this->fwrite($contents); + } finally { + if ($useLocking) $this->unlock($close); + elseif ($close) $this->close(); + } + } + + function serialize($object, bool $close=true, bool $alreadyLocked=false): void { + $this->putContents(serialize($object), $close, $alreadyLocked); + } + + function encodeJson($data, bool $close=true, bool $alreadyLocked=false): void { + $contents = json_encode($data, JSON_UNESCAPED_SLASHES + JSON_UNESCAPED_UNICODE); + $this->putContents($contents, $close, $alreadyLocked); + } + + /** + * annuler une tentative d'écriture commencée avec {@link self::canWrite()} + */ + function cancelWrite(bool $close=true): void { + if ($this->useLocking) $this->unlock($close); + elseif ($close) $this->close(); + } +} diff --git a/php/src/file/TStreamFilter.php b/php/src/file/TStreamFilter.php new file mode 100644 index 0000000..93063ae --- /dev/null +++ b/php/src/file/TStreamFilter.php @@ -0,0 +1,49 @@ +filters[] = [$filterName, $readWrite, $params]; + } + + function prependFilter(string $filterName, ?int $readWrite=null, $params=null): void { + if ($this->filters === null) $this->filters = []; + array_unshift($this->filters, [$filterName, $readWrite, $params]); + } + + function setEncodingFilter(string $from, string $to): void { + if ($to !== $from) { + $this->appendFilter("convert.iconv.$from.$to"); + } + } + + /** + * @param $fd resource + * @throws IOException + */ + protected function _streamAppendFilters($fd): void { + if ($this->filters !== null) { + foreach ($this->filters as [$filterName, $readWrite, $params]) { + if (stream_filter_append($fd, $filterName, $readWrite, $params) === false) { + throw new IOException("unable to add filter $filterName"); + } + } + $this->filters = null; + } + } + + /** + * @param $file _IFile + */ + protected function _appendFilters($file): void { + if ($this->filters !== null) { + foreach ($this->filters as [$filterName, $readWrite, $params]) { + $file->appendFilter($filterName, $readWrite, $params); + } + } + } +} diff --git a/php/src/file/TempStream.php b/php/src/file/TempStream.php new file mode 100644 index 0000000..28961c5 --- /dev/null +++ b/php/src/file/TempStream.php @@ -0,0 +1,28 @@ +maxMemory = $maxMemory ?? static::MAX_MEMORY; + parent::__construct($this->tempFd(), true, $throwOnError); + } + + /** @var int */ + protected $maxMemory; + + protected function tempFd() { + return fopen("php://temp/maxmemory:$this->maxMemory", "w+b"); + } + + function getResource() { + if ($this->fd === null) $this->fd = $this->tempFd(); + return parent::getResource(); + } +} diff --git a/php/src/file/TmpfileWriter.php b/php/src/file/TmpfileWriter.php new file mode 100644 index 0000000..2292235 --- /dev/null +++ b/php/src/file/TmpfileWriter.php @@ -0,0 +1,99 @@ +delete = true; + } elseif (is_file($destdir)) { + # si on spécifie un fichier qui existe le prendre comme "fichier + # temporaire" mais ne pas le supprimer automatiquement + $file = $destdir; + if (!path::is_qualified($file)) $file = path::join($tmpDir, $file); + $this->delete = false; + } else { + # un chemin qui n'existe pas: ne le sélectionner que si le répertoire + # existe. dans ce cas, le fichier sera créé automatiquement, mais pas + # supprimé + if (!is_dir(dirname($destdir))) { + throw new IOException("$destdir: no such file or directory"); + } + $file = $destdir; + $this->delete = false; + } + parent::__construct($file, $mode, $throwOnError, $allowLocking); + } + + /** @var bool */ + protected $delete; + + /** désactiver la suppression automatique du fichier temporaire */ + function keep(): self { + $this->delete = false; + return $this; + } + + function __destruct() { + $this->close(); + if ($this->delete) $this->delete(); + } + + /** supprimer le fichier. NB: le flux **n'est pas** fermé au préalable */ + function delete(): self { + if (file_exists($this->file)) unlink($this->file); + return $this; + } + + /** + * renommer le fichier. le flux est fermé d'abord + * + * @param int|null $defaultMode mode par défaut si le fichier destination + * n'existe pas. sinon, changer le mode du fichier temporaire à la valeur du + * fichier destination après renommage + * @param bool $setOwner si le propriétaire et/ou le groupe du fichier + * temporaire ne sont pas les mêmes que le fichier destination, tenter de + * changer le propriétaire et le groupe du fichier temporaire à la valeur + * du fichier destination après le renommage (nécessite les droits de root) + * @throws IOException + */ + function rename(string $dest, ?int $defaultMode=0644, bool $setOwner=true): void { + $this->close(); + $file = $this->file; + if (file_exists($dest)) { + $mode = fileperms($dest); + if ($setOwner) { + $tmpowner = fileowner($file); + $owner = fileowner($dest); + $tmpgroup = filegroup($file); + $group = filegroup($dest); + } else { + $owner = $group = null; + } + } else { + $mode = $defaultMode; + $owner = $group = null; + } + if (!rename($file, $dest)) { + throw new IOException("$file: unable to rename to $dest"); + } + $this->file = $dest; + if ($mode !== null) chmod($dest, $mode); + if ($owner !== null) { + if ($owner !== $tmpowner) chown($dest, $owner); + if ($group !== $tmpgroup) chgrp($dest, $group); + } + if ($mode !== null || $owner !== null) clearstatcache(true, $file); + } +} diff --git a/php/src/file/_File.php b/php/src/file/_File.php new file mode 100644 index 0000000..80a50f2 --- /dev/null +++ b/php/src/file/_File.php @@ -0,0 +1,39 @@ +file; + } + + /** @var string */ + protected $mode; + + function getMode(): string { + return $this->mode; + } + + /** @return resource */ + protected function open() { + return IOException::ensure_valid(@fopen($this->file, $this->mode)); + } + + /** @return resource */ + function getResource() { + if ($this->fd === null && $this->file !== null) $this->fd = $this->open(); + return parent::getResource(); + } + + /** streamer le contenu du fichier en sortie */ + function readfile(?string $contentType=null, ?string $charset=null, ?string $filename=null, string $disposition=null): bool { + if ($contentType !== null) http::content_type($contentType, $charset); + if ($filename !== null) http::download_as($filename, $disposition); + return readfile($this->file) !== false; + } +} diff --git a/php/src/file/_IFile.php b/php/src/file/_IFile.php new file mode 100644 index 0000000..f62560e --- /dev/null +++ b/php/src/file/_IFile.php @@ -0,0 +1,57 @@ +csvFlavour = csv_flavours::verifix($csvFlavour); + parent::__construct($output, $params); + } + + protected function _write(array $row, ?array $colStyles =null, ?array $rowStyle=null): void { + $this->fputcsv($row); + } + + function _sendContentType(): void { + http::content_type("text/csv"); + } + + protected function _checkOk(): bool { + $size = $this->ftell(); + if ($size === 0) return false; + $this->rewind(); + return true; + } +} diff --git a/php/src/file/csv/CsvReader.php b/php/src/file/csv/CsvReader.php new file mode 100644 index 0000000..317fda0 --- /dev/null +++ b/php/src/file/csv/CsvReader.php @@ -0,0 +1,48 @@ +csvFlavour = $params["csv_flavour"] ?? null; + $this->inputEncoding = $params["input_encoding"] ?? null; + } + + protected ?string $csvFlavour; + + protected ?string $inputEncoding; + + function getIterator() { + $input = $this->input; + if ($input instanceof IReader) { + $reader = $input; + } else { + $reader = new FileReader(file::fix_dash($input)); + } + $inputEncoding = $this->inputEncoding; + if ($inputEncoding !== null) { + $reader->appendFilter("convert.iconv.$inputEncoding.utf-8"); + } + $reader->setCsvFlavour($this->csvFlavour); + while (($row = $reader->fgetcsv()) !== null) { + foreach ($row as &$col) { + $this->verifixCol($col); + }; unset($col); + if ($this->cookRow($row)) { + yield $row; + $this->idest++; + } + $this->isrc++; + } + $reader->close(); + } +} diff --git a/php/src/file/csv/csv_flavours.php b/php/src/file/csv/csv_flavours.php new file mode 100644 index 0000000..4bc7bd9 --- /dev/null +++ b/php/src/file/csv/csv_flavours.php @@ -0,0 +1,59 @@ + ref_csv::OO_FLAVOUR, + "ooffice" => ref_csv::OO_FLAVOUR, + ref_csv::OOCALC => ref_csv::OO_FLAVOUR, + "xl" => ref_csv::XL_FLAVOUR, + "excel" => ref_csv::XL_FLAVOUR, + ref_csv::MSEXCEL => ref_csv::XL_FLAVOUR, + "dumb;" => ref_csv::DUMB_XL_FLAVOUR, + "dumb," => ref_csv::DUMB_OO_FLAVOUR, + "dumb" => ref_csv::DUMB_FLAVOUR, + ]; + + const ENCODINGS = [ + ref_csv::OO_FLAVOUR => ref_csv::OO_ENCODING, + ref_csv::XL_FLAVOUR => ref_csv::XL_ENCODING, + ref_csv::DUMB_FLAVOUR => ref_csv::DUMB_ENCODING, + ]; + + static final function verifix(?string $flavour): ?string { + if ($flavour === null) return null; + $lflavour = strtolower($flavour); + if (array_key_exists($lflavour, self::MAP)) { + $flavour = self::MAP[$lflavour]; + } + if (strlen($flavour) < 1) $flavour .= ","; + if (strlen($flavour) < 2) $flavour .= "\""; + if (strlen($flavour) < 3) $flavour .= "\\"; + return $flavour; + } + + static final function get_name(string $flavour): string { + if ($flavour == ref_csv::OO_FLAVOUR) return ref_csv::OOCALC; + elseif ($flavour == ref_csv::XL_FLAVOUR) return ref_csv::MSEXCEL; + else return $flavour; + } + + static final function get_params(string $flavour): array { + return [$flavour[0], $flavour[1], $flavour[2]]; + } + + static final function get_encoding(string $flavour): ?string { + return cl::get(self::ENCODINGS, $flavour); + } + + static final function is_dumb(string $flavour, ?string &$sep): bool { + if (!str::del_prefix($flavour, "xxx")) return false; + $sep = $flavour; + if (!$sep) $sep = ";"; + return true; + } +} diff --git a/php/src/file/tab/AbstractBuilder.php b/php/src/file/tab/AbstractBuilder.php new file mode 100644 index 0000000..3ec767d --- /dev/null +++ b/php/src/file/tab/AbstractBuilder.php @@ -0,0 +1,169 @@ +schema = $params["schema"] ?? static::SCHEMA; + $this->headers = $params["headers"] ?? static::HEADERS; + $this->useHeaders = $params["use_headers"] ?? static::USE_HEADERS; + $rows = $params["rows"] ?? null; + if (is_callable($rows)) $rows = $rows(); + $this->rows = $rows; + $this->index = 0; + $cookFunc = $params["cook_func"] ?? null; + if ($cookFunc !== null) $cookFunc = func::with($cookFunc)->bind($this); + $this->cookFunc = $cookFunc; + $this->output = $params["output"] ?? static::OUTPUT; + $maxMemory = $params["max_memory"] ?? null; + $throwOnError = $params["throw_on_error"] ?? null; + parent::__construct($maxMemory, $throwOnError); + } + + protected ?array $schema; + + protected ?array $headers; + + protected bool $useHeaders; + + protected ?iterable $rows; + + protected int $index; + + protected ?string $output; + + protected ?func $cookFunc; + + protected function ensureHeaders(?array $row=null): void { + if ($this->headers !== null || !$this->useHeaders) return; + if ($this->schema === null) $headers = null; + else $headers = array_keys($this->schema); + if ($headers === null && $row !== null) $headers = array_keys($row); + $this->headers = $headers; + } + + protected abstract function _write(array $row, ?array $colStyles =null, ?array $rowStyle=null): void; + + protected bool $wroteHeaders = false; + + function writeHeaders(?array $headers=null): void { + if ($this->wroteHeaders) return; + if ($this->useHeaders) { + if ($headers !== null) $this->headers = $headers; + else $this->ensureHeaders(); + if ($this->headers !== null) $this->_write($this->headers); + } + $this->wroteHeaders = true; + } + + protected function cookRow(?array $row): ?array { + if ($this->cookFunc !== null) { + $row = $this->cookFunc->prependArgs([$row])->invoke(); + } + if ($row !== null) { + foreach ($row as &$col) { + # formatter les dates + if ($col instanceof DateTime) { + $col = $col->format(); + } elseif ($col instanceof DateTimeInterface) { + $col = DateTime::with($col)->format(); + } + }; unset($col); + } + return $row; + } + + function write(?array $row, ?array $colsStyle=null, ?array $rowStyle=null): void { + $row = $this->cookRow($row); + if ($row === null) return; + $this->writeHeaders(array_keys($row)); + $this->_write($row, $colsStyle, $rowStyle); + $this->index++; + } + + function writeAll(?iterable $rows=null, ?array $rowStyle=null): void { + $unsetRows = false; + if ($rows === null) { + $rows = $this->rows; + $unsetRows = true; + } + if ($rows !== null) { + foreach ($rows as $row) { + $this->write(cl::with($row), null, $rowStyle); + } + } + if ($unsetRows) $this->rows = null; + } + + abstract protected function _sendContentType(): void; + + protected bool $sentHeaders = false; + + function sendHeaders(): void { + if ($this->sentHeaders) return; + $this->_sendContentType(); + $output = $this->output; + if ($output !== null) { + http::download_as(path::filename($output)); + } + $this->sentHeaders = true; + } + + protected function _build(?iterable $rows=null): void { + $this->writeAll($rows); + $this->writeHeaders(); + } + + abstract protected function _checkOk(): bool; + + protected bool $built = false, $closed = false; + + function build(?iterable $rows=null, bool $close=true): bool { + $ok = true; + if (!$this->built) { + $this->_build($rows); + $this->built = true; + } + if ($close && !$this->closed) { + $ok = $this->_checkOk(); + $this->closed = true; + } + return $ok; + } + + function sendFile(?iterable $rows=null): int { + if (!$this->built) { + $this->_build($rows); + $this->built = true; + } + if (!$this->closed) { + if (!$this->_checkOk()) return 0; + $this->closed = true; + } + $this->sendHeaders(); + return $this->fpassthru(); + } +} diff --git a/php/src/file/tab/AbstractReader.php b/php/src/file/tab/AbstractReader.php new file mode 100644 index 0000000..6418235 --- /dev/null +++ b/php/src/file/tab/AbstractReader.php @@ -0,0 +1,129 @@ +schema = $params["schema"] ?? static::SCHEMA; + $this->headers = $params["headers"] ?? static::HEADERS; + $this->useHeaders = $params["use_headers"] ?? static::USE_HEADERS; + $this->input = $params["input"] ?? static::INPUT; + $this->trim = boolval($params["trim"] ?? static::TRIM); + $this->emptyAsNull = boolval($params["empty_as_null"] ?? static::EMPTY_AS_NULL); + $this->parseNone = boolval($params["parse_none"] ?? static::PARSE_NONE); + $this->parseNumeric = boolval($params["parse_numeric"] ?? static::PARSE_NUMERIC); + $this->parseDate = boolval($params["parse_date"] ?? static::PARSE_DATE); + } + + protected ?array $schema; + + protected ?array $headers; + + protected bool $useHeaders; + + protected $input; + + protected bool $trim; + + protected bool $emptyAsNull; + + protected bool $parseNone; + + protected bool $parseNumeric; + + protected bool $parseDate; + + protected int $isrc = 0, $idest = 0; + + protected function cookRow(array &$row): bool { + if (!$this->useHeaders) return true; + if ($this->isrc == 0) { + # ligne d'en-tête + $headers = $this->headers; + if ($headers === null) { + if ($this->schema === null) $headers = null; + else $headers = array_keys($this->schema); + if ($headers === null) $headers = $row; + $this->headers = $headers; + } + return false; + } + A::ensure_size($row, count($this->headers)); + $row = array_combine($this->headers, $row); + return true; + } + + protected function verifixCol(&$col): void { + if ($this->trim && is_string($col)) { + $col = trim($col); + } + if ($this->emptyAsNull && $col === "") { + # valeur vide --> null + $col = null; + } + if (!is_string($col) || $this->parseNone) return; + if ($this->parseDate) { + if (DateTime::isa_datetime($col, true)) { + # datetime + $col = new DateTime($col); + } elseif (DateTime::isa_date($col, true)) { + # date + $col = new Date($col); + } + if (!is_string($col)) return; + } + $parseNumeric = $this->parseNumeric || substr($col, 0, 1) !== "0"; + if ($parseNumeric) { + $tmp = str_replace(",", ".", $col); + $float = strpos($tmp, ".") !== false; + if (is_numeric($tmp)) { + if ($float) $col = floatval($tmp); + else $col = intval($tmp); + } + } + } +} diff --git a/php/src/file/tab/IBuilder.php b/php/src/file/tab/IBuilder.php new file mode 100644 index 0000000..9cc794e --- /dev/null +++ b/php/src/file/tab/IBuilder.php @@ -0,0 +1,14 @@ +isExt(".csv")) { + $class = CsvBuilder::class; + } else { + $class = static::class; + if ($builder->isExt(".ods")) { + $params["ss_type"] = "ods"; + } else { + $params["ss_type"] = "xlsx"; + } + } + return new $class($builder->name, $params); + } + + if (is_string($builder)) { + $params["output"] = $builder; + } elseif (is_array($builder)) { + $params = cl::merge($builder, $params); + } elseif ($builder !== null) { + throw ValueException::invalid_type($builder, self::class); + } + + $output = $params["output"] ?? null; + $ssType = null; + if (is_string($output)) { + $ext = path::ext($output); + if ($output === "-" || $ext === ".csv") { + $class = CsvBuilder::class; + } elseif ($ext === ".ods") { + $ssType = "ods"; + } elseif ($ext === ".xlsx") { + $ssType = "xlsx"; + } else { + $ssType = "xlsx"; + } + } + $params["ss_type"] = $ssType; + if ($class === null) $class = static::class; + return new $class(null, $params); + } +} diff --git a/php/src/file/tab/TAbstractReader.php b/php/src/file/tab/TAbstractReader.php new file mode 100644 index 0000000..f6037c7 --- /dev/null +++ b/php/src/file/tab/TAbstractReader.php @@ -0,0 +1,55 @@ +isExt(".csv")) { + $class = CsvReader::class; + } else { + $class = static::class; + if ($reader->isExt(".ods")) { + $params["ss_type"] = "ods"; + } else { + $params["ss_type"] = "xlsx"; + } + } + return new $class($reader->getFile(), $params); + } + + if (is_string($reader)) { + $params["input"] = $reader; + } elseif (is_array($reader)) { + $params = cl::merge($reader, $params); + } elseif ($reader !== null) { + throw ValueException::invalid_type($reader, self::class); + } + + $input = $params["input"] ?? null; + $ssType = null; + if (is_string($input)) { + $ext = path::ext($input); + if ($input === "-" || $ext === ".csv") { + $class = CsvReader::class; + } elseif ($ext === ".ods") { + $ssType = "ods"; + } elseif ($ext === ".xlsx") { + $ssType = "xlsx"; + } else { + $ssType = "xlsx"; + } + } + $params["ss_type"] = $ssType; + if ($class === null) $class = static::class; + return new $class(null, $params); + } +} diff --git a/php/src/file/web/Upload.php b/php/src/file/web/Upload.php new file mode 100644 index 0000000..ce4d19b --- /dev/null +++ b/php/src/file/web/Upload.php @@ -0,0 +1,123 @@ + "Ceci n'est pas un fichier téléversé", + "nofile" => "Aucun fichier n'a été fourni", + "toobig" => "Le fichier que vous avez fourni est trop volumineux.", + "unknown" => "Une erreur s'est produite pendant le transfert du fichier. Veuillez réessayer.", + ]; + + protected static function error(string $message) { + return new ValueException(static::MESSAGES[$message]); + } + + function __construct(?array $file, bool $required=true, bool $check=true) { + parent::__construct($file); + if ($check) $this->check($required); + } + + function check(bool $required=true, bool $throw=true): bool { + $file = $this->data; + if ($file) { + $name = $file["name"] ?? null; + $type = $file["type"] ?? null; + $error = $file["error"] ?? null; + if (!is_scalar($name) || !is_scalar($type) || !is_scalar($error)) { + if ($throw) throw static::error("invalid"); + else return false; + } + switch ($error) { + case UPLOAD_ERR_OK: + break; + case UPLOAD_ERR_NO_FILE: + if ($required) { + if ($throw) throw self::error("nofile"); + else return false; + } + break; + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + if ($throw) throw self::error("toobig"); + else return false; + default: + if ($throw) self::error("unknown"); + else return false; + } + } elseif ($required) { + if ($throw) throw static::error("nofile"); + else return false; + } + return true; + } + + const _AUTO_PROPERTIES = [ + "tmpName" => "tmp_name", + "fullPath" => "full_path", + ]; + function &__get($name) { + $name = static::_AUTO_PROPERTIES[$name] ?? $name; + return parent::__get($name); + } + + function isError(): bool { + $error = $this->error; + return $error !== UPLOAD_ERR_OK && $error !== UPLOAD_ERR_NO_FILE; + } + + function isValid(): bool { + return $this->error === UPLOAD_ERR_OK; + } + + /** + * retourner true si le nom du fichier a une des extensions de $exts + * + * @param string|array $exts une ou plusieurs extensions qui sont vérifiées + */ + function isExt($exts): bool { + if ($exts === null) return false; + $ext = path::ext($this->name); + $exts = cl::with($exts); + return in_array($ext, $exts); + } + + /** @var ?string chemin du fichier, s'il a été déplacé */ + protected $file; + + function moveTo(string $dest): bool { + if ($this->file === null) { + sh::mkdirof($dest); + $moved = move_uploaded_file($this->tmpName, $dest); + if ($moved) $this->file = $dest; + } else { + $moved = false; + } + return $moved; + } + + function getFile(): string { + return $this->file ?? $this->tmpName; + } + + function getReader(): FileReader { + return new FileReader($this->getFile(), "r+b"); + } +} diff --git a/php/src/os/EOFException.php b/php/src/os/EOFException.php new file mode 100644 index 0000000..c7af63d --- /dev/null +++ b/php/src/os/EOFException.php @@ -0,0 +1,14 @@ +needsStdin = true; + $this->needsTty = true; + $this->sources = null; + $this->vars = null; + $this->cmds = []; + } + + function then($cmd, ?string $input=null, ?string $output=null): Cmd { + if ($this instanceof Cmd) { + $this->add($cmd, $input, $output); + return $this; + } else { + return (new Cmd($this))->add($cmd, $input, $output); + } + } + + function or($cmd, ?string $input=null, ?string $output=null): CmdOr { + if ($this instanceof CmdOr) { + $this->add($cmd, $input, $output); + return $this; + } else { + return (new CmdOr($this))->add($cmd, $input, $output); + } + } + + function and($cmd, ?string $input=null, ?string $output=null): CmdAnd { + if ($this instanceof CmdAnd) { + $this->add($cmd, $input, $output); + return $this; + } else { + return (new CmdAnd($this))->add($cmd, $input, $output); + } + } + + function pipe($cmd): CmdPipe { + if ($this instanceof CmdPipe) { + $this->add($cmd); + return $this; + } else { + return new CmdPipe([$this, $cmd]); + } + } + + function isNeedsStdin(): bool { + return $this->needsStdin; + } + + function setNeedsStdin(bool $needsStdin): void { + $this->needsStdin = $needsStdin; + } + + function isNeedsTty(): bool { + return $this->needsTty; + } + + function setNeedsTty(bool $needsTty): void { + $this->needsTty = $needsTty; + } + + function addSource(?string $source, bool $onlyIfExists=true): void { + if ($source === null) return; + if (!$onlyIfExists || file_exists($source)) { + $source = implode(" ", [".", sh::quote($source)]); + $this->sources[] = $source; + } + } + + function getSources(?string $sep=null): ?string { + if ($this->sources === null) return null; + if ($sep === null) $sep = "\n"; + return implode($sep, $this->sources); + } + + function addLiteralVars($vars, ?string $sep=null): void { + if (cv::z($vars)) return; + if (is_array($vars)) { + if ($sep === null) $sep = "\n"; + $vars = implode($sep, $vars); + } + $this->vars[] = strval($vars); + } + + function addVars(?array $vars): void { + if ($vars === null) return; + foreach ($vars as $name => $value) { + $var = []; + if (!is_array($value)) $var[] = "export "; + A::merge($var, [$name, "=", sh::quote($value)]); + $this->vars[] = implode("", $var); + } + } + + function getVars(?string $sep=null): ?string { + if ($this->vars === null) return null; + if ($sep === null) $sep = "\n"; + return implode($sep, $this->vars); + } + + function addPrefix($prefix): void { + $count = count($this->cmds); + if ($count == 0) return; + $cmd =& $this->cmds[$count - 1]; + if ($cmd instanceof ICmd) { + $cmd->addPrefix($prefix); + } elseif (is_array($prefix)) { + $prefix = sh::join($prefix); + $cmd = "$prefix $cmd"; + } else { + $cmd = "$prefix $cmd"; + } + } + + function addRedir(?string $redir, $output=null, bool $append=false, $input=null): void { + $count = count($this->cmds); + if ($count == 0) return; + + if ($output !== null) $output = escapeshellarg($output); + if ($input !== null) $input = escapeshellarg($input); + if ($redir === "default") $redir = null; + $gt = $append? ">>": ">"; + if ($redir === null) { + $redirs = []; + if ($input !== null) $redirs[] = "<$input"; + if ($output !== null) $redirs[] = "$gt$output"; + if ($redirs) $redir = implode(" ", $redir); + } else { + switch ($redir) { + case "outonly": + case "noerr": + if ($output !== null) $redir = "$gt$output 2>/dev/null"; + else $redir = "2>/dev/null"; + break; + case "erronly": + case "noout": + if ($output !== null) $redir = "2$gt$output >/dev/null"; + else $redir = "2>&1 >/dev/null"; + break; + case "both": + case "err2out": + if ($output !== null) $redir = "$gt$output 2>&1"; + else $redir = "2>&1"; + break; + case "none": + case "null": + $redir = ">/dev/null 2>&1"; + break; + } + } + if ($redir !== null) { + $cmd =& $this->cmds[$count - 1]; + if ($cmd instanceof ICmd) { + $cmd->addRedir($redir); + } else { + $cmd = "$cmd $redir"; + } + } + } + + abstract function getCmd(?string $sep=null, bool $exec=false): string; + + function passthru(int &$retcode=null): bool { + passthru($this->getCmd(), $retcode); + return $retcode == 0; + } + + function system(string &$output=null, int &$retcode=null): bool { + $lastLine = system($this->getCmd(), $retcode); + if ($lastLine !== false) $output = $lastLine; + return $retcode == 0; + } + + function exec(array &$output=null, int &$retcode=null): bool { + exec($this->getCmd(), $output, $retcode); + return $retcode == 0; + } + + function fork_exec(?int &$retcode=null, bool $wait=true): bool { + $cmd = $this->getCmd(null, true); + sh::_fork_exec($cmd, $retcode, $wait); + return $retcode == 0; + } +} diff --git a/php/src/os/proc/AbstractCmdList.php b/php/src/os/proc/AbstractCmdList.php new file mode 100644 index 0000000..25de171 --- /dev/null +++ b/php/src/os/proc/AbstractCmdList.php @@ -0,0 +1,53 @@ +sep = $sep; + $this->add($cmd, $input, $output); + } + + function addLiteral($cmd): self { + A::append_nn($this->cmds, $cmd); + return $this; + } + + function add($cmd, ?string $input=null, ?string $output=null): self { + if ($cmd !== null) { + if (!($cmd instanceof ICmd)) { + sh::verifix_cmd($cmd, null, $input, $output); + } + $this->cmds[] = $cmd; + } + return $this; + } + + function getCmd(?string $sep=null, bool $exec=false): string { + if ($sep === null) $sep = "\n"; + + $actualCmd = []; + A::append_nn($actualCmd, $this->getSources($sep)); + A::append_nn($actualCmd, $this->getVars($sep)); + + $parts = []; + foreach ($this->cmds as $cmd) { + if ($cmd instanceof ICmd) { + $cmd = "(".$cmd->getCmd($sep).")"; + } + $parts[] = $cmd; + } + if (count($parts) == 1 && $exec) $parts[0] = "exec $parts[0]"; + $actualCmd[] = implode($this->sep ?? $sep, $parts); + + return implode($sep, $actualCmd); + } +} diff --git a/php/src/os/proc/Cmd.php b/php/src/os/proc/Cmd.php new file mode 100644 index 0000000..c5d6634 --- /dev/null +++ b/php/src/os/proc/Cmd.php @@ -0,0 +1,19 @@ +add($command); + } + } + $this->input = $input; + $this->output = $output; + } + + function addLiteral($cmd): self { + A::append_nn($this->cmds, $cmd); + return $this; + } + + function add($cmd): self { + if ($cmd !== null) { + if (!($cmd instanceof ICmd)) { + sh::verifix_cmd($cmd); + } + $this->cmds[] = $cmd; + } + return $this; + } + + function setInput(?string $input=null): self { + $this->input = $input; + return $this; + } + + function setOutput(?string $output=null): self { + $this->output = $output; + return $this; + } + + function getCmd(?string $sep=null, bool $exec=false): string { + if ($sep === null) $sep = "\n"; + + $actualCmd = []; + A::append_nn($actualCmd, $this->getSources($sep)); + A::append_nn($actualCmd, $this->getVars($sep)); + + $parts = []; + foreach ($this->cmds as $cmd) { + if ($cmd instanceof ICmd) { + $cmd = "(".$cmd->getCmd($sep).")"; + } + $parts[] = $cmd; + } + $cmd = implode(" | ", $parts); + + $input = $this->input; + $output = $this->output; + if ($input !== null || $output !== null) { + $parts = []; + if ($input !== null) $parts[] = "<".escapeshellarg($input); + $parts[] = $cmd; + if ($output !== null) $parts[] = ">".escapeshellarg($output); + $cmd = implode(" ", $parts); + } + $actualCmd[] = $cmd; + + return implode($sep, $actualCmd); + } +} diff --git a/php/src/os/proc/ICmd.php b/php/src/os/proc/ICmd.php new file mode 100644 index 0000000..2659f66 --- /dev/null +++ b/php/src/os/proc/ICmd.php @@ -0,0 +1,82 @@ + $part) { + $key = self::_quote($key); + $val = self::_quote($part); + $parts[] = "[$key]=$val"; + } + } + return "(".implode(" ", $parts).")"; + } else { + return self::_quote(strval($value)); + } + } + + /** + * obtenir une commande shell à partir du tableau des arguments. + * à utiliser avec exec() + */ + static final function join(array $parts): string { + $count = count($parts); + for($i = 0; $i < $count; $i++) { + $parts[$i] = self::_quote(strval($parts[$i])); + } + return implode(" ", $parts); + } + + private static function add_redir(string &$cmd, ?string $redir, ?string $input, ?string $output): void { + if ($redir !== null) { + switch ($redir) { + case "outonly": + case "noerr": + $redir = "2>/dev/null"; + break; + case "erronly": + case "noout": + $redir = "2>&1 >/dev/null"; + break; + case "both": + case "err2out": + $redir = "2>&1"; + break; + case "none": + case "null": + $redir = ">/dev/null 2>&1"; + break; + case "default": + $redir = null; + break; + } + } + if ($input !== null) { + $redir = $redir !== null? "$redir ": ""; + $redir .= "<".escapeshellarg($input); + } + if ($output !== null) { + $redir = $redir !== null? "$redir ": ""; + $redir .= ">".escapeshellarg($output); + } + if ($redir !== null) $cmd .= " $redir"; + } + + /** + * Corriger la commande $cmd: + * - si c'est tableau, joindre les arguments avec {@link join()} + * - sinon, mettre les caractères en échappement avec {@link escapeshellarg()} + */ + static final function verifix_cmd(&$cmd, ?string $redir=null, ?string $input=null, ?string $output=null): void { + if (is_array($cmd)) $cmd = self::join($cmd); + else $cmd = escapeshellcmd(strval($cmd)); + self::add_redir($cmd, $redir, $input, $output); + } + + /** + * Lancer la commande spécifiée avec passthru() et retourner le code de retour + * dans la variable $retcode. $cmd doit déjà être formaté comme il convient + * + * voici la différence entre system(), passthru() et exec() + * +----------------+-----------------+----------------+----------------+ + * | Command | Displays Output | Can Get Output | Gets Exit Code | + * +----------------+-----------------+----------------+----------------+ + * | passthru() | Yes (raw) | No | Yes | + * | system() | Yes (as text) | Last line only | Yes | + * | exec() | No | Yes (array) | Yes | + * +----------------+-----------------+----------------+----------------+ + * + * @return bool true si la commande s'est lancée sans erreur, false sinon + */ + static final function _passthru(string $cmd, int &$retcode=null): bool { + passthru($cmd, $retcode); + return $retcode == 0; + } + + /** + * Comme {@link _passthru()} mais lancer la commande spécifiée avec system(). + * cf la doc de {@link _passthru()} pour les autres détails + */ + static final function _system(string $cmd, string &$output=null, int &$retcode=null): bool { + $last_line = system($cmd, $retcode); + if ($last_line !== false) $output = $last_line; + return $retcode == 0; + } + + /** + * Comme {@link _passthru()} mais lancer la commande spécifiée avec exec(). + * cf la doc de {@link _passthru()} pour les autres détails + */ + static final function _exec(string $cmd, array &$output=null, int &$retcode=null): bool { + exec($cmd, $output, $retcode); + return $retcode == 0; + } + + static function _waitpid(int $pid, ?int &$retcode=null): bool { + pcntl_waitpid($pid, $status); + if (pcntl_wifexited($status)) $retcode = pcntl_wexitstatus($status); + elseif (pcntl_wifsignaled($status)) $retcode = -pcntl_wtermsig($status); + else $retcode = app::EC_FORK_CHILD; + return $retcode == 0; + } + + /** + * Lancer la commande $cmd dans un processus fils via un shell et attendre la + * fin de son exécution. + * + * $cmd doit déjà être formaté comme il convient + */ + static final function _fork_exec(string $cmd, ?int &$retcode=null, bool $wait=true): bool { + $pid = pcntl_fork(); + if ($pid == -1) { + // parent, impossible de forker + throw new ExitError(app::EC_FORK_PARENT, "unable to fork"); + } elseif ($pid) { + // parent, fork ok + if ($wait) return self::_waitpid($pid, $retcode); + $retcode = null; + return true; + } + // child, fork ok + pcntl_exec("/bin/sh", ["-c", $cmd]); + throw StateException::unexpected_state(); + } + + /** + * Corriger la commande spécifiée avec {@link verifix_cmd()} puis la lancer + * avec passthru() et retourner le code de retour dans la variable $retcode + * + * $redir spécifie le type de redirection demandée: + * - "default" | null: $output reçoit STDOUT et STDERR n'est pas redirigé + * - "outonly" | "noerr": $output ne reçoit que STDOUT et STDERR est perdu + * - "erronly" | "noout": $output ne reçoit que STDERR et STDOUT est perdu + * - "both" | "err2out": $output reçoit STDOUT et STDERR + * - "none" | "null": STDOUT et STDERR sont perdus + * - sinon c'est une redirection spécifique, et la valeur est rajoutée telle + * quelle à la ligne de commande + * + * @return bool true si la commande s'est lancée sans erreur, false sinon + */ + static final function passthru($cmd, int &$retcode=null, ?string $redir=null): bool { + self::verifix_cmd($cmd, $redir); + return self::_passthru($cmd, $retcode); + } + + /** + * Comme {@link passthru()} mais lancer la commande spécifiée avec system(). + * Cf la doc de {@link passthru()} pour les autres détails + */ + static final function system($cmd, string &$output=null, int &$retcode=null, ?string $redir=null): bool { + self::verifix_cmd($cmd, $redir); + return self::_system($cmd, $output, $retcode); + } + + /** + * Comme {@link passthru()} mais lancer la commande spécifiée avec exec(). + * Cf la doc de {@link passthru()} pour les autres détails + */ + static final function exec($cmd, array &$output=null, int &$retcode=null, ?string $redir=null): bool { + self::verifix_cmd($cmd, $redir); + return self::_exec($cmd, $output, $retcode); + } + + /** + * Corriger la commande spécifiée avec {@link verifix_cmd()}, la préfixer de + * "exec" puis la lancer avec {@link _fork_exec()} + */ + static final function fork_exec($cmd, int &$retcode=null, ?string $redir=null): bool { + self::verifix_cmd($cmd, $redir); + return self::_fork_exec("exec $cmd", $retcode); + } + + ############################################################################# + + /** retourner le répertoire $HOME */ + static final function homedir(): string { + $homedir = getenv("HOME"); + if ($homedir === false) { + $homedir = posix_getpwuid(posix_getuid())["dir"]; + } + return path::abspath($homedir); + } + + /** s'assurer que le répertoire $dir existe */ + static final function mkdirp(string $dir): bool { + if (is_dir($dir)) return true; + return mkdir($dir, 0777, true); + } + + /** créer le répertoire qui va contenir le fichier $file */ + static final function mkdirof(string $file): bool { + if (file_exists($file)) return true; + $dir = path::dirname($file); + if (file_exists($dir)) return true; + return mkdir($dir, 0777, true); + } + + /** + * créer un répertoire avec un nom unique. ce répertoire doit être supprimé + * manuellement quand il n'est plus utilisé. + * + * @return string le chemin du répertoire + * @throws IOException si une erreur se produit (impossible de créer un + * répertoire unique après 2560 essais) + */ + static final function mktempdir(?string $prefix=null, ?string $basedir=null): string { + if ($basedir === null) $basedir = sys_get_temp_dir(); + if ($prefix !== null) $prefix .= "-"; + $max = 2560; + do { + $dir = "$basedir/$prefix".uniqid(); + $r = @mkdir($dir); + $max--; + } while ($r === false && $max > 0); + if ($r === false) { + throw IOException::last_error("$dir: unable to create directory"); + } + return $dir; + } + + /** + * Supprimer un répertoire créé avec mktempdir + * + * un minimum de vérification est effectué qu'il s'agit bien d'un répertoire + * généré par mktempdir + */ + static final function rmtempdir(string $tmpdir, ?string $prefix=null, ?string $basedir=null): void { + if ($basedir === null) $basedir = sys_get_temp_dir(); + if ($prefix !== null) $prefix .= "-"; + // 13 '?' parce que c'est la taille d'une chaine générée par uniqid() + if (fnmatch("$basedir/$prefix?????????????", $tmpdir)) { + self::exec(["rm", "-rf", $tmpdir]); + } else { + throw new IOException("$tmpdir: n'est pas un répertoire temporaire"); + } + } + + /** + * supprimer tous les répertoires temporaires qui ont été créés avec le + * suffixe spécifié dans le répertoire $basedir + */ + static final function cleantempdirs(string $prefix, ?string $basedir=null): void { + if ($basedir === null) $basedir = sys_get_temp_dir(); + $prefix .= "-"; + // 13 '?' parce que c'est la taille d'une chaine générée par uniqid() + $tmpdirs = glob("$basedir/$prefix?????????????", GLOB_ONLYDIR); + if ($tmpdirs) { + self::exec(["rm", "-rf", ...$tmpdirs]); + } + } + + static final function is_diff_file(string $f, string $g): bool { + if (!is_file($f) || !is_file($g)) return true; + self::exec(array("diff", "-q", $f, $g), $output, $retcode); + return $retcode !== 0; + } + + static final function is_same_file(string $f, string $g): bool { + if (!is_file($f) || !is_file($g)) return false; + self::exec(array("diff", "-q", $f, $g), $output, $retcode); + return $retcode === 0; + } + + static final function is_diff_link(string $f, string $g): bool { + if (!is_link($f) || !is_link($g)) return true; + return @readlink($f) !== @readlink($g); + } + + static final function is_same_link(string $f, string $g): bool { + if (!is_link($f) || !is_link($g)) return false; + return @readlink($f) === @readlink($g); + } + + ############################################################################# + + static function ls_all(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array { + $all = scandir($dir, $sorting_order); + if ($all === false) return []; + return array_values(array_filter($all, + function ($file) use ($pattern) { + if ($file === "." || $file === "..") return false; + return $pattern === null || fnmatch($pattern, $file); + } + )); + } + + static function ls_dirs(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array { + return array_values(array_filter(self::ls_all($dir, $pattern, $sorting_order), + function ($file) use ($dir) { + return path::is_dir(path::join($dir, $file)); + } + )); + } + + static function ls_files(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array { + return array_values(array_filter(self::ls_all($dir, $pattern, $sorting_order), + function ($file) use ($dir) { + return path::is_file(path::join($dir, $file)); + } + )); + } + + static function ls_pall(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array { + return array_map(function(string $name) use ($dir) { + return path::join($dir, $name); + }, self::ls_all($dir, $pattern, $sorting_order)); + } + + static function ls_pdirs(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array { + return array_map(function(string $name) use ($dir) { + return path::join($dir, $name); + }, self::ls_dirs($dir, $pattern, $sorting_order)); + } + + static function ls_pfiles(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array { + return array_map(function(string $name) use ($dir) { + return path::join($dir, $name); + }, self::ls_files($dir, $pattern, $sorting_order)); + } +} diff --git a/php/src/output/IMessenger.php b/php/src/output/IMessenger.php new file mode 100644 index 0000000..7b8fdec --- /dev/null +++ b/php/src/output/IMessenger.php @@ -0,0 +1,107 @@ + ou renommer `say` en `console`, et `ui` en `say` + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/php/src/output/_messenger.php b/php/src/output/_messenger.php new file mode 100644 index 0000000..1226c24 --- /dev/null +++ b/php/src/output/_messenger.php @@ -0,0 +1,74 @@ +clone($params); + } + + static final function __callStatic($name, $args) { + $name = str::us2camel($name); + call_user_func_array([static::get(), $name], $args); + } + + ############################################################################# + + const DEBUG = IMessenger::DEBUG; + const MINOR = IMessenger::MINOR; + const NORMAL = IMessenger::NORMAL; + const MAJOR = IMessenger::MAJOR; + const NONE = IMessenger::NONE; + + static function reset_params(?array $params=null): void { static::get()->resetParams($params); } + static function section($content, ?callable $func=null, ?int $level=null): void { static::get()->section($content, $func, $level); } + static function title($content, ?callable $func=null, ?int $level=null): void { static::get()->title($content, $func, $level); } + static function desc($content, ?int $level=null): void { static::get()->desc($content, $level); } + static function action($content, ?callable $func=null, ?int $level=null): void { static::get()->action($content, $func, $level); } + static function step($content, ?int $level=null): void { static::get()->step($content, $level); } + static function asuccess($content=null, ?int $override_level=null): void { static::get()->asuccess($content, $override_level); } + static function afailure($content=null, ?int $override_level=null): void { static::get()->afailure($content, $override_level); } + static function adone($content=null, ?int $override_level=null): void { static::get()->adone($content, $override_level); } + static function aresult($result=null, ?int $override_level=null): void { static::get()->aresult($result, $override_level); } + static function print($content, ?int $level=null): void { static::get()->print($content, $level); } + static function info($content, ?int $level=null): void { static::get()->info($content, $level); } + static function note($content, ?int $level=null): void { static::get()->note($content, $level); } + static function warning($content, ?int $level=null): void { static::get()->warning($content, $level); } + static function error($content, ?int $level=null): void { static::get()->error($content, $level); } + static function end(bool $all=false): void { static::get()->end($all); } + + static function debug($content): void { self::info($content, self::DEBUG);} + static function normal($content): void { self::info($content, self::NORMAL);} + static function minor($content): void { self::info($content, self::MINOR);} + static function important($content): void { self::info($content, self::MAJOR);} + static function attention($content): void { self::note($content, self::MAJOR);} + static function critwarning($content): void { self::warning($content, self::MAJOR);} + static function criterror($content): void { self::error($content, self::MAJOR);} +} diff --git a/php/src/output/console.php b/php/src/output/console.php new file mode 100644 index 0000000..0ef8c81 --- /dev/null +++ b/php/src/output/console.php @@ -0,0 +1,28 @@ +resetParams($params); + return self::$out; + } + + static function write(...$values): void { self::$out->write(...$values); } + static function print(...$values): void { self::$out->print(...$values); } + + static function iwrite(int $indentLevel, ...$values): void { self::$out->iwrite($indentLevel, ...$values); } + static function iprint(int $indentLevel, ...$values): void { self::$out->iprint($indentLevel, ...$values); } +} +out::reset(); diff --git a/php/src/output/say.php b/php/src/output/say.php new file mode 100644 index 0000000..9d8b6d0 --- /dev/null +++ b/php/src/output/say.php @@ -0,0 +1,28 @@ +msgs = []; + foreach ($msgs as $msg) { + if ($msg !== null) $this->msgs[] = $msg; + } + } + + /** @var IMessenger[] */ + protected $msgs; + + function resetParams(?array $params=null): void { foreach ($this->msgs as $msg) { $msg->resetParams($params); } } + function clone(?array $params=null): self { + $clone = clone $this; + foreach ($clone->msgs as &$msg) { + $msg = $msg->clone($params); + }; unset($msg); + return $clone; + } + function section($content, ?callable $func=null, ?int $level=null): void { + $useFunc = false; + foreach ($this->msgs as $msg) { + $msg->section($content, null, $level); + if ($msg instanceof _IMessenger) $useFunc = true; + } + if ($useFunc && $func !== null) { + try { + $func($this); + } finally { + /** @var _IMessenger $msg */ + foreach ($this->msgs as $msg) { + $msg->_endSection(); + } + } + } + } + function title($content, ?callable $func=null, ?int $level=null): void { + $useFunc = false; + $untils = []; + foreach ($this->msgs as $msg) { + if ($msg instanceof _IMessenger) { + $useFunc = true; + $untils[] = $msg->_getTitleMark(); + } + $msg->title($content, null, $level); + } + if ($useFunc && $func !== null) { + try { + $func($this); + } finally { + /** @var _IMessenger $msg */ + $index = 0; + foreach ($this->msgs as $msg) { + if ($msg instanceof _IMessenger) { + $msg->_endTitle($untils[$index++]); + } + } + } + } + } + function desc($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->desc($content, $level); } } + function action($content, ?callable $func=null, ?int $level=null): void { + $useFunc = false; + $untils = []; + foreach ($this->msgs as $msg) { + if ($msg instanceof _IMessenger) { + $useFunc = true; + $untils[] = $msg->_getActionMark(); + } + $msg->action($content, null, $level); + } + if ($useFunc && $func !== null) { + try { + $result = $func($this); + /** @var _IMessenger $msg */ + $index = 0; + foreach ($this->msgs as $msg) { + if ($msg->_getActionMark() > $untils[$index++]) { + $msg->aresult($result); + } + } + } catch (Exception $e) { + /** @var _IMessenger $msg */ + foreach ($this->msgs as $msg) { + $msg->afailure($e); + } + throw $e; + } finally { + /** @var _IMessenger $msg */ + $index = 0; + foreach ($this->msgs as $msg) { + $msg->_endAction($untils[$index++]); + } + } + } + } + function step($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->step($content, $level); } } + function asuccess($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->asuccess($content, $overrideLevel); } } + function afailure($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->afailure($content, $overrideLevel); } } + function adone($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->adone($content, $overrideLevel); } } + function aresult($result=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->aresult($result, $overrideLevel); } } + function print($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->print($content, $level); } } + function info($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->info($content, $level); } } + function note($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->note($content, $level); } } + function warning($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->warning($content, $level); } } + function error($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->error($content, $level); } } + function end(bool $all=false): void { foreach ($this->msgs as $msg) { $msg->end($all); } } +} diff --git a/php/src/output/std/StdMessenger.php b/php/src/output/std/StdMessenger.php new file mode 100644 index 0000000..3892a0d --- /dev/null +++ b/php/src/output/std/StdMessenger.php @@ -0,0 +1,722 @@ + self::DEBUG, + "minor" => self::MINOR, "verbose" => self::MINOR, + "normal" => self::NORMAL, + "major" => self::MAJOR, "quiet" => self::MAJOR, + "none" => self::NONE, "silent" => self::NONE, + ]; + + protected static function verifix_level($level, int $max_level=self::MAX_LEVEL): int { + if (!in_array($level, self::VALID_LEVELS, true)) { + $level = cl::get(self::LEVEL_MAP, $level, $level); + } + if (!in_array($level, self::VALID_LEVELS, true)) { + throw new Exception("$level: invalid level"); + } + if ($level > $max_level) { + throw new Exception("$level: level not allowed here"); + } + return $level; + } + + const GENERIC_PREFIXES = [ + self::MAJOR => [ + "section" => [true, "SECTION!", "===", "=", "=", "==="], + "title" => [false, "TITLE!", null, "T", "", "==="], + "desc" => ["DESC!", ">", ""], + "error" => ["CRIT.ERROR!", "E!", ""], + "warning" => ["CRIT.WARNING!", "W!", ""], + "note" => ["ATTENTION!", "N!", ""], + "info" => ["IMPORTANT!", "N!", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + self::NORMAL => [ + "section" => [true, "SECTION:", "---", "-", "-", "---"], + "title" => [false, "TITLE:", null, "T", "", "---"], + "desc" => ["DESC:", ">", ""], + "error" => ["ERROR:", "E", ""], + "warning" => ["WARNING:", "W", ""], + "note" => ["NOTE:", "N", ""], + "info" => ["INFO:", "I", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + self::MINOR => [ + "section" => [true, "section", null, ">>", "<<", null], + "title" => [false, "title", null, "t", "", null], + "desc" => ["desc", ">", ""], + "error" => ["error", "E", ""], + "warning" => ["warning", "W", ""], + "note" => ["note", "N", ""], + "info" => ["info", "I", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + self::DEBUG => [ + "section" => [true, "section", null, ">>", "<<", null], + "title" => [false, "title", null, "t", "", null], + "desc" => ["desc", ">", ""], + "error" => ["debugE", "e", ""], + "warning" => ["debugW", "w", ""], + "note" => ["debugN", "i", ""], + "info" => ["debug", "D", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + ]; + + const RESULT_PREFIXES = [ + "failure" => ["(FAILURE)", ""], + "success" => ["(SUCCESS)", ""], + "done" => [null, null], + ]; + + function __construct(?array $params=null) { + $output = cl::get($params, "output"); + $color = cl::get($params, "color"); + $indent = cl::get($params, "indent", static::INDENT); + + $defaultLevel = cl::get($params, "default_level"); + if ($defaultLevel === null) $defaultLevel = self::NORMAL; + $defaultLevel = self::verifix_level($defaultLevel); + + $debug = boolval(cl::get($params, "debug")); + $minLevel = cl::get($params, "min_level"); + if ($minLevel === null && $debug) $minLevel = self::DEBUG; + if ($minLevel === null) $minLevel = cl::get($params, "verbosity"); # alias + if ($minLevel === null) $minLevel = self::NORMAL; + $minLevel = self::verifix_level($minLevel, self::NONE); + + $addDate = boolval(cl::get($params, "add_date")); + $dateFormat = cl::get($params, "date_format", static::DATE_FORMAT); + $id = cl::get($params, "id"); + + $params = [ + "color" => $color, + "indent" => $indent, + ]; + if ($output !== null) { + $this->err = $this->out = new StdOutput($output, $params); + } else { + $this->out = new StdOutput(STDOUT, $params); + $this->err = new StdOutput(STDERR, $params); + } + $this->defaultLevel = $defaultLevel; + $this->minLevel = $minLevel; + $this->addDate = $addDate; + $this->dateFormat = $dateFormat; + $this->id = $id; + $this->inSection = false; + $this->titles = []; + $this->actions = []; + } + + function resetParams(?array $params=null): void { + $output = cl::get($params, "output"); + $color = cl::get($params, "color"); + $indent = cl::get($params, "indent"); + + $defaultLevel = cl::get($params, "default_level"); + if ($defaultLevel !== null) $defaultLevel = self::verifix_level($defaultLevel); + + $debug = cl::get($params, "debug"); + $minLevel = cl::get($params, "min_level"); + if ($minLevel === null && $debug !== null) $minLevel = $debug? self::DEBUG: self::NORMAL; + if ($minLevel === null) $minLevel = cl::get($params, "verbosity"); # alias + if ($minLevel !== null) $minLevel = self::verifix_level($minLevel, self::NONE); + + $addDate = cl::get($params, "add_date"); + $dateFormat = cl::get($params, "date_format"); + $id = cl::get($params, "id"); + + $params = [ + "output" => $output, + "color" => $color, + "indent" => $indent, + ]; + if ($this->out === $this->err) { + $this->out->resetParams($params); + } else { + # NB: si initialement [output] était null, et qu'on spécifie une valeur + # [output], alors les deux instances $out et $err sont mis à jour + # séparément avec la même valeur de output + # de plus, on ne peut plus revenir à la situation initiale avec une + # destination différente pour $out et $err + $this->out->resetParams($params); + $this->err->resetParams($params); + } + if ($defaultLevel !== null) $this->defaultLevel = $defaultLevel; + if ($minLevel !== null) $this->minLevel = $minLevel; + if ($addDate !== null) $this->addDate = boolval($addDate); + if ($dateFormat !== null) $this->dateFormat = $dateFormat; + if ($id !== null) $this->id = $id; + } + + function clone(?array $params=null): IMessenger { + $clone = clone $this; + if ($params !== null) $clone->resetParams($params); + #XXX faut-il marquer la section et les titres du clone à "print" => false? + # ou en faire des références au parent? + # dans tous les cas, on considère qu'il n'y a pas d'actions en cours, et on + # ne doit pas dépiler avec end() plus que l'état que l'on a eu lors du clone + return $clone; + } + + /** @var StdOutput la sortie standard */ + protected $out; + + /** @var StdOutput la sortie d'erreur */ + protected $err; + + /** @var int level par défaut dans lequel les messages sont affichés */ + protected $defaultLevel; + + /** @var int level minimum que doivent avoir les messages pour être affichés */ + protected $minLevel; + + /** @var bool faut-il ajouter la date à chaque ligne? */ + protected $addDate; + + /** @var string format de la date */ + protected $dateFormat; + + /** @var ?string identifiant de ce messenger, à ajouter à chaque ligne */ + protected $id; + + protected function getLinePrefix(): ?string { + $linePrefix = null; + if ($this->addDate) { + $date = date_create()->format($this->dateFormat); + $linePrefix .= "$date "; + } + if ($this->id !== null) { + $linePrefix .= "$this->id "; + } + return $linePrefix; + } + + protected function decrLevel(int $level, int $amount=-1): int { + $level += $amount; + if ($level < self::MIN_LEVEL) $level = self::MIN_LEVEL; + return $level; + } + + protected function checkLevel(?int &$level): bool { + if ($level === null) $level = $this->defaultLevel; + elseif ($level < 0) $level = $this->decrLevel($this->defaultLevel, $level); + return $level >= $this->minLevel; + } + + protected function getIndentLevel(bool $withActions=true): int { + $indentLevel = count($this->titles) - 1; + if ($indentLevel < 0) $indentLevel = 0; + if ($withActions) { + foreach ($this->actions as $action) { + if ($action["level"] < $this->minLevel) continue; + $indentLevel++; + } + } + return $indentLevel; + } + + protected function _printTitle(?string $linePrefix, int $level, + string $type, $content, + int $indentLevel, StdOutput $out): void { + $prefixes = self::GENERIC_PREFIXES[$level][$type]; + if ($prefixes[0]) $out->print(); + $content = cl::with($content); + if ($out->isColor()) { + $before = $prefixes[2]; + $prefix = $prefixes[3]; + $prefix2 = $prefix !== null? "$prefix ": null; + $suffix = $prefixes[4]; + $suffix2 = $suffix !== null? " $suffix": null; + $after = $prefixes[5]; + + $lines = $out->getLines(false, ...$content); + $maxlen = 0; + foreach ($lines as &$content) { + $line = $out->filterColors($content); + $len = mb_strlen($line); + if ($len > $maxlen) $maxlen = $len; + $content = [$content, $len]; + }; unset($content); + if ($before !== null) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, substr($before, 1), str_repeat($before[0], $maxlen), $suffix); + } + foreach ($lines as [$content, $len]) { + if ($linePrefix !== null) $out->write($linePrefix); + $padding = $len < $maxlen? str_repeat(" ", $maxlen - $len): null; + $out->iprint($indentLevel, $prefix2, $content, $padding, $suffix2); + } + if ($after !== null) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, substr($after, 1), str_repeat($after[0], $maxlen), $suffix); + } + } else { + $prefix = $prefixes[1]; + if ($prefix !== null) $prefix .= " "; + $prefix2 = str_repeat(" ", mb_strlen($prefix)); + $lines = $out->getLines(false, ...$content); + foreach ($lines as $content) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, $content); + $prefix = $prefix2; + } + } + } + + protected function _printAction(?string $linePrefix, int $level, + bool $printContent, $content, + bool $printResult, ?bool $rsuccess, $rcontent, + int $indentLevel, StdOutput $out): void { + $color = $out->isColor(); + if ($rsuccess === true) $type = "success"; + elseif ($rsuccess === false) $type = "failure"; + else $type = "done"; + $rprefixes = self::RESULT_PREFIXES[$type]; + if ($color) { + $rprefix = $rprefixes[1]; + $rprefix2 = null; + if ($rprefix !== null) { + $rprefix .= " "; + $rprefix2 = $out->filterColors($out->filterContent($rprefix)); + $rprefix2 = str_repeat(" ", mb_strlen($rprefix2)); + } + } else { + $rprefix = $rprefixes[0]; + if ($rprefix !== null) $rprefix .= " "; + $rprefix2 = str_repeat(" ", mb_strlen($rprefix)); + } + if ($printContent && $printResult) { + A::ensure_array($content); + if ($rcontent) { + $content[] = ": "; + $content[] = $rcontent; + } + $lines = $out->getLines(false, ...$content); + foreach ($lines as $content) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $rprefix, $content); + $rprefix = $rprefix2; + } + } elseif ($printContent) { + $prefixes = self::GENERIC_PREFIXES[$level]["step"]; + if ($color) { + $prefix = $prefixes[1]; + if ($prefix !== null) $prefix .= " "; + $prefix2 = $out->filterColors($out->filterContent($prefix)); + $prefix2 = str_repeat(" ", mb_strlen($prefix2)); + $suffix = $prefixes[2]; + } else { + $prefix = $prefixes[0]; + if ($prefix !== null) $prefix .= " "; + $prefix2 = str_repeat(" ", mb_strlen($prefix)); + $suffix = null; + } + A::ensure_array($content); + $content[] = ":"; + $lines = $out->getLines(false, ...$content); + foreach ($lines as $content) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, $content, $suffix); + $prefix = $prefix2; + } + } elseif ($printResult) { + if (!$rcontent) { + if ($type === "success") $rcontent = $color? "succès": ""; + elseif ($type === "failure") $rcontent = $color? "échec": ""; + elseif ($type === "done") $rcontent = "fait"; + } + $rprefix = " $rprefix"; + $rprefix2 = " $rprefix2"; + $lines = $out->getLines(false, $rcontent); + foreach ($lines as $rcontent) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $rprefix, $rcontent); + $rprefix = $rprefix2; + } + } + } + + protected function _printGeneric(?string $linePrefix, int $level, + string $type, $content, + int $indentLevel, StdOutput $out): void { + $prefixes = self::GENERIC_PREFIXES[$level][$type]; + $content = cl::with($content); + if ($out->isColor()) { + $prefix = $prefixes[1]; + $prefix2 = null; + if ($prefix !== null) { + $prefix .= " "; + $prefix2 = $out->filterColors($out->filterContent($prefix)); + $prefix2 = str_repeat(" ", mb_strlen($prefix2)); + } + $suffix = $prefixes[2]; + $lines = $out->getLines(false, ...$content); + foreach ($lines as $content) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, $content, $suffix); + $prefix = $prefix2; + } + } else { + $prefix = $prefixes[0]; + if ($prefix !== null) $prefix .= " "; + $prefix2 = str_repeat(" ", mb_strlen($prefix)); + $lines = $out->getLines(false, ...$content); + foreach ($lines as $content) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, $content); + $prefix = $prefix2; + } + } + } + + protected function _printGenericOrException(?int $level, string $type, $content, int $indentLevel, StdOutput $out): void { + $linePrefix = $this->getLinePrefix(); + # si $content contient des exceptions, les afficher avec un level moindre + $exceptions = null; + if (is_array($content)) { + $valueContent = null; + foreach ($content as $value) { + if ($value instanceof Throwable || $value instanceof ExceptionShadow) { + $exceptions[] = $value; + } else { + $valueContent[] = $value; + } + } + if ($valueContent === null) $content = null; + elseif (count($valueContent) == 1) $content = $valueContent[0]; + else $content = $valueContent; + } elseif ($content instanceof Throwable || $content instanceof ExceptionShadow) { + $exceptions[] = $content; + $content = null; + } + + $printActions = true; + $showContent = $this->checkLevel($level); + if ($content !== null && $showContent) { + $this->printActions(); $printActions = false; + $this->_printGeneric($linePrefix, $level, $type, $content, $indentLevel, $out); + } + if ($exceptions !== null) { + $level1 = $this->decrLevel($level); + $showTraceback = $this->checkLevel($level1); + foreach ($exceptions as $exception) { + # tout d'abord userMessage + if ($exception instanceof UserException) { + $userMessage = UserException::get_user_message($exception); + $showSummary = true; + } else { + $userMessage = UserException::get_summary($exception); + $showSummary = false; + } + if ($userMessage !== null && $showContent) { + if ($printActions) { $this->printActions(); $printActions = false; } + $this->_printGeneric($linePrefix, $level, $type, $userMessage, $indentLevel, $out); + } + # puis summary et traceback + if ($showTraceback) { + if ($printActions) { $this->printActions(); $printActions = false; } + if ($showSummary) { + $summary = UserException::get_summary($exception); + $this->_printGeneric($linePrefix, $level1, $type, $summary, $indentLevel, $out); + } + $traceback = UserException::get_traceback($exception); + $this->_printGeneric($linePrefix, $level1, $type, $traceback, $indentLevel, $out); + } + } + } + } + + /** @var bool est-on dans une section? */ + protected $inSection; + + /** @var array section qui est en attente d'affichage */ + protected $section; + + function section($content, ?callable $func=null, ?int $level=null): void { + $this->_endSection(); + $this->inSection = true; + if (!$this->checkLevel($level)) return; + $this->section = [ + "line_prefix" => $this->getLinePrefix(), + "level" => $level, + "content" => $content, + "print_content" => true, + ]; + if ($func !== null) { + try { + $func($this); + } finally { + $this->_endSection(); + } + } + } + + protected function printSection() { + $section =& $this->section; + if ($section !== null && $section["print_content"]) { + $this->_printTitle( + $section["line_prefix"], $section["level"], + "section", $section["content"], + 0, $this->err); + $section["print_content"] = false; + } + } + + function _endSection(): void { + $this->inSection = false; + $this->section = null; + } + + /** @var array */ + protected $titles; + + /** @var array */ + protected $title; + + function _getTitleMark(): int { + return count($this->titles); + } + + function title($content, ?callable $func=null, ?int $level=null): void { + if (!$this->checkLevel($level)) return; + $until = $this->_getTitleMark(); + $this->titles[] = [ + "line_prefix" => $this->getLinePrefix(), + "level" => $level, + "content" => $content, + "print_content" => true, + "descs" => [], + "print_descs" => false, + ]; + $this->title =& $this->titles[$until]; + if ($func !== null) { + try { + $func($this); + } finally { + $this->_endTitle($until); + } + } + } + + function desc($content, ?int $level=null): void { + if (!$this->checkLevel($level)) return; + $title =& $this->title; + $title["descs"][] = [ + "line_prefix" => $this->getLinePrefix(), + "level" => $level, + "content" => $content, + ]; + $title["print_descs"] = true; + } + + protected function printTitles(): void { + $this->printSection(); + $err = $this->err; + $indentLevel = 0; + foreach ($this->titles as &$title) { + if ($title["print_content"]) { + $this->_printTitle( + $title["line_prefix"], $title["level"], + "title", $title["content"], + $indentLevel, $err); + $title["print_content"] = false; + } + if ($title["print_descs"]) { + foreach ($title["descs"] as $desc) { + $this->_printGeneric( + $desc["line_prefix"], $desc["level"], + "desc", $desc["content"], + $indentLevel, $err); + } + $title["descs"] = []; + $title["print_descs"] = false; + } + $indentLevel++; + }; unset($title); + } + + function _endTitle(?int $until=null): void { + if ($until === null) $until = $this->_getTitleMark() - 1; + while (count($this->titles) > $until) { + array_pop($this->titles); + } + if ($this->titles) { + $this->title =& $this->titles[count($this->titles) - 1]; + } else { + $this->titles = []; + unset($this->title); + } + } + + /** @var array */ + protected $actions; + + /** @var array */ + protected $action; + + function _getActionMark(): int { + return count($this->actions); + } + + function action($content, ?callable $func=null, ?int $level=null): void { + $this->checkLevel($level); + $until = $this->_getActionMark(); + $this->actions[] = [ + "line_prefix" => $this->getLinePrefix(), + "level" => $level, + "content" => $content, + "print_content" => true, + "result_success" => null, + "result_content" => null, + ]; + $this->action =& $this->actions[$until]; + if ($func !== null) { + try { + $result = $func($this); + if ($this->_getActionMark() > $until) { + $this->aresult($result); + } + } catch (Exception $e) { + $this->afailure($e); + throw $e; + } finally { + $this->_endAction($until); + } + } + } + + function printActions(bool $endAction=false, ?int $overrideLevel=null): void { + $this->printTitles(); + $err = $this->err; + $indentLevel = $this->getIndentLevel(false); + $lastIndex = count($this->actions) - 1; + $index = 0; + foreach ($this->actions as &$action) { + $mergeResult = $index++ == $lastIndex && $endAction; + $linePrefix = $action["line_prefix"]; + $level = $overrideLevel?? $action["level"]; + $content = $action["content"]; + $printContent = $action["print_content"]; + $rsuccess = $action["result_success"]; + $rcontent = $action["result_content"]; + if ($level < $this->minLevel) continue; + if ($mergeResult) { + $this->_printAction( + $linePrefix, $level, + $printContent, $content, + true, $rsuccess, $rcontent, + $indentLevel, $err); + } elseif ($printContent) { + $this->_printAction( + $linePrefix, $level, + $printContent, $content, + false, $rsuccess, $rcontent, + $indentLevel, $err); + $action["print_content"] = false; + } + $indentLevel++; + }; unset($action); + if ($endAction) $this->_endAction(); + } + + function step($content, ?int $level=null): void { + $this->_printGenericOrException($level, "step", $content, $this->getIndentLevel(), $this->err); + } + + function asuccess($content=null, ?int $overrideLevel=null): void { + if (!$this->actions) $this->action(null); + $this->action["result_success"] = true; + $this->action["result_content"] = $content; + $this->printActions(true, $overrideLevel); + } + + function afailure($content=null, ?int $overrideLevel=null): void { + if (!$this->actions) $this->action(null); + $this->action["result_success"] = false; + $this->action["result_content"] = $content; + $this->printActions(true, $overrideLevel); + } + + function adone($content=null, ?int $overrideLevel=null): void { + if (!$this->actions) $this->action(null); + $this->action["result_success"] = null; + $this->action["result_content"] = $content; + $this->printActions(true, $overrideLevel); + } + + function aresult($result=null, ?int $overrideLevel=null): void { + if (!$this->actions) $this->action(null); + if ($result === true) $this->asuccess(null, $overrideLevel); + elseif ($result === false) $this->afailure(null, $overrideLevel); + elseif ($result instanceof Exception) $this->afailure($result, $overrideLevel); + else $this->adone($result, $overrideLevel); + } + + function _endAction(?int $until=null): void { + if ($until === null) $until = $this->_getActionMark() - 1; + while (count($this->actions) > $until) { + array_pop($this->actions); + } + if ($this->actions) { + $this->action =& $this->actions[count($this->actions) - 1]; + } else { + $this->actions = []; + unset($this->action); + } + } + + function print($content, ?int $level=null): void { + $this->_printGenericOrException($level, "print", $content, $this->getIndentLevel(), $this->out); + } + + function info($content, ?int $level=null): void { + $this->_printGenericOrException($level, "info", $content, $this->getIndentLevel(), $this->err); + } + + function note($content, ?int $level=null): void { + $this->_printGenericOrException($level, "note", $content, $this->getIndentLevel(), $this->err); + } + + function warning($content, ?int $level=null): void { + $this->_printGenericOrException($level, "warning", $content, $this->getIndentLevel(), $this->err); + } + + function error($content, ?int $level=null): void { + $this->_printGenericOrException($level, "error", $content, $this->getIndentLevel(), $this->err); + } + + function end(bool $all=false): void { + if ($all) { + while ($this->actions) $this->adone(); + while ($this->titles) $this->_endTitle(); + $this->_endSection(); + } elseif ($this->actions) { + $this->_endAction(); + } elseif ($this->titles) { + $this->_endTitle(); + } else { + $this->_endSection(); + } + } +} diff --git a/php/src/output/std/StdOutput.php b/php/src/output/std/StdOutput.php new file mode 100644 index 0000000..c30c466 --- /dev/null +++ b/php/src/output/std/StdOutput.php @@ -0,0 +1,248 @@ + "0", + "bold" => "1", + "faint" => "2", + "underlined" => "4", + "reverse" => "7", + "normal" => "22", + "black" => "30", + "red" => "31", + "green" => "32", + "yellow" => "33", + "blue" => "34", + "magenta" => "35", + "cyan" => "36", + "white" => "37", + "default" => "39", + "black-bg" => "40", + "red-bg" => "41", + "green-bg" => "42", + "yellow-bg" => "43", + "blue-bg" => "44", + "magenta-bg" => "45", + "cyan-bg" => "46", + "white-bg" => "47", + "default-bg" => "49", + ]; + + const COLOR_MAP = [ + "z" => "reset", + "o" => "black", + "r" => "red", + "g" => "green", + "y" => "yellow", + "b" => "blue", + "m" => "magenta", + "c" => "cyan", + "w" => "white", + "O" => "black_bg", + "R" => "red_bg", + "G" => "green_bg", + "Y" => "yellow_bg", + "B" => "blue_bg", + "M" => "magenta_bg", + "C" => "cyan_bg", + "W" => "white_bg", + "@" => "bold", + "-" => "faint", + "_" => "underlined", + "~" => "reverse", + "n" => "normal", + ]; + + /** + * @param resource|null $outf + * @throws Exception si la destination est un fichier et que son ouverture a + * échoué + */ + function __construct($output=null, ?array $params=null) { + if ($output !== null) $params["output"] = $output; + elseif (!isset($params["output"])) $params["output"] = STDOUT; + if (!isset($params["filter_tags"])) $params["filter_tags"] = true; + if (!isset($params["indent"])) $params["indent"] = " "; + $this->resetParams($params); + } + + function resetParams(?array $params=null): void { + $output = cl::get($params, "output"); + $maskErrors = null; + $color = cl::get($params, "color"); + $filterTags = cl::get($params, "filter_tags"); + $indent = cl::get($params, "indent"); + $flush = cl::get($params, "flush"); + + if ($output instanceof Stream) $output = $output->getResource(); + if ($output !== null) { + if ($output === "php://stdout") { + $outf = STDOUT; + } elseif ($output === "php://stderr") { + $outf = STDERR; + } elseif (!is_resource($output)) { + # si $outf est un nom de fichier, vérifier que l'ouverture se fait sans + # erreur. à partir de là, plus aucune gestion d'erreur n'est faite, à + # part afficher les erreurs d'écriture la première fois qu'elles se + # produisent + $maskErrors = false; + $outf = @fopen($output, "ab"); + if ($outf === false) { + $error = error_get_last(); + if ($error !== null) $message = $error["message"]; + else $message = "$output: open error"; + throw new Exception($message); + } + if ($flush === null) $flush = true; + } else { + $outf = $output; + } + $this->outf = $outf; + $this->maskErrors = $maskErrors; + if ($color === null) $color = stream_isatty($outf); + if ($flush === null) $flush = false; + } + if ($color !== null) $this->color = boolval($color); + if ($filterTags !== null) $this->filterTags = boolval($filterTags); + if ($indent !== null) $this->indent = strval($indent); + if ($flush !== null) $this->flush = boolval($flush); + } + + /** @var resource */ + protected $outf; + + /** @var bool faut-il masquer les erreurs d'écriture? */ + protected $maskErrors; + + /** @var bool faut-il autoriser la sortie en couleur? */ + protected $color; + + function isColor(): bool { + return $this->color; + } + + /** @var bool faut-il enlever les tags dans la sortie? */ + protected $filterTags; + + /** @var string indentation unitaire */ + protected $indent; + + /** @var bool faut-il flush le fichier après l'écriture de chaque ligne */ + protected $flush; + + function isatty(): bool { + return stream_isatty($this->outf); + } + + private static function replace_colors(array $ms): string { + $colors = []; + foreach (preg_split('/\s+/', $ms[1]) as $color) { + while ($color && !cl::has(self::COLORS, $color)) { + $alias = substr($color, 0, 1); + $colors[] = self::COLOR_MAP[$alias]; + $color = substr($color, 1); + } + if ($color) $colors[] = $color; + } + $text = "\x1B["; + $first = true; + foreach ($colors as $color) { + if (!$color) continue; + if ($first) $first = false; + else $text .= ";"; + $text .= self::COLORS[$color]; + } + $text .= "m"; + return $text; + } + function filterContent(string $text): string { + # couleur au début + $text = preg_replace_callback('/]*)>/', [self::class, "replace_colors"], $text); + # reset à la fin + $text = preg_replace('/<\/color>/', "\x1B[0m", $text); + # enlever les tags classiques + if ($this->filterTags) { + $text = preg_replace('/<[^>]*>/', "", $text); + } + return $text; + } + function filterColors(string $text): string { + return preg_replace('/\x1B\[.*?m/', "", $text); + } + + function getIndent(int $indentLevel): string { + return str_repeat($this->indent, $indentLevel); + } + + function getLines(bool $withNl, ...$values): array { + $values = c::resolve($values, null, false); + if (!$values) return []; + $text = c::to_string($values, false); + if ($text === "") return [""]; + $text = $this->filterContent($text); + if (!$this->color) $text = $this->filterColors($text); + $lines = explode("\n", $text); + $max = count($lines) - 1; + if ($withNl) { + for ($i = 0; $i < $max; $i++) { + $lines[$i] .= "\n"; + } + } + if ($lines[$max] === "") unset($lines[$max]); + return $lines; + } + + private function _fwrite($outf, string $data): void { + if ($this->maskErrors === null) { + # masquer les erreurs d'écriture en permanence + @fwrite($outf, $data); + return; + } + # masquer uniquement la première erreur, jusqu'à ce que l'erreur disparaisse + if ($this->maskErrors) $r = @fwrite($outf, $data); + else $r = fwrite($outf, $data); + $this->maskErrors = $r === false; + } + + function writeLines($indent, array $lines, bool $addNl=false): void { + $outf = $this->outf; + foreach ($lines as $line) { + if ($indent !== null) $this->_fwrite($outf, $indent); + $this->_fwrite($outf, $line); + if ($addNl) $this->_fwrite($outf, "\n"); + } + if ($this->flush) @fflush($outf); + } + + function write(...$values): void { + $this->writeLines(null, $this->getLines(true, ...$values)); + } + + function print(...$values): void { + $values[] = "\n"; + $this->writeLines(null, $this->getLines(true, ...$values)); + } + + function iwrite(int $indentLevel, ...$values): void { + $indent = $this->getIndent($indentLevel); + $this->writeLines($indent, $this->getLines(true, ...$values)); + } + + function iprint(int $indentLevel, ...$values): void { + $values[] = "\n"; + $indent = $this->getIndent($indentLevel); + $this->writeLines($indent, $this->getLines(true, ...$values)); + } +} diff --git a/php/src/output/std/_IMessenger.php b/php/src/output/std/_IMessenger.php new file mode 100644 index 0000000..9b54b59 --- /dev/null +++ b/php/src/output/std/_IMessenger.php @@ -0,0 +1,19 @@ +offsetExists($key)) return $array->offsetGet($key); + else return $default; + } else { + if (!is_array($array)) $array = cl::with($array); + return cl::get($array, $key, $default); + } + } + + /** spécifier la valeur d'une clé */ + static final function set(&$array, $key, $value) { + if ($array instanceof ArrayAccess) { + $array->offsetSet($key, $value); + } else { + cl::set($array, $key, $value); + } + return $value; + } + + /** initialiser $dest avec les valeurs de $values */ + static final function set_values(&$array, ?array $values): void { + if ($values === null) return; + foreach ($values as $key => $value) { + self::set($array, $key, $value); + } + } + + /** incrémenter la valeur de la clé */ + static final function inc(&$array, $key): int { + if ($array instanceof ArrayAccess) { + $value = (int)$array->offsetGet($key); + $array->offsetSet($key, ++$value); + return $value; + } else { + A::ensure_array($array); + $value = (int)cl::get($array, $key); + return $array[$key] = ++$value; + } + } + + /** décrémenter la valeur de la clé */ + static final function dec(&$array, $key, bool $allow_negative=false): int { + if ($array instanceof ArrayAccess) { + $value = (int)$array->offsetGet($key); + if ($allow_negative || $value > 0) $array->offsetSet($key, --$value); + return $value; + } else { + A::ensure_array($array); + $value = (int)cl::get($array, $key); + if ($allow_negative || $value > 0) $array[$key] = --$value; + return $value; + } + } + + /** + * fusionner $merge dans la valeur de la clé, qui est d'abord transformé en + * tableau si nécessaire + */ + static final function merge(&$array, $key, $merge): void { + if ($array instanceof ArrayAccess) { + $value = $array->offsetGet($key); + $value = cl::merge($value, $merge); + $array->offsetSet($key, $value); + } else { + A::ensure_array($array); + $array[$key] = cl::merge($array[$key], $merge); + } + } + + /** + * ajouter $value à la valeur de la clé, qui est d'abord transformé en + * tableau si nécessaire + */ + static final function append(&$array, $key, $value): void { + if ($array instanceof ArrayAccess) { + $value = $array->offsetGet($key); + cl::set($value, null, $value); + $array->offsetSet($key, $value); + } else { + A::ensure_array($array); + cl::set($array[$key], null, $value); + } + } +} diff --git a/php/src/php/coll/AutoArray.php b/php/src/php/coll/AutoArray.php new file mode 100644 index 0000000..82d1904 --- /dev/null +++ b/php/src/php/coll/AutoArray.php @@ -0,0 +1,44 @@ +has($name)) return true; + $properties = self::_AUTO_PROPERTIES(); + if ($properties === null) return false; + return array_key_exists($name, $properties); + } + function __get($name) { + $properties = self::_AUTO_PROPERTIES(); + if ($this->has($name)) return $this->get($name); + $pkey = cl::get($properties, $name, $name); + return cl::pget($this->data, $pkey); + } + function __set($name, $value) { + $properties = self::_AUTO_PROPERTIES(); + if ($this->has($name)) { + $this->set($name, $value); + } else { + $pkey = cl::get($properties, $name, $name); + cl::pset($this->data, $pkey, $value); + } + } + function __unset($name) { + $properties = self::_AUTO_PROPERTIES(); + if ($this->has($name)) { + $this->del($name); + } else { + $pkey = cl::get($properties, $name, $name); + cl::pdel($this->data, $pkey); + } + } +} diff --git a/php/src/php/coll/BaseArray.php b/php/src/php/coll/BaseArray.php new file mode 100644 index 0000000..ff809fc --- /dev/null +++ b/php/src/php/coll/BaseArray.php @@ -0,0 +1,117 @@ +reset($data); + } + + /** @var array */ + protected $data; + + function &wrappedArray(): ?array { return $this->data; } + + function __toString(): string { return var_export($this->data, true); } + #function __debugInfo() { return $this->data; } + function reset(?array &$data): void { $this->data =& $data; } + function count(): int { return $this->data !== null? count($this->data): 0; } + function keys(): array { return $this->data !== null? array_keys($this->data): []; } + + ############################################################################# + # base + + function has($key): bool { + return $this->data !== null && array_key_exists($key, $this->data); + } + function &get($key, $default=null) { + if ($this->data !== null && array_key_exists($key, $this->data)) { + return $this->data[$key]; + } else return $default; + } + function set($key, $value): void { + if ($key === null) $this->data[] = $value; + else $this->data[$key] = $value; + } + function del($key): void { + unset($this->data[$key]); + } + + function offsetExists($offset): bool { return $this->has($offset); } + function &offsetGet($offset) { return $this->get($offset); } + function offsetSet($offset, $value) { $this->set($offset, $value); } + function offsetUnset($offset) { $this->del($offset); } + + function __isset($name) { return $this->has($name); } + function &__get($name) { return $this->get($name); } + function __set($name, $value) { $this->set($name, $value); } + function __unset($name) { $this->del($name); } + + ############################################################################# + # iterator + + /** @var bool */ + private $valid = false; + + function rewind() { + if ($this->data !== null) { + $first = reset($this->data); + $this->valid = $first !== false || key($this->data) !== null; + } else { + $this->valid = false; + } + } + function valid(): bool { return $this->valid; } + function key() { return key($this->data); } + function current() { return current($this->data); } + function next() { + $next = next($this->data); + $this->valid = $next !== false || key($this->data) !== null; + } + + ############################################################################# + # divers + + function phas($pkey): bool { return cl::phas($this->data, $pkey); } + function pget($pkey, $default=null): bool { return cl::pget($this->data, $pkey, $default); } + function pset($pkey, $value): void { cl::pset($this->data, $pkey, $value); } + function pdel($pkey): void { cl::pdel($this->data, $pkey); } + + function contains($value, bool $strict=false): bool { + if ($value === null || $this->data === null) return false; + return in_array($value, $this->data, $strict); + } + + function add($value, bool $unique=true, bool $strict=false): bool { + if ($unique && $this->contains($value, $strict)) return false; + $this->set(null, $value); + return true; + } + + function addAll(?array $values, bool $unique=true, bool $strict=false): void { + if ($values === null) return; + $index = 0; + foreach ($values as $key => $value) { + if ($key === $index) { + $this->add($value, $unique, $strict); + $index++; + } else { + $this->set($key, $value); + } + } + } + + function resetAll(?array $values): void { + $this->data = null; + $this->addAll($values); + } +} diff --git a/php/src/php/content/IContent.php b/php/src/php/content/IContent.php new file mode 100644 index 0000000..7fd4386 --- /dev/null +++ b/php/src/php/content/IContent.php @@ -0,0 +1,11 @@ +content = $content; + } + + protected IContent $content; + + function print(): void { + $content = $this->content->getContent(); + c::write($content); + } + + function __call($name, $args) { + $content = func::call([$this->content, $name], ...$args); + c::write($content); + } +} diff --git a/php/src/php/content/README.md b/php/src/php/content/README.md new file mode 100644 index 0000000..0e7eb5a --- /dev/null +++ b/php/src/php/content/README.md @@ -0,0 +1,77 @@ +# nulib\php\content + +un contenu (ou "content") est une liste de valeurs, avec une syntaxe pour que +certains éléments soient dynamiquement calculés. + +le contenu final est résolu selon les règles suivantes: +- Si le contenu n'est pas un tableau: + - une chaine est quotée avec `htmlspecialchars()` + - un scalaire ou une instance d'objet sont pris tels quels +- Sinon, le contenu doit être un tableau, séquentiel ou associatif, ça n'a pas + d'incidence + - les éléments scalaires ou instance d'objets sont pris tels quels + - les Closure sont appelés dès la résolution, et leur valeur de retour est + considéré comme un contenu *statique* inséré tel quel dans le flux i.e dans + l'exemple suivant $c1 et $c2 sont globalement équivalents: + ~~~php + $closure = function() { ... } + $c1 = [...$before, $closure, ...$after]; + $c2 = [...$before, ...c::q($closure()), ...$after]; + # $c1 == $c2, sauf si $closure() retourne des valeurs qui peuvent être + # considérées comme du contenu dynamique + ~~~ + - les tableaux représentent un traitement dynamique: appel de fonction, + instanciation, etc. le contenu effectif n'est évalué que lors de l'affichage + +Les syntaxes possibles sont: + +`[[], $args...]` +: contenu statique: les valeurs $args... sont insérées dans le flux du contenu + sans modification. c'est la seule façon d'insérer un tableau dans la liste des + valeurs (on peut aussi utiliser une Closure, mais ce n'est pas toujours + possible, notamment si le contenu est une constante) + +`["class_or_function", $args...]` +`[["class_or_function"], $args...]` +`[["function", $args0...], $args1...]` +`[["class", null, $args0...], $args1...]` +: instantiation ou appel de fonction + +`["->method", $args...]` +`[["->method"], $args...]` +`[[null, "method"], $args...]` +`[[null, "method", $args0...], $args1...]` +: appel de méthode sur l'objet contexte spécifié lors de la résolution du contenu + +`[[$object, "method"], $args...]` +`[[$object, "method", $args0...], $args1...]` +: appel de méthode sur l'objet spécifié + +`[["class", "method"], $args...]` +`[["class", "method", $args0...], $args1...]` +: appel de méthode statique de la classe spécifiée + +Le fait de transformer un contenu en une liste de valeurs statiques s'appelle +la résolution. la résolution se fait par rapport à un objet contexte, qui est +utilisé lors des appels de méthodes. + +Lors des appels de fonctions ou des instanciations, les $arguments sont tous des +contenus: +- une valeur scalaire ou une instance est passée inchangée +- un tableau est traité comme un contenu avec les règles ci-dessus + +## Affichage d'un contenu + +Deux interfaces sont utilisées pour modéliser un élément de contenu à afficher: +- IContent: objet capable de produire du contenu +- IPrintable: objet capable d'afficher un contenu + +Tous les autres éléments de contenus sont transformés en string avant affichage. +Un système de formatters permet de définir des fonctions ou méthodes à utiliser +pour formatter des objets de certains types. + +Lors de l'affichage du contenu, deux éléments contigûs $a et $b sont affichés +séparés par un espace si $a se termine par un mot (éventuellement terminé par +un point '.') et $b commence par un mot. + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/php/src/php/content/c.php b/php/src/php/content/c.php new file mode 100644 index 0000000..ebf4c52 --- /dev/null +++ b/php/src/php/content/c.php @@ -0,0 +1,168 @@ + $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; + $values = self::q(func::call($func)); + 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); + $value = func::with($func, $args)->bind($object_or_class)->invoke(); + } + } + 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..fcec77b --- /dev/null +++ b/php/src/php/func.php @@ -0,0 +1,726 @@ +"; + } + + 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, ?array &$args=null, bool $strict=true, ?string &$reason=null): bool { + if ($strict) $reason = null; + if ($func instanceof ReflectionFunction) return true; + $rargs = null; + 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]; + $rargs = array_slice($func, 2); + } 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 = "$f: is a class"; + return false; + } + if (!function_exists($f)) { + $reason = "$f: function not found"; + return false; + } + } + $func = [false, $f]; + if ($rargs) $args = cl::merge($rargs, $args); + 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, $args, $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, ?array &$args=null, bool $strict=true, ?string &$reason=null): bool { + if ($strict) $reason = null; + if ($func instanceof ReflectionClass) return true; + $rargs = null; + 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]; + $rargs = array_slice($func, 2); + } 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 && !class_exists($c)) { + $reason = "$c: class not found"; + return false; + } + $func = [$c, false]; + if ($rargs) $args = cl::merge($rargs, $args); + 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, $args, $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 + * @param array|null &$args + * la méthode est liée (ne pas uniquement faire une vérification syntaxique) + */ + static function verifix_static(&$func, ?array &$args = null, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool { + if ($strict) $reason = null; + if ($func instanceof ReflectionMethod) { + $bound = false; + return true; + } + $rargs = null; + 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; + $rargs = array_slice($func, 2); + } else { + return false; + } + if ($strict) { + [$c, $f] = $cf; + $reason = null; + if ($bound) { + if (!class_exists($c)) { + $reason = "$c: class not found"; + return false; + } + if (!method_exists($c, $f)) { + $reason = "$c::$f: method not found"; + return false; + } + $method = new ReflectionMethod($c, $f); + if (!$method->isStatic()) return false; + } else { + $reason = "$c::$f: not bound"; + } + } + $func = $cf; + if ($rargs) $args = cl::merge($rargs, $args); + 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, $args, $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, ?array &$args=null, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool { + if ($strict) $reason = null; + if ($func instanceof ReflectionMethod) { + $bound = false; + return true; + } + $rargs = null; + 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; + $rargs = array_slice($func, 2); + } else { + return false; + } + if ($strict) { + [$c, $f] = $cf; + $reason = null; + if ($bound) { + if (!is_object($c) && !class_exists($c)) { + $reason = "$c: class not found"; + return false; + } + if (!method_exists($c, $f)) { + $reason = "$c::$f: method not found"; + return false; + } + $method = new ReflectionMethod($c, $f); + if ($method->isStatic()) return false; + } else { + $reason = "$c::$f: not bound"; + } + } + $func = $cf; + if ($rargs) $args = cl::merge($rargs, $args); + 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, $args, $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); + } + + private static function _with($func, ?array $args=null, bool $strict=true, ?string &$reason=null): ?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, $args, $strict, $reason)) { + return new self(self::TYPE_FUNCTION, $func, $args, false, $reason); + } elseif (self::verifix_class($func, $args, $strict, $reason)) { + return new self(self::TYPE_CLASS, $func, $args, false, $reason); + } elseif (self::verifix_method($func, $args, $strict, $bound, $reason)) { + return new self(self::TYPE_METHOD, $func, $args, $bound, $reason); + } elseif (self::verifix_static($func, $args, $strict, $bound, $reason)) { + return new self(self::TYPE_STATIC, $func, $args, $bound, $reason); + } + return null; + } + + static function with($func, ?array $args=null, bool $strict=true): self { + if ($func instanceof self) return $func; + $func = self::_with($func, $args, $strict, $reason); + if ($func !== null) return $func; + throw self::not_a_callable($func, $reason); + } + + static function withn($func, ?array $args=null, bool $strict=true): ?self { + if ($func === null) return null; + else return self::with($func, $args, $strict); + } + + 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 is_callable($func): bool { + $func = self::_with($func); + if ($func === null) return false; + if (!$func->isBound()) return false; + return $func->type !== self::TYPE_CLASS; + } + + static function call($func, ...$args) { + return self::with($func)->invoke($args); + } + + /** + * si $value est une fonction, l'appeler + * si $value ou le résultat de l'appel est un Traversable, le résoudre + * sinon retourner $value tel quel + * + * en définitive, la valeur de retour de cette fonction est soit un scalaire, + * soit un array, soit un objet qui n'est pas Traversable + * @return mixed + */ + static function get_value($value, ...$args) { + if ($value instanceof self) $value = $value->invoke($args); + elseif (is_callable($value)) $value = self::call($value, ...$args); + if ($value instanceof Traversable) $value = cl::all($value); + return $value; + } + + /** + * si $value est une fonction, l'appeler + * si $value ou le résultat de l'appel est un Traversable, le retourner + * sinon retourner $value en tant qu'array + */ + static function get_iterable($value, ...$args): ?iterable { + if ($value instanceof self) $value = $value->invoke($args); + elseif (is_callable($value)) $value = self::call($value, ...$args); + if ($value instanceof Traversable) return $value; + else return cl::withn($value); + } + + ############################################################################# + + 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; + + function replaceArgs(?array $args): self { + $this->prefixArgs = $args?? []; + return $this; + } + + function prependArgs(?array $args, ?int $stripCount=null): self { + if ($stripCount !== null || $args !== null) { + array_splice($this->prefixArgs, 0, $stripCount ?? 0, $args); + } + return $this; + } + + function appendArgs(?array $args, ?int $stripCount=null): self { + if ($stripCount !== null || $args !== null) { + $stripCount ??= 0; + if ($stripCount > 0) array_splice($this->prefixArgs, -$stripCount); + $this->prefixArgs = array_merge($this->prefixArgs, $args); + } + return $this; + } + + 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, bool $rebind=false, bool $replace=false): self { + if ($this->type !== self::TYPE_METHOD) return $this; + if (!$rebind && $this->isBound()) return $this; + + [$c, $f] = $this->func; + if ($replace) { + $c = $object; + $this->func = [$c, $f]; + $this->updateReflection(new ReflectionMethod($c, $f)); + } elseif ($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..b036844 --- /dev/null +++ b/php/src/php/mprop.php @@ -0,0 +1,122 @@ +getMethod($method); + } catch (ReflectionException $e) { + return oprop::get($object, $property, $default); + } + return 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); + } + 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/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..210e6ba --- /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_numeric($duration)) $duration = "PT${duration}S"; + if ($duration instanceof \DateInterval) { + $this->y = $duration->y; + $this->m = $duration->m; + $this->d = $duration->d; + $this->h = $duration->h; + $this->i = $duration->i; + $this->s = $duration->s; + $this->invert = $duration->invert; + $this->days = $duration->days; + } elseif (!is_string($duration)) { + throw new InvalidArgumentException("duration must be a string"); + } else { + if (substr($duration, 0, 1) == "-") { + $duration = substr($duration, 1); + $invert = true; + } else { + $invert = false; + } + parent::__construct($duration); + if ($invert) $this->invert = 1; + } + } + + function __toString(): string { + return self::to_string($this); + } +} diff --git a/php/src/php/time/DateTime.php b/php/src/php/time/DateTime.php new file mode 100644 index 0000000..1fcd632 --- /dev/null +++ b/php/src/php/time/DateTime.php @@ -0,0 +1,343 @@ +format("Ymd\\THis"); + $Z = $datetime->format("P"); + if ($Z === "+00:00") $Z = "Z"; + return "$YmdHMS$Z"; + } + + const DEFAULT_FORMAT = "d/m/Y H:i:s"; + const INT_FORMATS = [ + "year" => "Y", + "month" => "m", + "day" => "d", + "hour" => "H", + "minute" => "i", + "second" => "s", + "wday" => "N", + "wnum" => "W", + ]; + const STRING_FORMATS = [ + "timezone" => "P", + "datetime" => "d/m/Y H:i:s", + "date" => "d/m/Y", + "Ymd" => "Ymd", + "YmdHMS" => "Ymd\\THis", + "YmdHMSZ" => [self::class, "_YmdHMSZ_format"], + ]; + + static function clone(DateTimeInterface $dateTime): self { + if ($dateTime instanceof static) return clone $dateTime; + $clone = new static(); + $clone->setTimestamp($dateTime->getTimestamp()); + $clone->setTimezone($dateTime->getTimezone()); + return $clone; + } + + /** + * corriger une année à deux chiffres qui est située dans le passé et + * retourner l'année à 4 chiffres. + * + * par exemple, si l'année courante est 2019, alors: + * - fix_past_year('18') === '2018' + * - fix_past_year('19') === '1919' + * - fix_past_year('20') === '1920' + */ + static function fix_past_year(int $year): int { + if ($year < 100) { + $y = getdate(); $y = $y["year"]; + $r = $y % 100; + $c = $y - $r; + if ($year >= $r) $year += $c - 100; + else $year += $c; + } + return $year; + } + + /** + * corriger une année à deux chiffres et retourner l'année à 4 chiffres. + * l'année charnière entre année passée et année future est 70 + * + * par exemple, si l'année courante est 2019, alors: + * - fix_past_year('18') === '2018' + * - fix_past_year('19') === '2019' + * - fix_past_year('20') === '2020' + * - fix_past_year('69') === '2069' + * - fix_past_year('70') === '1970' + * - fix_past_year('71') === '1971' + */ + static function fix_any_year(int $year): int { + if ($year < 100) { + $y = intval(date("Y")); + $r = $y % 100; + $c = $y - $r; + if ($year >= 70) $year += $c - 100; + else $year += $c; + } + return $year; + } + + function __construct($datetime="now", DateTimeZone $timezone=null) { + $datetime ??= "now"; + if ($datetime instanceof \DateTimeInterface) { + if ($timezone === null) $timezone = $datetime->getTimezone(); + parent::__construct(); + $this->setTimestamp($datetime->getTimestamp()); + $this->setTimezone($timezone); + } elseif (is_int($datetime)) { + parent::__construct("now", $timezone); + $this->setTimestamp($datetime); + } elseif (is_string($datetime)) { + $Y = $H = $Z = null; + if (preg_match(self::DMY_PATTERN, $datetime, $ms)) { + $Y = $ms[3] ?? null; + if ($Y !== null) $Y = self::fix_any_year(intval($Y)); + else $Y = intval(date("Y")); + $m = intval($ms[2]); + $d = intval($ms[1]); + } elseif (preg_match(self::YMD_PATTERN, $datetime, $ms)) { + $Y = $ms[1]; + if (strlen($Y) == 2) $Y = self::fix_any_year(intval($ms[1])); + else $Y = intval($Y); + $m = intval($ms[2]); + $d = intval($ms[3]); + } elseif (preg_match(self::DMYHIS_PATTERN, $datetime, $ms)) { + $Y = $ms[3]; + if ($Y !== null) $Y = self::fix_any_year(intval($Y)); + else $Y = intval(date("Y")); + $m = intval($ms[2]); + $d = intval($ms[1]); + $H = intval($ms[4]); + $M = intval($ms[5]); + $S = intval($ms[6] ?? 0); + } elseif (preg_match(self::YMDHISZ_PATTERN, $datetime, $ms)) { + $Y = $ms[1]; + if (strlen($Y) == 2) $Y = self::fix_any_year(intval($ms[1])); + else $Y = intval($Y); + $m = intval($ms[2]); + $d = intval($ms[3]); + $H = intval($ms[4]); + $M = intval($ms[5]); + $S = intval($ms[6] ?? 0); + $Z = $ms[7] ?? null; + } + if ($Y !== null) { + if ($H === null) $datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d); + else $datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H, $M, $S); + if ($Z !== null) $timezone = new DateTimeZone("UTC"); + } + parent::__construct($datetime, $timezone); + } elseif (is_array($datetime) && ($datetime = self::parse_array($datetime)) !== null) { + [$Y, $m, $d, $H, $M, $S, $tz] = $datetime; + if ($H === null && $M === null && $S === null) { + $datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d); + } else { + $datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H ?? 0, $M ?? 0, $S ?? 0); + } + if ($tz === "Z" || $tz === "z") $tz = "UTC"; + $timezone = $tz !== null? new DateTimeZone($tz): null; + parent::__construct($datetime, $timezone); + } else { + throw new InvalidArgumentException("datetime must be a string or an instance of DateTimeInterface"); + } + } + + function diff($target, $absolute=false): DateInterval { + return new DateInterval(parent::diff($target, $absolute)); + } + + function format($format=self::DEFAULT_FORMAT): string { + return \DateTime::format($format); + } + + /** + * modifier cet objet pour que l'heure soit à 00:00:00 ce qui le rend propice + * à l'utilisation comme borne inférieure d'une période + */ + function wrapStartOfDay(): self { + $this->setTime(0, 0); + return $this; + } + + /** + * modifier cet objet pour que l'heure soit à 23:59:59.999999 ce qui le rend + * propice à l'utilisation comme borne supérieure d'une période + */ + function wrapEndOfDay(): self { + $this->setTime(23, 59, 59, 999999); + return $this; + } + + function getPrevDay(int $nbDays=1, bool $skipWeekend=false): self { + if ($nbDays == 1 && $skipWeekend && $this->wday == 1) { + $nbdays = 3; + } + return static::with($this->sub(new \DateInterval("P${nbDays}D"))); + } + + function getNextDay(int $nbDays=1, bool $skipWeekend=false): self { + if ($nbDays == 1 && $skipWeekend) { + $wday = $this->wday; + if ($wday > 5) $nbDays = 8 - $this->wday; + } + return static::with($this->add(new \DateInterval("P${nbDays}D"))); + } + + function getElapsedAt(?DateTime $now=null, ?int $resolution=null): string { + return Elapsed::format_at($this, $now, $resolution); + } + + function getElapsedSince(?DateTime $now=null, ?int $resolution=null): string { + return Elapsed::format_since($this, $now, $resolution); + } + + function getElapsedDelay(?DateTime $now=null, ?int $resolution=null): string { + return Elapsed::format_delay($this, $now, $resolution); + } + + function __toString(): string { + return $this->format(); + } + + function __get($name) { + if (array_key_exists($name, self::INT_FORMATS)) { + $format = self::INT_FORMATS[$name]; + if (is_callable($format)) return $format($this); + else return intval($this->format($format)); + } elseif (array_key_exists($name, self::STRING_FORMATS)) { + $format = self::STRING_FORMATS[$name]; + if (is_callable($format)) return $format($this); + else return $this->format($format); + } + throw new InvalidArgumentException("Unknown property $name"); + } +} diff --git a/php/src/php/time/Delay.php b/php/src/php/time/Delay.php new file mode 100644 index 0000000..4afcf37 --- /dev/null +++ b/php/src/php/time/Delay.php @@ -0,0 +1,179 @@ + [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 __clone() { + $this->dest = clone $this->dest; + } + + 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_numeric($duration) && $duration < 0) { + $this->dest->sub(DateInterval::with(-$duration)); + } else { + $this->dest->add(DateInterval::with($duration)); + } + } + + function subDuration($duration) { + if (is_numeric($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 @@ + ["int", self::ACCESS_AUTO, "type d'accès: clé ou propriété"], + "allow_empty" => ["bool", true, "la chaine vide est-elle autorisée?"], + "allow_null" => ["bool", true, "la valeur null est-elle autorisée?"], + ]; + + const ACCESS_PARAMS_SCHEMA = [ + "allow_empty" => ["bool", true, "la chaine vide est-elle autorisée?"], + "allow_null" => ["bool", null, "la valeur null est-elle autorisée?"], + "allow_false" => ["bool", null, "la valeur false est-elle autorisée?"], + "protect_dest" => ["bool", null, "faut-il protéger la destination?"], + ]; + + const VALUE_ACCESS_PARAMS_SCHEMA = [ + "allow_null" => ["bool", false], + "allow_false" => ["bool", true], + "protect_dest" => ["bool", false], + ]; + + const ARRAY_ACCESS_PARAMS_SCHEMA = [ + "allow_null" => ["bool", true], + "allow_false" => ["bool", false], + "protect_dest" => ["bool", true], + "key_prefix" => ["?string", null, "préfixe des clés pour les méthodes ensureXxx()"], + "key_suffix" => ["?string", null, "suffixe des clés pour les méthodes ensureXxx()"], + ]; + + const PROPERTY_ACCESS_PARAMS_SCHEMA = [ + "allow_null" => ["bool", true], + "allow_false" => ["bool", false], + "protect_dest" => ["bool", true], + "key_prefix" => ["?string", null, "préfixe des clés pour les méthodes ensureXxx()"], + "key_suffix" => ["?string", null, "suffixe des clés pour les méthodes ensureXxx()"], + "map_names" => ["bool", true, "faut-il mapper les clés en camelCase?"] + ]; +} diff --git a/php/src/ref/schema/ref_schema.php b/php/src/ref/schema/ref_schema.php new file mode 100644 index 0000000..37bea48 --- /dev/null +++ b/php/src/ref/schema/ref_schema.php @@ -0,0 +1,86 @@ + ["string", null, "nature du schéma", + "allowed_values" => ["scalar", "assoc", "list"], + ], + "compute_func" => ["?callable", null, "fonction qui calcule les valeurs des champs computed"], + "validate_func" => ["?callable", null, "fonction qui vérifie la conformité de l'objet dans son ensemble"], + ]; + + /** @var array meta-schéma d'une valeur */ + const VALUE_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"], + "size" => ["?int", null, "nom de caractères ou de chiffres de la valeur"], + "precision" => ["?int", null, "nombre de chiffres après la virgule pour une valeur numérique flottante"], + "" => ["array", ["scalar"], "nature du schéma", + "schema" => self::NATURE_METASCHEMA, + ], + "schema" => ["?array", null, "schéma de la valeur si c'est un array"], + "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"], + "computed" => ["?bool", null, "ce champ est-il calculé? si oui, il n'est pas demandé en entrée ni validé"], + ]; + + const MESSAGES = [ + "missing" => "vous devez spécifier cette valeur", + "unavailable" => "vous devez spécifier cette valeur", + "null" => "cette valeur ne doit pas être nulle", + "empty" => "cette valeur ne doit pas être vide", + "invalid" => "cette valeur est invalide", + ]; + + const PARAMS_SCHEMA = [ + "analyze" => ["bool", true, "faut-il analyser la valeur?"], + "reanalyze" => ["bool", true, "faut-il forcer l'analyse de la valeur?"], + "normalize" => ["bool", true, "faut-il normaliser la valeur?"], + "renormalize" => ["bool", true, "faut-il forcer la normalisation de la valeur?"], + "throw" => ["bool", true, "faut-il lancer une exception en cas d'erreur?"], + //...ref_input::INPUT_PARAMS_SCHEMA, + ]; + + /** @var array clés supplémentaires de schéma de la nature scalaire */ + const SCALAR_NATURE_METASCHEMA = [ + ]; + + const SCALAR_PARAMS_SCHEMA = [ + ]; + + /** @var array clés supplémentaires de schéma de la nature associative */ + const ASSOC_NATURE_METASCHEMA = [ + "ensure_array" => ["bool", null, "faut-il s'assurer que le tableau destination est non nul?"], + "ensure_assoc" => ["bool", null, "faut-il s'assurer que le tableau destination est associatif?"], + "ensure_keys" => ["bool", null, "faut-il s'assurer que toutes les clés existent avec la valeur par défaut?"], + "ensure_order" => ["bool", null, "faut-il s'assurer que les clés soient dans l'ordre?"], + ]; + + const ASSOC_PARAMS_SCHEMA = [ + "ensure_array" => ["bool", false], + "ensure_assoc" => ["bool", true], + "ensure_keys" => ["bool", true], + "ensure_order" => ["bool", true], + ]; + + /** @var array clés supplémentaires de schéma de la nature liste */ + const LIST_NATURE_METASCHEMA = [ + ]; + + const LIST_PARAMS_SCHEMA = [ + ]; +} diff --git a/php/src/ref/schema/ref_types.php b/php/src/ref/schema/ref_types.php new file mode 100644 index 0000000..d7ce1d4 --- /dev/null +++ b/php/src/ref/schema/ref_types.php @@ -0,0 +1,11 @@ + "bool", + "integer" => "int", + "flt" => "float", "double" => "float", "dbl" => "float", + "func" => "callable", "function" => "callable", + ]; +} diff --git a/php/src/ref/web/ref_mimetypes.php b/php/src/ref/web/ref_mimetypes.php new file mode 100644 index 0000000..d896c13 --- /dev/null +++ b/php/src/ref/web/ref_mimetypes.php @@ -0,0 +1,12 @@ + $length) { + if ($ellips && $length > 3) $s = substr($s, 0, $length - 3)."..."; + else $s = substr($s, 0, $length); + } + if ($suffix !== null) $s .= $suffix; + return $s; + } + + /** trimmer $s */ + static final function trim(?string $s): ?string { + if ($s === null) return null; + else return trim($s); + } + + /** trimmer $s à gauche */ + static final function ltrim(?string $s): ?string { + if ($s === null) return null; + else return ltrim($s); + } + + /** trimmer $s à droite */ + static final function rtrim(?string $s): ?string { + if ($s === null) return null; + else return rtrim($s); + } + + static final function left(?string $s, int $size): ?string { + if ($s === null) return null; + else return str_pad($s, $size); + } + + static final function right(?string $s, int $size): ?string { + if ($s === null) return null; + else return str_pad($s, $size, " ", STR_PAD_LEFT); + } + + static final function center(?string $s, int $size): ?string { + if ($s === null) return null; + else return str_pad($s, $size, " ", STR_PAD_BOTH); + } + + static final function pad0(?string $s, int $size): ?string { + if ($s === null) return null; + else return str_pad($s, $size, "0", STR_PAD_LEFT); + } + + static final function lower(?string $s): ?string { + if ($s === null) return null; + else return strtolower($s); + } + + static final function lower1(?string $s): ?string { + if ($s === null) return null; + else return lcfirst($s); + } + + static final function upper(?string $s): ?string { + if ($s === null) return null; + else return strtoupper($s); + } + + static final function upper1(?string $s): ?string { + if ($s === null) return null; + else return ucfirst($s); + } + + static final function upperw(?string $s, ?string $delimiters=null): ?string { + if ($s === null) return null; + if ($delimiters !== null) return ucwords($s, $delimiters); + else return ucwords($s, " _-\t\r\n\f\v"); + } + + protected static final function _starts_with(string $prefix, string $s, ?int $min_len=null): bool { + if ($prefix === $s) return true; + $len = strlen($prefix); + if ($min_len !== null && ($len < $min_len || $len > strlen($s))) return false; + return $len == 0 || $prefix === substr($s, 0, $len); + } + + /** + * tester si $s commence par $prefix + * par exemple: + * - starts_with("", "whatever") est true + * - starts_with("fi", "first") est true + * - starts_with("no", "yes") est false + * + * si $min_len n'est pas null, c'est la longueur minimum requise de $prefix + * pour qu'on teste la correspondance. dans le cas contraire, la valeur de + * retour est toujours false, sauf s'il y a égalité. e.g + * - starts_with("a", "abc", 2) est false + * - starts_with("a", "a", 2) est true + */ + static final function starts_with(?string $prefix, ?string $s, ?int $min_len=null): bool { + if ($s === null || $prefix === null) return false; + else return self::_starts_with($prefix, $s, $min_len); + } + + /** Retourner $s sans le préfixe $prefix s'il existe */ + static final function without_prefix(?string $prefix, ?string $s): ?string { + if ($s === null || $prefix === null) return $s; + if (self::_starts_with($prefix, $s)) $s = substr($s, strlen($prefix)); + return $s; + } + + /** + * modifier $s en place pour supprimer le préfixe $prefix s'il existe + * + * retourner true si le préfixe a été enlevé. + */ + static final function del_prefix(?string &$s, ?string $prefix): bool { + if ($s === null || !self::_starts_with($prefix, $s)) return false; + $s = self::without_prefix($prefix, $s); + return true; + } + + /** + * Retourner $s avec le préfixe $prefix + * + * Si $unless_exists, ne pas ajouter le préfixe s'il existe déjà + */ + static final function with_prefix(?string $prefix, ?string $s, ?string $sep=null, bool $unless_exists=false): ?string { + if ($s === null || $prefix === null) return $s; + if (!self::_starts_with($prefix, $s) || !$unless_exists) $s = $prefix.$sep.$s; + return $s; + } + + /** + * modifier $s en place pour ajouter le préfixe $prefix + * + * retourner true si le préfixe a été ajouté. + */ + static final function add_prefix(?string &$s, ?string $prefix, bool $unless_exists=true): bool { + if (($s === null || self::_starts_with($prefix, $s)) && $unless_exists) return false; + $s = self::with_prefix($prefix, $s, null, $unless_exists); + return true; + } + + protected static final function _ends_with(string $suffix, string $s, ?int $min_len=null): bool { + if ($suffix === $s) return true; + $len = strlen($suffix); + if ($min_len !== null && ($len < $min_len || $len > strlen($s))) return false; + return $len == 0 || $suffix === substr($s, -$len); + } + + /** + * tester si $string se termine par $suffix + * par exemple: + * - ends_with("", "whatever") est true + * - ends_with("st", "first") est true + * - ends_with("no", "yes") est false + * + * si $min_len n'est pas null, c'est la longueur minimum requise de $prefix + * pour qu'on teste la correspondance. dans le cas contraire, la valeur de + * retour est toujours false, sauf s'il y a égalité. e.g + * - ends_with("c", "abc", 2) est false + * - ends_with("c", "c", 2) est true + */ + static final function ends_with(?string $suffix, ?string $s, ?int $min_len=null): bool { + if ($s === null || $suffix === null) return false; + else return self::_ends_with($suffix, $s, $min_len); + } + + /** Retourner $s sans le suffixe $suffix s'il existe */ + static final function without_suffix(?string $suffix, ?string $s): ?string { + if ($s === null || $suffix === null) return $s; + if (self::_ends_with($suffix, $s)) $s = substr($s, 0, -strlen($suffix)); + return $s; + } + + /** + * modifier $s en place pour supprimer le suffixe $suffix s'il existe + * + * retourner true si le suffixe a été enlevé. + */ + static final function del_suffix(?string &$s, ?string $suffix): bool { + if ($s === null || !self::_ends_with($suffix, $s)) return false; + $s = self::without_suffix($suffix, $s); + return true; + } + + /** + * Retourner $s avec le suffixe $suffix + * + * Si $unless_exists, ne pas ajouter le suffixe s'il existe déjà + */ + static final function with_suffix(?string $suffix, ?string $s, ?string $sep=null, bool $unless_exists=false): ?string { + if ($s === null || $suffix === null) return $s; + if (!self::_ends_with($suffix, $s) || !$unless_exists) $s = $s.$sep.$suffix; + return $s; + } + + /** + * modifier $s en place pour ajouter le suffixe $suffix + * + * retourner true si le suffixe a été ajouté. + */ + static final function add_suffix(?string &$s, ?string $suffix, bool $unless_exists=true): bool { + if (($s === null || self::_ends_with($suffix, $s)) && $unless_exists) return false; + $s = self::with_suffix($suffix, $s, null, $unless_exists); + return true; + } + + /** + * vérifier si $s a le préfixe $prefix + * - si $prefix commence par /, c'est une expression régulière, et elle doit + * matcher $s + * - sinon $s doit commencer par la chaine $prefix + */ + static final function match_prefix(?string $s, ?string $prefix): bool { + if ($s === null || $prefix === null) return false; + if (substr($prefix, 0, 1) === "/") { + return preg_match($prefix, $s); + } else { + return self::_starts_with($prefix, $s); + } + } + + /** + * 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; + } + + /** + * dans $s, faire les remplacements $key => $value du tableau $replaces + * + * si $verifix_order, le tableau est réordonné par taille de chaine source + */ + static final function replace(?string $s, ?array $replaces, bool $verifix_order=true): ?string { + if ($s === null || $replaces === null) return $s; + if ($verifix_order) { + uksort($replaces, function ($a, $b) { + return -cv::compare(strlen($a), strlen($b)); + }); + } + return str_replace(array_keys($replaces), array_values($replaces), $s); + } + + /** si $text est non vide, ajouter $prefix$text$suffix à $s en séparant la valeur avec un espace */ + static final function add(?string &$s, ?string $text, ?string $prefix=null, ?string $suffix=null): void { + self::addsep($s, " ", $text, $prefix, $suffix); + } + + /** splitter $s en deux chaines séparées par $sep */ + static final function split_pair(?string $s, string $sep=":"): array { + if ($s === null) return [null, null]; + $parts = explode($sep, $s, 2); + if ($parts === false) return [null, null]; + if (count($parts) < 2) $parts[] = null; + return $parts; + } + + /** retourner $line sans son caractère de fin de ligne */ + static final function strip_nl(?string $line): ?string { + if ($line === null) return null; + if (substr($line, -2) == "\r\n") { + $line = substr($line, 0, -2); + } elseif (substr($line, -1) == "\n") { + $line = substr($line, 0, -1); + } elseif (substr($line, -1) == "\r") { + $line = substr($line, 0, -1); + } + return $line; + } + + /** + * normaliser le caractère de fin de ligne: tous les occurrences de [CR]LF et CR sont remplacées par LF + */ + static final function norm_nl(?string $s): ?string { + if ($s === null) return null; + $s = str_replace("\r\n", "\n", $s); + $s = str_replace("\r", "\n", $s); + return $s; + } + + /** découper la chaine sur tout ensemble de caractères espaces */ + static final function split_tokens(?string $s): ?array { + $s = self::trim(self::norm_nl($s)); + if ($s === null) return null; + elseif ($s === "") return []; + else return preg_split('/\s+/', $s); + } + + /** + * découper la chaine: + * - avec preg_split si $sep est une expression régulière /re/ + * - avec explode sinon + */ + static final function split(string $sep, ?string $s): ?array { + if ($s === null) return null; + if ($sep === "") { + return [$s]; + } elseif (substr($sep, 0, 1) === "/") { + return preg_split($sep, $s); + } else { + return explode($sep, $s); + } + } + + /** + * joindre les éléments de $parts comme avec implode(), mais en ignorant les + * valeurs fausses (cela n'inclue pas la chaine "0") + * + * pour chaque valeur du tableau avec une clé associative, c'est la clé qui + * est utilisée mais uniquement si la valeur est vraie + */ + static final function join(string $glue, ?iterable $values): ?string { + if ($values === null) return null; + $pieces = []; + $index = 0; + foreach ($values as $key => $value) { + if (is_array($value)) $value = self::join($glue, $value); + if ($key === $index) { + $index++; + if (cv::t($value)) $pieces[] = $value; + } elseif (cv::t($value)) { + $pieces[] = $key; + } + } + return implode($glue, $pieces); + } + + /** + * comme {@link join()} mais en ignorant les valeurs fausses selon les règles + * de PHP + */ + static final function pjoin(string $glue, ?iterable $values): ?string { + if ($values === null) return null; + $pieces = []; + $index = 0; + foreach ($values as $key => $value) { + if (is_array($value)) $value = self::join($glue, $value); + if ($key === $index) { + $index++; + if ($value) $pieces[] = $value; + } elseif ($value) { + $pieces[] = $key; + } + } + return implode($glue, $pieces); + } + + const CAMEL_PATTERN0 = '/([A-Z0-9]+)$/A'; + const CAMEL_PATTERN1 = '/([A-Z0-9]+)[A-Z]/A'; + const CAMEL_PATTERN2 = '/([A-Z]?[^A-Z]+)/A'; + const CAMEL_PATTERN3 = '/([A-Z][^A-Z]*)/A'; + + /** + * convertir une chaine de la forme "camelCase" en "under_score". le premier + * ensemble de caractères en majuscule est considéré comme étant en minuscule + * + * par exemple: + * - 'myCamelCase' devient 'my_camel_case' + * - 'AValue' devient 'a_value' + * - 'UPPERValue' devient 'upper_value' + * - 'UPPER' devient 'upper' + * - 'aXYZ' devient 'a_x_y_z' + * + * $delimiter est le séparateur en sortie ('_' par défaut) + * $upper indique s'il faut transformer le résultat en majuscule + */ + static final function camel2us(?string $camel, bool $upper=false, string $delimiter="_"): ?string { + if ($camel === null || $camel === "") return $camel; + $prefix = null; + if (preg_match('/^(_+)(.*)/', $camel, $ms)) { + $prefix = $ms[1]; + $camel = $ms[2]; + } + $parts = []; + if (preg_match(self::CAMEL_PATTERN0, $camel, $ms, PREG_OFFSET_CAPTURE)) { + # que des majuscules + } elseif (preg_match(self::CAMEL_PATTERN1, $camel, $ms, PREG_OFFSET_CAPTURE)) { + # préfixe en majuscule + } elseif (preg_match(self::CAMEL_PATTERN2, $camel, $ms, PREG_OFFSET_CAPTURE)) { + # préfixe en minuscule + } else { + throw ValueException::invalid_kind($camel, "camel string"); + } + $parts[] = strtolower($ms[1][0]); + $index = intval($ms[1][1]) + strlen($ms[1][0]); + while (preg_match(self::CAMEL_PATTERN3, $camel, $ms, PREG_OFFSET_CAPTURE, $index) !== false && $ms) { + $parts[] = strtolower($ms[1][0]); + $index = intval($ms[1][1]) + strlen($ms[1][0]); + } + $us = implode($delimiter, $parts); + if ($upper) $us = strtoupper($us); + return "$prefix$us"; + } + + const US_PATTERN = '/([ _\-\t\r\n\f\v])/'; + + /** + * convertir une chaine de la forme "under_score" en "camelCase" + * + * par exemple, 'my_camel_case' devient 'myCamelCalse' + * et 'UPPER_VALUE' devient 'upperValue' + * + * si la chaine de départ ne contient pas de delimiter, e.g 'myValue', elle + * est retournée inchangée + */ + static final function us2camel(?string $us, ?string $delimiters=null): ?string { + if ($us === null || $us === "") return $us; + $prefix = null; + if (preg_match('/^(_+)(.*)/', $us, $ms)) { + $prefix = $ms[1]; + $us = $ms[2]; + } + if ($delimiters === null) $pattern = self::US_PATTERN; + else $pattern = '/(['.preg_quote($delimiters).'])/'; + $parts = preg_split($pattern, $us); + $count = count($parts); + if ($count == 1) return $us; + for ($i = 0; $i < $count; $i++) { + $part = strtolower($parts[$i]); + if ($i > 0) $part = ucfirst($part); + $parts[$i] = $part; + } + return $prefix.implode("", $parts); + } +} diff --git a/php/src/text/Word.php b/php/src/text/Word.php new file mode 100644 index 0000000..a91b03b --- /dev/null +++ b/php/src/text/Word.php @@ -0,0 +1,279 @@ +fem = $fem; + $this->le = $le; + $this->ce = $ce; + $this->du = $du; + $this->au = $au; + $this->w = $spec; + } + + /** + * retourner le mot sans article + * + * @param bool|int $amount nombre du nom, avec l'équivalence false===0 et + * true===2. à partir de 2, le mot est ecrit au pluriel + * @param bool|string $fem genre du nom avec lequel accorder les adjectifs, + * avec l'équivalence false==="M" et true==="F" + */ + function w($amount=1, bool $upper1=false, $fem=null): string { + if ($amount === true) $amount = 2; + elseif ($amount === false) $amount = 0; + $amount = abs($amount); + $fem ??= $this->fem; + $w = $this->w; + # marque du nombre + if ($amount <= 1) { + $w = preg_replace('/#[sx]/', "", $w); + } else { + $w = preg_replace('/#([sx])/', "$1", $w); + } + # marque du genre + if ($fem === "f" || $fem === "F") $fem = true; + elseif ($fem === "m" || $fem === "M") $fem = false; + $repl = $fem? "$1": ""; + $w = preg_replace('/#([e])/', $repl, $w); + # mise en majuscule + if ($upper1) { + if (strpos($w, "^") === false) { + # uniquement la première lettre + $w = txt::upper1($w); + } else { + # toutes les lettres qui suivent les occurences de ^ + $w = preg_replace_callback('/\^([[:alpha:]])/u', function ($ms) { + return mb_strtoupper($ms[1]); + }, $w); + } + } + return $w; + } + + /** + * retourner le mot sans article avec la première lettre en majuscule. + * alias pour $this->w($amount, true, $fem) + * + * @param bool|int $amount + */ + function u($amount=1): string { + return $this->w($amount, true); + } + + /** + * retourner l'adjectif accordé avec le genre spécifié. + * alias pour $this->w($amount, false, $fem) + * + * @param bool|int $amount + */ + function a($fem=false, $amount=1): string { + return $this->w($amount, false, $fem); + } + + /** retourner le mot sans article et avec la quantité */ + function q(int $amount=1): string { + return $amount." ".$this->w($amount); + } + + /** retourner le mot sans article et avec la quantité $amount/$max */ + function r(int $amount, int $max): string { + return "$amount/$max ".$this->w($amount); + } + + /** retourner le mot avec l'article indéfini et la quantité */ + function un(int $amount=1): string { + $abs_amount = abs($amount); + if ($abs_amount == 0) { + $aucun = $this->fem? "aucune ": "aucun "; + return $aucun.$this->w($amount); + } elseif ($abs_amount == 1) { + $un = $this->fem? "une ": "un "; + return $un.$this->w($amount); + } else { + return "les $amount ".$this->w($amount); + } + } + + /** retourner le mot avec l'article indéfini mais sans la quantité */ + function _un(int $amount=1): string { + $abs_amount = abs($amount); + if ($abs_amount <= 1) { + $un = $this->fem? "une ": "un "; + return $un.$this->w($amount); + } else { + return "les ".$this->w($amount); + } + } + + function le(int $amount=1): string { + $abs_amount = abs($amount); + if ($abs_amount == 0) { + $le = $this->fem? "la 0 ": "le 0 "; + return $le.$this->w($amount); + } elseif ($abs_amount == 1) { + return $this->le.$this->w($amount); + } else { + return "les $amount ".$this->w($amount); + } + } + + function _le(int $amount=1): string { + $abs_amount = abs($amount); + if ($abs_amount <= 1) { + return $this->le.$this->w($amount); + } else { + return "les ".$this->w($amount); + } + } + + function ce(int $amount=1): string { + $abs_amount = abs($amount); + if ($abs_amount == 0) { + $ce = $this->fem? "cette 0 ": "ce 0 "; + return $ce.$this->w($amount); + } elseif ($abs_amount == 1) { + return $this->ce.$this->w($amount); + } else { + return "ces $amount ".$this->w($amount); + } + } + + function _ce(int $amount=1): string { + $abs_amount = abs($amount); + if ($abs_amount <= 1) { + return $this->ce.$this->w($amount); + } else { + return "ces ".$this->w($amount); + } + } + + function du(int $amount=1): string { + $abs_amount = abs($amount); + if ($abs_amount == 0) { + $du = $this->fem? "de la 0 ": "du 0 "; + return $du.$this->w($amount); + } elseif ($abs_amount == 1) { + return $this->du.$this->w($amount); + } else { + return "des $amount ".$this->w($amount); + } + } + + function _du(int $amount=1): string { + $abs_amount = abs($amount); + if ($abs_amount <= 1) { + return $this->du.$this->w($amount); + } else { + return "des ".$this->w($amount); + } + } + + function au(int $amount=1): string { + $abs_amount = abs($amount); + if ($abs_amount == 0) { + $au = $this->fem? "à la 0 ": "au 0 "; + return $au.$this->w($amount); + } elseif ($abs_amount == 1) { + return $this->au.$this->w($amount); + } else { + return "aux $amount ".$this->w($amount); + } + } + + function _au(int $amount=1): string { + $abs_amount = abs($amount); + if ($abs_amount <= 1) { + return $this->au.$this->w($amount); + } else { + return "aux ".$this->w($amount); + } + } +} diff --git a/php/src/text/words.php b/php/src/text/words.php new file mode 100644 index 0000000..7906c30 --- /dev/null +++ b/php/src/text/words.php @@ -0,0 +1,19 @@ +q($count); + } + + static function r(int $count, int $max, string $spec, bool $adjective=true): string { + $word = new Word($spec, $adjective); + return $word->r($count, $max); + } +} diff --git a/php/src/tools/BgLauncherApp.php b/php/src/tools/BgLauncherApp.php new file mode 100644 index 0000000..88aa522 --- /dev/null +++ b/php/src/tools/BgLauncherApp.php @@ -0,0 +1,124 @@ + "lancer un script en tâche de fond", + "usage" => "ApplicationClass args...", + + "sections" => [ + parent::VERBOSITY_SECTION, + ], + + ["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS, + "help" => "Afficher des informations sur la tâche", + ], + ["-s", "--start", "name" => "action", "value" => self::ACTION_START, + "help" => "Démarrer la tâche", + ], + ["-k", "--stop", "name" => "action", "value" => self::ACTION_STOP, + "help" => "Arrêter la tâche", + ], + ]; + + protected int $action = self::ACTION_START; + + protected ?array $args = null; + + static function show_infos(RunFile $runfile, ?int $level=null): void { + msg::print($runfile->getDesc(), $level); + msg::print(yaml::with(["data" => $runfile->read()]), ($level ?? 0) - 1); + } + + function main() { + $args = $this->args; + + $appClass = $args[0] ?? null; + if ($appClass === null) { + self::die("Vous devez spécifier la classe de l'application"); + } + $appClass = $args[0] = str_replace("/", "\\", $appClass); + if (!class_exists($appClass)) { + self::die("$appClass: classe non trouvée"); + } + + $useRunfile = constant("$appClass::USE_RUNFILE"); + if (!$useRunfile) { + self::die("Cette application ne supporte le lancement en tâche de fond"); + } + + $runfile = app::with($appClass)->getRunfile(); + switch ($this->action) { + case self::ACTION_START: + $argc = count($args); + $appClass::_manage_runfile($argc, $args, $runfile); + if ($runfile->warnIfLocked()) self::exit(app::EC_LOCKED); + array_splice($args, 0, 0, [ + PHP_BINARY, + path::abspath(NULIB_APP_app_launcher), + ]); + app::params_putenv(); + self::_start($args, $runfile); + break; + case self::ACTION_STOP: + self::_stop($runfile); + self::show_infos($runfile, -1); + break; + case self::ACTION_INFOS: + self::show_infos($runfile); + break; + } + } + + public static function _start(array $args, Runfile $runfile): void { + $pid = pcntl_fork(); + if ($pid == -1) { + # parent, impossible de forker + throw new ExitError(app::EC_FORK_PARENT, "Unable to fork"); + } elseif (!$pid) { + # child, fork ok + $runfile->wfPrepare($pid); + $outfile = $runfile->getOutfile() ?? "/tmp/NULIB_APP_app_console.out"; + $exitcode = app::EC_FORK_CHILD; + try { + # rediriger STDIN, STDOUT et STDERR + fclose(fopen($outfile, "wb")); // vider le fichier + fclose(STDIN); $in = fopen("/dev/null", "rb"); + fclose(STDOUT); $out = fopen($outfile, "ab"); + fclose(STDERR); $err = fopen($outfile, "ab"); + # puis lancer la commande + $cmd = new Cmd($args); + $cmd->addSource("/g/init.env"); + $cmd->addRedir("both", $outfile, true); + $cmd->fork_exec($exitcode, false); + sh::_waitpid(-$pid, $exitcode); + } finally { + $runfile->wfReaped($exitcode); + } + } + } + + public static function _stop(Runfile $runfile): bool { + $data = $runfile->read(); + $pid = $runfile->_getCid($data); + msg::action("stop $pid"); + if ($runfile->wfKill($reason)) { + msg::asuccess(); + return true; + } else { + msg::afailure($reason); + return false; + } + } +} diff --git a/php/src/tools/SteamTrainApp.php b/php/src/tools/SteamTrainApp.php new file mode 100644 index 0000000..7e00ef0 --- /dev/null +++ b/php/src/tools/SteamTrainApp.php @@ -0,0 +1,53 @@ + self::TITLE, + "description" => << 1, + "help" => "spécifier le nombre d'étapes", + ], + ["-f", "--force-enabled", "value" => true, + "help" => "lancer la commande même si les tâches planifiées sont désactivées", + ], + ["-n", "--no-install-signal-handler", "value" => false, + "help" => "ne pas installer le gestionnaire de signaux", + ], + ]; + + protected $count = 100; + + protected bool $forceEnabled = false; + + protected bool $installSignalHandler = true; + + function main() { + app::check_bgapplication_enabled($this->forceEnabled); + if ($this->installSignalHandler) app::install_signal_handler(); + $count = intval($this->count); + msg::info("Starting train for ".words::q($count, "step#s")); + app::action("Running train...", $count); + for ($i = 1; $i <= $count; $i++) { + msg::print("Tchou-tchou! x $i"); + app::step(); + sleep(1); + } + msg::info("Stopping train at ".new DateTime()); + } +} diff --git a/php/src/tools/pman/ComposerFile.php b/php/src/tools/pman/ComposerFile.php new file mode 100644 index 0000000..c3dd7a1 --- /dev/null +++ b/php/src/tools/pman/ComposerFile.php @@ -0,0 +1,172 @@ +composerFile = $composerFile; + $this->load(); + } + + protected string $composerFile; + + function getComposerFile(): string { + return $this->composerFile; + } + + protected ?array $data = null; + + protected function load(): array { + if ($this->data === null) { + $this->data = json::load($this->composerFile); + } + return $this->data; + } + + function getv(string $pkey="", $default=null) { + return cl::pget($this->data, $pkey, $default); + } + + function geta(string $pkey="", ?array $default=[]): ?array { + return cl::withn($this->getv($pkey, $default)); + } + + function getRequires(): array { + return $this->geta("require"); + } + + function setRequire(string $dep, string $version): void { + $this->setPkey("require.$dep", $version); + } + + function getRequireDevs(): array { + return $this->geta("require-dev"); + } + + function setRequireDev(string $dep, string $version): void { + $this->setPkey("require-dev.$dep", $version); + } + + function getRepositories(): array { + return $this->geta("repositories"); + } + + function setRepositories(array $repositories): void { + $this->data["repositories"] = $repositories; + } + + function setPkey(string $pkey, $value): void { + cl::pset($this->data, $pkey, $value); + } + + function delPkey(string $pkey): void { + cl::pdel($this->data, $pkey); + } + + const PATHS = [ + "nulib/php" => "nulib", + ]; + + function selectProfile(string $profile, ComposerPmanFile $config): void { + $config = $config->getProfileConfig($profile, $this->getRequires(), $this->getRequireDevs()); + // corriger les liens + $deps = cl::merge(array_keys($config["require"]), array_keys($config["require-dev"])); + $paths = []; + foreach ($deps as $dep) { + $path = cl::get(self::PATHS, $dep, $dep); + $path = str_replace("/", "-", $path); + $path = "../$path"; + $paths[$dep] = $path; + } + if ($config["link"]) { + // Ajouter les liens + $adds = []; + $repositories = $this->getRepositories(); + foreach ($deps as $dep) { + $found = false; + foreach ($repositories as $repository) { + if ($repository["type"] === "path" && $repository["url"] === $paths[$dep]) { + $found = true; + break; + } + } + if (!$found) { + $adds[] = [ + "type" => "path", + "url" => $paths[$dep], + ]; + } + } + if ($adds) { + $this->setRepositories(cl::merge($adds, $repositories)); + } + } else { + // Supprimer les liens + $dels = []; + $repositories = $this->getRepositories(); + foreach ($deps as $dep) { + foreach ($repositories as $key => $repository) { + if ($repository["type"] === "path" && $repository["url"] === $paths[$dep]) { + $dels[] = $key; + break; + } + } + } + if ($dels) { + foreach (array_reverse($dels) as $key) { + unset($repositories[$key]); + } + $this->setRepositories(array_values($repositories)); + } + } + // corriger les versions + foreach ($config["require"] as $dep => $version) { + $this->data["require"][$dep] = $version; + } + foreach ($config["require-dev"] as $dep => $version) { + $this->data["require-dev"][$dep] = $version; + } + } + + function getLocalDeps(): array { + $deps = cl::merge(array_keys($this->getRequires()), array_keys($this->getRequireDevs())); + $paths = []; + foreach ($deps as $dep) { + $path = cl::get(self::PATHS, $dep, $dep); + $path = str_replace("/", "-", $path); + $path = "../$path"; + $paths[$dep] = $path; + } + $repositories = $this->getRepositories(); + $localDeps = []; + foreach ($deps as $dep) { + foreach ($repositories as $key => $repository) { + if ($repository["type"] === "path" && $repository["url"] === $paths[$dep]) { + $localDeps[$dep] = $repository["url"]; + break; + } + } + } + return $localDeps; + } + + function print(): void { + $contents = json::with($this->data, json::INDENT_TABS); + if ($contents) echo "$contents\n"; + } + + function write(): void { + $contents = json::with($this->data, json::INDENT_TABS); + file::writer($this->composerFile)->putContents("$contents\n"); + } +} diff --git a/php/src/tools/pman/ComposerPmanFile.php b/php/src/tools/pman/ComposerPmanFile.php new file mode 100644 index 0000000..d8c6474 --- /dev/null +++ b/php/src/tools/pman/ComposerPmanFile.php @@ -0,0 +1,110 @@ +configFile = $configFile; + $this->load(); + } + + protected string $configFile; + + function getConfigFile(): string { + return $this->configFile; + } + + protected ?array $data = null; + + protected function load(): array { + if ($this->data === null) { + $data = yaml::load($this->configFile); + $composer =& $data["composer"]; + A::ensure_array($composer); + A::ensure_array($composer["profiles"]); + A::ensure_array($composer["match_require"]); + A::ensure_array($composer["match_require-dev"]); + foreach ($composer["profiles"] as $profileName) { + $profile =& $composer[$profileName]; + A::ensure_array($profile); + $profile["link"] = boolval($profile["link"] ?? false); + A::ensure_array($profile["require"]); + A::ensure_array($profile["require-dev"]); + } + $this->data = $data; + } + return $this->data; + } + + function getProfileConfig(string $profile, ?array $composerRequires=null, ?array $composerRequireDevs=null): array { + $config = $this->data["composer"][$profile] ?? null; + if ($config === null) { + throw new ValueException("$profile: profil invalide"); + } + if ($composerRequires !== null) { + $matchRequires = $this->data["composer"]["match_require"]; + foreach ($composerRequires as $dep => $version) { + $found = false; + foreach ($matchRequires as $matchRequire) { + if (str::match_prefix($dep, $matchRequire)) { + $found = true; + break; + } + } + $require = $config["require"][$dep] ?? null; + if ($found && $require === null) { + $config["require"][$dep] = $version; + } + } + } + if ($composerRequireDevs !== null) { + $matchRequireDevs = $this->data["composer"]["match_require-dev"]; + foreach ($composerRequireDevs as $dep => $version) { + $found = false; + foreach ($matchRequireDevs as $matchRequireDev) { + if (str::match_prefix($dep, $matchRequireDev)) { + $found = true; + break; + } + } + $requireDev = $config["require-dev"][$dep] ?? null; + if ($found && $requireDev === null) { + $config["require"][$dep] = $version; + } + } + } + return $config; + } + + function print(): void { + yaml::dump($this->data); + } +} diff --git a/php/src/txt.php b/php/src/txt.php new file mode 100644 index 0000000..9857473 --- /dev/null +++ b/php/src/txt.php @@ -0,0 +1,294 @@ + $length) { + if ($ellips && $length > 3) $s = mb_substr($s, 0, $length - 3)."..."; + else $s = mb_substr($s, 0, $length); + } + if ($suffix !== null) $s .= $suffix; + return $s; + } + + /** trimmer $s */ + static final function trim(?string $s): ?string { + if ($s === null) return null; + return trim($s); + } + + /** trimmer $s à gauche */ + static final function ltrim(?string $s): ?string { + if ($s === null) return null; + return ltrim($s); + } + + /** trimmer $s à droite */ + static final function rtrim(?string $s): ?string { + if ($s === null) return null; + return rtrim($s); + } + + static final function left(?string $s, int $size): ?string { + if ($s === null) return null; + return mb_str_pad($s, $size); + } + + static final function right(?string $s, int $size): ?string { + if ($s === null) return null; + return mb_str_pad($s, $size, " ", STR_PAD_LEFT); + } + + static final function center(?string $s, int $size): ?string { + if ($s === null) return null; + return mb_str_pad($s, $size, " ", STR_PAD_BOTH); + } + + static final function pad0(?string $s, int $size): ?string { + if ($s === null) return null; + return mb_str_pad($s, $size, "0", STR_PAD_LEFT); + } + + static final function lower(?string $s): ?string { + if ($s === null) return null; + return mb_strtolower($s); + } + + static final function lower1(?string $s): ?string { + if ($s === null) return null; + return mb_strtolower(mb_substr($s, 0, 1)).mb_substr($s, 1); + } + + static final function upper(?string $s): ?string { + if ($s === null) return null; + return mb_strtoupper($s); + } + + static final function upper1(?string $s): ?string { + if ($s === null) return null; + return mb_strtoupper(mb_substr($s, 0, 1)).mb_substr($s, 1); + } + + static final function upperw(?string $s, ?string $delimiters=null): ?string { + if ($s === null) return null; + if ($delimiters === null) $delimiters = " _-\t\r\n\f\v"; + $pattern = "/([".preg_quote($delimiters)."])/u"; + $words = preg_split($pattern, $s, -1, PREG_SPLIT_DELIM_CAPTURE); + $max = count($words) - 1; + $ucwords = []; + for ($i = 0; $i < $max; $i += 2) { + $s = $words[$i]; + $ucwords[] = mb_strtoupper(mb_substr($s, 0, 1)).mb_substr($s, 1); + $ucwords[] = $words[$i + 1]; + } + $s = $words[$max]; + $ucwords[] = mb_strtoupper(mb_substr($s, 0, 1)).mb_substr($s, 1); + return implode("", $ucwords); + } + + protected static final function _starts_with(string $prefix, string $s, ?int $min_len=null): bool { + if ($prefix === $s) return true; + $len = mb_strlen($prefix); + if ($min_len !== null && ($len < $min_len || $len > mb_strlen($s))) return false; + return $len == 0 || $prefix === mb_substr($s, 0, $len); + } + + /** + * tester si $s commence par $prefix + * par exemple: + * - starts_with("", "whatever") est true + * - starts_with("fi", "first") est true + * - starts_with("no", "yes") est false + * + * si $min_len n'est pas null, c'est la longueur minimum requise de $prefix + * pour qu'on teste la correspondance. dans le cas contraire, la valeur de + * retour est toujours false, sauf s'il y a égalité. e.g + * - starts_with("a", "abc", 2) est false + * - starts_with("a", "a", 2) est true + */ + static final function starts_with(?string $prefix, ?string $s, ?int $min_len=null): bool { + if ($s === null || $prefix === null) return false; + else return self::_starts_with($prefix, $s, $min_len); + } + + /** Retourner $s sans le préfixe $prefix s'il existe */ + static final function without_prefix(?string $prefix, ?string $s): ?string { + if ($s === null || $prefix === null) return $s; + if (self::_starts_with($prefix, $s)) $s = mb_substr($s, mb_strlen($prefix)); + return $s; + } + + /** + * modifier $s en place pour supprimer le préfixe $prefix s'il existe + * + * retourner true si le préfixe a été enlevé. + */ + static final function del_prefix(?string &$s, ?string $prefix): bool { + if ($s === null || !self::_starts_with($prefix, $s)) return false; + $s = self::without_prefix($prefix, $s); + return true; + } + + /** + * Retourner $s avec le préfixe $prefix + * + * Si $unless_exists, ne pas ajouter le préfixe s'il existe déjà + */ + static final function with_prefix(?string $prefix, ?string $s, ?string $sep=null, bool $unless_exists=false): ?string { + if ($s === null || $prefix === null) return $s; + if (!self::_starts_with($prefix, $s) || !$unless_exists) $s = $prefix.$sep.$s; + return $s; + } + + /** + * modifier $s en place pour ajouter le préfixe $prefix + * + * retourner true si le préfixe a été ajouté. + */ + static final function add_prefix(?string &$s, ?string $prefix, bool $unless_exists=true): bool { + if (($s === null || self::_starts_with($prefix, $s)) && $unless_exists) return false; + $s = self::with_prefix($prefix, $s, null, $unless_exists); + return true; + } + + protected static final function _ends_with(string $suffix, string $s, ?int $min_len=null): bool { + if ($suffix === $s) return true; + $len = mb_strlen($suffix); + if ($min_len !== null && ($len < $min_len || $len > mb_strlen($s))) return false; + return $len == 0 || $suffix === mb_substr($s, -$len); + } + + /** + * tester si $string se termine par $suffix + * par exemple: + * - ends_with("", "whatever") est true + * - ends_with("st", "first") est true + * - ends_with("no", "yes") est false + * + * si $min_len n'est pas null, c'est la longueur minimum requise de $prefix + * pour qu'on teste la correspondance. dans le cas contraire, la valeur de + * retour est toujours false, sauf s'il y a égalité. e.g + * - ends_with("c", "abc", 2) est false + * - ends_with("c", "c", 2) est true + */ + static final function ends_with(?string $suffix, ?string $s, ?int $min_len=null): bool { + if ($s === null || $suffix === null) return false; + else return self::_ends_with($suffix, $s, $min_len); + } + + /** Retourner $s sans le suffixe $suffix s'il existe */ + static final function without_suffix(?string $suffix, ?string $s): ?string { + if ($s === null || $suffix === null) return $s; + if (self::_ends_with($suffix, $s)) $s = mb_substr($s, 0, -mb_strlen($suffix)); + return $s; + } + + /** + * modifier $s en place pour supprimer le suffixe $suffix s'il existe + * + * retourner true si le suffixe a été enlevé. + */ + static final function del_suffix(?string &$s, ?string $suffix): bool { + if ($s === null || !self::_ends_with($suffix, $s)) return false; + $s = self::without_suffix($suffix, $s); + return true; + } + + /** + * Retourner $s avec le suffixe $suffix + * + * Si $unless_exists, ne pas ajouter le suffixe s'il existe déjà + */ + static final function with_suffix(?string $suffix, ?string $s, ?string $sep=null, bool $unless_exists=false): ?string { + if ($s === null || $suffix === null) return $s; + if (!self::_ends_with($suffix, $s) || !$unless_exists) $s = $s.$sep.$suffix; + return $s; + } + + /** + * modifier $s en place pour ajouter le suffixe $suffix + * + * retourner true si le suffixe a été ajouté. + */ + static final function add_suffix(?string &$s, ?string $suffix, bool $unless_exists=true): bool { + if (($s === null || self::_ends_with($suffix, $s)) && $unless_exists) return false; + $s = self::with_suffix($suffix, $s, null, $unless_exists); + return true; + } + + /** + * ajouter $sep$prefix$text$suffix à $s si $text est non vide + * + * NB: ne rajouter $sep que si $s est non vide + */ + static final function addsep(?string &$s, ?string $sep, ?string $text, ?string $prefix=null, ?string $suffix=null): void { + if (!$text) return; + if ($s) $s .= $sep; + $s .= $prefix.$text.$suffix; + } + + /** si $text est non vide, ajouter $prefix$text$suffix à $s en séparant la valeur avec un espace */ + static final function add(?string &$s, ?string $text, ?string $prefix=null, ?string $suffix=null): void { + self::addsep($s, " ", $text, $prefix, $suffix); + } + + ############################################################################# + # divers + + /** + * supprimer les diacritiques de la chaine $text + * + * la translitération se fait avec les règles de la locale spécifiée. + * NB: la translitération ne fonctionne pas si LC_CTYPE == C ou POISX + */ + static final function remove_diacritics(?string $text, string $locale="fr_FR.UTF-8"): ?string { + if ($text === null) return null; + #XXX est-ce thread-safe? + $olocale = setlocale(LC_CTYPE, 0); + try { + setlocale(LC_CTYPE, $locale); + $clean = @iconv("UTF-8", "US-ASCII//TRANSLIT", $text); + if ($clean === false) $clean = ""; + return $clean; + } finally { + setlocale(LC_CTYPE, $olocale); + } + } +} diff --git a/php/src/web/curl/CurlException.php b/php/src/web/curl/CurlException.php new file mode 100644 index 0000000..53fda92 --- /dev/null +++ b/php/src/web/curl/CurlException.php @@ -0,0 +1,22 @@ + $value) { + if (is_array($value)) { + self::parse_files($files, "$pkey.$key", $value, $lastkey); + } else { + cl::pset($files, "$pkey.$key.$lastkey", $value); + } + } + } + + /** @var array */ + private static $_files; + + static function _files(?array $_files=null): ?array { + if (self::$_files === null) { + $files = []; + if ($_files === null) $_files = $_FILES; + foreach ($_files as $pkey => $values) { + $name = $values["name"] ?? null; + $type = $values["type"] ?? null; + $error = $values["error"] ?? null; + if (is_scalar($name) && is_scalar($type) && is_scalar($error)) { + $files[$pkey] = $values; + } else { + self::parse_files($files, $pkey, $values["name"], "name"); + self::parse_files($files, $pkey, $values["type"], "type"); + self::parse_files($files, $pkey, $values["tmp_name"], "tmp_name"); + self::parse_files($files, $pkey, $values["error"], "error"); + self::parse_files($files, $pkey, $values["size"], "size"); + $full_path = $values["full_path"] ?? null; + if ($full_path !== null) { + self::parse_files($files, $pkey, $full_path, "full_path"); + } + } + } + self::$_files = $files; + } + return self::$_files; + } + + static function get(string $pkey, bool $required=true, bool $check=true): Upload { + $_files = self::_files(); + return new Upload(cl::pget($_files, $pkey), $required, $check); + } + + static function all(string $pkey, bool $required=true, bool $check=true) { + $_files = self::_files(); + $uploads = []; + foreach (cl::pget($_files, $pkey) as $file) { + $uploads[] = new Upload($file, $required, $check); + } + return $uploads; + } +} diff --git a/php/support/copy-templates b/php/support/copy-templates new file mode 100755 index 0000000..a426860 --- /dev/null +++ b/php/support/copy-templates @@ -0,0 +1,39 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../../load.sh" || exit 1 + +declare -A DESTDIRS=( + [template-_bg_launcher.php]=sbin + [template-.launcher.php]=_cli + [template-_wrapper.sh]=_cli +) +declare -A MODES=( + [template-_bg_launcher.php]=+x + [template-.launcher.php]= + [template-_wrapper.sh]=+x +) + +projdir= +args=( + "copier les templates dans le projet en cours" + #"usage" + -d:,--projdir: . +) +parse_args "$@"; set -- "${args[@]}" + +if [ -n "$projdir" ]; then + cd "$projdir" || die +fi + +[ -f composer.json ] || die "$(basename "$(dirname "$(pwd)")"): n'est pas un projet composer" + +setx -a templates=ls_files "$MYDIR" "template-*" +for template in "${templates[@]}"; do + destdir="${DESTDIRS[$template]}" + [ -n "$destdir" ] || die "$template: la destination n'est pas configurée" + mode="${MODES[$template]}" + destname="${template#template-}" + + tail -n+4 "$MYDIR/$template" >"$destdir/$destname" + [ -n "$mode" ] && chmod "$mode" "$destdir/$destname" +done diff --git a/php/support/template-.launcher.php b/php/support/template-.launcher.php new file mode 100644 index 0000000..eeb2b41 --- /dev/null +++ b/php/support/template-.launcher.php @@ -0,0 +1,12 @@ +# TODO Faire une copie de ce script dans un répertoire de l'application web +# (dans le répertoire _cli/ par défaut) et modifier les paramètres si nécessaire +#------------------------------------------------------------------------------- + __DIR__ . '/..', + "appcode" => \app\config\bootstrap::APPCODE, +]; +require __DIR__.'/../vendor/nulib/php/php/src/app/cli/include-launcher.php'; diff --git a/php/support/template-_bg_launcher.php b/php/support/template-_bg_launcher.php new file mode 100644 index 0000000..88147f5 --- /dev/null +++ b/php/support/template-_bg_launcher.php @@ -0,0 +1,18 @@ +# TODO Faire une copie de ce script dans un répertoire de l'application web +# (dans le répertoire sbin/ par défaut) et modifier les paramètres si nécessaire +#------------------------------------------------------------------------------- + __DIR__.'/..', + "appcode" => \app\config\bootstrap::APPCODE, +]); +BgLauncherApp::run(); diff --git a/php/support/template-_wrapper.sh b/php/support/template-_wrapper.sh new file mode 100644 index 0000000..22c34f6 --- /dev/null +++ b/php/support/template-_wrapper.sh @@ -0,0 +1,132 @@ +# TODO Faire une copie de ce script dans un répertoire de l'application web +# (dans le répertoire _cli/ par défaut) et modifier les paramétres si nécessaire +#------------------------------------------------------------------------------- +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +# S'assurer que le script PHP est lancé avec l'utilisateur www-data +# Tous les chemins suivants sont relatifs au répertoire qui contient ce script + +# Chemin relatif de la racine du projet +PROJPATH=.. + +# Chemin relatif vers le lanceur PHP +LAUNCHERPATH=.launcher.php + +# Chemin relatif des scripts PHP wrappés +WRAPPEDPATH= + +# Nom du service dans docker-compose.yml utilisé pour lancer les commandes +COMPOSE_SERVICE=web + +############################################################################### + +case "${RUNPHP_MODE:-auto}" in +auto) + RUNPHP_MODE= + [ -f /.dockerenv ] && RUNPHP_MODE=docker + [ -z "$RUNPHP_MODE" ] && + [ -f /proc/self/mountinfo ] && + grep -q ' /docker/' /proc/self/mountinfo && + RUNPHP_MODE=docker + [ -z "$RUNPHP_MODE" ] && + [ -f /proc/1/cgroup ] && + grep -q ':/docker/' /proc/1/cgroup && + RUNPHP_MODE=docker + [ -n "$RUNPHP_MODE" ] || RUNPHP_MODE=host + ;; +docker) RUNPHP_MODE=docker;; +host) RUNPHP_MODE=host;; +direct) RUNPHP_MODE=direct;; +*) RUNPHP_MODE=host;; +esac + +MYDIR="$(dirname -- "$0")" +MYNAME="$(basename -- "$0")" +if [ ! -L "$0" ]; then + echo "\ +$0 +Ce script doit être lancé en tant que lien symbolique avec un nom de la forme +'monscript.php' et lance le script PHP du même nom situé dans le même répertoire +avec l'utilisateur www-data" + + if [ "$RUNPHP_MODE" == host -o "$RUNPHP_MODE" == direct ]; then + echo "\ +---------------------------------------- +Vérification des liens..." + cd "$MYDIR" + for i in *.php*; do + [ -f "$i" ] || continue + name="bin/${i%.*}.php" + dest="../_cli/_wrapper.sh" + link="../bin/${i%.*}.php" + if [ -L "$link" ]; then + echo "* $name OK" + elif [ -e "$link" ]; then + echo "* $name KO (not a link)" + else + echo "* $name NEW" + ln -s "$dest" "$link" || exit 1 + fi + done + fi + exit 0 +fi + +MYTRUESELF="$(readlink -f "$0")" +MYTRUEDIR="$(dirname -- "$MYTRUESELF")" +PROJDIR="$(cd "$MYTRUEDIR${PROJPATH:+/$PROJPATH}"; pwd)" + +if [ "$RUNPHP_MODE" == host ]; then + args=( + docker compose run + ${RUNPHP_BUILD:+--build} + --rm + ) + cwd="$(pwd)" + mounted= + if [ "$PROJDIR" == "$HOME" -o "${PROJDIR#$HOME/}" != "$PROJDIR" ]; then + # monter HOME + args+=(-v "$HOME:$HOME") + [ "${cwd#$HOME/}" != "$cwd" ] && mounted=1 + else + # monter uniquement le répertoire du projet + args+=(-v "$PROJDIR:$PROJDIR") + [ "${cwd#$PROJDIR/}" != "$cwd" ] && mounted=1 + fi + if [ -z "$mounted" ]; then + echo "Impossible de mapper le répertoire courant avec les montages du container" + exit 1 + fi + args+=( + --workdir "$cwd" + "$COMPOSE_SERVICE" + exec "$MYNAME" + "$@" + ) + cd "$PROJDIR" + exec "${args[@]}" +fi + +launcher="$MYTRUEDIR/$LAUNCHERPATH" +class="$MYTRUEDIR${WRAPPEDPATH:+/$WRAPPEDPATH}/${MYNAME%.php}.phpc" +script="$MYTRUEDIR${WRAPPEDPATH:+/$WRAPPEDPATH}/${MYNAME%.php}.php" + +[ -f /g/init.env ] && source /g/init.env + +www_data="${DEVUSER_USERENT%%:*}" +[ -n "$www_data" ] || www_data=www-data + +cmd=(php "$launcher") +[ -n "$MEMPROF_PROFILE" ] && cmd+=(-dextension=memprof.so) +if [ -f "$class" ]; then + cmd+=("$(<"$class")") +else + cmd+=("$script") +fi +cmd+=("$@") + +if [ "$(id -u)" -eq 0 ]; then + su-exec "$www_data" "${cmd[@]}" +else + exec "${cmd[@]}" +fi diff --git a/php/tbin/.gitignore b/php/tbin/.gitignore new file mode 100644 index 0000000..f3b938a --- /dev/null +++ b/php/tbin/.gitignore @@ -0,0 +1 @@ +/*.db diff --git a/php/tbin/test_mysql.php b/php/tbin/test_mysql.php new file mode 100644 index 0000000..e2eb555 --- /dev/null +++ b/php/tbin/test_mysql.php @@ -0,0 +1,42 @@ + "mysql", + "name" => "mysql:host=mysql.devel.self;dbname=jclain;charset=utf8", + "user" => "jclain", +]); + +class MyChannel extends CapacitorChannel { + const TABLE_NAME = "my"; + const COLUMN_DEFINITIONS = [ + "name" => "varchar(64) not null", + "age" => "integer", + "num" => ["integer"], + ]; + + function getItemValues($item): ?array { + $item = cl::with($item); + return [ + "name" => cl::first($item), + "age" => $item["age"] ?? null, + "num" => rand(), + ]; + } +} + +new Capacitor(new MysqlStorage($db), $channel = new MyChannel()); + +$channel->charge("hello world"); +$channel->charge(["bonjour monde"]); +$channel->charge(["gutten tag", "age" => 15]); diff --git a/php/tbin/test_pgsql.php b/php/tbin/test_pgsql.php new file mode 100644 index 0000000..1b10f35 --- /dev/null +++ b/php/tbin/test_pgsql.php @@ -0,0 +1,41 @@ + "pegase-dre.self", + "dbname" => "dre", + "user" => "root", + "password" => "admin", + #"user" => "reader", + #"password" => "reader", +]); + +class MyChannel extends CapacitorChannel { + const TABLE_NAME = "my"; + const COLUMN_DEFINITIONS = [ + "name" => "varchar not null", + "age" => "integer", + "num" => ["integer"], + ]; + + function getItemValues($item): ?array { + $item = cl::with($item); + return [ + "name" => cl::first($item), + "age" => $item["age"] ?? null, + "num" => rand(), + ]; + } +} + +new Capacitor(new PgsqlStorage($db), $channel = new MyChannel()); + +$channel->charge("hello world"); +$channel->charge(["bonjour monde"]); +$channel->charge(["gutten tag", "age" => 15]); diff --git a/php/tbin/test_sqlite.php b/php/tbin/test_sqlite.php new file mode 100644 index 0000000..e88e3be --- /dev/null +++ b/php/tbin/test_sqlite.php @@ -0,0 +1,34 @@ + "varchar not null", + "age" => "integer", + "num" => ["integer"], + ]; + + function getItemValues($item): ?array { + $item = cl::with($item); + return [ + "name" => cl::first($item), + "age" => $item["age"] ?? null, + "num" => rand(), + ]; + } +} + +new Capacitor(new SqliteStorage($db), $channel = new MyChannel()); + +$channel->charge("hello world"); +$channel->charge(["bonjour monde"]); +$channel->charge(["gutten tag", "age" => 15]); 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/clTest.php b/php/tests/clTest.php new file mode 100644 index 0000000..bedec17 --- /dev/null +++ b/php/tests/clTest.php @@ -0,0 +1,37 @@ + 42, "b" => "tesxt"]; $arrayKeys = array_keys($array); + $missingArray = ["c" => true]; $missingArrayKeys = array_keys($missingArray); + $ref = ["a" => "int", "b" => "text"]; $refKeys = array_keys($ref); + $missingRef = ["c" => "bool"]; $missingRefKeys = array_keys($missingRef); + $xarray = ["parasite0", "a" => 42, "parasite1", "b" => "tesxt"]; + + $this->checkKeys(null, null, true, [], [], []); + $this->checkKeys(null, [], true, [], [], []); + $this->checkKeys([], null, true, [], [], []); + $this->checkKeys([], [], true, [], [], []); + + $this->checkKeys(null, $ref, false, [], [], $refKeys); + $this->checkKeys([], $ref, false, [], [], $refKeys); + + $this->checkKeys($array, null, true, [], $arrayKeys, []); + $this->checkKeys($array, [], true, [], $arrayKeys, []); + + $this->checkKeys($array, $ref, true, $arrayKeys, [], []); + $this->checkKeys(cl::merge($array, $missingArray), $ref, true, $arrayKeys, $missingArrayKeys, []); + $this->checkKeys($array, cl::merge($ref, $missingRef), false, $arrayKeys, [], $missingRefKeys); + + $this->checkKeys($xarray, $ref, false, $arrayKeys, [0, 1], []); + } +} diff --git a/php/tests/db/_private/_baseTest.php b/php/tests/db/_private/_baseTest.php new file mode 100644 index 0000000..cd2d758 --- /dev/null +++ b/php/tests/db/_private/_baseTest.php @@ -0,0 +1,22 @@ + ["any", $values], + ], $sql, $bindings); + self::assertSame([ + "value = any(array[:value, :value2])", + ], $sql); + self::assertSame([ + "value" => 1, + "value2" => "string", + ], $bindings); + } +} diff --git a/php/tests/db/sqlite/.gitignore b/php/tests/db/sqlite/.gitignore new file mode 100644 index 0000000..6ab0f32 --- /dev/null +++ b/php/tests/db/sqlite/.gitignore @@ -0,0 +1 @@ +/capacitor.db* diff --git a/php/tests/db/sqlite/ChannelMigrationTest.php b/php/tests/db/sqlite/ChannelMigrationTest.php new file mode 100644 index 0000000..1946179 --- /dev/null +++ b/php/tests/db/sqlite/ChannelMigrationTest.php @@ -0,0 +1,75 @@ +charge([ + "name" => $name, + "value" => $value, + "date_cre" => $dateCre, + "date_mod" => $dateMod, + "age" => $age, + ]); + } + } + + function testMigration() { + $storage = new SqliteStorage(__DIR__.'/capacitor.db'); + $data = [ + ["first", "premier", new DateTime(), new DateTime(), 15], + ["second", "deuxieme", new DateTime(), new DateTime(), 15], + ]; + + new Capacitor($storage, $channel = new MyChannel()); + $channel->reset(true); + $this->addData($channel, $data); + + new Capacitor($storage, $channel = new MyChannelV2()); + $this->addData($channel, $data); + + new Capacitor($storage, $channel = new MyChannelV3()); + $this->addData($channel, $data); + + $sql = $channel->getCapacitor()->getCreateSql(); + $class = MyChannelV3::class; + $expected = <<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_" => "sersum", + ]; + + 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..a6e23a1 --- /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:", [ + "migration" => "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:", [ + "migration" => "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:", [ + "migration" => "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..3d069dd --- /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; + _sqliteQuery::parse_conds(["col = 'value'"], $sql, $params); + self::assertSame(["col = 'value'"], $sql); + self::assertNull($params); + + $sql = $params = null; + _sqliteQuery::parse_conds([["col = 'value'"]], $sql, $params); + self::assertSame(["col = 'value'"], $sql); + self::assertNull($params); + + $sql = $params = null; + _sqliteQuery::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; + _sqliteQuery::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; + _sqliteQuery::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; + _sqliteQuery::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; + _sqliteQuery::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; + _sqliteQuery::parse_conds(["col" => ["in", "one"]], $sql, $params); + self::assertSame(["col in (:col)"], $sql); + self::assertSame(["col" => "one"], $params); + + $sql = $params = null; + _sqliteQuery::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; + _sqliteQuery::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; + _sqliteQuery::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; + _sqliteQuery::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; + _sqliteQuery::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; + _sqliteQuery::parse_set_values(null, $sql, $params); + self::assertNull($sql); + self::assertNull($params); + + $sql = $params = null; + _sqliteQuery::parse_set_values([], $sql, $params); + self::assertNull($sql); + self::assertNull($params); + + $sql = $params = null; + _sqliteQuery::parse_set_values(["col = 'value'"], $sql, $params); + self::assertSame(["col = 'value'"], $sql); + self::assertNull($params); + + $sql = $params = null; + _sqliteQuery::parse_set_values([["col = 'value'"]], $sql, $params); + self::assertSame(["col = 'value'"], $sql); + self::assertNull($params); + + $sql = $params = null; + _sqliteQuery::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; + _sqliteQuery::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; + _sqliteQuery::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/db/sqlite/impl/MyChannel.php b/php/tests/db/sqlite/impl/MyChannel.php new file mode 100644 index 0000000..3cf5b69 --- /dev/null +++ b/php/tests/db/sqlite/impl/MyChannel.php @@ -0,0 +1,33 @@ + "varchar not null primary key", + "value" => "varchar", + ]; + + const VERSION = 1; + + function __construct() { + parent::__construct(); + $this->version = static::VERSION; + } + + protected int $version; + + function getItemValues($item): ?array { + + return [ + "name" => "{$item["name"]}$this->version", + "value" => "{$item["value"]} v$this->version", + "date_cre" => $item["date_cre"] ?? null, + "date_mod" => $item["date_mod"] ?? null, + "age" => $item["age"] ?? null, + ]; + } +} diff --git a/php/tests/db/sqlite/impl/MyChannelV2.php b/php/tests/db/sqlite/impl/MyChannelV2.php new file mode 100644 index 0000000..68f18bf --- /dev/null +++ b/php/tests/db/sqlite/impl/MyChannelV2.php @@ -0,0 +1,14 @@ + "varchar", + "value" => "varchar", + ["dates", + "date_cre" => "datetime", + "date_mod" => "datetime", + ], + ]; +} diff --git a/php/tests/db/sqlite/impl/MyChannelV3.php b/php/tests/db/sqlite/impl/MyChannelV3.php new file mode 100644 index 0000000..e6e2170 --- /dev/null +++ b/php/tests/db/sqlite/impl/MyChannelV3.php @@ -0,0 +1,17 @@ + "varchar", + "value" => "varchar", + ["dates", + "date_cre" => "datetime", + "date_mod" => "datetime", + ], + ["infos", + "age" => "integer", + ], + ]; +} 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/funcTest.php b/php/tests/php/funcTest.php new file mode 100644 index 0000000..a382660 --- /dev/null +++ b/php/tests/php/funcTest.php @@ -0,0 +1,1200 @@ +", + 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, $args, 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, $args, 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"], + false, 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, $args, 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, $args, 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, $args, 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, $args, 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() { + # bind if not already bound + $func = func::with([C1::class, "tmethod"]); + // bind + self::assertSame(11, $func->bind(new C1(0))->invoke()); + // pas de bind, puis que déjà bound + self::assertSame(11, $func->bind(new C1(1))->invoke()); + // même si l'objet est de type différent, pas de bind + self::assertSame(11, $func->bind(new C0())->invoke()); + + # force rebind + $func = func::with([C1::class, "tmethod"]); + // objets du même type + self::assertSame(11, $func->bind(new C1(0), true)->invoke()); + self::assertSame(12, $func->bind(new C1(1), true)->invoke()); + // objets de type différent + self::assertException(ValueException::class, function() use ($func) { + $func->bind(new C0(), true)->invoke(); + }); + self::assertSame(11, $func->bind(new C0(), true, true)->invoke()); + } + + function testModifyArgs() { + $closure = function(...$args) { return $args; }; + + self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->replaceArgs(["x", "y", "z"])->invoke()); + + self::assertSame(["x", "y", "z", "a", "b", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(["x", "y", "z"])->invoke()); + self::assertSame(["x", "y", "z", "a", "b", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(["x", "y", "z"], 0)->invoke()); + self::assertSame(["x", "y", "z", "b", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(["x", "y", "z"], 1)->invoke()); + self::assertSame(["x", "y", "z", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(["x", "y", "z"], 2)->invoke()); + self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->prependArgs(["x", "y", "z"], 3)->invoke()); + self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->prependArgs(["x", "y", "z"], 4)->invoke()); + + self::assertSame(["a", "b", "c", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(["x", "y", "z"])->invoke()); + self::assertSame(["a", "b", "c", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(["x", "y", "z"], 0)->invoke()); + self::assertSame(["a", "b", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(["x", "y", "z"], 1)->invoke()); + self::assertSame(["a", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(["x", "y", "z"], 2)->invoke()); + self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(["x", "y", "z"], 3)->invoke()); + self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(["x", "y", "z"], 4)->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/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/strTest.php b/php/tests/strTest.php new file mode 100644 index 0000000..c03997e --- /dev/null +++ b/php/tests/strTest.php @@ -0,0 +1,51 @@ + "premier", + "second" => "deuxieme", + ])); + self::assertSame("avant OK", str::replace("prefix prefixsuffix", [ + "prefix" => "avant", + "prefixsuffix" => "OK", + ])); + self::assertSame("avant avantsuffix", str::replace("prefix prefixsuffix", [ + "prefix" => "avant", + "prefixsuffix" => "OK", + ], false)); + } + + function test_split_tokens() { + self::assertNull(str::split_tokens(null)); + self::assertSame([], str::split_tokens("")); + self::assertSame(["token"], str::split_tokens("token")); + self::assertSame(["t", "u", "v"], str::split_tokens(" t u v ")); + self::assertSame(["t", "u", "v", "w"], str::split_tokens("\nt\n\nu\r\nv\rw")); + } + + function test_camel2us() { + self::assertSame("a", str::camel2us("a")); + self::assertSame("aa", str::camel2us("aa")); + self::assertSame("aaa", str::camel2us("aaa")); + self::assertSame("a", str::camel2us("A")); + self::assertSame("aa", str::camel2us("Aa")); + self::assertSame("aa", str::camel2us("AA")); + self::assertSame("aaa", str::camel2us("Aaa")); + self::assertSame("aaa", str::camel2us("AAA")); + self::assertSame("a_aa", str::camel2us("AAa")); + self::assertSame("a_b", str::camel2us("aB")); + self::assertSame("aa_bb", str::camel2us("aaBb")); + self::assertSame("aaa_bbb", str::camel2us("aaaBbb")); + self::assertSame("aa_bb", str::camel2us("AaBb")); + self::assertSame("aaa_bbb", str::camel2us("AaaBbb")); + + self::assertSame("_aaa", str::camel2us("_aaa")); + self::assertSame("__aaa_bbb", str::camel2us("__aaaBbb")); + self::assertSame("___aaa_bbb", str::camel2us("___AaaBbb")); + } +} diff --git a/php/tests/web/uploadsTest.php b/php/tests/web/uploadsTest.php new file mode 100644 index 0000000..0ccc458 --- /dev/null +++ b/php/tests/web/uploadsTest.php @@ -0,0 +1,200 @@ + [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + # name=multiple[], name=multiple[] + 'multiple' => [ + 'name' => [ + 0 => '', + 1 => '', + ], + 'type' => [ + 0 => '', + 1 => '', + ], + 'tmp_name' => [ + 0 => '', + 1 => '', + ], + 'error' => [ + 0 => 4, + 1 => 4, + ], + 'size' => [ + 0 => 0, + 1 => 0, + ], + ], + # name=onelevel[a], name=onelevel[b] + 'onelevel' => [ + 'name' => [ + 'a' => '', + 'b' => '', + ], + 'type' => [ + 'a' => '', + 'b' => '', + ], + 'tmp_name' => [ + 'a' => '', + 'b' => '', + ], + 'error' => [ + 'a' => 4, + 'b' => 4, + ], + 'size' => [ + 'a' => 0, + 'b' => 0, + ], + ], + # name=multiplelevel[a][], name=multiplelevel[a][], name=multiplelevel[b][], name=multiplelevel[b][] + 'multiplelevel' => [ + 'name' => [ + 'a' => [ + 0 => '', + 1 => '', + ], + 'b' => [ + 0 => '', + 1 => '', + ], + ], + 'type' => [ + 'a' => [ + 0 => '', + 1 => '', + ], + 'b' => [ + 0 => '', + 1 => '', + ], + ], + 'tmp_name' => [ + 'a' => [ + 0 => '', + 1 => '', + ], + 'b' => [ + 0 => '', + 1 => '', + ], + ], + 'error' => [ + 'a' => [ + 0 => 4, + 1 => 4, + ], + 'b' => [ + 0 => 4, + 1 => 4, + ], + ], + 'size' => [ + 'a' => [ + 0 => 0, + 1 => 0, + ], + 'b' => [ + 0 => 0, + 1 => 0, + ], + ], + ], + ]; + + const PARSED = [ + # name="simple" + 'simple' => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + # name=multiple[], name=multiple[] + 'multiple' => [ + 0 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + 1 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + ], + # name=onelevel[a], name=onelevel[b] + 'onelevel' => [ + 'a' => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + 'b' => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + ], + # name=multiplelevel[a][], name=multiplelevel[a][], name=multiplelevel[b][], name=multiplelevel[b][] + 'multiplelevel' => [ + 'a' => [ + 0 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + 1 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + ], + 'b' => [ + 0 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + 1 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + ], + ], + ]; + + function test_files() { + self::assertSame(self::PARSED, uploads::_files(self::_FILES)); + } +} diff --git a/runphp/Dockerfile.runphp b/runphp/Dockerfile.runphp new file mode 100644 index 0000000..f6bcfbc --- /dev/null +++ b/runphp/Dockerfile.runphp @@ -0,0 +1,30 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr/dist + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=php /g/ /g/ +RUN /g/build @php-cli php-utils + +ENTRYPOINT ["/g/entrypoint"] diff --git a/runphp/Dockerfile.runphp+ic b/runphp/Dockerfile.runphp+ic new file mode 100644 index 0000000..48f376b --- /dev/null +++ b/runphp/Dockerfile.runphp+ic @@ -0,0 +1,43 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr/dist + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/legacytools AS legacytools +FROM $REGISTRY/src/instantclient AS instantclient +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +COPY --from=instantclient /g/ /g/ +COPY --from=instantclient /src/ /src/ +RUN /g/build _instantclient_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=legacytools /g/ /g/ +RUN /g/build nutools + +COPY --from=php /g/ /g/ +RUN /g/build @php-cli php-utils + +COPY --from=instantclient /g/ /g/ +COPY --from=builder /opt/oracle/ /opt/oracle/ +RUN /g/build instantclient + +ENTRYPOINT ["/g/entrypoint"] diff --git a/runphp/build b/runphp/build new file mode 100755 index 0000000..3f7c7b6 --- /dev/null +++ b/runphp/build @@ -0,0 +1,256 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +MYDIR="$(cd "$(dirname -- "$0")"; pwd)" +RUNPHP="$MYDIR/runphp" +"$RUNPHP" --bs --ue --ci || exit 1 +RUNPHP_STANDALONE= +PROJDIR=; COMPOSERDIR=; COMPOSERPHAR=; VENDORDIR=; BUILDENV0=; BUILDENV= +BUILD_IMAGES=(php-apache mariadb10); export BUILD_FLAVOUR=; DIST=; IMAGENAME= +DISTFILES=(); TEMPLATEFILES=(); VARFILES=() +source "$RUNPHP" || exit 1 +source "$PROJDIR/$VENDORDIR/nulib/php/load.sh" || exit 1 +require: template + +# 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 + +BUILD_ARGS=( + DIST NDIST + REGISTRY + APT_PROXY + APT_MIRROR + SEC_MIRROR + TIMEZONE +) + +function dklsnet() { + docker network ls --no-trunc --format '{{.Name}}' -f name="$1" 2>/dev/null +} + +function dklsimg() { + local image="$1" version="$2" + docker image ls --no-trunc --format '{{.Repository}}:{{.Tag}}' "$image${version:+:$version}" 2>/dev/null +} + +function dklsct() { + # afficher le container dont l'image correspondante est $1 + docker ps --no-trunc --format '{{.Image}} {{.Names}}' | awk -v image="$1" '$1 == image { print $2 }' +} + +function dkrunning() { + # vérifier si le container d'image $1 tourne + [ -n "$(dklsct "$@")" ] +} + +function dclsct() { + # afficher les containers correspondant à $1(=docker-compose.yml) + docker compose ${1:+-f "$1"} ps -q +} + +function dcrunning() { + # vérifier si les containers correspondant à $1(=docker-compose.yml) tournent + # si $2 est spécifié, c'est le nombre de service qui doit tourner + if [ -n "$2" ]; then + [ "$(dclsct "${@:1:1}" | wc -l)" -eq "$2" ] + else + [ -n "$(dclsct "${@:1:1}")" ] + fi +} + +function build_check_env() { + eval "$(template_locals)" + local updatedenv file name + local -a updatedfiles files + + if template_copy_missing "$PROJDIR/$BUILDENV0"; then + updated=1 + updatedenv=1 + fi + for file in "${DISTFILES[@]}"; do + if [ -f "$PROJDIR/$file" ]; then + if template_copy_missing "$PROJDIR/$file"; then + updated=1 + setx name=basename -- "$file" + name="${name#.}"; name="${name%.}" + setx file=dirname -- "$file" + file="$file/$name" + updatedfiles+=("$file") + fi + elif [ -d "$PROJDIR/$file" ]; then + local dir="$PROJDIR/$file" + setx -a files=find "$dir" -type f -name ".*.dist" + for file in "${files[@]}"; do + if template_copy_missing "$file"; then + updated=1 + setx name=basename -- "$file" + name="${name#.}"; name="${name%.}" + setx file=dirname -- "$file" + file="$file/$name" + updatedfiles+=("${file#$PROJDIR/}") + fi + done + else + ewarn "$file: fichier dist introuvable" + fi + done + for file in "${TEMPLATEFILES[@]}"; do + if [ -f "$PROJDIR/$file" ]; then + template_copy_replace "$PROJDIR/$file" + elif [ -d "$PROJDIR/$file" ]; then + local dir="$PROJDIR/$file" + setx -a files=find "$dir" -type f -name ".*.template" + for file in "${files[@]}"; do + template_copy_replace "$file" + done + else + ewarn "$file: fichier template introuvable" + fi + done + + local -a varfiles + for file in "${VARFILES[@]}"; do + varfiles+=("$PROJDIR/$file") + done + template_process_userfiles "${varfiles[@]}" + + if [ -n "$updated" ]; then + enote "IMPORTANT: vous devez paramétrer certains fichiers avant de pouvoir construire les images" + if [ -n "$updatedenv" ]; then + if [ $(id -u) -ne 0 ]; then + setx userent=getent passwd "$(id -un)" + setx userent=qval "$userent" + setx groupent=getent group "$(id -gn)" + setx groupent=qval "$groupent" + sed -i " +/^#DEVUSER_.*=/s/^#// +/^DEVUSER_USERENT=/s/=.*/=${userent//\//\\\/}/ +/^DEVUSER_GROUPENT=/s/=.*/=${groupent//\//\\\/}/ +" "$PROJDIR/$BUILDENV" + fi + einfo "\ +Veuillez vérifier le fichier $BUILDENV + ${EDITOR:-nano} $BUILDENV" + fi + [ ${#updatedfiles[*]} -gt 0 ] && einfo "\ +Le cas échéant, veuillez vérifier ce(s) fichier(s) + ${EDITOR:-nano} $(qvals "${updatedfiles[@]}")" + enote "ENSUITE, vous pourrez relancer la commande" + return 1 + fi +} + +function _build() { + local dockerfile image="${PRIVAREG:+$PRIVAREG/}${IMAGENAME%/*}/$1" + if [ -n "$ForceBuild" -o -z "$(dklsimg "$image")" ]; then + estep "Construction de $image" + dockerfiles=( + "$MYDIR/Dockerfile.$1.local" + "$MYDIR/Dockerfile.$1$BUILD_FLAVOUR" + "$PROJDIR/$VENDORDIR/nulib/php/dockerfiles/Dockerfile.$1$BUILD_FLAVOUR" + "$MYDIR/Dockerfile.$1" + "$PROJDIR/$VENDORDIR/nulib/php/dockerfiles/Dockerfile.$1" + ) + for dockerfile in "${dockerfiles[@]}"; do + [ -f "$dockerfile" ] && break + done + args=( + -f "$dockerfile" + ${Pull:+--pull} + ${NoCache:+--no-cache} + ${PlainOutput:+--progress plain} + -t "$image" + ) + for arg in "${BUILD_ARGS[@]}"; do + args+=(--build-arg "$arg=${!arg}") + done + for arg in "${!PROXY_VARS[@]}"; do + args+=(--build-arg "$arg=${PROXY_VARS[$arg]}") + done + for host in "${HOST_MAPPINGS[@]}"; do + args+=(--add-host "$host") + done + docker build "${args[@]}" "$PROJDIR" || die + if [ -n "$Push" ]; then + if [ -n "$PRIVAREG" ]; then + estep "Poussement de $image" + docker push "$image" || die + else + ewarn "PRIVAREG non défini: impossible de pousser l'image" + fi + fi + fi +} +function build_images() { + local image sourced + + [ $# -gt 0 ] || set -- runphp "${BUILD_IMAGES[@]}" + for image in "$@"; do + case "$image" in + runphp) + [ ${#Configs[*]} -gt 0 ] && export RUNPHP_FORCE_BUILDENVS="${Configs[*]}" + local -a args=(--bs) + [ "$ForceBuild" != all ] && args+=(--ue) + [ -n "$Pull" ] && args+=(--pull) + [ -n "$NoCache" ] && args+=(--no-cache) + "$RUNPHP" "${args[@]}" || die + ;; + *) + if [ -z "$sourced" ]; then + [ ${#Configs[*]} -gt 0 ] || Configs=("$PROJDIR/$BUILDENV") + for config in "${Configs[@]}"; do + source "$config" + done + after_source_buildenv + read -a HOST_MAPPINGS <<<"${HOST_MAPPINGS// +/ }" + sourced=1 + fi + _build "$image" + ;; + esac + done +} + +action=build +Configs=() +ForceBuild= +Pull= +NoCache= +PlainOutput= +Push= +args=( + "Construire les images pour le projet" + #"usage" + --check-only action=none "++Ne faire que la vérification de l'environnement" + -c:,--config:BUILDENV Configs "Spécifier un fichier d'environnement pour le build" + -r,--rebuild ForceBuild=1 "Forcer la (re)construction des images" + -R,--rebuild-all ForceBuild=all "++Comme --rebuild, mais reconstruire aussi runphp" + -U,--pull Pull=1 "++Forcer le re-téléchargement des images dépendantes" + -j,--no-cache NoCache=1 "++Construire l'image en invalidant le cache" + -D,--plain-output PlainOutput=1 "++Afficher le détail du build" + -p,--push Push=1 "Pousser les images vers le registry après construction" +) +parse_args "$@"; set -- "${args[@]}" + +if [ ${#Configs[*]} -gt 0 ]; then + aconfigs=() + for config in "${Configs[@]}"; do + setx config=abspath "$config" + aconfigs+=("$config") + done + Configs=("${aconfigs[@]}") + # pas de vérification d'environnement si on spécifie Configs + # ne pas oublier d'implémenter un traitement spécifique si build_check_env() + # contient d'autres vérifications +else + build_check_env || die +fi +[ "$action" == none ] && exit 0 + +case "$action" in +build) build_images "$@";; +*) die "$action: action non implémentée";; +esac diff --git a/runphp/dot-build.env.dist b/runphp/dot-build.env.dist new file mode 100644 index 0000000..7baed50 --- /dev/null +++ b/runphp/dot-build.env.dist @@ -0,0 +1,21 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +# Source des paquets et proxy +APT_PROXY= +APT_MIRROR=default +SEC_MIRROR=default + +# Timezone du serveur +TIMEZONE=Europe/Paris + +# registre docker privé d'après lequel sont nommées les images +PRIVAREG= + +################################################################################ +# Ne pas toucher à partir d'ici + +REGISTRY=pubdocker.univ-reunion.fr/dist +DIST=d12 +IMAGENAME=nulib/ +#DEVUSER_USERENT=user:x:1000:1000:User,,,:/home/user:/bin/bash +#DEVUSER_GROUPENT=user:x:1000: diff --git a/runphp/dot-dkbuild.env.dist b/runphp/dot-dkbuild.env.dist new file mode 100644 index 0000000..b3319ea --- /dev/null +++ b/runphp/dot-dkbuild.env.dist @@ -0,0 +1,12 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +# Source des paquets et proxy +setenv APT_PROXY= +setenv APT_MIRROR=default +setenv SEC_MIRROR=default + +# Timezone du serveur +setenv TIMEZONE=Europe/Paris + +setenv REGISTRY=pubdocker.univ-reunion.fr/dist +setenv PRIVAREG= diff --git a/runphp/dot-runphp.conf b/runphp/dot-runphp.conf new file mode 100644 index 0000000..0c2f16a --- /dev/null +++ b/runphp/dot-runphp.conf @@ -0,0 +1,8 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +# Chemin vers runphp, e.g sbin/runphp +RUNPHP= + +# Si RUNPHP n'est pas défini, les variables suivantes peuvent être définies +#DIST=d12 +#REGISTRY=pubdocker.univ-reunion.fr/dist diff --git a/runphp/runphp b/runphp/runphp new file mode 100755 index 0000000..e24aba5 --- /dev/null +++ b/runphp/runphp @@ -0,0 +1,662 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +# Script permettant de lancer une commande dans docker et/ou de bootstrapper +# l'utilisation de nulib dans un projet PHP +# Les fichiers suivants doivent être copiés à un endroit quelconque du projet: +# - runphp (ce script, à générer avec update-runphp.sh) +# - Dockerfile.runphp +# Les fichiers suivants peuvent être intégrés dans le projet comme exemples: +# - dot-build.env.dist (à renommer en .build.env.dist) +# - dot-dkbuild.env.dist (indiquer qu'il faut le copier en ~/.dkbuild.env) +# Par défaut, ce script assume que runphp est copié dans le répertoire sbin/ +# du projet, et que le fichier composer.json et le répertoire vendor/ sont à la +# racine du projet. Le cas échéant, modifier les valeurs ci-dessous +(return 0 2>/dev/null) && _sourced=1 || _sourced= + +############################################################################### +# Modifier les valeurs suivantes si nécessaire +#SOF:runphp.userconf:ne pas modifier cette ligne + +# répertoire du projet. ce chemin doit être absolu. s'il est relatif, il est +# exprimé par rapport au répertoire de ce script +PROJDIR= + +# composer: répertoire du projet composer (celui qui contient le fichier +# composer.json), chemin de composer.phar et répertoire vendor. ces chemins +# doivent être relatifs à $PROJDIR +COMPOSERDIR= +COMPOSERPHAR= +VENDORDIR= + +# fichier de configuration pour le build +BUILDENV0= +BUILDENV= + +# Listes des images que le script build construit automatiquement +BUILD_IMAGES=(php-apache mariadb10) +BUILD_FLAVOUR= + +## En ce qui concerne DIST et IMAGENAME, les valeurs dans BUILDENV prennent le +## dessus. si BUILDENV *n'est pas* utilisé, ces valeurs peuvent être spécifiées +## ici + +# version de debian à utiliser pour l'image +# d12=php8.2, d11=php7.4, d10=php7.3 +DIST= + +# Nom de base de l'image (sans le registry), e.g prefix/ +IMAGENAME= + +## Fichiers .dist et .template +# Lors du build, les fichiers de la forme .name.dist sont copiés vers un +# fichier name sauf s'il existe déjà. Si un fichier name.dist.local existe, il +# est utilisé à la place de .name.dist +# Les fichiers de la forme .name.template sont copiés *systématiquement* vers +# le fichier name. Si un fichier name.template.local existe, il est utilisé à +# la place de .name.template + +# Liste de fichiers (ou de répertoires à considérer). Pour chaque répertoire, +# les fichiers .*.dist dans l'arborescence du répertoire sont recherchés +DISTFILES=() + +# Liste de fichiers (ou de répertoires à considérer). Pour chaque répertoire, +# les fichiers .*.template dans l'arborescence du répertoire sont recherchés +TEMPLATEFILES=() + +# Fichiers contenant les valeurs des variables utilisées pour l'interpolation +# des fichiers dist et template. Seules les variables définies dans ces fichiers +# sont interpolées. +VARFILES=() + +#EOF:runphp.userconf:ne pas modifier cette ligne +################################################################################ + +# Ne pas modifier à partir d'ici + +if [ -n "$_sourced" ]; then + if [ "${0#-}" != "$0" ]; then + # sourcé depuis la ligne de commande + MYSELF="${BASH_SOURCE[1]}" + else + # sourcé depuis un script + MYSELF="${BASH_SOURCE[0]}" + fi + MYDIR="$(cd "$(dirname -- "$MYSELF")"; pwd)" + MYNAME="$(basename -- "$MYSELF")" +else + MYDIR="$(cd "$(dirname -- "$0")"; pwd)" + MYNAME="$(basename -- "$0")" +fi +if [ -f "$MYDIR/runphp.userconf.local" ]; then + source "$MYDIR/runphp.userconf.local" +fi + +DEFAULT_DIST=d12 +if [ -n "$RUNPHP_STANDALONE" ]; then + PROJDIR="$RUNPHP_PROJDIR" + + COMPOSERDIR=. + COMPOSERPHAR= + VENDORDIR=vendor + BUILDENV0= + BUILDENV= + DIST="${RUNPHP_DIST:-$DEFAULT_DIST}" + IMAGENAME=nulib/ + + PRIVAREG=docker.io + REGISTRY="$RUNPHP_REGISTRY" + + [ -n "$RUNPHP_BUILD_FLAVOUR" ] && BUILD_FLAVOUR="$RUNPHP_BUILD_FLAVOUR" + +else + [ -n "$PROJDIR" ] || PROJDIR="$(dirname -- "$MYDIR")" + [ "${PROJDIR#/}" != "$PROJDIR" ] || PROJDIR="$(cd "$MYDIR/$PROJDIR"; pwd)" + + [ -n "$COMPOSERDIR" ] || COMPOSERDIR=. + [ -n "$COMPOSERPHAR" ] || COMPOSERPHAR=sbin/composer.phar + [ -n "$VENDORDIR" ] || VENDORDIR=vendor + [ -n "$BUILDENV0" ] || BUILDENV0=.build.env.dist + [ -n "$BUILDENV" ] || BUILDENV=build.env + [ -n "$DIST" ] || DIST="$DEFAULT_DIST" + [ -n "$IMAGENAME" ] || IMAGENAME=nulib/ + + [ "$COMPOSERPHAR" == none ] && COMPOSERPHAR= + [ "$BUILDENV0" == none ] && BUILDENV0= + [ "$BUILDENV" == none ] && BUILDENV= +fi +[ "$BUILD_FLAVOUR" == none ] && BUILD_FLAVOUR= + +function after_source_buildenv() { + NDIST="${DIST#d}" +} +after_source_buildenv + +[ -n "$_sourced" ] && return 0 + +function eecho() { echo "$*" 1>&2; } +function eerror() { eecho "ERROR: $*"; } +function die() { [ $# -gt 0 ] && eerror "$*"; exit 1; } +function is_defined() { [ -n "$(declare -p "$1" 2>/dev/null)" ]; } +function in_path() { [ -n "$1" -a -x "$(which "$1" 2>/dev/null)" ]; } + +function composer() { + if [ -n "$PROJDIR" -a -n "$COMPOSERDIR" ]; then + cd "$PROJDIR/$COMPOSERDIR" || exit 1 + fi + if [ -n "$PROJDIR" -a -n "$COMPOSERPHAR" -a -x "$PROJDIR/$COMPOSERPHAR" ]; then + "$PROJDIR/$COMPOSERPHAR" "$@" + elif in_path composer; then + command composer "$@" + elif [ -x /usr/bin/composer ]; then + /usr/bin/composer "$@" + elif [ -x /usr/local/bin/composer ]; then + /usr/local/bin/composer "$@" + else + die "impossible de trouver composer" + fi + if [ -n "$PROJDIR" -a -z "$RUNPHP_STANDALONE" -a -f composer.lock ]; then + cp composer.lock "$PROJDIR/.composer.lock.runphp" + fi +} + +function host_parse_args() { + # analyser les arguments et calculer les opérations à lancer: bootstrap, + # composer install et/ou commande + + # faut-il lancer bootstrap? + Bootstrap=auto + # faut-il installer les dépendances? + Composer= + # commande à lancer + Cmd=() + + if [ "$1" == --runphp-bootstrap -o "$1" == --bs ]; then + Bootstrap=1 + shift + elif [ "$1" == --runphp-exec ]; then + Bootstrap= + shift + elif [ "$1" == --runphp-install -o "$1" == --ci ]; then + Composer=ci + shift + elif [ "$1" == --runphp-update -o "$1" == --cu ]; then + Composer=cu + shift + fi + Cmd=("$@") +} + +function host_ensure_image() { + # calculer le nom de l'image runphp + local dfdir suffix dockerfiles dockerfile + local privareg imagename + if [ -z "$Image" ]; then + [ -n "$RUNPHP_STANDALONE" ] && dfdir="$RUNPHP_STANDALONE/runphp" || dfdir="$MYDIR" + dockerfiles=( + "_local:$dfdir/Dockerfile.runphp.local" + "${BUILD_FLAVOUR//+/_}:$dfdir/Dockerfile.runphp$BUILD_FLAVOUR" + ":$dfdir/Dockerfile.runphp" + ) + for dockerfile in "${dockerfiles[@]}"; do + suffix="${dockerfile%:*}" + dockerfile="${dockerfile##*:}" + [ -f "$dockerfile" ] && break + done + Dockerfile="$dockerfile" + + [[ "$IMAGENAME" == */ ]] && imagename=runphp || imagename="${IMAGENAME%/*}/runphp" + privareg="$PRIVAREG" + if [ "$imagename" == runphp ]; then + [ -z "$privareg" -o "$privareg" == docker.io ] && privareg=docker.io/library + else + [ -z "$privareg" ] && privareg=docker.io + fi + Image="$privareg/$imagename$suffix:$DIST" + fi +} + +function host_check_image() { + # vérifier que l'image runphp existe + local image="$Image" + for prefix in docker.io/library/ docker.io; do + if [ "${image#$prefix}" != "$image" ]; then + image="${image#$prefix}" + break + fi + done + [ -n "$(docker image ls --no-trunc --format '{{.Repository}}:{{.Tag}}' "$image" 2>/dev/null)" ] +} + +function host_check_projdir() { + # vérifier le projet pour voir s'il faut installer les dépendances + [ -n "$RUNPHP_STANDALONE" ] && return + + local install + if [ ! -f "$PROJDIR/$VENDORDIR/nulib/php/load.sh" ]; then + install=1 + elif [ ! -f "$PROJDIR/.composer.lock.runphp" ]; then + install=1 + elif ! diff -q "$PROJDIR/$COMPOSERDIR/composer.lock" "$PROJDIR/.composer.lock.runphp" >&/dev/null; then + install=1 + fi + if [ -n "$install" ]; then + [ -n "$Composer" ] || Composer=ci + fi +} + +function host_init_env() { + ## Charger ~/.dkbuild.env + + APT_PROXY= + APT_MIRROR= + SEC_MIRROR= + TIMEZONE= + PRIVAREG= + REGISTRY= + PROFILE= + HOST_MAPPINGS=() + function default_profile() { + PROFILE="$1" + } + function profile() { + local profile + for profile in "$@"; do + [ "$profile" == "$PROFILE" ] && return 0 + done + return 1 + } + function setenv() { + eval "export $1" + } + function default() { + local command="$1"; shift + local nv n v + case "$command" in + docker) + for nv in "$@"; do + [[ "$nv" == *=* ]] || continue + n="${nv%%=*}" + v="${nv#*=}" + case "$n" in + host-mappings) + read -a ns <<<"$v" + for v in "${ns[@]}"; do + HOST_MAPPINGS+=("$v") + done + ;; + esac + done + ;; + esac + } + envs=() + if [ -z "$RUNPHP_IGNORE_DEFAULTS" ]; then + envs+=( + /etc/default/dkbuild + ~/etc/default.${HOSTNAME%%.*}/dkbuild + ~/etc/default/dkbuild + ) + fi + envs+=(~/.dkbuild.env) + for env in "${envs[@]}"; do + [ -f "$env" ] && source "$env" + done + [ -n "$APT_PROXY" ] || APT_PROXY= + [ -n "$APT_MIRROR" ] || APT_MIRROR=default + [ -n "$SEC_MIRROR" ] || SEC_MIRROR=default + [ -n "$TIMEZONE" ] || TIMEZONE=Europe/Paris + [ -n "$PRIVAREG" ] || PRIVAREG= + [ -n "$REGISTRY" ] || REGISTRY=pubdocker.univ-reunion.fr/dist + + ## Charger la configuration + + # Recenser les valeur de proxy + for var in {HTTPS,ALL,NO}_PROXY {http,https,all,no}_proxy; do + is_defined "$var" && PROXY_VARS[${var,,}]="${!var}" + done + + # Paramètres de montage + if [ -n "$RUNPHP_NO_USE_RSLAVE" ]; then + UseRslave= + elif [ -n "$RUNPHP_USE_RSLAVE" ]; then + UseRslave=1 + elif [ -e /proc/sys/fs/binfmt_misc/WSLInterop ]; then + # pas de mount propagation sous WSL + UseRslave= + else + UseRslave=1 + fi + + Image= + if [ -n "$RUNPHP_FORCE_BUILDENVS" ]; then + eval "Configs=($RUNPHP_FORCE_BUILDENVS)" + elif [ -n "$BUILDENV" -a -f "$PROJDIR/$BUILDENV" ]; then + Configs=("$PROJDIR/$BUILDENV") + elif [ -n "$BUILDENV0" -a -f "$PROJDIR/$BUILDENV0" ]; then + Configs=("$PROJDIR/$BUILDENV0") + else + Configs=() + fi + for config in "${Configs[@]}"; do + source "$config" || exit 1 + done + after_source_buildenv + + Chdir= + Verbose="$RUNPHP_VERBOSE" + Exec= +} + +function host_docker_build() { + BUILD_ARGS=( + DIST NDIST + REGISTRY + APT_PROXY + APT_MIRROR + SEC_MIRROR + TIMEZONE + ) + + SOPTS=+d:9876543210:c:UjDx:z:r:p + LOPTS=help,dist:,d19,d18,d17,d16,d15,d14,d13,d12,d11,d10,config:,ue,unless-exists,pull,nc,no-cache,po,plain-output,apt-proxy:,timezone:,privareg:,push,ci,no-use-rslave + args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args" + + Dist= + UnlessExists= + Pull= + NoCache= + PlainOutput= + while [ $# -gt 0 ]; do + case "$1" in + --) shift; break;; + --help) + eecho "\ +runphp: construire l'image docker + +USAGE + $MYNAME --bs [options...] + +OPTIONS + -c, --config build.env + -d, --dist DIST + --ue, --unless-exists + -U, --pull + -j, --nc, --no-cache + -D, --po, --plain-output + -x, --apt-proxy APT_PROXY + -z, --timezone TIMEZONE + -r, --privareg PRIVAREG + -p, --push + paramètres pour la consruction de l'image + --ci + --cu + lancer composer install (resp. update) s'il y a eu bootstrap + --no-use-rslave + paramètre montage des volumes" + exit 0 + ;; + -c|--config) shift; Configs+=("$1");; + -d|--dist) shift; Dist="$1";; + -[0-9]) Dist="d1${1#-}";; + --d*) Dist="${1#--}";; + --ue|--unless-exists) UnlessExists=1;; + -U|--pull) Pull=1;; + -j|--nc|--no-cache) NoCache=1;; + -D|--po|--plain-output) PlainOutput=1;; + -x|--apt-proxy) shift; APT_PROXY="$1";; + -z|--timezone) shift; TIMEZONE="$1";; + -r|--privareg) shift; PRIVAREG="$1";; + -p|--push) Push=1;; + --ci) Composer=ci;; + --cu) Composer=cu;; + --no-use-rslave) UseRslave=;; + *) die "$1: option non configurée";; + esac + shift + done + Cmd=("$@") + + for config in "${Configs[@]}"; do + if [ "$config" == none ]; then + Configs=() + break + fi + done + if [ ${#Configs[*]} -gt 0 ]; then + for config in "${Configs[@]}"; do + source "$config" || exit 1 + done + after_source_buildenv + fi + [ -n "$Dist" ] && DIST="$Dist" + + Image= + host_ensure_image + host_check_image && exists=1 || exists= + + if [ -z "$UnlessExists" -o -z "$exists" ]; then + eecho "== bootstrapping $Image" + args=( + -f "$Dockerfile" + ${Pull:+--pull} + ${NoCache:+--no-cache} + ${BuildPlain:+--progress plain} + -t "$Image" + ) + for arg in "${BUILD_ARGS[@]}"; do + args+=(--build-arg "$arg=${!arg}") + done + for arg in "${!PROXY_VARS[@]}"; do + args+=(--build-arg "$arg=${PROXY_VARS[$arg]}") + done + for host in "${HOST_MAPPINGS[@]}"; do + args+=(--add-host "$host") + done + mkdir -p /tmp/runphp-build + docker build "${args[@]}" /tmp/runphp-build || exit 1 + + if [ -n "$Push" -a -n "$PRIVAREG" ]; then + eecho "== Pushing $Image" + docker push "$Image" || exit 1 + fi + else + # s'il n'y a pas eu bootstrap, alors ne pas lancer la commande composer + Composer= + fi +} + +function host_docker_run() { + # lancer une commande avec docker + if [ "$1" == composer ]; then + : # pas d'analyse d'argument pour composer + else + SOPTS=+w: + LOPTS=help,chdir:,no-use-rslave + args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args" + + while [ $# -gt 0 ]; do + case "$1" in + --) shift; break;; + --help) + eecho "\ +runphp: lancer une commande dans un environnement PHP déterminé + +USAGE + $MYNAME --bs ... + $MYNAME ci|cu|composer + $MYNAME [options] command [args...] + +COMMANDES SPECIALES + --bs + faire un bootstrap de l'image runphp + +COMMANDES COMPOSER + ci + cu + installer/mettre à jour les dépendances du projet avec composer + composer [args...] + lancer composer avec les arguments spécifiés. + +pour les commandes ci-dessus, l'option --chdir est ignorée: le répertoire +courant est forcé au répertoire du projet composer + +OPTIONS + -w, --chdir CHDIR + aller dans le répertoire spécifié avant de lancer la commande + --no-use-rslave + paramètre montage des volumes" + exit 0 + ;; + -w|--chdir) shift; Chdir="$1";; + --no-use-rslave) UseRslave=;; + *) die "$1: option non configurée";; + esac + shift + done + fi + + args=( + run -it --rm + --name "runphp-$(basename -- "$1")-$$" + -e RUNPHP_MODE=docker + ) + for arg in "${!PROXY_VARS[@]}"; do + args+=(-e "$arg=${PROXY_VARS[$arg]}") + done + if [ -n "$RUNPHP_STANDALONE" ]; then + args+=( + -e "RUNPHP_STANDALONE=$RUNPHP_STANDALONE" + -e "RUNPHP_PROJDIR=$PROJDIR" + ) + fi + for host in "${HOST_MAPPINGS[@]}"; do + args+=(--add-host "$host") + done + + # monter le répertoire qui contient $PROJDIR + mount_composer= + mount_runphp=1 + if [ -z "$PROJDIR" -o "${PROJDIR#$HOME/}" != "$PROJDIR" -o "$PROJDIR" == "$HOME" ]; then + # bind mount $HOME + args+=(-v "$HOME:$HOME${UseRslave:+:rslave}") + [ -n "$RUNPHP_STANDALONE" ] && + [ "${RUNPHP_STANDALONE#$HOME/}" != "$RUNPHP_STANDALONE" ] && + mount_runphp= + elif [ -n "$PROJDIR" ]; then + # bind mount uniquement le répertoire du projet + args+=(-v "$PROJDIR:$PROJDIR${UseRslave:+:rslave}") + mount_composer=1 + [ "$RUNPHP_STANDALONE" == "$PROJDIR" ] && mount_runphp= + fi + if [ -n "$mount_composer" -a -d "$HOME/.composer" ]; then + # monter la configuration de composer + args+=(-v "$HOME/.composer:$HOME/.composer") + fi + if [ -n "$RUNPHP_STANDALONE" -a -n "$mount_runphp" ]; then + args+=(-v "$RUNPHP_STANDALONE:$RUNPHP_STANDALONE") + fi + args+=(-w "$(pwd)") + + # lancer avec l'utilisateur courant + if [ $(id -u) -ne 0 ]; then + # si c'est un utilisateur lambda, il faut monter les informations + # nécessaires. composer est déjà monté via $HOME + args+=( + -e DEVUSER_USERENT="$(getent passwd "$(id -un)")" + -e DEVUSER_GROUPENT="$(getent group "$(id -gn)")" + ) + fi + + args+=( + "$Image" + exec "$0" ${Chdir:+-w "$Chdir"} + ) + [ -n "$Verbose" ] && eecho "\$ docker ${args[*]} $*" + ${Exec:+exec} docker "${args[@]}" "$@" +} + +function container_exec() { + # lancer la commande $@. cette fonction doit être lancée dans le container + # docker + if [ -n "$DEVUSER_USERENT" ]; then + user="${DEVUSER_USERENT%%:*}" + export DEVUSER_USERENT= + export DEVUSER_GROUPENT= + if in_path su-exec; then + exec su-exec "$user" "$0" "$@" + else + exec runuser -u "$user" -- "$0" "$@" + fi + fi + + if [ "$1" == composer ]; then + : # pas d'analyse d'argument pour composer + else + 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 + fi + + if [ $# -eq 0 ]; then + die "no command specified" + elif [ "$1" == ci ]; then + eecho "== installing composer dependencies" + shift + composer i "$@" + elif [ "$1" == cu ]; then + eecho "== upgrading composer dependencies" + shift + composer u "$@" + elif [ "$1" == composer ]; then + "$@" + else + if [ -n "$chdir" ]; then + cd "$chdir" || exit 1 + fi + exec "$@" + fi +} + +################################################################################ + +if [ "$RUNPHP_MODE" != docker ]; then + # Lancement depuis l'extérieur du container + declare -A PROXY_VARS + host_parse_args "$@" + host_init_env + + Image= + if [ "$Bootstrap" == auto ]; then + host_ensure_image + host_check_image && Bootstrap= || Bootstrap=1 + fi + if [ -n "$Bootstrap" ]; then + host_docker_build "${Cmd[@]}" || exit $? + else + host_ensure_image + fi + + host_check_projdir + if [ -n "$Composer" ]; then + host_docker_run "$Composer" || exit $? + fi + + if [ ${#Cmd[*]} -gt 0 ]; then + Exec=1 host_docker_run "${Cmd[@]}" + fi + +else + # Lancement depuis l'intérieur du container + container_exec "$@" +fi diff --git a/runphp/runphp.userconf.local b/runphp/runphp.userconf.local new file mode 100644 index 0000000..a8aa3ed --- /dev/null +++ b/runphp/runphp.userconf.local @@ -0,0 +1,3 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +BUILD_FLAVOUR=+ic diff --git a/runphp/template.sh b/runphp/template.sh new file mode 100755 index 0000000..233c527 --- /dev/null +++ b/runphp/template.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +# Modèle de script utilisant runphp pour lancer un traitement dans un container +# RUNPHP est le chemin relatif vers runphp à partir du chemin du script +RUNPHP=sbin/runphp + +MYDIR="$(dirname -- "$0")"; MYNAME="$(basename -- "$0")" +if [ -z "$_RUNPHP_IN_DOCKER" ]; then + "$MYDIR/$RUNPHP" --bs --ue --ci || exit 1 + exec "$MYDIR/$RUNPHP" "$0" "$@" +fi +source "$MYDIR/$RUNPHP" || exit 1 +source "$PROJDIR/$VENDORDIR/nulib/php/load.sh" || exit 1 + +args=( + "description" + #"usage" +) +parse_args "$@"; set -- "${args[@]}" + +echo "je tourne dans un container..." +sleep 1000 diff --git a/runphp/update-runphp.sh b/runphp/update-runphp.sh new file mode 100755 index 0000000..05403fe --- /dev/null +++ b/runphp/update-runphp.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname "$0")/../load.sh" || exit 1 + +function parse_runphp() { + local runphp="$1" preamble="$2" conf="$3" postamble="$4" + local conf0 + ac_set_tmpfile conf0 + # extraire la configuration depuis le fichier + <"$runphp" awk -v preamble="$preamble" -v conf="$conf0" -v postamble="$postamble" ' +BEGIN { out = preamble } +{ + if ($0 ~ /SOF:runphp.userconf:/) { + if (out != "") print >out + out = conf + } else if ($0 ~ /EOF:runphp.userconf:/) { + out = postamble + if (out != "") print >out + } else { + if (out != "") print >out + } +} +' + # mettre en forme le fichier conf: pas de lignes vides avant et après + <"$conf0" >"$conf" awk ' +BEGIN { p = 0; have_pending = 0; pending = "" } +$0 != "" { p = 1 } +p == 1 { + if ($0 != "") { + if (have_pending) print pending + print + have_pending = 0 + pending = "" + } else { + if (!have_pending) have_pending = 1 + else pending = pending "\n" + } +} +' + ac_clean "$conf0" +} + +projdir= +args=( + "Mettre à jour le script runphp" + "[path/to/runphp]" + -d:,--projdir:PROJDIR . "Copier les fichiers pour un projet de l'université de la Réunion" +) +parse_args "$@"; set -- "${args[@]}" + +if [ -n "$projdir" ]; then + setx projdir=abspath "$projdir" + set -- "$projdir/sbin/runphp" +fi + +runphp="${1:-.}" +if [ -d "$runphp" ]; then + runphp="$runphp/runphp" +elif [ ! -e "$runphp" ]; then + [ "${runphp%/runphp}" != "$runphp" ] || runphp="$runphp/runphp" +fi + +setx runphp=abspath "$runphp" +setx rundir=dirname -- "$runphp" +[ "$rundir" == "$MYDIR" ] && exit 0 +[ -d "$rundir" ] || mkdir -p "$rundir" + +# Tout d'abord, isoler chaque partie du fichier local +ac_set_tmpfile preamble +ac_set_tmpfile conf +ac_set_tmpfile postamble +parse_runphp "$MYDIR/runphp" "$preamble" "$conf" "$postamble" + +# Puis analyser le fichier destination +if [ -f "$runphp" ]; then + ac_set_tmpfile userconf + parse_runphp "$runphp" "" "$userconf" "" + initial_config= + +elif [ -n "$projdir" ]; then + # forcer BUILDENV0=..env.dist et BUILDENV=.env pour les projets de + # l'université de la Réunion + ac_set_tmpfile userconf + sed <"$conf" >"$userconf" ' +/^BUILDENV0=/s/=.*/=..env.dist/ +/^BUILDENV=/s/=.*/=.env/ +' + initial_config=1 + +else + userconf="$conf" + initial_config=1 +fi + +# (Re)construire le fichier destination +( + cat "$preamble" + echo + cat "$userconf" + echo + cat "$postamble" +) >"$runphp" +[ -x "$runphp" ] || chmod +x "$runphp" + +rsync -lpt "$MYDIR/Dockerfile.runphp" "$rundir/" + +if [ -n "$projdir" ]; then + if testdiff "$rundir/build" "$MYDIR/build"; then + cp "$MYDIR/build" "$rundir/build" + chmod +x "$rundir/build" + fi + if [ ! -f "$projdir/..env.dist" ]; then + sed <"$MYDIR/dot-build.env.dist" >"$projdir/..env.dist" ' +/^IMAGENAME=/s/=.*\//='"$(basename -- "$projdir")"'\// +' + initial_config=1 + fi + if [ ! -f "$projdir/.runphp.conf" ]; then + sed <"$MYDIR/dot-runphp.conf" >"$projdir/.runphp.conf" ' +/^RUNPHP=/s/=.*/=sbin\/runphp/ +' + fi +fi + +[ -n "$initial_config" ] diff --git a/wip/_pci b/wip/_pci new file mode 100755 index 0000000..bc02cc3 --- /dev/null +++ b/wip/_pci @@ -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/wip/_pp b/wip/_pp new file mode 100755 index 0000000..4023184 --- /dev/null +++ b/wip/_pp @@ -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/wip/_pu b/wip/_pu new file mode 100755 index 0000000..7c227b6 --- /dev/null +++ b/wip/_pu @@ -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/wip/donk b/wip/donk new file mode 100755 index 0000000..b93ad81 --- /dev/null +++ b/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/wip/pman.md b/wip/pman.md new file mode 100644 index 0000000..3968ad8 --- /dev/null +++ b/wip/pman.md @@ -0,0 +1,14 @@ +# pman + +outil pour gérer les projets PHP +* p, pci, pp, pu: gestion courante git. + ces outils peuvent agir sur un ensemble de projets, notamment tous les + projets dépendants du projet courant +* pver: gestion des versions. + calculer la prochaine version en respectant semver +* pmer: gérer les branches de features et hotfixes. +* prel: faire une release. + ces outils peuvent agir sur les projets dépendants: faire une release sur un + projet downstream, ou synchroniser la version depuis un projet upstream + +-*- 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