diff --git a/.composer.lock.runphp b/.composer.lock.runphp new file mode 100644 index 0000000..bf3295e --- /dev/null +++ b/.composer.lock.runphp @@ -0,0 +1,1798 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "356c1dcfe9eee39e9e6eadff4f63cdfe", + "packages": [], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:15:36+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.12.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2024-06-12T14:39:25+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.3.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + }, + "time": "2024-10-08T18:51:32+00:00" + }, + { + "name": "nulib/tests", + "version": "7.4", + "source": { + "type": "git", + "url": "https://git.univ-reunion.fr/sda-php/nulib-tests.git", + "reference": "6ce8257560b42e8fb3eea03eba84d3877c9648ca" + }, + "require": { + "php": ">=7.3", + "phpunit/phpunit": "^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "nulib\\tests\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "nulib\\tests\\": "tests" + } + }, + "authors": [ + { + "name": "Jephte Clain", + "email": "Jephte.Clain@univ-reunion.fr" + } + ], + "description": "fonctions et classes pour les tests", + "time": "2024-03-26T10:56:17+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.32", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:23:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.21", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.6", + "sebastian/global-state": "^5.0.7", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-09-19T10:50:18+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:33:00+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:35:11+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^7.4" + }, + "platform-dev": { + "ext-posix": "*", + "ext-pcntl": "*", + "ext-curl": "*" + }, + "plugin-api-version": "2.2.0" +} diff --git a/.composer.yaml b/.composer.yaml index 81afa48..ea509c7 100644 --- a/.composer.yaml +++ b/.composer.yaml @@ -1,8 +1,4 @@ # -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8 -composer_php_min: '7.3' -composer_php_max: '8.0' -composer_registry: pubdocker.univ-reunion.fr -composer_image: image/phpbuilder:d10 require: branch: master: diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/codeception.xml b/.idea/codeception.xml new file mode 100644 index 0000000..9da3754 --- /dev/null +++ b/.idea/codeception.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/.idea/nulib.iml b/.idea/nulib.iml index cfcc99b..2f97961 100644 --- a/.idea/nulib.iml +++ b/.idea/nulib.iml @@ -2,8 +2,8 @@ - + diff --git a/.idea/php.xml b/.idea/php.xml index 988f6b9..430b858 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -12,39 +12,39 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - + - + diff --git a/.idea/phpspec.xml b/.idea/phpspec.xml new file mode 100644 index 0000000..ec7e1d4 --- /dev/null +++ b/.idea/phpspec.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/phpunit.xml b/.idea/phpunit.xml new file mode 100644 index 0000000..4f8104c --- /dev/null +++ b/.idea/phpunit.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 8306744..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/.runphp.conf b/.runphp.conf new file mode 100644 index 0000000..89af070 --- /dev/null +++ b/.runphp.conf @@ -0,0 +1,8 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +# Chemin vers runphp, e.g sbin/runphp +RUNPHP= + +# Si RUNPHP n'est pas défini, les variables suivantes peuvent être définies +DIST=d11 +#REGISTRY=pubdocker.univ-reunion.fr diff --git a/.udir b/.udir new file mode 100644 index 0000000..1f19bde --- /dev/null +++ b/.udir @@ -0,0 +1,30 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +# Utiliser 'udir --help-vars' pour une description de la signification des +# variables suivantes: +udir_desc="librairies de base pour scripts bash, awk, php, python" +udir_note="" +udir_types=(uinst) +uinc=release +uinc_options=() +uinc_args=() +preconfig_scripts=() +configure_variables=(dest) +configure_dest_for=(lib/profile.d/nulib) +config_scripts=(lib/uinst/conf) +install_profiles=true +profiledir=lib/profile.d +bashrcdir=lib/bashrc.d +defaultdir=lib/default +workdir_rsync_options=() +workdir_excludes=() +workdir_includes=() +copy_files=true +destdir=/opt +destdir_override_userhost= +destdir_ssh= +destdir_force_remote= +srcdir=. +files=() +owner=root: +modes=(u=rwX,g=rX,o=rX) +root_scripts=(lib/uinst/rootconf) diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..5ea289c --- /dev/null +++ b/TODO.md @@ -0,0 +1,15 @@ +# nulib + +* runners + * [ ] rnlphp -- lancer un programme php avec la bonne version (+docker le cas échéant) + * [ ] utilisable en shebang + * [ ] utilisable en tant que lien: lance `../php/bin/$MYNAME.php` + * [ ] frontend pour composer + * [ ] rnljava -- lancer un programme java avec la bonne version (+docker le cas échéant) + * [ ] frontend pour maven + * [ ] rnlawk -- lancer un script awk + * [ ] rnlpy3 -- lancer un script python3 + * [ ] rnlsh -- lancer un shell avec les librairies bash / lancer un script +* MYTRUEDIR, MYTRUENAME, MYTRUESELF -- résoudre les liens symboliques + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/awk/src/base.array.awk b/awk/src/base.array.awk new file mode 100644 index 0000000..bd5ac32 --- /dev/null +++ b/awk/src/base.array.awk @@ -0,0 +1,157 @@ +# -*- coding: utf-8 mode: awk -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +function mkindices(values, indices, i, j) { + array_new(indices) + j = 1 + for (i in values) { + indices[j++] = int(i) + } + return asort(indices) +} +function array_new(dest) { + dest[0] = 0 # forcer awk à considérer dest comme un tableau + delete dest +} +function array_newsize(dest, size, i) { + dest[0] = 0 # forcer awk à considérer dest comme un tableau + delete dest + size = int(size) + for (i = 1; i <= size; i++) { + dest[i] = "" + } +} +function array_len(values, count, i) { + # length(array) a un bug sur awk 3.1.5 + # cette version est plus lente mais fonctionne toujours + count = 0 + for (i in values) { + count++ + } + return count +} +function array_copy(dest, src, count, indices, i) { + array_new(dest) + count = mkindices(src, indices) + for (i = 1; i <= count; i++) { + dest[indices[i]] = src[indices[i]] + } +} +function array_getlastindex(src, count, indices) { + count = mkindices(src, indices) + if (count == 0) return 0 + return indices[count] +} +function array_add(dest, value, lastindex) { + lastindex = array_getlastindex(dest) + dest[lastindex + 1] = value +} +function array_deli(dest, i, l) { + i = int(i) + if (i == 0) return + l = array_len(dest) + while (i < l) { + dest[i] = dest[i + 1] + i++ + } + delete dest[l] +} +function array_del(dest, value, ignoreCase, i) { + do { + i = key_index(value, dest, ignoreCase) + if (i != 0) array_deli(dest, i) + } while (i != 0) +} +function array_extend(dest, src, count, lastindex, indices, i) { + lastindex = array_getlastindex(dest) + count = mkindices(src, indices) + for (i = 1; i <= count; i++) { + dest[lastindex + i] = src[indices[i]] + } +} +function array_fill(dest, i) { + array_new(dest) + for (i = 1; i <= NF; i++) { + dest[i] = $i + } +} +function array_getline(src, count, indices, i, j) { + $0 = "" + count = mkindices(src, indices) + for (i = 1; i <= count; i++) { + j = indices[i] + $j = src[j] + } +} +function array_appendline(src, count, indices, i, nf, j) { + count = mkindices(src, indices) + nf = NF + for (i = 1; i <= count; i++) { + j = nf + indices[i] + $j = src[indices[i]] + } +} +function in_array(value, values, ignoreCase, i) { + if (ignoreCase) { + value = tolower(value) + for (i in values) { + if (tolower(values[i]) == value) return 1 + } + } else { + for (i in values) { + if (values[i] == value) return 1 + } + } + return 0 +} +function key_index(value, values, ignoreCase, i) { + if (ignoreCase) { + value = tolower(value) + for (i in values) { + if (tolower(values[i]) == value) return int(i) + } + } else { + for (i in values) { + if (values[i] == value) return int(i) + } + } + return 0 +} +function array2s(values, prefix, sep, suffix, noindices, first, i, s) { + if (!prefix) prefix = "[" + if (!sep) sep = ", " + if (!suffix) suffix = "]" + s = prefix + first = 1 + for (i in values) { + if (first) first = 0 + else s = s sep + if (!noindices) s = s "[" i "]=" + s = s values[i] + } + s = s suffix + return s +} +function array2so(values, prefix, sep, suffix, noindices, count, indices, i, s) { + if (!prefix) prefix = "[" + if (!sep) sep = ", " + if (!suffix) suffix = "]" + s = prefix + count = mkindices(values, indices) + for (i = 1; i <= count; i++) { + if (i > 1) s = s sep + if (!noindices) s = s "[" indices[i] "]=" + s = s values[indices[i]] + } + s = s suffix + return s +} +function array_join(values, sep, prefix, suffix, count, indices, i, s) { + s = prefix + count = mkindices(values, indices) + for (i = 1; i <= count; i++) { + if (i > 1) s = s sep + s = s values[indices[i]] + } + s = s suffix + return s +} diff --git a/awk/src/base.awk b/awk/src/base.awk new file mode 100644 index 0000000..65c79fe --- /dev/null +++ b/awk/src/base.awk @@ -0,0 +1,5 @@ +# -*- coding: utf-8 mode: awk -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +@include "base.core.awk" +@include "base.array.awk" +@include "base.date.awk" +@include "base.tools.awk" diff --git a/awk/src/base.core.awk b/awk/src/base.core.awk new file mode 100644 index 0000000..49a4b58 --- /dev/null +++ b/awk/src/base.core.awk @@ -0,0 +1,141 @@ +# -*- coding: utf-8 mode: awk -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +function num(s) { + if (s ~ /^[0-9]+$/) return int(s) + else return s +} +function ord(s, i) { + s = substr(s, 1, 1) + i = index(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", s) + if (i != 0) i += 32 - 1 + return i +} +function hex(i, s) { + s = sprintf("%x", i) + if (length(s) < 2) s = "0" s + return s +} +function qhtml(s) { + gsub(/&/, "\\&", s) + gsub(/"/, "\\"", s) + gsub(/>/, "\\>", s) + gsub(/", s) + gsub(/"/, "\"", s) + gsub(/&/, "\\&", s) + return s +} +function qawk(s) { + gsub(/\\/, "\\\\", s) + gsub(/"/, "\\\"", s) + gsub(/\n/, "\\n", s) + return "\"" s "\"" +} +function qval(s) { + gsub(/'/, "'\\''", s) + return "'" s "'" +} +function sqval(s) { + return " " qval(s) +} +function qvals( i, line) { + line = "" + for (i = 1; i <= NF; i++) { + if (i > 1) line = line " " + line = line qval($i) + } + return line +} +function sqvals() { + return " " qvals() +} +function qarr(values, prefix, i, count, line) { + line = prefix + count = array_len(values) + for (i = 1; i <= count; i++) { + if (i > 1 || line != "") line = line " " + line = line qval(values[i]) + } + return line +} +function qregexp(s) { + gsub(/[[\\.^$*+?()|{]/, "\\\\&", s) + return s +} +function qsubrepl(s) { + gsub(/\\/, "\\\\", s) + gsub(/&/, "\\\\&", s) + return s +} +function qgrep(s) { + gsub(/[[\\.^$*]/, "\\\\&", s) + return s +} +function qegrep(s) { + gsub(/[[\\.^$*+?()|{]/, "\\\\&", s) + return s +} +function qsql(s, suffix) { + gsub(/'/, "''", s) + return "'" s "'" (suffix != ""? " " suffix: "") +} +function cqsql(s, suffix) { + return "," qsql(s, suffix) +} +function unquote_mysqlcsv(s) { + gsub(/\\n/, "\n", s) + gsub(/\\t/, "\t", s) + gsub(/\\0/, "\0", s) + gsub(/\\\\/, "\\", s) + return s +} +function sval(s) { + if (s == "") return s + else return " " s +} +function cval(s, suffix) { + suffix = suffix != ""? " " suffix: "" + if (s == "") return s + else return "," s suffix +} + +function printto(s, output) { + if (output == "") { + print s + } else if (output ~ /^>>/) { + sub(/^>>/, "", output) + print s >>output + } else if (output ~ /^>/) { + sub(/^>/, "", output) + print s >output + } else if (output ~ /^\|&/) { + sub(/^\|&/, "", output) + print s |&output + } else if (output ~ /^\|/) { + sub(/^\|/, "", output) + print s |output + } else { + print s >output + } +} +function find_line(input, field, value, orig, line) { + orig = $0 + line = "" + while ((getline 0) { + if ($field == value) { + line = $0 + break + } + } + close(input) + $0 = orig + return line +} +function merge_line(input, field, key, line) { + line = find_line(input, field, $key) + if (line != "") $0 = $0 FS line +} diff --git a/awk/src/base.date.awk b/awk/src/base.date.awk new file mode 100644 index 0000000..48e3eff --- /dev/null +++ b/awk/src/base.date.awk @@ -0,0 +1,52 @@ +# -*- coding: utf-8 mode: awk -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +function date__parse_fr(date, parts, y, m, d) { + if (match(date, /([0-9][0-9]?)\/([0-9][0-9]?)\/([0-9][0-9][0-9][0-9])/, parts)) { + y = int(parts[3]) + m = int(parts[2]) + d = int(parts[1]) + return mktime(sprintf("%04i %02i %02i 00 00 00 +0400", y, m, d)) + } else if (match(date, /([0-9][0-9]?)\/([0-9][0-9]?)\/([0-9][0-9])/, parts)) { + basey = int(strftime("%Y")); basey = basey - basey % 100 + y = basey + int(parts[3]) + m = int(parts[2]) + d = int(parts[1]) + return mktime(sprintf("%04i %02i %02i 00 00 00 +0400", y, m, d)) + } + return -1 +} +function date__parse_mysql(date, parts, y, m, d) { + if (match(date, /([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])/, parts)) { + y = int(parts[1]) + m = int(parts[2]) + d = int(parts[3]) + return mktime(sprintf("%04i %02i %02i 00 00 00 +0400", y, m, d)) + } + return -1 +} +function date__parse_any(date, serial) { + serial = date__parse_fr(date) + if (serial == -1) serial = date__parse_mysql(date) + return serial +} +function date_serial(date) { + return date__parse_any(date) +} +function date_parse(date, serial) { + serial = date__parse_any(date) + if (serial == -1) return date + return strftime("%d/%m/%Y", serial) +} +function date_monday(date, serial, dow) { + serial = date__parse_any(date) + if (serial == -1) return date + dow = strftime("%u", serial) + serial -= (dow - 1) * 86400 + return strftime("%d/%m/%Y", serial) +} +function date_add(date, nbdays, serial) { + serial = date__parse_any(date) + if (serial == -1) return date + serial += nbdays * 86400 + return strftime("%d/%m/%Y", serial) +} diff --git a/awk/src/base.tools.awk b/awk/src/base.tools.awk new file mode 100644 index 0000000..64f6d89 --- /dev/null +++ b/awk/src/base.tools.awk @@ -0,0 +1,20 @@ +BEGIN { + srand() +} + +function get_random_password( password, max, LETTERS) { + LETTERS = "AZERTYUIOPQSDFGHJKLMWXCVBNazertyuiopqsdfghjklmwxcvbn0123456789" + max = length(LETTERS) + password = "" + for (i = 0; i < 16; i++) { + password = password substr(LETTERS, int(rand() * max), 1) + } + return password +} + +function should_generate_password() { + return $0 ~ /XXXRANDOMXXX/ +} +function generate_password() { + sub(/XXXRANDOMXXX/, get_random_password()) +} diff --git a/awk/src/csv.awk b/awk/src/csv.awk new file mode 100644 index 0000000..c58e41b --- /dev/null +++ b/awk/src/csv.awk @@ -0,0 +1,201 @@ +# -*- coding: utf-8 mode: awk -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +@include "base.core.awk" +@include "base.array.awk" + +function csv__parse_quoted(line, destl, colsep, qchar, echar, pos, tmpl, nextc, resl) { + line = substr(line, 2) + resl = "" + while (1) { + pos = index(line, qchar) + if (pos == 0) { + # chaine mal terminee + resl = resl line + destl[0] = "" + destl[1] = 0 + return resl + } + if (echar != "" && pos > 1) { + # tenir compte du fait qu"un caratère peut être mis en échappement + prevc = substr(line, pos - 1, 1) + quotec = substr(line, pos, 1) + nextc = substr(line, pos + 1, 1) + if (prevc == echar) { + # qchar en échappement + tmpl = substr(line, 1, pos - 2) + resl = resl tmpl quotec + line = substr(line, pos + 1) + continue + } + tmpl = substr(line, 1, pos - 1) + if (nextc == colsep || nextc == "") { + # fin de champ ou fin de ligne + resl = resl tmpl + destl[0] = substr(line, pos + 2) + destl[1] = nextc == colsep + return resl + } else { + # erreur de syntaxe: guillemet non mis en échappement + # ignorer cette erreur et prendre le guillemet quand meme + resl = resl tmpl quotec + line = substr(line, pos + 1) + } + } else { + # pas d"échappement pour qchar. il est éventuellement doublé + tmpl = substr(line, 1, pos - 1) + quotec = substr(line, pos, 1) + nextc = substr(line, pos + 1, 1) + if (nextc == colsep || nextc == "") { + # fin de champ ou fin de ligne + resl = resl tmpl + destl[0] = substr(line, pos + 2) + destl[1] = nextc == colsep + return resl + } else if (nextc == qchar) { + # qchar en echappement + resl = resl tmpl quotec + line = substr(line, pos + 2) + } else { + # erreur de syntaxe: guillemet non mis en échappement + # ignorer cette erreur et prendre le guillemet quand meme + resl = resl tmpl quotec + line = substr(line, pos + 1) + } + } + } +} +function csv__parse_unquoted(line, destl, colsep, qchar, echar, pos) { + pos = index(line, colsep) + if (pos == 0) { + destl[0] = "" + destl[1] = 0 + return line + } else { + destl[0] = substr(line, pos + 1) + destl[1] = 1 + return substr(line, 1, pos - 1) + } +} +function csv__array_parse(fields, line, nbfields, colsep, qchar, echar, shouldparse, destl, i) { + array_new(fields) + array_new(destl) + i = 1 + shouldparse = 0 + # shouldparse permet de gérer le cas où un champ vide est en fin de ligne. + # en effet, après "," il faut toujours parser, même si line=="" + while (shouldparse || line != "") { + if (index(line, qchar) == 1) { + value = csv__parse_quoted(line, destl, colsep, qchar, echar) + line = destl[0] + shouldparse = destl[1] + } else { + value = csv__parse_unquoted(line, destl, colsep, qchar, echar) + line = destl[0] + shouldparse = destl[1] + } + fields[i] = value + i = i + 1 + } + if (nbfields) { + nbfields = int(nbfields) + i = array_len(fields) + while (i < nbfields) { + i++ + fields[i] = "" + } + } + return array_len(fields) +} +BEGIN { + DEFAULT_COLSEP = "," + DEFAULT_QCHAR = "\"" + DEFAULT_ECHAR = "" +} +function array_parsecsv2(fields, line, nbfields, colsep, qchar, echar) { + return csv__array_parse(fields, line, nbfields, colsep, qchar, echar) +} +function array_parsecsv(fields, line, nbfields, colsep, qchar, echar) { + if (colsep == "") colsep = DEFAULT_COLSEP + if (qchar == "") qchar = DEFAULT_QCHAR + if (echar == "") echar = DEFAULT_ECHAR + return csv__array_parse(fields, line, nbfields, colsep, qchar, echar) +} +function parsecsv(line, fields) { + array_parsecsv(fields, line) + array_getline(fields) + return NF +} +function getlinecsv(file, fields) { + if (file) { + getline 1) line = line colsep + if (qchar != "" && index(value, qchar) != 0) { + if (echar != "") gsub(qchar, quote_subrepl(echar) "&", value); + else gsub(qchar, "&&", value); + } + if (qchar != "" && (index(value, mvsep) != 0 || index(value, colsep) != 0 || index(value, qchar) != 0 || csv__should_quote(value))) { + line = line qchar value qchar + } else { + line = line value + } + } + return line +} +function array_formatcsv(fields) { + return array_formatcsv2(fields, ",", ";", "\"", "") +} +function array_printcsv(fields, output) { + printto(array_formatcsv(fields), output) +} +function get_formatcsv( fields) { + array_fill(fields) + return array_formatcsv(fields) +} +function formatcsv() { + $0 = get_formatcsv() +} +function printcsv(output, fields) { + array_fill(fields) + array_printcsv(fields, output) +} +function array_findcsv(fields, input, field, value, nbfields, orig, found, i) { + array_new(orig) + array_fill(orig) + array_new(fields) + found = 0 + while ((getline 0) { + array_parsecsv(fields, $0, nbfields) + if (fields[field] == value) { + found = 1 + break + } + } + close(input) + array_getline(orig) + if (!found) { + delete fields + if (nbfields) { + nbfields = int(nbfields) + i = array_len(fields) + while (i < nbfields) { + i++ + fields[i] = "" + } + } + } + return found +} diff --git a/awk/src/enc.base64.awk b/awk/src/enc.base64.awk new file mode 100644 index 0000000..3ce38e2 --- /dev/null +++ b/awk/src/enc.base64.awk @@ -0,0 +1,57 @@ +# -*- coding: utf-8 mode: awk -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +function base64__and(var, x, l_res, l_i) { + l_res = 0 + for (l_i = 0; l_i < 8; l_i++) { + if (var%2 == 1 && x%2 == 1) l_res = l_res/2 + 128 + else l_res /= 2 + var = int(var/2) + x = int(x/2) + } + return l_res +} +# Rotate bytevalue left x times +function base64__lshift(var, x) { + while(x > 0) { + var *= 2 + x-- + } + return var +} +# Rotate bytevalue right x times +function base64__rshift(var, x) { + while(x > 0) { + var = int(var/2) + x-- + } + return var +} +BEGIN { + BASE64__BYTES = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" +} +function b64decode(src, result, base1, base2, base3, base4) { + result = "" + while (length(src) > 0) { + # Specify byte values + base1 = substr(src, 1, 1) + base2 = substr(src, 2, 1) + base3 = substr(src, 3, 1); if (base3 == "") base3 = "=" + base4 = substr(src, 4, 1); if (base4 == "") base4 = "=" + # Now find numerical position in BASE64 string + byte1 = index(BASE64__BYTES, base1) - 1 + if (byte1 < 0) byte1 = 0 + byte2 = index(BASE64__BYTES, base2) - 1 + if (byte2 < 0) byte2 = 0 + byte3 = index(BASE64__BYTES, base3) - 1 + if (byte3 < 0) byte3 = 0 + byte4 = index(BASE64__BYTES, base4) - 1 + if (byte4 < 0) byte4 = 0 + # Reconstruct ASCII string + result = result sprintf( "%c", base64__lshift(base64__and(byte1, 63), 2) + base64__rshift(base64__and(byte2, 48), 4) ) + if (base3 != "=") result = result sprintf( "%c", base64__lshift(base64__and(byte2, 15), 4) + base64__rshift(base64__and(byte3, 60), 2) ) + if (base4 != "=") result = result sprintf( "%c", base64__lshift(base64__and(byte3, 3), 6) + byte4 ) + # Decrease incoming string with 4 + src = substr(src, 5) + } + return result +} diff --git a/bash/TODO.md b/bash/TODO.md new file mode 100644 index 0000000..9eca406 --- /dev/null +++ b/bash/TODO.md @@ -0,0 +1,45 @@ +# nulib/bash + +## template + +* [x] pour tout fichier source `.file.template`, considérer avant + `file.template.local` s'il existe, ce qui permet à un utilisateur de + remplacer le modèle livré. + cela a-t-il du sens de supporter aussi file.dist.local? vu que ça ne sert + qu'une seule fois? ça ne mange pas de pain... + +## args + +* [x] support des couples d'options --option et --no-option qui mettent à jour + tous les deux la variables option. ceci: + ~~~ + --option . + --no-option . + ~~~ + est équivalent à ceci: + ~~~ + --option '$inc@ option' + --no-option '$dec@ option' + ~~~ + dec@ est une nouvelle fonction qui décrémente et remplace par une chaine vide + quand on arrive à zéro +* [x] args: support des noms d'argument pour améliorer l'affichage de l'aide. + par exemple la définition + ~~~ + -f:file,--input input= "spécifier le fichier en entrée" + ~~~ + donnera cette aide: + ~~~ + -f, --input FILE + spécifier le fichier + ~~~ +* [ ] args: après le support des noms d'arguments, ajouter la génération + automatique de l'auto-complétion basée sur ces informations. certains noms + seraient normalisés: `file` pour un fichier, `dir` pour un répertoire, `env` + pour une variable d'environnement, etc. + on pourrait même considérer mettre des patterns pour la sélection, e.g + ~~~ + "-C,--config:file:*.conf *.cnf" input= "spécifier le fichier de configuration" + ~~~ + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/bash/src/TEMPLATE b/bash/src/TEMPLATE new file mode 100644 index 0000000..aba64fa --- /dev/null +++ b/bash/src/TEMPLATE @@ -0,0 +1,4 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: TEMPLATE "DESCRIPTION" + diff --git a/bash/src/_output_color.sh b/bash/src/_output_color.sh new file mode 100644 index 0000000..ce0dd77 --- /dev/null +++ b/bash/src/_output_color.sh @@ -0,0 +1,77 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +function __esection() { + local -a lines + local lsep prefix="$(__edate)$(__eindent0)" + local length="${COLUMNS:-80}" + setx lsep=__complete "$prefix" "$length" - + + tooenc "$COULEUR_BLEUE$lsep$COULEUR_NORMALE" + [ -n "$*" ] || return 0 + length=$((length - 1)) + setx -a lines=echo "$1" + for line in "${lines[@]}"; do + setx line=__complete "$prefix- $line" "$length" + tooenc "$COULEUR_BLEUE$line-$COULEUR_NORMALE" + done + tooenc "$COULEUR_BLEUE$lsep$COULEUR_NORMALE" +} +function __etitle() { + local -a lines; local maxlen=0 + local prefix="$(__edate)$(__eindent0)" + + setx -a lines=echo "$1" + for line in "${lines[@]}"; do + [ ${#line} -gt $maxlen ] && maxlen=${#line} + tooenc "${prefix}${COULEUR_BLEUE}T $line$COULEUR_NORMALE" + done + maxlen=$((maxlen + 2)) + tooenc "${prefix}${COULEUR_BLEUE}T$(__complete "" $maxlen -)${COULEUR_NORMALE}" +} +function __edesc() { + local -a lines + local prefix="$(__edate)$(__eindent0)" + + setx -a lines=echo "$1" + for line in "${lines[@]}"; do + tooenc "${prefix}${COULEUR_BLEUE}>${COULEUR_NORMALE} $line" + done +} +function __ebanner() { + local -a lines + local lsep prefix="$(__edate)$(__eindent0)" + local length="${COLUMNS:-80}" + setx lsep=__complete "$prefix" "$length" = + + tooenc "$COULEUR_ROUGE$lsep" + length=$((length - 1)) + setx -a lines=echo "$1" + for line in "" "${lines[@]}" ""; do + setx line=__complete "$prefix= $line" "$length" + tooenc "$line=" + done + tooenc "$lsep$COULEUR_NORMALE" +} +function __eimportant() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}!${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __eattention() { tooenc "$(__edate)$(__eindent0)${COULEUR_JAUNE}*${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __eerror() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}E${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __ewarn() { tooenc "$(__edate)$(__eindent0)${COULEUR_JAUNE}W${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __enote() { tooenc "$(__edate)$(__eindent0)${COULEUR_VERTE}N${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __einfo() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLEUE}I${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __edebug() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLANCHE}D${COULEUR_NORMALE} $(__eindent "$1" " ")"; } + +function __estep() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepe() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepw() { tooenc "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepn() { tooenc "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepi() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estep_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepe_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepw_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepn_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __estepi_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } + +function __action() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __asuccess() { tooenc "$(__edate)$(__eindent0)${COULEUR_VERTE}✔${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __afailure() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}✘${COULEUR_NORMALE} $(__eindent "$1" " ")"; } +function __adone() { tooenc "$(__edate)$(__eindent0)$(__eindent "$1")"; } diff --git a/bash/src/_output_vanilla.sh b/bash/src/_output_vanilla.sh new file mode 100644 index 0000000..c37509d --- /dev/null +++ b/bash/src/_output_vanilla.sh @@ -0,0 +1,66 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +function __esection() { + local -a lines + local lsep prefix="$(__edate)$(__eindent0)" + local length="${COLUMNS:-80}" + setx lsep=__complete "$prefix" "$length" - + + tooenc "$lsep" + [ -n "$*" ] || return 0 + length=$((length - 1)) + setx -a lines=echo "$1" + for line in "${lines[@]}"; do + setx line=__complete "$prefix- $line" "$length" + tooenc "$line-" + done + tooenc "$lsep" +} +function __etitle() { + local p="TITLE: " i=" " + tooenc "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")" +} +function __edesc() { + local p="DESC: " i=" " + tooenc "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")" +} +function __ebanner() { + local -a lines + local lsep prefix="$(__edate)$(__eindent0)" + local length="${COLUMNS:-80}" + setx lsep=__complete "$prefix" "$length" = + + tooenc "$lsep" + length=$((length - 1)) + setx -a lines=echo "$1" + for line in "" "${lines[@]}" ""; do + setx line=__complete "$prefix= $line" "$length" + tooenc "$line=" + done + tooenc "$lsep" +} +function __eimportant() { tooenc "$(__edate)$(__eindent0)IMPORTANT! $(__eindent "$1" " ")"; } +function __eattention() { tooenc "$(__edate)$(__eindent0)ATTENTION! $(__eindent "$1" " ")"; } +function __eerror() { tooenc "$(__edate)$(__eindent0)ERROR: $(__eindent "$1" " ")"; } +function __ewarn() { tooenc "$(__edate)$(__eindent0)WARNING: $(__eindent "$1" " ")"; } +function __enote() { tooenc "$(__edate)$(__eindent0)NOTE: $(__eindent "$1" " ")"; } +function __einfo() { tooenc "$(__edate)$(__eindent0)INFO: $(__eindent "$1" " ")"; } +function __edebug() { tooenc "$(__edate)$(__eindent0)DEBUG: $(__eindent "$1" " ")"; } +function __eecho() { tooenc "$(__edate)$(__eindent0)$(__eindent "$1")"; } +function __eecho_() { tooenc_ "$(__edate)$(__eindent0)$(__eindent "$1")"; } + +function __estep() { tooenc "$(__edate)$(__eindent0). $(__eindent "$1" " ")"; } +function __estepe() { tooenc "$(__edate)$(__eindent0).E $(__eindent "$1" " ")"; } +function __estepw() { tooenc "$(__edate)$(__eindent0).W $(__eindent "$1" " ")"; } +function __estepn() { tooenc "$(__edate)$(__eindent0).N $(__eindent "$1" " ")"; } +function __estepi() { tooenc "$(__edate)$(__eindent0).I $(__eindent "$1" " ")"; } +function __estep_() { tooenc_ "$(__edate)$(__eindent0). $(__eindent "$1" " ")"; } +function __estepe_() { tooenc_ "$(__edate)$(__eindent0).E $(__eindent "$1" " ")"; } +function __estepw_() { tooenc_ "$(__edate)$(__eindent0).W $(__eindent "$1" " ")"; } +function __estepn_() { tooenc_ "$(__edate)$(__eindent0).N $(__eindent "$1" " ")"; } +function __estepi_() { tooenc_ "$(__edate)$(__eindent0).I $(__eindent "$1" " ")"; } + +function __action() { tooenc "$(__edate)$(__eindent0)ACTION: $(__eindent "$1" " ")"; } +function __asuccess() { tooenc "$(__edate)$(__eindent0)(OK) $(__eindent "$1" " ")"; } +function __afailure() { tooenc "$(__edate)$(__eindent0)(KO) $(__eindent "$1" " ")"; } +function __adone() { tooenc "$(__edate)$(__eindent0)$(__eindent "$1")"; } diff --git a/bash/src/base.args.sh b/bash/src/base.args.sh new file mode 100644 index 0000000..393a7c5 --- /dev/null +++ b/bash/src/base.args.sh @@ -0,0 +1,486 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.args "Fonctions de base: analyse d'arguments" + +function: local_args "Afficher des commandes pour rendre locales des variables utilisées par parse_args() + +Cela permet d'utiliser parse_args() à l'intérieur d'une fonction." +function local_args() { + echo "local -a args" + echo "local NULIB_ARGS_ONERROR_RETURN=1" + echo "local NULIB_VERBOSITY=\"\$NULIB_VERBOSITY\"" + echo "local NULIB_INTERACTION=\"\$NULIB_INTERACTION\"" +} + +function: parse_args "Analyser les arguments de la ligne de commande à partir des définitions du tableau args + +Cette fonction s'utilise ainsi: +~~~"' +args=( + [desc] + [usage] + [+|-] + -o,--longopt action [optdesc] + -a:,--mandarg: action [optdesc] + -b::,--optarg:: action [optdesc] +) +parse_args "$@"; set -- "${args[@]}" +~~~'" + +au retour de la fonction, args contient les arguments qui n'ont pas été traités +automatiquement. + +les options --help et --help++ sont automatiquement gérées. avec --help, seules +les options standards sont affichées. --help++ affiche toutes les options. les +descriptions sont utilisées pour l'affichage de l'aide. une option avancée est +identifiée par une description qui commence par ++ + +desc +: description de l'objet du script ou de la fonction. cette valeur est + facultative + +usage +: description des arguments du script, sans le nom du script. par exemple la + valeur '[options] FILE' générera le texte d'aide suivant: + ~~~ + USAGE + \$MYNAME [options] FILE + ~~~ + Peut contenir autant de lignes que nécessaire. Chaque ligne est préfixée du + nom du script, jusqu'à la première ligne vide. Ensuite, les lignes sont + affichées telles quelles. + Le premier espace est ignoré, ce qui permet de spécifier des USAGEs avec une + option, e.g ' -c VALUE' + ++|- +: méthode d'analyse des arguments. + * Par défaut, les options sont valides n'importe où sur la ligne de commande. + * Avec '+', l'analyse s'arrête au premier argument qui n'est pas une option. + * Avec '-', les options sont valides n'importe où sur la ligne de commande, + mais les arguments ne sont pas réordonnés, et apparaissent dans l'ordre de + leur mention. IMPORTANT: dans ce cas, aucun argument ni option n'est traité, + c'est à la charge de l'utilisateur. Au retour de la fonction, args contient + l'ensemble des arguments tels qu'analysés par getopt + +-o, --longopt +: option sans argument + +-a:, --mandarg: +: option avec argument obligatoire + + l'option peut être suivi d'une valeur qui décrit l'argument attendu e.g + -a:file pour un fichier. cette valeur est mise en majuscule lors de + l'affichage de l'aide. pour le moment, cette valeur n'est pas signifiante. + +-b::, --optarg:: +: option avec argument facultatif + + l'option peut être suivi d'une valeur qui décrit l'argument attendu e.g + -b::file pour un fichier. cette valeur est mise en majuscule lors de + l'affichage de l'aide. pour le moment, cette valeur n'est pas signifiante. + +action +: action à effectuer si cette option est utilisée. plusieurs syntaxes sont valides: + * 'NAME' met à jour la variable en fonction du type d'argument: l'incrémenter + pour une option sans argument, lui donner la valeur spécifiée pour une + option avec argument, ajouter la valeur spécifiée au tableau si l'option est + spécifiée plusieurs fois. + la valeur spéciale '.' calcule une valeur de NAME en fonction du nom de + l'option la plus longue. par exemple, les deux définitions suivantes sont + équivalentes: + ~~~ + -o,--short,--very-long . + -o,--short,--very-long very_long + ~~~ + De plus, la valeur spéciale '.' traite les options de la forme --no-opt + comme l'inverse des option --opt. par exemple, les deux définitions + suivantes sont équivalentes: + ~~~ + --opt . --no-opt . + --opt opt --no-opt '\$dec@ opt' + ~~~ + * 'NAME=VALUE' pour une option sans argument, forcer la valeur spécifiée; pour + une option avec argument, prendre la valeur spécifiée comme valeur par + défaut si la valeur de l'option est vide + * '\$CMD' CMD est évalué avec eval *dès* que l'option est rencontrée. + les valeurs suivantes sont initialisées: + * option_ est l'option utilisée, e.g --long-opt + * value_ est la valeur de l'option + les fonctions suivantes sont définies: + * 'inc@ NAME' incrémente la variable NAME -- c'est le comportement de set@ si + l'option est sans argument + * 'dec@ NAME' décrémente la variable NAME, et la rend vide quand le compte + arrive à zéro + * 'res@ NAME VALUE' initialise la variable NAME avec la valeur de l'option (ou + VALUE si la valeur de l'option est vide) -- c'est le comportement de set@ + si l'option prend un argument + * 'add@ NAME VALUE' ajoute la valeur de l'option (ou VALUE si la valeur de + l'option est vide) au tableau NAME. + * 'set@ NAME' met à jour la variable NAME en fonction de la définition de + l'option (avec ou sans argument, ajout ou non à un tableau) + +optdesc +: description de l'option. cette valeur est facultative. si la description + commence par ++, c'est une option avancée qui n'est pas affichée par défaut." +function parse_args() { + eval "$NULIB__DISABLE_SET_X" + local __r= + local __DIE='[ -n "$NULIB_ARGS_ONERROR_RETURN" ] && return 1 || die' + + if ! is_array args; then + eerror "Invalid args definition: args must be defined" + __r=1 + fi + # distinguer les descriptions des définition d'arguments + local __USAGE __DESC + local -a __DEFS __ARGS + __ARGS=("$@") + set -- "${args[@]}" + [ "${1#-}" == "$1" ] && { __DESC="$1"; shift; } + [ "${1#-}" == "$1" ] && { __USAGE="$1"; shift; } + if [ -n "$__r" ]; then + : + elif [ $# -gt 0 -a "$1" != "+" -a "${1#-}" == "$1" ]; then + eerror "Invalid args definition: third arg must be an option" + __r=1 + else + __DEFS=("$@") + __parse_args || __r=1 + fi + eval "$NULIB__ENABLE_SET_X" + if [ -n "$__r" ]; then + eval "$__DIE" + fi +} +function __parse_args() { + ## tout d'abord, construire la liste des options + local __AUTOH=1 __AUTOHELP=1 # faut-il gérer automatiquement l'affichage de l'aide? + local __AUTOD=1 __AUTODEBUG=1 # faut-il rajouter les options -D et --debug + local __ADVHELP # y a-t-il des options avancées? + local __popt __sopts __lopts + local -a __defs + set -- "${__DEFS[@]}" + while [ $# -gt 0 ]; do + case "$1" in + +) __popt="$1"; shift; continue;; + -) __popt="$1"; shift; continue;; + -*) IFS=, read -a __defs <<<"$1"; shift;; + *) eerror "Invalid arg definition: expected option, got '$1'"; eval "$__DIE";; + esac + # est-ce que l'option prend un argument? + local __def __witharg __valdesc + __witharg= + for __def in "${__defs[@]}"; do + if [ "${__def%::*}" != "$__def" ]; then + [ "$__witharg" != : ] && __witharg=:: + elif [ "${__def%:*}" != "$__def" ]; then + __witharg=: + fi + done + # définitions __sopts et __lopts + for __def in "${__defs[@]}"; do + __def="${__def%%:*}" + if [[ "$__def" == --* ]]; then + # --longopt + __def="${__def#--}" + __lopts="$__lopts${__lopts:+,}$__def$__witharg" + elif [[ "$__def" == -* ]] && [ ${#__def} -eq 2 ]; then + # -o + __def="${__def#-}" + __sopts="$__sopts$__def$__witharg" + [ "$__def" == h ] && __AUTOH= + [ "$__def" == D ] && __AUTOD= + else + # -longopt ou longopt + __def="${__def#-}" + __lopts="$__lopts${__lopts:+,}$__def$__witharg" + fi + [ "$__def" == help -o "$__def" == help++ ] && __AUTOHELP= + [ "$__def" == debug ] && __AUTODEBUG= + done + # sauter l'action + shift + # sauter la description le cas échéant + if [ "${1#-}" == "$1" ]; then + [ "${1#++}" != "$1" ] && __ADVHELP=1 + shift + fi + done + + [ -n "$__AUTOH" ] && __sopts="${__sopts}h" + [ -n "$__AUTOHELP" ] && __lopts="$__lopts${__lopts:+,}help,help++" + [ -n "$__AUTOD" ] && __sopts="${__sopts}D" + [ -n "$__AUTODEBUG" ] && __lopts="$__lopts${__lopts:+,}debug" + + __sopts="$__popt$__sopts" + local -a __getopt_args + __getopt_args=(-n "$MYNAME" ${__sopts:+-o "$__sopts"} ${__lopts:+-l "$__lopts"} -- "${__ARGS[@]}") + + ## puis analyser et normaliser les arguments + if args="$(getopt -q "${__getopt_args[@]}")"; then + eval "set -- $args" + else + # relancer pour avoir le message d'erreur + LANG=C getopt "${__getopt_args[@]}" 2>&1 1>/dev/null + eval "$__DIE" + fi + + ## puis traiter les options + local __defname __resvalue __decvalue __defvalue __add __action option_ name_ value_ + function inc@() { + eval "[ -n \"\$$1\" ] || let $1=0" + eval "let $1=$1+1" + } + function dec@() { + eval "[ -n \"\$$1\" ] && let $1=$1-1" + eval "[ \"\$$1\" == 0 ] && $1=" + } + function res@() { + local __value="${value_:-$2}" + eval "$1=\"\$__value\"" + } + function add@() { + local __value="${value_:-$2}" + eval "$1+=(\"\$__value\")" + } + function set@() { + if [ -n "$__resvalue" ]; then + res@ "$@" + elif [ -n "$__witharg" ]; then + if is_array "$1"; then + add@ "$@" + elif ! is_defined "$1"; then + # première occurrence: variable + res@ "$@" + else + # deuxième occurence: tableau + [ -z "${!1}" ] && eval "$1=()" + add@ "$@" + fi + elif [ -n "$__decvalue" ]; then + dec@ "$@" + else + inc@ "$@" + fi + } + function showhelp@() { + local help="$MYNAME" showadv="$1" + if [ -n "$__DESC" ]; then + help="$help: $__DESC" + fi + + local first usage nl=$'\n' + local prefix=" $MYNAME " + local usages="${__USAGE# }" + [ -n "$usages" ] || usages="[options]" + help="$help + +USAGE" + first=1 + while [ -n "$usages" ]; do + usage="${usages%%$nl*}" + if [ "$usage" != "$usages" ]; then + usages="${usages#*$nl}" + else + usages= + fi + if [ -n "$first" ]; then + first= + [ -z "$usage" ] && continue + else + [ -z "$usage" ] && prefix= + fi + help="$help +$prefix$usage" + done + + set -- "${__DEFS[@]}" + first=1 + while [ $# -gt 0 ]; do + case "$1" in + +) shift; continue;; + -) shift; continue;; + -*) IFS=, read -a __defs <<<"$1"; shift;; + esac + if [ -n "$first" ]; then + first= + help="$help${nl}${nl}OPTIONS" + if [ -n "$__AUTOHELP" -a -n "$__ADVHELP" ]; then + help="$help + --help++ + Afficher l'aide avancée" + fi + fi + # est-ce que l'option prend un argument? + __witharg= + __valdesc=value + for __def in "${__defs[@]}"; do + if [ "${__def%::*}" != "$__def" ]; then + [ "$__witharg" != : ] && __witharg=:: + [ -n "${__def#*::}" ] && __valdesc="[${__def#*::}]" + elif [ "${__def%:*}" != "$__def" ]; then + __witharg=: + [ -n "${__def#*:}" ] && __valdesc="${__def#*:}" + fi + done + # description de l'option + local first=1 thelp tdesc + for __def in "${__defs[@]}"; do + __def="${__def%%:*}" + if [[ "$__def" == --* ]]; then + : # --longopt + elif [[ "$__def" == -* ]] && [ ${#__def} -eq 2 ]; then + : # -o + else + # -longopt ou longopt + __def="--${__def#-}" + fi + if [ -n "$first" ]; then + first= + thelp="${nl} " + else + thelp="$thelp, " + fi + thelp="$thelp$__def" + done + [ -n "$__witharg" ] && thelp="$thelp ${__valdesc^^}" + # sauter l'action + shift + # prendre la description le cas échéant + if [ "${1#-}" == "$1" ]; then + tdesc="$1" + if [ "${tdesc#++}" != "$tdesc" ]; then + # option avancée + if [ -n "$showadv" ]; then + tdesc="${tdesc#++}" + else + thelp= + fi + fi + [ -n "$thelp" ] && thelp="$thelp${nl} ${tdesc//$nl/$nl }" + shift + fi + [ -n "$thelp" ] && help="$help$thelp" + done + uecho "$help" + exit 0 + } + if [ "$__popt" != - ]; then + while [ $# -gt 0 ]; do + if [ "$1" == -- ]; then + shift + break + fi + [[ "$1" == -* ]] || break + option_="$1"; shift + __parse_opt "$option_" + if [ -n "$__witharg" ]; then + # l'option prend un argument + value_="$1"; shift + else + # l'option ne prend pas d'argument + value_= + fi + eval "$__action" + done + fi + unset -f inc@ res@ add@ set@ showhelp@ + args=("$@") +} +function __parse_opt() { + # $1 est l'option spécifiée + local option_="$1" + set -- "${__DEFS[@]}" + while [ $# -gt 0 ]; do + case "$1" in + +) shift; continue;; + -) shift; continue;; + -*) IFS=, read -a __defs <<<"$1"; shift;; + esac + # est-ce que l'option prend un argument? + __witharg= + for __def in "${__defs[@]}"; do + if [ "${__def%::*}" != "$__def" ]; then + [ "$__witharg" != : ] && __witharg=:: + elif [ "${__def%:*}" != "$__def" ]; then + __witharg=: + fi + done + # nom le plus long + __defname= + local __found= + for __def in "${__defs[@]}"; do + __def="${__def%%:*}" + [ "$__def" == "$option_" ] && __found=1 + if [[ "$__def" == --* ]]; then + # --longopt + __def="${__def#--}" + [ ${#__def} -gt ${#__defname} ] && __defname="$__def" + elif [[ "$__def" == -* ]] && [ ${#__def} -eq 2 ]; then + # -o + __def="${__def#-}" + [ ${#__def} -gt ${#__defname} ] && __defname="$__def" + else + # -longopt ou longopt + __def="${__def#-}" + [ ${#__def} -gt ${#__defname} ] && __defname="$__def" + fi + done + __defname="${__defname//-/_}" + # analyser l'action + __decvalue= + if [ "${1#\$}" != "$1" ]; then + name_="$__defname" + __resvalue= + __defvalue= + __action="${1#\$}" + else + if [ "$1" == . ]; then + name_="$__defname" + __resvalue= + __defvalue= + if [ "${name_#no_}" != "$name_" ]; then + name_="${name_#no_}" + __decvalue=1 + fi + elif [[ "$1" == *=* ]]; then + name_="${1%%=*}" + __resvalue=1 + __defvalue="${1#*=}" + else + name_="$1" + __resvalue= + __defvalue= + fi + __action="$(qvals set@ "$name_" "$__defvalue")" + fi + shift + # sauter la description le cas échéant + [ "${1#-}" == "$1" ] && shift + + [ -n "$__found" ] && return 0 + done + if [ -n "$__AUTOH" -a "$option_" == -h ]; then + __action="showhelp@" + return 0 + fi + if [ -n "$__AUTOHELP" ]; then + if [ "$option_" == --help ]; then + __action="showhelp@" + return 0 + elif [ "$option_" == --help++ ]; then + __action="showhelp@ ++" + return 0 + fi + fi + if [ -n "$__AUTOD" -a "$option_" == -D ]; then + __action=set_debug + return 0 + fi + if [ -n "$__AUTODEBUG" -a "$option_" == --debug ]; then + __action=set_debug + return 0 + fi + # ici, l'option n'a pas été trouvée, on ne devrait pas arriver ici + eerror "Unexpected option '$option_'"; eval "$__DIE" +} diff --git a/bash/src/base.array.sh b/bash/src/base.array.sh new file mode 100644 index 0000000..9c47e71 --- /dev/null +++ b/bash/src/base.array.sh @@ -0,0 +1,360 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.array "Fonctions de base: gestion des variables tableaux" + +function: array_count "retourner le nombre d'éléments du tableau \$1" +function array_count() { + eval "echo \${#$1[*]}" +} + +function: array_isempty "tester si le tableau \$1 est vide" +function array_isempty() { + eval "[ \${#$1[*]} -eq 0 ]" +} + +function: array_new "créer un tableau vide dans la variable \$1" +function array_new() { + eval "$1=()" +} + +function: array_copy "copier le contenu du tableau \$2 dans le tableau \$1" +function array_copy() { + eval "$1=(\"\${$2[@]}\")" +} + +function: array_add "ajouter les valeurs \$2..@ à la fin du tableau \$1" +function array_add() { + local __aa_a="$1"; shift + eval "$__aa_a+=(\"\$@\")" +} + +function: array_ins "insérer les valeurs \$2..@ au début du tableau \$1" +function array_ins() { + local __aa_a="$1"; shift + eval "$__aa_a=(\"\$@\" \"\${$__aa_a[@]}\")" +} + +function: array_del "supprimer *les* valeurs \$2 du tableau \$1" +function array_del() { + local __ad_v + local -a __ad_vs + eval ' +for __ad_v in "${'"$1"'[@]}"; do + if [ "$__ad_v" != "$2" ]; then + __ad_vs=("${__ad_vs[@]}" "$__ad_v") + fi +done' + array_copy "$1" __ad_vs +} + +function: array_addu "ajouter la valeur \$2 au tableau \$1, si la valeur n'y est pas déjà + +Retourner vrai si la valeur a été ajoutée" +function array_addu() { + local __as_v + eval ' +for __as_v in "${'"$1"'[@]}"; do + [ "$__as_v" == "$2" ] && return 1 +done' + array_add "$1" "$2" + return 0 +} + +function: array_insu "insérer la valeur \$2 au début du tableau tableau \$1, si la valeur n'y est pas déjà + +Retourner vrai si la valeur a été ajoutée." +function array_insu() { + local __as_v + eval ' +for __as_v in "${'"$1"'[@]}"; do + [ "$__as_v" == "$2" ] && return 1 +done' + array_ins "$1" "$2" + return 0 +} + +function: array_fillrange "Initialiser le tableau \$1 avec les nombres de \$2(=1) à \$3(=10) avec un step de \$4(=1)" +function array_fillrange() { + local -a __af_vs + local __af_i="${2:-1}" __af_to="${3:-10}" __af_step="${4:-1}" + while [ "$__af_i" -le "$__af_to" ]; do + __af_vs=("${__af_vs[@]}" "$__af_i") + __af_i=$(($__af_i + $__af_step)) + done + array_copy "$1" __af_vs +} + +function: array_eq "tester l'égalité des tableaux \$1 et \$2" +function array_eq() { + local -a __ae_a1 __ae_a2 + array_copy __ae_a1 "$1" + array_copy __ae_a2 "$2" + [ ${#__ae_a1[*]} -eq ${#__ae_a2[*]} ] || return 1 + local __ae_v __ae_i=0 + for __ae_v in "${__ae_a1[@]}"; do + [ "$__ae_v" == "${__ae_a2[$__ae_i]}" ] || return 1 + __ae_i=$(($__ae_i + 1)) + done + return 0 +} + +function: array_contains "tester si le tableau \$1 contient la valeur \$2" +function array_contains() { + local __ac_v + eval ' +for __ac_v in "${'"$1"'[@]}"; do + [ "$__ac_v" == "$2" ] && return 0 +done' + return 1 +} + +function: array_icontains "tester si le tableau \$1 contient la valeur \$2, sans tenir compte de la casse" +function array_icontains() { + local __ac_v + eval ' +for __ac_v in "${'"$1"'[@]}"; do + [ "${__ac_v,,} == "${2,,}" ] && return 0 +done' + return 1 +} + +function: array_find "si le tableau \$1 contient la valeur \$2, afficher l'index de la valeur. Si le tableau \$3 est spécifié, afficher la valeur à l'index dans ce tableau" +function array_find() { + local __af_i __af_v + __af_i=0 + eval ' +for __af_v in "${'"$1"'[@]}"; do + if [ "$__af_v" == "$2" ]; then + if [ -n "$3" ]; then + recho "${'"$3"'[$__af_i]}" + else + echo "$__af_i" + fi + return 0 + fi + __af_i=$(($__af_i + 1)) +done' + return 1 +} + +function: array_reverse "Inverser l'ordre des élément du tableau \$1" +function array_reverse() { + local -a __ar_vs + local __ar_v + array_copy __ar_vs "$1" + array_new "$1" + for __ar_v in "${__ar_vs[@]}"; do + array_ins "$1" "$__ar_v" + done +} + +function: array_replace "dans le tableau \$1, remplacer toutes les occurences de \$2 par \$3..*" +function array_replace() { + local __ar_sn="$1"; shift + local __ar_f="$1"; shift + local -a __ar_s __ar_d + local __ar_v + array_copy __ar_s "$__ar_sn" + for __ar_v in "${__ar_s[@]}"; do + if [ "$__ar_v" == "$__ar_f" ]; then + __ar_d=("${__ar_d[@]}" "$@") + else + __ar_d=("${__ar_d[@]}" "$__ar_v") + fi + done + array_copy "$__ar_sn" __ar_d +} + +function: array_each "Pour chacune des valeurs ITEM du tableau \$1, appeler la fonction \$2 avec les arguments (\$3..@ ITEM)" +function array_each() { + local __ae_v + local -a __ae_a + array_copy __ae_a "$1"; shift + for __ae_v in "${__ae_a[@]}"; do + "$@" "$__ae_v" + done +} + +function: array_map "Pour chacune des valeurs ITEM du tableau \$1, appeler la fonction \$2 avec les arguments (\$3..@ ITEM), et remplacer la valeur par le résultat de la fonction" +function array_map() { + local __am_v + local -a __am_a __am_vs + local __am_an="$1"; shift + local __am_f="$1"; shift + array_copy __am_a "$__am_an" + for __am_v in "${__am_a[@]}"; do + __am_vs=("${__am_vs[@]}" "$("$__am_f" "$@" "$__am_v")") + done + array_copy "$__am_an" __am_vs +} + +function: array_first "afficher la première valeur du tableau \$1" +function array_first() { + eval "recho \"\${$1[@]:0:1}\"" +} + +function: array_last "afficher la dernière valeur du tableau \$1" +function array_last() { + eval "recho \"\${$1[@]: -1:1}\"" +} + +function: array_copy_firsts "copier toutes les valeurs du tableau \$2(=\$1) dans le tableau \$1, excepté la dernière" +function array_copy_firsts() { + eval "$1=(\"\${${2:-$1}[@]:0:\$((\${#${2:-$1}[@]}-1))}\")" +} + +function: array_copy_lasts "copier toutes les valeurs du tableau \$2(=\$1) dans le tableau \$1, excepté la première" +function array_copy_lasts() { + eval "$1=(\"\${${2:-$1}[@]:1}\")" +} + +function: array_extend "ajouter le contenu du tableau \$2 au tableau \$1" +function array_extend() { + eval "$1=(\"\${$1[@]}\" \"\${$2[@]}\")" +} + +function: array_extendu "ajouter chacune des valeurs du tableau \$2 au tableau \$1, si ces valeurs n'y sont pas déjà + +Retourner vrai si au moins une valeur a été ajoutée" +function array_extendu() { + local __ae_v __ae_s=1 + eval ' +for __ae_v in "${'"$2"'[@]}"; do + array_addu "$1" "$__ae_v" && __ae_s=0 +done' + return "$__ae_s" +} + +function: array_extend_firsts "ajouter toutes les valeurs du tableau \$2 dans le tableau \$1, excepté la dernière" +function array_extend_firsts() { + eval "$1=(\"\${$1[@]}\" \"\${$2[@]:0:\$((\${#$2[@]}-1))}\")" +} + +function: array_extend_lasts "ajouter toutes les valeurs du tableau \$2 dans le tableau \$1, excepté la première" +function array_extend_lasts() { + eval "$1=(\"\${$1[@]}\" \"\${$2[@]:1}\")" +} + +function: array_xsplit "créer le tableau \$1 avec chaque élément de \$2 (un ensemble d'éléments séparés par \$3, qui vaut ':' par défaut)" +function array_xsplit() { + eval "$1=($(recho_ "$2" | lawk -v RS="${3:-:}" ' +{ + gsub(/'\''/, "'\'\\\\\'\''") + print "'\''" $0 "'\''" +}'))" #" +} + +function: array_xsplitc "variante de array_xsplit() où le séparateur est ',' par défaut" +function array_xsplitc() { + array_xsplit "$1" "$2" "${3:-,}" +} + +function: array_split "créer le tableau \$1 avec chaque élément de \$2 (un ensemble d'éléments séparés par \$3, qui vaut ':' par défaut) + +Les éléments vides sont ignorés. par exemple \"a::b\" est équivalent à \"a:b\"" +function array_split() { + eval "$1=($(recho_ "$2" | lawk -v RS="${3:-:}" ' +/^$/ { next } +{ + gsub(/'\''/, "'\'\\\\\'\''") + print "'\''" $0 "'\''" +}'))" #" +} + +function: array_splitc "variante de array_split() où le séparateur est ',' par défaut" +function array_splitc() { + array_split "$1" "$2" "${3:-,}" +} + +function: array_xsplitl "créer le tableau \$1 avec chaque ligne de \$2" +function array_xsplitl() { + eval "$1=($(recho_ "$2" | nl2lf | lawk ' +{ + gsub(/'\''/, "'\'\\\\\'\''") + print "'\''" $0 "'\''" +}'))" #" +} + +function: array_splitl "créer le tableau \$1 avec chaque ligne de \$2 + +Les lignes vides sont ignorés." +function array_splitl() { + eval "$1=($(recho_ "$2" | nl2lf | lawk ' +/^$/ { next } +{ + gsub(/'\''/, "'\'\\\\\'\''") + print "'\''" $0 "'\''" +}'))" #" +} + +function: array_join "afficher le contenu du tableau \$1 sous forme d'une liste de valeurs séparées par \$2 (qui vaut ':' par défaut) + +* Si \$1==\"@\", alors les éléments du tableaux sont les arguments de la fonction à partir de \$3 +* Si \$1!=\"@\" et que le tableau est vide, afficher \$3 +* Si \$1!=\"@\", \$4 et \$5 sont des préfixes et suffixes à rajouter à chaque élément" +function array_join() { + local __aj_an __aj_l __aj_j __aj_s="${2:-:}" __aj_pf __aj_sf + if [ "$1" == "@" ]; then + __aj_an="\$@" + shift; shift + else + __aj_an="\${$1[@]}" + __aj_pf="$4" + __aj_sf="$5" + fi + eval ' +for __aj_l in "'"$__aj_an"'"; do + __aj_j="${__aj_j:+$__aj_j'"$__aj_s"'}$__aj_pf$__aj_l$__aj_sf" +done' + if [ -n "$__aj_j" ]; then + recho "$__aj_j" + elif [ "$__aj_an" != "\$@" -a -n "$3" ]; then + recho "$3" + fi +} + +function: array_joinc "afficher les éléments du tableau \$1 séparés par ','" +function array_joinc() { + array_join "$1" , "$2" "$3" "$4" +} + +function: array_joinl "afficher les éléments du tableau \$1 à raison d'un élément par ligne" +function array_joinl() { + array_join "$1" " +" "$2" "$3" "$4" +} + +function: array_mapjoin "map le tableau \$1 avec la fonction \$2, puis afficher le résultat en séparant chaque élément par \$3 + +Les arguments et la sémantique sont les mêmes que pour array_join() en +tenant compte de l'argument supplémentaire \$2 qui est la fonction pour +array_map() (les autres arguments sont décalés en conséquence)" +function array_mapjoin() { + local __amj_src="$1" __amj_func="$2" __amj_sep="$3" + shift; shift; shift + if [ "$__amj_src" == "@" ]; then + local -a __amj_tmpsrc + __amj_tmpsrc=("$@") + __amj_src=__amj_tmpsrc + set -- + fi + local -a __amj_tmp + array_copy __amj_tmp "$__amj_src" + array_map __amj_tmp "$__amj_func" + array_join __amj_tmp "$__amj_sep" "$@" +} + +function: array_fix_paths "Corriger les valeurs du tableau \$1. Les valeurs contenant le séparateur \$2(=':') sont séparées en plusieurs valeurs. + +Par exemple avec le tableau input=(a b:c), le résultat est input=(a b c)" +function array_fix_paths() { + local __afp_an="$1" __afp_s="${2:-:}" + local -a __afp_vs + local __afp_v + array_copy __afp_vs "$__afp_an" + array_new "$__afp_an" + for __afp_v in "${__afp_vs[@]}"; do + array_split __afp_v "$__afp_v" "$__afp_s" + array_extend "$__afp_an" __afp_v + done +} diff --git a/bash/src/base.bool.sh b/bash/src/base.bool.sh new file mode 100644 index 0000000..8ed84a7 --- /dev/null +++ b/bash/src/base.bool.sh @@ -0,0 +1,50 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.bool "Fonctions de base: valeurs booléennes" + +function: is_yes 'retourner vrai si $1 est une valeur "oui"' +function is_yes() { + case "${1,,}" in + o|oui|y|yes|v|vrai|t|true|on) return 0;; + esac + isnum "$1" && [ "$1" -ne 0 ] && return 0 + return 1 +} + +function: is_no 'retourner vrai si $1 est une valeur "non"' +function is_no() { + case "${1,,}" in + n|non|no|f|faux|false|off) return 0;; + esac + isnum "$1" && [ "$1" -eq 0 ] && return 0 + return 1 +} + +function: normyesval 'remplacer les valeurs des variables $1..* par la valeur normalisée de leur valeur "oui"' +function normyesval() { + while [ $# -gt 0 ]; do + is_yes "${!1}" && _setv "$1" 1 || _setv "$1" "" + shift + done +} + +function: setb 'Lancer la commande $2..@ en supprimant la sortie standard. Si la commande +retourne vrai, assigner la valeur 1 à la variable $1. Sinon, lui assigner la +valeur "" +note: en principe, la syntaxe est "setb var cmd args...". cependant, la +syntaxe "setb var=cmd args..." est supportée aussi' +function setb() { + local __s_var="$1"; shift + if [[ "$__s_var" == *=* ]]; then + set -- "${__s_var#*=}" "$@" + __s_var="${__s_var%%=*}" + fi + local __s_r + if "$@" >/dev/null; then + eval "$__s_var=1" + else + __s_r=$? + eval "$__s_var=" + return $__s_r + fi +} diff --git a/bash/src/base.core.sh b/bash/src/base.core.sh new file mode 100644 index 0000000..36f5979 --- /dev/null +++ b/bash/src/base.core.sh @@ -0,0 +1,458 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.core "Fonctions de base: fondement" + +function: echo_ "afficher la valeur \$* sans passer à la ligne" +function echo_() { echo -n "$*"; } + +function: recho "afficher une valeur brute. + +contrairement à la commande echo, ne reconnaitre aucune option (i.e. -e, -E, -n +ne sont pas signifiants)" +function recho() { + if [[ "${1:0:2}" == -[eEn] ]]; then + local first="${1:1}"; shift + echo -n - + echo "$first" "$@" + else + echo "$@" + fi +} + +function: recho_ "afficher une valeur brute, sans passer à la ligne. + +contrairement à la commande echo, ne reconnaitre aucune option (i.e. -e, -E, -n +ne sont pas signifiants)" +function recho_() { + if [[ "${1:0:2}" == -[eEn] ]]; then + local first="${1:1}"; shift + echo -n - + echo -n "$first" "$@" + else + echo -n "$@" + fi +} + +function: _qval "Dans la chaine \$*, remplacer: +~~~ +\\ par \\\\ +\" par \\\" +\$ par \\\$ +\` par \\\` +~~~ + +Cela permet de quoter une chaine à mettre entre guillements. + +note: la protection de ! n'est pas effectuée, parce que le comportement du shell +est incohérent entre le shell interactif et les scripts. Pour une version plus +robuste, il est nécessaire d'utiliser un programme externe tel que sed ou awk" +function _qval() { + local s="$*" + s="${s//\\/\\\\}" + s="${s//\"/\\\"}" + s="${s//\$/\\\$}" + s="${s//\`/\\\`}" + recho_ "$s" +} + +function: should_quote "Tester si la chaine \$* doit être mise entre quotes" +function should_quote() { + # pour optimiser, toujours mettre entre quotes si plusieurs arguments sont + # spécifiés ou si on spécifie une chaine vide ou de plus de 80 caractères + [ $# -eq 0 -o $# -gt 1 -o ${#1} -eq 0 -o ${#1} -gt 80 ] && return 0 + # sinon, tester si la chaine contient des caractères spéciaux + local s="$*" + s="${s//[a-zA-Z0-9]/}" + s="${s//,/}" + s="${s//./}" + s="${s//+/}" + s="${s//\//}" + s="${s//-/}" + s="${s//_/}" + s="${s//=/}" + [ -n "$s" ] +} + +function: qval "Afficher la chaine \$* quotée avec \"" +function qval() { + echo -n \" + _qval "$@" + echo \" +} + +function: qvalm "Afficher la chaine \$* quotée si nécessaire avec \"" +function qvalm() { + if should_quote "$@"; then + echo -n \" + _qval "$@" + echo \" + else + recho "$@" + fi +} + +function: qvalr "Afficher la chaine \$* quotée si nécessaire avec \", sauf si elle est vide" +function qvalr() { + if [ -z "$*" ]; then + : + elif should_quote "$@"; then + echo -n \" + _qval "$@" + echo \" + else + recho "$@" + fi +} + +function: qvals "Afficher chaque argument de cette fonction quotée le cas échéant avec \", chaque valeur étant séparée par un espace" +function qvals() { + local arg first=1 + for arg in "$@"; do + [ -z "$first" ] && echo -n " " + if should_quote "$arg"; then + echo -n \" + _qval "$arg" + echo -n \" + else + recho_ "$arg" + fi + first= + done + [ -z "$first" ] && echo +} + +function: qwc "Dans la chaine \$*, remplacer: +~~~ + \\ par \\\\ +\" par \\\" +\$ par \\\$ +\` par \\\` +~~~ +puis quoter la chaine avec \", sauf les wildcards *, ? et [class] + +Cela permet de quoter une chaine permettant de glober des fichiers, e.g +~~~ +eval \"ls \$(qwc \"\$value\")\" +~~~ + +note: la protection de ! n'est pas effectuée, parce que le comportement du shell +est incohérent entre le shell interactif et les scripts. Pour une version plus +robuste, il est nécessaire d'utiliser un programme externe tel que sed ou awk" +function qwc() { + local s="$*" + s="${s//\\/\\\\}" + s="${s//\"/\\\"}" + s="${s//\$/\\\$}" + s="${s//\`/\\\`}" + local r a b c + while [ -n "$s" ]; do + a=; b=; c= + a=; [[ "$s" == *\** ]] && { a="${s%%\**}"; a=${#a}; } + b=; [[ "$s" == *\?* ]] && { b="${s%%\?*}"; b=${#b}; } + c=; [[ "$s" == *\[* ]] && { c="${s%%\[*}"; c=${#c}; } + if [ -z "$a" -a -z "$b" -a -z "$c" ]; then + r="$r\"$s\"" + break + fi + if [ -n "$a" ]; then + [ -n "$b" ] && [ $a -lt $b ] && b= + [ -n "$c" ] && [ $a -lt $c ] && c= + fi + if [ -n "$b" ]; then + [ -n "$a" ] && [ $b -lt $a ] && a= + [ -n "$c" ] && [ $b -lt $c ] && c= + fi + if [ -n "$c" ]; then + [ -n "$a" ] && [ $c -lt $a ] && a= + [ -n "$b" ] && [ $c -lt $b ] && b= + fi + if [ -n "$a" ]; then # PREFIX* + a="${s%%\**}" + s="${s#*\*}" + [ -n "$a" ] && r="$r\"$a\"" + r="$r*" + elif [ -n "$b" ]; then # PREFIX? + a="${s%%\?*}" + s="${s#*\?}" + [ -n "$a" ] && r="$r\"$a\"" + r="$r?" + elif [ -n "$c" ]; then # PREFIX[class] + a="${s%%\[*}" + b="${s#*\[}"; b="${b%%\]*}" + s="${s:$((${#a} + ${#b} + 2))}" + [ -n "$a" ] && r="$r\"$a\"" + r="$r[$b]" + fi + done + recho_ "$r" +} + +function: qlines "Traiter chaque ligne de l'entrée standard pour en faire des chaines quotées avec '" +function qlines() { + sed "s/'/'\\\\''/g; s/.*/'&'/g" +} + +function: setv "initialiser la variable \$1 avec la valeur \$2..* + +note: en principe, la syntaxe est 'setv var values...'. cependant, la syntaxe 'setv var=values...' est supportée aussi" +function setv() { + local s__var="$1"; shift + if [[ "$s__var" == *=* ]]; then + set -- "${s__var#*=}" "$@" + s__var="${s__var%%=*}" + fi + eval "$s__var=\"\$*\"" +} + +function: _setv "Comme la fonction setv() mais ne supporte que la syntaxe '_setv var values...' + +Cette fonction est légèrement plus rapide que setv()" +function _setv() { + local s__var="$1"; shift + eval "$s__var=\"\$*\"" +} + +function: echo_setv "Afficher la commande qui serait lancée par setv \"\$@\"" +function echo_setv() { + local s__var="$1"; shift + if [[ "$s__var" == *=* ]]; then + set -- "${s__var#*=}" "$@" + s__var="${s__var%%=*}" + fi + echo "$s__var=$(qvalr "$*")" +} + +function: echo_setv2 "Afficher la commande qui recrée la variable \$1. + +Equivalent à +~~~ +echo_setv \"\$1=\${!1}\" +~~~ + +Si d'autres arguments que le nom de la variable sont spécifiés, cette fonction +se comporte comme echo_setv()" +function echo_setv2() { + local s__var="$1"; shift + if [[ "$s__var" == *=* ]]; then + set -- "${s__var#*=}" "$@" + s__var="${s__var%%=*}" + fi + if [ $# -eq 0 ]; then + echo_setv "$s__var" "${!s__var}" + else + echo_setv "$s__var" "$@" + fi +} + +function: seta "initialiser le tableau \$1 avec les valeurs \$2..@ + +note: en principe, la syntaxe est 'seta array values...'. cependant, la syntaxe +'seta array=values...' est supportée aussi" +function seta() { + local s__array="$1"; shift + if [[ "$s__array" == *=* ]]; then + set -- "${s__array#*=}" "$@" + s__array="${s__array%%=*}" + fi + eval "$s__array=(\"\$@\")" +} + +function: _seta "Comme la fonction seta() mais ne supporte que la syntaxe '_seta array values...' + +Cette fonction est légèrement plus rapide que seta()" +function _seta() { + local s__array="$1"; shift + eval "$s__array=(\"\$@\")" +} + +function: echo_seta "Afficher la commande qui serait lancée par seta \"\$@\"" +function echo_seta() { + local s__var="$1"; shift + if [[ "$s__var" == *=* ]]; then + set -- "${s__var#*=}" "$@" + s__var="${s__var%%=*}" + fi + echo "$s__var=($(qvals "$@"))" +} + +function: echo_seta2 "Afficher la commande qui recrée le tableau \$1 + +Si d'autres arguments que le nom de tableau sont spécifiés, cette fonction se +comporte comme echo_seta()" +function echo_seta2() { + local s__var="$1"; shift + if [[ "$s__var" == *=* ]]; then + set -- "${s__var#*=}" "$@" + s__var="${s__var%%=*}" + elif [ $# -eq 0 ]; then + eval "set -- \"\${$s__var[@]}\"" + fi + echo "$s__var=($(qvals "$@"))" +} + +function: setx "Initialiser une variable avec le résultat d'une commande + +* syntaxe 1: initialiser la variable \$1 avec le résultat de la commande \"\$2..@\" + ~~~ + setx var cmd + ~~~ + note: en principe, la syntaxe est 'setx var cmd args...'. cependant, la syntaxe + 'setx var=cmd args...' est supportée aussi + +* syntaxe 2: initialiser le tableau \$1 avec le résultat de la commande + \"\$2..@\", chaque ligne du résultat étant un élément du tableau + ~~~ + setx -a array cmd + ~~~ + note: en principe, la syntaxe est 'setx -a array cmd args...'. cependant, la + syntaxe 'setx -a array=cmd args...' est supportée aussi" +function setx() { + if [ "$1" == -a ]; then + shift + local s__array="$1"; shift + if [[ "$s__array" == *=* ]]; then + set -- "${s__array#*=}" "$@" + s__array="${s__array%%=*}" + fi + eval "$s__array=($("$@" | qlines))" + else + local s__var="$1"; shift + if [[ "$s__var" == *=* ]]; then + set -- "${s__var#*=}" "$@" + s__var="${s__var%%=*}" + fi + eval "$s__var="'"$("$@")"' + fi +} + +function: _setvx "Comme la fonction setx() mais ne supporte que l'initialisation d'une variable scalaire avec la syntaxe '_setvx var cmd args...' pour gagner (un peu) en rapidité d'exécution." +function _setvx() { + local s__var="$1"; shift + eval "$s__var="'"$("$@")"' +} + +function: _setax "Comme la fonction setx() mais ne supporte que l'initialisation d'un tableau avec la syntaxe '_setax array cmd args...' pour gagner (un peu) en rapidité d'exécution." +function _setax() { + local s__array="$1"; shift + eval "$s__array=($("$@" | qlines))" +} + +function: is_defined "tester si la variable \$1 est définie" +function is_defined() { + [ -n "$(declare -p "$1" 2>/dev/null)" ] +} + +function: is_array "tester si la variable \$1 est un tableau" +function is_array() { + [[ "$(declare -p "$1" 2>/dev/null)" =~ declare\ -[^\ ]*a[^\ ]*\ ]] +} + +function: array_local "afficher les commandes pour faire une copie dans la variable locale \$1 du tableau \$2" +function array_local() { + if [ "$1" == "$2" ]; then + declare -p "$1" 2>/dev/null || echo "local -a $1" + else + echo "local -a $1; $1=(\"\${$2[@]}\")" + fi +} + +function: upvar "Implémentation de upvar() de http://www.fvue.nl/wiki/Bash:_Passing_variables_by_reference + +USAGE +~~~ +local varname && upvar varname values... +~~~ +* @param varname Variable name to assign value to +* @param values Value(s) to assign. If multiple values (> 1), an array is + assigned, otherwise a single value is assigned." +function upvar() { + if unset -v "$1"; then + if [ $# -lt 2 ]; then + eval "$1=\"\$2\"" + else + eval "$1=(\"\${@:2}\")" + fi + fi +} + +function: array_upvar "Comme upvar() mais force la création d'un tableau, même s'il y a que 0 ou 1 argument" +function array_upvar() { + unset -v "$1" && eval "$1=(\"\${@:2}\")" +} + +function: upvars "Implémentation modifiée de upvars() de http://www.fvue.nl/wiki/Bash:_Passing_variables_by_reference + +Par rapport à l'original, il n'est plus nécessaire de préfixer une variable +scalaire avec -v, et -a peut être spécifié sans argument. + +USAGE +~~~ +local varnames... && upvars [varname value | -aN varname values...]... +~~~ +* @param -a assigns remaining values to varname as array +* @param -aN assigns next N values to varname as array. Returns 1 if wrong + number of options occurs" +function upvars() { + while [ $# -gt 0 ]; do + case "$1" in + -a) + unset -v "$2" && eval "$2=(\"\${@:3}\")" + break + ;; + -a*) + unset -v "$2" && eval "$2=(\"\${@:3:${1#-a}}\")" + shift $((${1#-a} + 2)) || return 1 + ;; + *) + unset -v "$1" && eval "$1=\"\$2\"" + shift; shift + ;; + esac + done +} + +function: set_debug "Passer en mode DEBUG" +function set_debug() { + export NULIB_DEBUG=1 +} + +function: is_debug "Tester si on est en mode DEBUG" +function is_debug() { + [ -n "$NULIB_DEBUG" ] +} + +function: lawk "Lancer GNUawk avec la librairie 'base'" +function lawk() { + gawk -i base.awk "$@" +} + +function: cawk "Lancer GNUawk avec LANG=C et la librairie 'base' + +Le fait de forcer la valeur de LANG permet d'éviter les problèmes avec la locale" +function cawk() { + LANG=C gawk -i base.awk "$@" +} + +function: lsort "Lancer sort avec support de la locale courante" +function: csort "Lancer sort avec LANG=C pour désactiver le support de la locale + +Avec LANG!=C, sort utilise les règles de la locale pour le tri, et par +exemple, avec LANG=fr_FR.UTF-8, la locale indique que les ponctuations doivent +être ignorées." +function lsort() { sort "$@"; } +function csort() { LANG=C sort "$@"; } + +function: lgrep "Lancer grep avec support de la locale courante" +function: cgrep "Lancer grep avec LANG=C pour désactiver le support de la locale" +function lgrep() { grep "$@"; } +function cgrep() { LANG=C grep "$@"; } + +function: lsed "Lancer sed avec support de la locale courante" +function: csed "Lancer sed avec LANG=C pour désactiver le support de la locale" +function lsed() { sed "$@"; } +function csed() { LANG=C sed "$@"; } + +function: ldiff "Lancer diff avec support de la locale courante" +function: cdiff "Lancer diff avec LANG=C pour désactiver le support de la locale" +function ldiff() { diff "$@"; } +function cdiff() { LANG=C diff "$@"; } diff --git a/bash/src/base.init.sh b/bash/src/base.init.sh new file mode 100644 index 0000000..de5ae8c --- /dev/null +++ b/bash/src/base.init.sh @@ -0,0 +1,53 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.init "Fonctions de base: initialiser l'environnement" + +if [ -z "$NULIB_NO_INIT_ENV" ]; then + # Emplacement du script courant + if [ "$0" == "-bash" ]; then + MYNAME= + MYDIR= + MYSELF= + elif [ ! -f "$0" -a -f "${0#-}" ]; then + MYNAME="$(basename -- "${0#-}")" + MYDIR="$(dirname -- "${0#-}")" + MYDIR="$(cd "$MYDIR"; pwd)" + MYSELF="$MYDIR/$MYNAME" + else + MYNAME="$(basename -- "$0")" + MYDIR="$(dirname -- "$0")" + MYDIR="$(cd "$MYDIR"; pwd)" + MYSELF="$MYDIR/$MYNAME" + fi + [ -n "$NULIBDIR" ] || NULIBDIR="$MYDIR" + + # Repertoire temporaire + [ -z "$TMPDIR" -a -d "$HOME/tmp" ] && TMPDIR="$HOME/tmp" + [ -z "$TMPDIR" ] && TMPDIR="${TMP:-${TEMP:-/tmp}}" + export TMPDIR + + # User + [ -z "$USER" -a -n "$LOGNAME" ] && export USER="$LOGNAME" + + # Le fichier nulibrc doit être chargé systématiquement + [ -f /etc/debian_chroot ] && NULIB_CHROOT=1 + [ -f /etc/nulibrc ] && . /etc/nulibrc + [ -f ~/.nulibrc ] && . ~/.nulibrc + + # Type de système sur lequel tourne le script + UNAME_SYSTEM=`uname -s` + [ "${UNAME_SYSTEM#CYGWIN}" != "$UNAME_SYSTEM" ] && UNAME_SYSTEM=Cygwin + [ "${UNAME_SYSTEM#MINGW32}" != "$UNAME_SYSTEM" ] && UNAME_SYSTEM=Mingw + UNAME_MACHINE=`uname -m` + if [ -n "$NULIB_CHROOT" ]; then + # Dans un chroot, il est possible de forcer les valeurs + [ -n "$NULIB_UNAME_SYSTEM" ] && eval "UNAME_SYSTEM=$NULIB_UNAME_SYSTEM" + [ -n "$NULIB_UNAME_MACHINE" ] && eval "UNAME_MACHINE=$NULIB_UNAME_MACHINE" + fi + + # Nom d'hôte respectivement avec et sans domaine + # contrairement à $HOSTNAME, cette valeur peut être spécifiée, comme par ruinst + [ -n "$MYHOST" ] || MYHOST="$HOSTNAME" + [ -n "$MYHOSTNAME" ] || MYHOSTNAME="${HOSTNAME%%.*}" + export MYHOST MYHOSTNAME +fi diff --git a/bash/src/base.input.sh b/bash/src/base.input.sh new file mode 100644 index 0000000..9e9acf0 --- /dev/null +++ b/bash/src/base.input.sh @@ -0,0 +1,581 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.input "Fonctions de base: saisie" + +function toienc() { +# $1 étant une variable contenant une chaine encodée dans l'encoding d'entrée $2 +# (qui vaut par défaut $NULIB_INPUT_ENCODING), transformer cette chaine en +# utf-8 + local __var="$1" __from="${2:-$NULIB_INPUT_ENCODING}" + if [ "$__from" != "NULIB__UTF8" ]; then + _setv "$__var" "$(iconv -f "$__from" -t utf-8 <<<"${!__var}")" + fi +} + +function uread() { +# Lire une valeur sur stdin et la placer dans la variable $1. On assume que la +# valeur en entrée est encodée dans $NULIB_INPUT_ENCODING + [ $# -gt 0 ] || set -- REPLY + local __var + read "$@" + for __var in "$@"; do + [ -z "$__var" -o "${__var:0:1}" == "-" ] && continue # ignorer les options + toienc "$__var" + done +} + +function set_interaction() { :;} +function is_interaction() { return 1; } +function check_interaction() { return 0; } +function get_interaction_option() { :;} + +function ask_yesno() { +# Afficher le message $1 suivi de [oN] ou [On] suivant que $2 vaut O ou N, puis +# lire la réponse. Retourner 0 si la réponse est vrai, 1 sinon. +# Si $1 est une option, elle est utilisée avec check_interaction pour savoir si +# on est en mode interactif ou non. A ce moment-là, les valeurs sont décalées +# ($2=message, $3=default) +# Si $2 vaut C, la valeur par défaut est N si on est interactif, O sinon +# Si $2 vaut X, la valeur par défaut est O si on est interactif, N sinon + local interactive=1 + if [[ "$1" == -* ]]; then + if [ "$1" != -- ]; then + check_interaction "$1" || interactive= + fi + shift + else + check_interaction -c || interactive= + fi + local default="${2:-N}" + if [ "$default" == "C" ]; then + [ -n "$interactive" ] && default=N || default=O + elif [ "$default" == "X" ]; then + [ -n "$interactive" ] && default=O || default=N + fi + if [ -n "$interactive" ]; then + local message="$1" + local prompt="[oN]" + local r + is_yes "$default" && prompt="[On]" + if [ -n "$message" ]; then + __eecho_ "$message" 1>&2 + else + __eecho_ "Voulez-vous continuer?" 1>&2 + fi + tooenc_ " $prompt " 1>&2 + uread r + is_yes "${r:-$default}" + else + is_yes "$default" + fi +} + +function ask_any() { +# Afficher le message $1 suivi du texte "[$2]" (qui vaut par défaut +Oq), puis +# lire la réponse. Les lettres de la chaine de format $2 sont numérotées de 0 à +# $((${#2} - 1)). Le code de retour est le numéro de la lettre qui a été +# sélectionnée. Cette fonction est une généralisation de ask_yesno() pour +# n'importe quel ensemble de lettres. +# La première lettre en majuscule est la lettre sélectionnée par défaut. +# La lettre O matche toutes les lettres qui signifient oui: o, y, 1, v, t +# La lettre N matche toutes les lettres qui signifient non: n, f, 0 +# Il y a des raccourcis: +# +O --> On +# +N --> oN +# +C --> oN si on est en mode interactif, On sinon +# +X --> On si on est en mode interactifn oN sinon +# Si $1 est une option, elle est utilisée avec check_interaction pour savoir si +# on est en mode interactif ou non. A ce moment-là, les valeurs sont décalées +# ($2=message, $3=format) + local interactive=1 + if [[ "$1" == -* ]]; then + if [ "$1" != -- ]; then + check_interaction "$1" || interactive= + fi + shift + else + check_interaction -c || interactive= + fi + local format="${2:-+Oq}" + format="${format/+O/On}" + format="${format/+N/oN}" + if [ -n "$interactive" ]; then + format="${format/+C/oN}" + format="${format/+X/On}" + else + format="${format/+C/On}" + format="${format/+X/oN}" + fi + local i count="${#format}" + + if [ -n "$interactive" ]; then + local message="${1:-Voulez-vous continuer?}" + local prompt="[$format]" + local r f lf defi + while true; do + __eecho_ "$message $prompt " 1>&2 + uread r + r="$(strlower "${r:0:1}")" + i=0; defi= + while [ $i -lt $count ]; do + f="${format:$i:1}" + lf="$(strlower "$f")" + [ "$r" == "$lf" ] && return $i + if [ -z "$defi" ]; then + [ -z "${f/[A-Z]/}" ] && defi="$i" + fi + if [ "$lf" == o ]; then + case "$r" in o|y|1|v|t) return $i;; esac + elif [ "$lf" == n ]; then + case "$r" in n|f|0) return $i;; esac + fi + i=$(($i + 1)) + done + [ -z "$r" ] && return ${defi:-0} + done + else + i=0 + while [ $i -lt $count ]; do + f="${format:$i:1}" + [ -z "${f/[A-Z]/}" ] && return $i + i=$(($i + 1)) + done + return 0 + fi +} + +function read_value() { +# Afficher le message $1 suivi de la valeur par défaut [$3] si elle est non +# vide, puis lire la valeur donnée par l'utilisateur. Cette valeur doit être non +# vide si $4(=O) est vrai. La valeur saisie est placée dans la variable +# $2(=value) +# Si $1 est une option, elle est utilisée avec check_interaction pour savoir si +# on est en mode interactif ou non. A ce moment-là, les valeurs sont décalées +# ($2=message, $3=variable, $4=default, $5=required) +# En mode non interactif, c'est la valeur par défaut qui est sélectionnée. Si +# l'utilisateur requière que la valeur soit non vide et que la valeur par défaut +# est vide, afficher un message d'erreur et retourner faux +# read_password() est comme read_value(), mais la valeur saisie n'est pas +# affichée, ce qui la rend appropriée pour la lecture d'un mot de passe. + local -a __rv_opts; local __rv_readline=1 __rv_showdef=1 __rv_nl= + __rv_opts=() + [ -n "$NULIB_NO_READLINE" ] && __rv_readline= + __rv_read "$@" +} + +function read_password() { + local -a __rv_opts __rv_readline= __rv_showdef= __rv_nl=1 + __rv_opts=(-s) + __rv_read "$@" +} + +function __rv_read() { + local __rv_int=1 + if [[ "$1" == -* ]]; then + if [ "$1" != -- ]; then + check_interaction "$1" || __rv_int= + fi + shift + else + check_interaction -c || __rv_int= + fi + local __rv_msg="$1" __rv_v="${2:-value}" __rv_d="$3" __rv_re="${4:-O}" + if [ -z "$__rv_int" ]; then + # En mode non interactif, retourner la valeur par défaut + if is_yes "$__rv_re" && [ -z "$__rv_d" ]; then + eerror "La valeur par défaut de $__rv_v doit être non vide" + return 1 + fi + _setv "$__rv_v" "$__rv_d" + return 0 + fi + + local __rv_r + while true; do + if [ -n "$__rv_msg" ]; then + __eecho_ "$__rv_msg" 1>&2 + else + __eecho_ "Entrez la valeur" 1>&2 + fi + if [ -n "$__rv_readline" ]; then + tooenc_ ": " 1>&2 + uread -e ${__rv_d:+-i"$__rv_d"} "${__rv_opts[@]}" __rv_r + else + if [ -n "$__rv_d" ]; then + if [ -n "$__rv_showdef" ]; then + tooenc_ " [$__rv_d]" 1>&2 + else + tooenc_ " [****]" 1>&2 + fi + fi + tooenc_ ": " 1>&2 + uread "${__rv_opts[@]}" __rv_r + [ -n "$__rv_nl" ] && echo + fi + __rv_r="${__rv_r:-$__rv_d}" + if [ -n "$__rv_r" ] || ! is_yes "$__rv_re"; then + _setv "$__rv_v" "$__rv_r" + return 0 + fi + done +} + +function simple_menu() { +# Afficher un menu simple dont les éléments sont les valeurs du tableau +# $2(=options). L'option choisie est placée dans la variable $1(=option) +# -t TITLE: spécifier le titre du menu +# -m YOUR_CHOICE: spécifier le message d'invite pour la sélection de l'option +# -d DEFAULT: spécifier l'option par défaut. Par défaut, prendre la valeur +# actuelle de la variable $1(=option) + eval "$(local_args)" + local __sm_title= __sm_yourchoice= __sm_default= + args=( + -t:,--title __sm_title= + -m:,--prompt __sm_yourchoice= + -d:,--default __sm_default= + ) + parse_args "$@"; set -- "${args[@]}" + + local __sm_option_var="${1:-option}" __sm_options_var="${2:-options}" + local __sm_option __sm_options + __sm_options="$__sm_options_var[*]" + if [ -z "${!__sm_options}" ]; then + eerror "Le tableau $__sm_options_var doit être non vide" + return 1 + fi + [ -z "$__sm_default" ] && __sm_default="${!__sm_option_var}" + + array_copy __sm_options "$__sm_options_var" + local __sm_c=0 __sm_i __sm_choice + while true; do + if [ "$__sm_c" == "0" ]; then + # Afficher le menu + [ -n "$__sm_title" ] && __eecho "=== $__sm_title ===" 1>&2 + __sm_i=1 + for __sm_option in "${__sm_options[@]}"; do + if [ "$__sm_option" == "$__sm_default" ]; then + __eecho "$__sm_i*- $__sm_option" 1>&2 + else + __eecho "$__sm_i - $__sm_option" 1>&2 + fi + let __sm_i=$__sm_i+1 + done + fi + + # Afficher les choix + if [ -n "$__sm_yourchoice" ]; then + __eecho_ "$__sm_yourchoice" 1>&2 + else + __eecho_ "Entrez le numéro de l'option choisie" 1>&2 + fi + tooenc_ ": " 1>&2 + uread __sm_choice + + # Valeur par défaut + if [ -z "$__sm_choice" -a -n "$__sm_default" ]; then + __sm_option="$__sm_default" + break + fi + # Vérifier la saisie + if [ -n "$__sm_choice" -a -z "${__sm_choice//[0-9]/}" ]; then + if [ "$__sm_choice" -gt 0 -a "$__sm_choice" -le "${#__sm_options[*]}" ]; then + __sm_option="${__sm_options[$(($__sm_choice - 1))]}" + break + else + eerror "Numéro d'option incorrect" + fi + else + eerror "Vous devez saisir le numéro de l'option choisie" + fi + + let __sm_c=$__sm_c+1 + if [ "$__sm_c" -eq 5 ]; then + # sauter une ligne toutes les 4 tentatives + tooenc "" 1>&2 + __sm_c=0 + fi + done + _setv "$__sm_option_var" "$__sm_option" +} + +function actions_menu() { +# Afficher un menu dont les éléments sont les valeurs du tableau $4(=options), +# et une liste d'actions tirées du tableau $3(=actions). L'option choisie est +# placée dans la variable $2(=option). L'action choisie est placée dans la +# variable $1(=action) +# Un choix est saisi sous la forme [action]num_option +# -t TITLE: spécifier le titre du menu +# -m OPT_YOUR_CHOICE: spécifier le message d'invite pour la sélection de +# l'action et de l'option +# -M ACT_YOUR_CHOICE: spécifier le message d'invite dans le cas où aucune option +# n'est disponible. Dans ce cas, seules les actions vides sont possibles. +# -e VOID_ACTION: spécifier qu'une action est vide, c'est à dire qu'elle ne +# requière pas d'être associée à une option. Par défaut, la dernière action +# est classée dans cette catégorie puisque c'est l'action "quitter" +# -d DEFAULT_ACTION: choisir l'action par défaut. par défaut, c'est la première +# action. +# -q QUIT_ACTION: choisir l'option "quitter" qui provoque la sortie du menu sans +# choix. par défaut, c'est la dernière action. +# -o DEFAULT_OPTION: choisir l'option par défaut. par défaut, prendre la valeur +# actuelle de la variable $2(=option) + eval "$(local_args)" + local -a __am_action_descs __am_options __am_void_actions + local __am_tmp __am_select_action __am_select_option __am_title __am_optyc __am_actyc + local __am_default_action=auto __am_quit_action=auto + local __am_default_option= + args=( + -t:,--title __am_title= + -m:,--prompt __am_optyc= + -M:,--no-prompt __am_actyc= + -e: __am_void_actions + -d: __am_default_action= + -q: __am_quit_action= + -o: __am_default_option= + ) + parse_args "$@"; set -- "${args[@]}" + + __am_tmp="${1:-action}"; __am_select_action="${!__am_tmp}" + __am_tmp="${2:-option}"; __am_select_option="${!__am_tmp}" + [ -n "$__am_default_option" ] && __am_select_option="$__am_default_option" + array_copy __am_action_descs "${3:-actions}" + array_copy __am_options "${4:-options}" + + eerror_unless [ ${#__am_action_descs[*]} -gt 0 ] "Vous devez spécifier le tableau des actions" || return + __actions_menu || return 1 + _setv "${1:-action}" "$__am_select_action" + _setv "${2:-option}" "$__am_select_option" +} + +function __actions_menu() { + local title="$__am_title" + local optyc="$__am_optyc" actyc="$__am_actyc" + local default_action="$__am_default_action" + local quit_action="$__am_quit_action" + local select_action="$__am_select_action" + local select_option="$__am_select_option" + local -a action_descs options void_actions + array_copy action_descs __am_action_descs + array_copy options __am_options + array_copy void_actions __am_void_actions + + # Calculer la liste des actions valides + local no_options + array_isempty options && no_options=1 + + local -a actions + local tmp action name + for tmp in "${action_descs[@]}"; do + splitfsep2 "$tmp" : action name + [ -n "$action" ] || action="${name:0:1}" + action="$(strlower "$action")" + array_addu actions "$action" + done + + # Calculer l'action par défaut + if [ "$default_action" == auto ]; then + # si action par défaut non spécifiée, alors prendre la première action + default_action="$select_action" + if [ -n "$default_action" ]; then + array_contains actions "$default_action" || default_action= + fi + [ -n "$default_action" ] || default_action="${actions[0]}" + fi + default_action="${default_action:0:1}" + default_action="$(strlower "$default_action")" + + # Calculer l'action quitter par défaut + if [ "$quit_action" == auto ]; then + # si action par défaut non spécifiée, alors prendre la dernière action, + # s'il y a au moins 2 actions + if [ ${#actions[*]} -gt 1 ]; then + quit_action="${actions[@]:$((-1)):1}" + array_addu void_actions "$quit_action" + fi + fi + quit_action="${quit_action:0:1}" + quit_action="$(strlower "$quit_action")" + + # Calculer la ligne des actions à afficher + local action_title + for tmp in "${action_descs[@]}"; do + splitfsep2 "$tmp" : action name + [ -n "$action" ] || action="${name:0:1}" + [ -n "$name" ] || name="$action" + action="$(strlower "$action")" + if [ -n "$no_options" ]; then + if ! array_contains void_actions "$action"; then + array_del actions "$action" + continue + fi + fi + [ "$action" == "$default_action" ] && name="$name*" + action_title="${action_title:+$action_title/}$name" + done + if [ -n "$default_action" ]; then + # si action par défaut invalide, alors pas d'action par défaut + array_contains actions "$default_action" || default_action= + fi + if [ -n "$quit_action" ]; then + # si action quitter invalide, alors pas d'action quitter + array_contains actions "$quit_action" || quit_action= + fi + + # Type de menu + if [ -n "$no_options" ]; then + if array_isempty void_actions; then + eerror "Aucune option n'est définie. Il faut définir le tableau des actions vides" + return 1 + fi + __void_actions_menu + else + __options_actions_menu + fi +} + +function __void_actions_menu() { + local c=0 choice + while true; do + if [ $c -eq 0 ]; then + [ -n "$title" ] && __etitle "$title" 1>&2 + __eecho_ "=== Actions disponibles: " 1>&2 + tooenc "$action_title" 1>&2 + fi + if [ -n "$actyc" ]; then + __eecho_ "$actyc" 1>&2 + elif [ -n "$optyc" ]; then + __eecho_ "$optyc" 1>&2 + else + __eecho_ "Entrez l'action à effectuer" 1>&2 + fi + tooenc_ ": " 1>&2 + uread choice + if [ -z "$choice" -a -n "$default_action" ]; then + select_action="$default_action" + break + fi + + # vérifier la saisie + choice="${choice:0:1}" + choice="$(strlower "$choice")" + if array_contains actions "$choice"; then + select_action="$choice" + break + elif [ -n "$choice" ]; then + eerror "$choice: action incorrecte" + else + eerror "vous devez saisir l'action à effectuer" + fi + let c=$c+1 + if [ $c -eq 5 ]; then + # sauter une ligne toutes les 4 tentatives + tooenc "" 1>&2 + c=0 + fi + done + __am_select_action="$select_action" + __am_select_option= +} + +function __options_actions_menu() { + local c=0 option choice action option + while true; do + if [ $c -eq 0 ]; then + [ -n "$title" ] && __etitle "$title" 1>&2 + i=1 + for option in "${options[@]}"; do + if [ "$option" == "$select_option" ]; then + tooenc "$i*- $option" 1>&2 + else + tooenc "$i - $option" 1>&2 + fi + let i=$i+1 + done + __estepn_ "Actions disponibles: " 1>&2 + tooenc "$action_title" 1>&2 + fi + if [ -n "$optyc" ]; then + __eecho_ "$optyc" 1>&2 + else + __eecho_ "Entrez l'action et le numéro de l'option choisie" 1>&2 + fi + tooenc_ ": " 1>&2 + uread choice + + # vérifier la saisie + if [ -z "$choice" -a -n "$default_action" ]; then + action="$default_action" + if array_contains void_actions "$action"; then + select_action="$action" + select_option= + break + elif [ -n "$select_option" ]; then + select_action="$action" + break + fi + fi + action="${choice:0:1}" + action="$(strlower "$action")" + if array_contains actions "$action"; then + # on commence par un code d'action valide. cool :-) + if array_contains void_actions "$action"; then + select_action="$action" + select_option= + break + else + option="${choice:1}" + option="${option// /}" + if [ -z "$option" -a -n "$select_option" ]; then + select_action="$action" + break + elif [ -z "$option" ]; then + eerror "vous devez saisir le numéro de l'option" + elif isnum "$option"; then + if [ $option -gt 0 -a $option -le ${#options[*]} ]; then + select_action="$action" + select_option="${options[$(($option - 1))]}" + break + fi + else + eerror "$option: numéro d'option incorrecte" + fi + fi + elif isnum "$choice"; then + # on a simplement donné un numéro d'option + action="$default_action" + if [ -n "$action" ]; then + if array_contains void_actions "$action"; then + select_action="$action" + select_option= + break + else + option="${choice// /}" + if [ -z "$option" ]; then + eerror "vous devez saisir le numéro de l'option" + elif isnum "$option"; then + if [ $option -gt 0 -a $option -le ${#options[*]} ]; then + select_action="$action" + select_option="${options[$(($option - 1))]}" + break + fi + else + eerror "$option: numéro d'option incorrecte" + fi + fi + else + eerror "Vous devez spécifier l'action à effectuer" + fi + elif [ -n "$choice" ]; then + eerror "$choice: action et/ou option incorrecte" + else + eerror "vous devez saisir l'action à effectuer" + fi + let c=$c+1 + if [ $c -eq 5 ]; then + # sauter une ligne toutes les 4 tentatives + tooenc "" 1>&2 + c=0 + fi + done + __am_select_action="$select_action" + __am_select_option="$select_option" +} diff --git a/bash/src/base.num.sh b/bash/src/base.num.sh new file mode 100644 index 0000000..b54e0ee --- /dev/null +++ b/bash/src/base.num.sh @@ -0,0 +1,30 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.num "Fonctions de base: gestion des valeurs numériques" + +function: isnum 'retourner vrai si $1 est une valeur numérique entière (positive ou négative)' +function isnum() { + [ ${#1} -gt 0 ] || return 1 + local v="${1#-}" + [ ${#v} -gt 0 ] || return 1 + v="${v//[0-9]/}" + [ -z "$v" ] +} + +function: ispnum 'retourner vrai si $1 est une valeur numérique entière positive' +function ispnum() { + [ ${#1} -gt 0 ] || return 1 + [ -z "${1//[0-9]/}" ] +} + +function: isrnum 'retourner vrai si $1 est une valeur numérique réelle (positive ou négative) +le séparateur décimal peut être . ou ,' +function isrnum() { + [ ${#1} -gt 0 ] || return 1 + local v="${1#-}" + [ ${#v} -gt 0 ] || return 1 + v="${v//./}" + v="${v//,/}" + v="${v//[0-9]/}" + [ -z "$v" ] +} diff --git a/bash/src/base.output.sh b/bash/src/base.output.sh new file mode 100644 index 0000000..d120126 --- /dev/null +++ b/bash/src/base.output.sh @@ -0,0 +1,601 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.output "Fonctions de base: affichage" +nulib__load: _output_vanilla + +NULIB__TAB=$'\t' +NULIB__LATIN1=iso-8859-1 +NULIB__LATIN9=iso-8859-15 +NULIB__UTF8=utf-8 + +[ -n "$LANG" ] || export LANG=fr_FR.UTF-8 +if [ ! -x "$(which iconv 2>/dev/null)" ]; then + function iconv() { cat; } +fi + +function nulib__lang_encoding() { + case "${LANG,,}" in + *@euro) echo "iso-8859-15";; + *.utf-8|*.utf8) echo "utf-8";; + *) echo "iso-8859-1";; + esac +} + +function nulib__norm_encoding() { + local enc="${1,,}" + enc="${enc//[-_]/}" + case "$enc" in + latin|latin1|iso8859|iso88591|8859|88591) echo "iso-8859-1";; + latin9|iso885915|885915) echo "iso-8859-15";; + utf|utf8) echo "utf-8";; + *) echo "$1";; + esac +} + +function nulib__init_encoding() { + local default_encoding="$(nulib__lang_encoding)" + [ -n "$default_encoding" ] || default_encoding=utf-8 + [ -n "$NULIB_OUTPUT_ENCODING" ] || NULIB_OUTPUT_ENCODING="$default_encoding" + NULIB_OUTPUT_ENCODING="$(nulib__norm_encoding "$NULIB_OUTPUT_ENCODING")" + [ -n "$NULIB_INPUT_ENCODING" ] || NULIB_INPUT_ENCODING="$NULIB_OUTPUT_ENCODING" + NULIB_INPUT_ENCODING="$(nulib__norm_encoding "$NULIB_INPUT_ENCODING")" +} +[ -n "$NULIB_LANG" -a -z "$LANG" ] && export NULIB_LANG LANG="$NULIB_LANG" +nulib__init_encoding + +function noerror() { +# lancer la commande "$@" et masquer son code de retour + [ $# -gt 0 ] || set : + "$@" || return 0 +} + +function noout() { +# lancer la commande "$@" en supprimant sa sortie standard + [ $# -gt 0 ] || return 0 + "$@" >/dev/null +} + +function noerr() { +# lancer la commande "$@" en supprimant sa sortie d'erreur + [ $# -gt 0 ] || return 0 + "$@" 2>/dev/null +} + +function isatty() { +# tester si STDOUT n'est pas une redirection + tty -s <&1 +} + +function in_isatty() { +# tester si STDIN n'est pas une redirection + tty -s +} + +function out_isatty() { +# tester si STDOUT n'est pas une redirection. identique à isatty() + tty -s <&1 +} + +function err_isatty() { +# tester si STDERR n'est pas une redirection + tty -s <&2 +} + +################################################################################ + +function tooenc() { +# $1 étant une chaine encodée en utf-8, l'afficher dans l'encoding de sortie $2 +# qui vaut par défaut $NULIB_OUTPUT_ENCODING + local value="$1" to="${2:-$NULIB_OUTPUT_ENCODING}" + if [ "$to" == "$NULIB__UTF8" ]; then + recho "$value" + else + iconv -f -utf-8 -t "$to" <<<"$value" + fi +} +function uecho() { tooenc "$*"; } + +function tooenc_() { +# $1 étant une chaine encodée en utf-8, l'afficher sans passer à la ligne dans +# l'encoding de sortie $2 qui vaut par défaut $NULIB_OUTPUT_ENCODING + local value="$1" to="${2:-$NULIB_OUTPUT_ENCODING}" + if [ "$to" == "$NULIB__UTF8" ]; then + recho_ "$value" + else + recho_ "$value" | iconv -f utf-8 -t "$to" + fi +} +function uecho_() { tooenc_ "$*"; } + +export NULIB_QUIETLOG +export NULIB__TMPLOG +function: quietc "\ +N'afficher la sortie de la commande \$@ que si on est en mode DEBUG ou si elle se termine en erreur" +function quietc() { + local r + [ -z "$NULIB__TMPLOG" ] && ac_set_tmpfile NULIB__TMPLOG + "$@" >&"$NULIB__TMPLOG"; r=$? + [ -n "$NULIB_QUIETLOG" ] && cat "$NULIB__TMPLOG" >>"$NULIB_QUIETLOG" + [ $r -ne 0 -o -n "$NULIB_DEBUG" ] && cat "$NULIB__TMPLOG" + return $r +} + +function: quietc_logto "\ +Si quietc est utilisé, sauvegarder quand même la sortie dans le fichier \$1 + +Utiliser l'option -a pour ajouter au fichier au lieu de l'écraser e.g + quietc_logto -a path/to/logfile + +Tous les autres arguments sont du contenu ajoutés au fichier, e.g + quietc_logto -a path/to/logfile \"\\ +================================================================================ += \$(date +%F-%T)\"" +function quietc_logto() { + local append + if [ "$1" == -a ]; then + shift + append=1 + fi + NULIB_QUIETLOG="$1"; shift + [ -n "$NULIB_QUIETLOG" ] || return + if [ -z "$append" ]; then + >"$NULIB_QUIETLOG" + fi + if [ $# -gt 0 ]; then + echo "$*" >>"$NULIB_QUIETLOG" + fi +} + +function: quietc_echo "Ajouter \$* dans le fichier mentionné par quietc_logto() le cas échéant" +function quietc_echo() { + if [ -n "$NULIB_QUIETLOG" ]; then + echo "$*" >>"$NULIB_QUIETLOG" + fi +} + +# faut-il dater les messages des fonctions e* et action? +# Faire NULIB_ELOG_DATE=1 en début de script pour activer cette fonctionnalité +# faut-il rajouter aussi le nom du script? (nécessite NULIB_ELOG_DATE) +# Faire NULIB_ELOG_MYNAME=1 en début de script pour activer cette fonctionnalité +export NULIB_ELOG_DATE NULIB_ELOG_MYNAME +function __edate() { + [ -n "$NULIB_ELOG_DATE" ] || return + local prefix="$(date +"[%F %T.%N] ")" + [ -n "$NULIB_ELOG_MYNAME" ] && prefix="$prefix$MYNAME " + echo "$prefix" +} + +export NULIB_ELOG_OVERWRITE +function __set_no_colors() { :; } +function elogto() { +# Activer NULIB_ELOG_DATE et rediriger STDOUT et STDERR vers le fichier $1 +# Si deux fichiers sont spécifiés, rediriger STDOUT vers $1 et STDERR vers $2 +# Si aucun fichier n'est spécifié, ne pas faire de redirection +# Si la redirection est activée, forcer l'utilisation de l'encoding UTF8 +# Si NULIB_ELOG_OVERWRITE=1, alors le fichier en sortie est écrasé. Sinon, les +# lignes en sortie lui sont ajoutées + NULIB_ELOG_DATE=1 + NULIB_ELOG_MYNAME=1 + if [ -n "$1" -a -n "$2" ]; then + LANG=fr_FR.UTF8 + NULIB_OUTPUT_ENCODING="$NULIB__UTF8" + __set_no_colors 1 + if [ -n "$NULIB_ELOG_OVERWRITE" ]; then + exec >"$1" 2>"$2" + else + exec >>"$1" 2>>"$2" + fi + elif [ -n "$1" ]; then + LANG=fr_FR.UTF8 + NULIB_OUTPUT_ENCODING="$NULIB__UTF8" + __set_no_colors 1 + if [ -n "$NULIB_ELOG_OVERWRITE" ]; then + exec >"$1" 2>&1 + else + exec >>"$1" 2>&1 + fi + fi +} + +# variables utilisées pour l'affichage indenté des messages et des titres +# NULIB__ESTACK est la liste des invocations de 'etitle' et 'action' en cours +export NULIB__ESTACK NULIB__INDENT= +function __eindent0() { +# afficher le nombre d'espaces correspondant à l'indentation + local indent="${NULIB__ESTACK//?/ }" + indent="${indent% }$NULIB__INDENT" + [ -n "$indent" ] && echo "$indent" +} +function __eindent() { +# indenter les lignes de $1, sauf la première + local -a lines; local line first=1 + local indent="$(__eindent0)$2" + setx -a lines=echo "$1" + for line in "${lines[@]}"; do + if [ -n "$first" ]; then + recho "$line" + first= + else + recho "$indent$line" + fi + done +} + +function __complete() { + # compléter $1 avec $3 jusqu'à obtenir une taille de $2 caractères + local columns="${COLUMNS:-80}" + local line="$1" maxi="${2:-$columns}" sep="${3:- }" + while [ ${#line} -lt $maxi ]; do + line="$line$sep" + done + echo "$line" +} + +PRETTYOPTS=() +function set_verbosity() { :;} +function check_verbosity() { return 0; } +function get_verbosity_option() { :;} + +# tester respectivement si on doit afficher les messages d'erreur, +# d'avertissement, d'information, de debug +function show_error() { return 0; } +function show_warn() { return 0; } +function show_info() { return 0; } +function show_verbose() { return 0; } +function show_debug() { [ -n "$DEBUG" ]; } + +# note: toutes les fonctions d'affichage e* écrivent sur stderr + +function esection() { +# Afficher une section. Toutes les indentations sont remises à zéro + show_info || return + eval "$NULIB__DISABLE_SET_X" + NULIB__ESTACK= + __esection "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} + +function etitle() { +# Afficher le titre $1. Le contenu sous des titres imbriqués est affiché +# indenté. +# - si $2..$* est spécifié, c'est une commande qui est lancée dans le contexte +# du titre, ensuite le titre est automatiquement terminé +# - sinon il faut terminer le titre explicitement avec eend + local title="$1"; shift + # etitle + NULIB__ESTACK="$NULIB__ESTACK:t" + if show_info; then + eval "$NULIB__DISABLE_SET_X" + __etitle "$title" 1>&2 + eval "$NULIB__ENABLE_SET_X" + fi + # commande + local r=0 + if [ $# -gt 0 ]; then + "$@"; r=$? + eend + fi + return $r +} + + +function eend() { +# Terminer un titre + if [ "${NULIB__ESTACK%:t}" != "$NULIB__ESTACK" ]; then + NULIB__ESTACK="${NULIB__ESTACK%:t}" + fi +} + +function edesc() { +# Afficher une description sous le titre courant + show_info || return + eval "$NULIB__DISABLE_SET_X" + __edesc "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} + +function ebanner() { +# Afficher un message très important encadré, puis attendre 5 secondes + show_error || return + eval "$NULIB__DISABLE_SET_X" + __ebanner "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" + sleep 5 +} + +function eimportant() { +# Afficher un message très important + show_error || return + eval "$NULIB__DISABLE_SET_X" + __eimportant "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} +function qimportant() { eimportant "$*"; quietc_echo "IMPORTANT: $*"; } + +function eattention() { +# Afficher un message important + show_warn || return + eval "$NULIB__DISABLE_SET_X" + __eattention "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} +function qattention() { eattention "$*"; quietc_echo "ATTENTION: $*"; } + +function eerror() { +# Afficher un message d'erreur + show_error || return + eval "$NULIB__DISABLE_SET_X" + __eerror "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} +function qerror() { eerror "$*"; quietc_echo "ERROR: $*"; } + +function eerror_unless() { + # Afficher $1 avec eerror() si la commande $2..@ retourne FAUX. dans tous les cas, retourner le code de retour de la commande. + local msg="$1"; shift + local r=1 + if [ $# -eq 0 ]; then + [ -n "$msg" ] && eerror "$msg" + else + "$@"; r=$? + if [ $r -ne 0 -a -n "$msg" ]; then + eerror "$msg" + fi + fi + return $r +} + +function eerror_if() { + # Afficher $1 avec eerror() si la commande $2..@ retourne VRAI. dans tous les cas, retourner le code de retour de la commande. + local msg="$1"; shift + local r=0 + if [ $# -gt 0 ]; then + "$@"; r=$? + if [ $r -eq 0 -a -n "$msg" ]; then + eerror "$msg" + fi + fi + return $r +} + +function set_die_return() { + NULIB__DIE="return 1" +} +function die() { + [ $# -gt 0 ] && eerror "$@" + local die="${NULIB__DIE:-exit 1}" + eval "$die" || return +} + +function die_with { + [ $# -gt 0 ] && eerror "$1" + shift + [ $# -gt 0 ] && "$@" + local die="${NULIB__DIE:-exit 1}" + eval "$die" || return +} + +function die_unless() { + # Afficher $1 et quitter le script avec die() si la commande $2..@ retourne FAUX + local msg="$1"; shift + local die="${NULIB__DIE:-exit 1}" + local r=1 + if [ $# -eq 0 ]; then + [ -n "$msg" ] && eerror "$msg" + else + "$@"; r=$? + if [ $r -ne 0 -a -n "$msg" ]; then + eerror "$msg" + fi + fi + if [ $r -ne 0 ]; then + eval "${die% *} $r" || return $r + fi + return $r +} + +function die_if() { + # Afficher $1 et quitter le script avec die() si la commande $2..@ retourne VRAI. sinon, retourner le code de retour de la commande + local msg="$1"; shift + local die="${NULIB__DIE:-exit 1}" + local r=0 + if [ $# -gt 0 ]; then + "$@"; r=$? + if [ $r -eq 0 -a -n "$msg" ]; then + eerror "$msg" + fi + fi + if [ $r -eq 0 ]; then + eval "${die% *} $r" || return $r + fi + return $r +} + +function exit_with { + if [ $# -gt 0 ]; then "$@"; fi + exit $? +} + +function ewarn() { +# Afficher un message d'avertissement + show_warn || return + eval "$NULIB__DISABLE_SET_X" + __ewarn "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} +function qwarn() { ewarn "$*"; quietc_echo "WARN: $*"; } + +function enote() { +# Afficher un message d'information de même niveau qu'un avertissement + show_info || return + eval "$NULIB__DISABLE_SET_X" + __enote "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} +function qnote() { enote "$*"; quietc_echo "NOTE: $*"; } + +function einfo() { +# Afficher un message d'information + show_info || return + eval "$NULIB__DISABLE_SET_X" + __einfo "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} +function qinfo() { einfo "$*"; quietc_echo "INFO: $*"; } + +function eecho() { +# Afficher un message d'information sans préfixe + show_info || return + eval "$NULIB__DISABLE_SET_X" + __eecho "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} +function qecho() { eecho "$*"; quietc_echo "$*"; } + +function eecho_() { + show_info || return + eval "$NULIB__DISABLE_SET_X" + __eecho_ "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} + +function trace() { +# Afficher la commande $1..@, la lancer, puis afficher son code d'erreur si une +# erreur se produit + local r cmd="$(qvals "$@")" + show_info && { __eecho "\$ $cmd" 1>&2; } + "$@"; r=$? + if [ $r -ne 0 ]; then + if show_info; then + __eecho "^ [EC #$r]" 1>&2 + elif show_error; then + __eecho "^ $cmd [EC #$r]" 1>&2 + fi + fi + return $r +} + +function trace_error() { +# Lancer la commande $1..@, puis afficher son code d'erreur si une erreur se +# produit. La différence avec trace() est que la commande n'est affichée que si +# une erreur se produit. + local r + "$@"; r=$? + if [ $r -ne 0 ]; then + local cmd="$(qvals "$@")" + if show_error; then + __eecho "^ $cmd [EC #$r]" 1>&2 + fi + fi + return $r +} + +function edebug() { +# Afficher un message de debug + show_debug || return + eval "$NULIB__DISABLE_SET_X" + __edebug "$*" 1>&2 + eval "$NULIB__ENABLE_SET_X" +} + +# Afficher la description d'une opération. Cette fonction est particulièrement +# appropriée dans le contexte d'un etitle. +# Les variantes e (error), w (warning), n (note), i (info) permettent d'afficher +# des couleurs différentes, mais toutes sont du niveau info. +function estep() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estep "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepe() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepe "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepw() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepw "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepn() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepn "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepi() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepi "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estep_() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estep_ "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepe_() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepe_ "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepw_() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepw_ "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepn_() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepn_ "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } +function estepi_() { show_info || return; eval "$NULIB__DISABLE_SET_X"; __estepi_ "$*" 1>&2; eval "$NULIB__ENABLE_SET_X"; } + +function qstep() { estep "$*"; quietc_echo "* $*"; } + +function action() { +# commencer l'action $1 +# - si $2..$* est spécifié, c'est une commande qui est lancée dans le contexte +# de l'action, ensuite l'action est terminée en succès ou en échec suivant le +# code de retour. ne pas afficher la sortie de la commande comme avec quietc() +# - sinon il faut terminer le titre explicitement avec eend + eval "$NULIB__DISABLE_SET_X" + local action="$1"; shift + local r=0 + if [ $# -gt 0 ]; then + [ -z "$NULIB__TMPLOG" ] && ac_set_tmpfile NULIB__TMPLOG + [ -n "$action" ] && quietc_echo "$(__edate) ACTION: $action:" + "$@" >&"$NULIB__TMPLOG"; r=$? + [ -n "$NULIB_QUIETLOG" ] && cat "$NULIB__TMPLOG" >>"$NULIB_QUIETLOG" + if [ $r -ne 0 -o -n "$NULIB_DEBUG" ]; then + NULIB__ESTACK="$NULIB__ESTACK:a" + [ -n "$action" ] && action="$action:" + __action "$action" 1>&2 + cat "$NULIB__TMPLOG" + aresult $r + else + if [ $r -eq 0 ]; then + [ -n "$action" ] || action="succès" + __asuccess "$action" 1>&2 + else + [ -n "$action" ] || action="échec" + __afailure "$action" 1>&2 + fi + fi + else + NULIB__ESTACK="$NULIB__ESTACK:a" + [ -n "$action" ] && action="$action:" + __action "$action" 1>&2 + fi + eval "$NULIB__ENABLE_SET_X" + return $r +} + +function asuccess() { +# terminer l'action en cours avec le message de succès $* + [ "${NULIB__ESTACK%:a}" != "$NULIB__ESTACK" ] || return + eval "$NULIB__DISABLE_SET_X" + [ -n "$*" ] || set -- "succès" + NULIB__INDENT=" " __asuccess "$*" 1>&2 + NULIB__ESTACK="${NULIB__ESTACK%:a}" + eval "$NULIB__ENABLE_SET_X" + return 0 +} +function afailure() { +# terminer l'action en cours avec le message d'échec $* + [ "${NULIB__ESTACK%:a}" != "$NULIB__ESTACK" ] || return + eval "$NULIB__DISABLE_SET_X" + [ -n "$*" ] || set -- "échec" + NULIB__INDENT=" " __afailure "$*" 1>&2 + NULIB__ESTACK="${NULIB__ESTACK%:a}" + eval "$NULIB__ENABLE_SET_X" + return 1 +} +function aresult() { +# terminer l'action en cours avec un message de succès ou d'échec $2..* en +# fonction du code de retour $1 (0=succès, sinon échec) + [ "${NULIB__ESTACK%:a}" != "$NULIB__ESTACK" ] || return + eval "$NULIB__DISABLE_SET_X" + local r="${1:-0}"; shift + if [ "$r" == 0 ]; then + [ -n "$*" ] || set -- "succès" + NULIB__INDENT=" " __asuccess "$*" 1>&2 + else + [ -n "$*" ] || set -- "échec" + NULIB__INDENT=" " __afailure "$*" 1>&2 + fi + NULIB__ESTACK="${NULIB__ESTACK%:a}" + eval "$NULIB__ENABLE_SET_X" + return $r +} +function adone() { +# terminer l'action en cours avec le message neutre $* + [ "${NULIB__ESTACK%:a}" != "$NULIB__ESTACK" ] || return + eval "$NULIB__DISABLE_SET_X" + [ -n "$*" ] && NULIB__INDENT=" " __adone "$*" 1>&2 + NULIB__ESTACK="${NULIB__ESTACK%:a}" + eval "$NULIB__ENABLE_SET_X" + return 0 +} diff --git a/bash/src/base.path.sh b/bash/src/base.path.sh new file mode 100644 index 0000000..8df86b6 --- /dev/null +++ b/bash/src/base.path.sh @@ -0,0 +1,304 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.path "Fonctions de base: gestion des chemins et des fichiers" + +function: in_path "tester l'existence d'un programme dans le PATH" +function in_path() { + [ -n "$1" -a -x "$(which "$1" 2>/dev/null)" ] +} + +function: delpath "supprimer le chemin \$1 de \$2(=PATH)" +function delpath() { + local _qdir="${1//\//\\/}" + eval "export ${2:-PATH}; ${2:-PATH}"'="${'"${2:-PATH}"'#$1:}"; '"${2:-PATH}"'="${'"${2:-PATH}"'%:$1}"; '"${2:-PATH}"'="${'"${2:-PATH}"'//:$_qdir:/:}"; [ "$'"${2:-PATH}"'" == "$1" ] && '"${2:-PATH}"'=' +} + +function: addpath "Ajouter le chemin \$1 à la fin, dans \$2(=PATH), s'il n'y existe pas déjà" +function addpath() { + local _qdir="${1//\//\\/}" + eval "export ${2:-PATH}; "'[ "${'"${2:-PATH}"'#$1:}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'%:$1}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'//:$_qdir:/:}" == "$'"${2:-PATH}"'" -a "$'"${2:-PATH}"'" != "$1" ] && '"${2:-PATH}"'="${'"${2:-PATH}"':+$'"${2:-PATH}"':}$1"' +} + +function: inspathm "Ajouter le chemin \$1 au début, dans \$2(=PATH), s'il n'y existe pas déjà" +function inspathm() { + local _qdir="${1//\//\\/}" + eval "export ${2:-PATH}; "'[ "${'"${2:-PATH}"'#$1:}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'%:$1}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'//:$_qdir:/:}" == "$'"${2:-PATH}"'" -a "$'"${2:-PATH}"'" != "$1" ] && '"${2:-PATH}"'="$1${'"${2:-PATH}"':+:$'"${2:-PATH}"'}"' +} + +function: inspath "S'assurer que le chemin \$1 est au début de \$2(=PATH)" +function inspath() { + delpath "$@" + inspathm "$@" +} + +function: push_cwd "enregistrer le répertoire courant dans la variable \$2(=cwd) et se placer dans le répertoire \$1" +function push_cwd() { + eval "${2:-cwd}"'="$(pwd)"' + cd "$1" +} +function: pop_cwd "se placer dans le répertoire \${!\$2}(=\$cwd) puis retourner le code d'erreur \$1(=0)" +function pop_cwd() { + eval 'cd "$'"${2:-cwd}"'"' + return "${1:-0}" +} + +################################################################################ +## fichiers temporaires + +function: mktempf "générer un fichier temporaire et retourner son nom" +function mktempf() { + mktemp "${1:-"$TMPDIR/tmp.XXXXXX"}" +} + +function: mktempd "générer un répertoire temporaire et retourner son nom" +function mktempd() { + mktemp -d "${1:-"$TMPDIR/tmp.XXXXXX"}" +} + +function ac__forgetall() { NULIB__AC_FILES=(); } +ac__forgetall +function ac__trap() { + local file + for file in "${NULIB__AC_FILES[@]}"; do + [ -e "$file" ] && rm -rf "$file" 2>/dev/null + done + ac__forgetall +} +trap ac__trap 1 3 15 EXIT + +function: autoclean "\ +Ajouter les fichiers spécifiés à la liste des fichiers à supprimer à la fin du +programme" +function autoclean() { + local file + for file in "$@"; do + [ -n "$file" ] && NULIB__AC_FILES=("${NULIB__AC_FILES[@]}" "$file") + done +} + +function: ac_cleanall "\ +Supprimer *tous* les fichiers temporaires gérés par autoclean tout de suite." +function ac_cleanall() { + ac__trap +} + +function: ac_clean "\ +Supprimer les fichier temporaires \$1..@ si et seulement s'ils ont été générés +par ac_set_tmpfile() ou ac_set_tmpdir()" +function ac_clean() { + local file acfile found + local -a acfiles + for acfile in "${NULIB__AC_FILES[@]}"; do + found= + for file in "$@"; do + if [ "$file" == "$acfile" ]; then + found=1 + [ -e "$file" ] && rm -rf "$file" 2>/dev/null + break + fi + done + [ -z "$found" ] && acfiles=("${acfiles[@]}" "$acfile") + done + NULIB__AC_FILES=("${acfiles[@]}") +} + +function: ac_set_tmpfile "\ +Créer un fichier temporaire avec le motif \$2, l'ajouter à la liste des +fichiers à supprimer en fin de programme, et mettre sa valeur dans la +variable \$1 + +En mode debug, si (\$5 est vide ou \${!5} est une valeur vraie), et si \$3 n'est +pas vide, prendre ce fichier au lieu de générer un nouveau fichier temporaire. +Si \$4==keep, ne pas écraser le fichier \$3 s'il existe." +function ac_set_tmpfile() { + local se__d + if is_debug; then + if [ -n "$5" ]; then + is_yes "${!5}" && se__d=1 + else + se__d=1 + fi + fi + if [ -n "$se__d" -a -n "$3" ]; then + _setv "$1" "$3" + [ -f "$3" -a "$4" == keep ] || >"$3" + else + local se__t="$(mktempf "$2")" + autoclean "$se__t" + _setv "$1" "$se__t" + fi +} + +function: ac_set_tmpdir "\ +Créer un répertoire temporaire avec le motif \$2, l'ajouter à la liste des +fichiers à supprimer en fin de programme, et mettre sa valeur dans la +variable \$1 + +En mode debug, si (\$4 est vide ou \${!4} est une valeur vraie), et si \$3 n'est +pas vide, prendre ce nom de répertoire au lieu de créer un nouveau répertoire +temporaire" +function ac_set_tmpdir() { + local sr__d + if is_debug; then + if [ -n "$4" ]; then + is_yes "${!4}" && sr__d=1 + else + sr__d=1 + fi + fi + if [ -n "$sr__d" -a -n "$3" ]; then + _setv "$1" "$3" + mkdir -p "$3" + else + local sr__t="$(mktempd "$2")" + autoclean "$sr__t" + _setv "$1" "$sr__t" + fi +} + +################################################################################ +## manipulation de chemins + +#XXX repris tel quel depuis nutools, à migrer + +function normpath() { +# normaliser le chemin $1, qui est soit absolu, soit relatif à $2 (qui vaut +# $(pwd) par défaut) + local -a parts + local part ap + IFS=/ read -a parts <<<"$1" + if [ "${1#/}" != "$1" ]; then + ap=/ + elif [ -n "$2" ]; then + ap="$2" + else + ap="$(pwd)" + fi + for part in "${parts[@]}"; do + if [ "$part" == "." ]; then + continue + elif [ "$part" == ".." ]; then + ap="${ap%/*}" + [ -n "$ap" ] || ap=/ + else + [ "$ap" != "/" ] && ap="$ap/" + ap="$ap$part" + fi + done + echo "$ap" +} +function __normpath() { + # normaliser dans les cas simple le chemin absolu $1. sinon retourner 1. + # cette fonction est utilisée par abspath() + if [ -d "$1" ]; then + if [ -x "$1" ]; then + # le cas le plus simple: un répertoire dans lequel on peut entrer + (cd "$1"; pwd) + return 0 + fi + elif [ -f "$1" ]; then + local dn="$(dirname -- "$1")" bn="$(basename -- "$1")" + if [ -x "$dn" ]; then + # autre cas simple: un fichier situé dans un répertoire dans lequel + # on peut entrer + (cd "$dn"; echo "$(pwd)/$bn") + return 0 + fi + fi + return 1 +} +function abspath() { +# Retourner un chemin absolu vers $1. Si $2 est non nul et si $1 est un chemin +# relatif, alors $1 est exprimé par rapport à $2, sinon il est exprimé par +# rapport au répertoire courant. +# Si le chemin n'existe pas, il n'est PAS normalisé. Sinon, les meilleurs +# efforts sont faits pour normaliser le chemin. + local ap="$1" + if [ "${ap#/}" != "$ap" ]; then + # chemin absolu. on peut ignorer $2 + __normpath "$ap" && return + else + # chemin relatif. il faut exprimer le chemin par rapport à $2 + local cwd + if [ -n "$2" ]; then + cwd="$(abspath "$2")" + else + cwd="$(pwd)" + fi + ap="$cwd/$ap" + __normpath "$ap" && return + fi + # dans les cas spéciaux, il faut calculer "manuellement" le répertoire absolu + normpath "$ap" +} + +function ppath() { +# Dans un chemin *absolu*, remplacer "$HOME" par "~" et "$(pwd)/" par "", afin +# que le chemin soit plus facile à lire. Le répertoire courant est spécifié par +# $2 ou $(pwd) si $2 est vide + local path="$1" cwd="$2" + + path="$(abspath "$path")" # essayer de normaliser le chemin + [ -n "$cwd" ] || cwd="$(pwd)" + + [ "$path" == "$cwd" ] && path="." + [ "$cwd" != "/" -a "$cwd" != "$HOME" ] && path="${path#$cwd/}" + [ "${path#$HOME/}" != "$path" ] && path="~${path#$HOME}" + + echo "$path" +} +function ppath2() { +# Comme ppath() mais afficher '.' comme '. ($dirname)' pour la joliesse + local path="$1" cwd="$2" + + path="$(abspath "$path")" # essayer de normaliser le chemin + [ -n "$cwd" ] || cwd="$(pwd)" + + if [ "$path" == "$cwd" ]; then + path=". ($(basename -- "$path"))" + else + [ "$cwd" != "/" -a "$cwd" != "$HOME" ] && path="${path#$cwd/}" + [ "${path#$HOME/}" != "$path" ] && path="~${path#$HOME}" + fi + + echo "$path" +} + +function relpath() { +# Afficher le chemin relatif de $1 par rapport à $2. Si $2 n'est pas spécifié, +# on prend le répertoire courant. Si $1 ou $2 ne sont pas des chemins absolus, +# il sont transformés en chemins absolus par rapport à $3. Si $1==$2, retourner +# une chaine vide + local p="$(abspath "$1" "$3")" cwd="$2" + if [ -z "$cwd" ]; then + cwd="$(pwd)" + else + cwd="$(abspath "$cwd" "$3")" + fi + if [ "$p" == "$cwd" ]; then + echo "" + elif [ "${p#$cwd/}" != "$p" ]; then + echo "${p#$cwd/}" + else + local rp + while [ -n "$cwd" -a "${p#$cwd/}" == "$p" ]; do + rp="${rp:+$rp/}.." + cwd="${cwd%/*}" + done + rp="$rp/${p#$cwd/}" + # ${rp%//} traite le cas $1==/ + echo "${rp%//}" + fi +} +function relpathx() { +# Comme relpath, mais pour un chemin vers un exécutable qu'il faut lancer: +# s'assurer qu'il y a une spécification de chemin, e.g. ./script + local p="$(relpath "$@")" + if [ -z "$p" ]; then + echo . + elif [ "${p#../}" != "$p" -o "${p#./}" != "$p" ]; then + echo "$p" + else + echo "./$p" + fi +} diff --git a/bash/src/base.sh b/bash/src/base.sh new file mode 100644 index 0000000..cd88e49 --- /dev/null +++ b/bash/src/base.sh @@ -0,0 +1,22 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +# shim pour les fonctions de nulib.sh au cas où ce module n'est pas chargée +if [ -z "$NULIBDIR" -o "$NULIBDIR" != "$NULIBINIT" ]; then + function module:() { :; } + function function:() { :; } + function require:() { :; } +fi +##@include base.init.sh +##@include base.core.sh +##@include base.str.sh +##@include base.num.sh +##@include base.bool.sh +##@include base.array.sh +##@include base.split.sh +##@include base.path.sh +##@include base.args.sh +##@include base.tools.sh +##@include base.input.sh +##@include base.output.sh +module: base "Chargement de tous les modules base.*" +require: base.init base.core base.str base.num base.bool base.array base.split base.path base.args base.tools base.input base.output diff --git a/bash/src/base.split.sh b/bash/src/base.split.sh new file mode 100644 index 0000000..dac4eeb --- /dev/null +++ b/bash/src/base.split.sh @@ -0,0 +1,188 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.split "Fonctions de base: analyse et découpage de valeurs" + +function: splitfsep "\ +Découper \$1 de la forme first[SEPsecond] entre first, qui est placé dans la +variable \$3(=first) et second, qui est placée dans la variable \$4(=second). \$2 +est la valeur SEP. Le découpage est faite sur la *première* occurence de SEP." +function splitfsep() { + if [[ "$1" == *"$2"* ]]; then + setv "${3:-first}" "${1%%$2*}" + setv "${4:-second}" "${1#*$2}" + else + setv "${3:-first}" "$1" + setv "${4:-second}" + fi +} + +function: splitfsep2 "\ +Découper \$1 de la forme [firstSEP]second entre first, qui est placé dans la +variable \$3(=first) et second, qui est placée dans la variable \$4(=second). \$2 +est la valeur SEP. Le découpage est faite sur la *première* occurence de SEP." +function splitfsep2() { + if [[ "$1" == *"$2"* ]]; then + setv "${3:-first}" "${1%%$2*}" + setv "${4:-second}" "${1#*$2}" + else + setv "${3:-first}" + setv "${4:-second}" "$1" + fi +} + +function: splitlsep "\ +Découper \$1 de la forme first[SEPsecond] entre first, qui est placé dans la +variable \$3(=first) et second, qui est placée dans la variable \$4(=second). \$2 +est la valeur SEP. Le découpage est faite sur la *dernière* occurence de SEP." +function splitlsep() { + if [[ "$1" == *"$2"* ]]; then + setv "${3:-first}" "${1%$2*}" + setv "${4:-second}" "${1##*$2}" + else + setv "${3:-first}" "$1" + setv "${4:-second}" + fi +} + +function: splitlsep2 "\ +Découper \$1 de la forme [firstSEP]second entre first, qui est placé dans la +variable \$3(=first) et second, qui est placée dans la variable \$4(=second). \$2 +est la valeur SEP. Le découpage est faite sur la *dernière* occurence de SEP." +function splitlsep2() { + if [[ "$1" == *"$2"* ]]; then + setv "${3:-first}" "${1%$2*}" + setv "${4:-second}" "${1##*$2}" + else + setv "${3:-first}" + setv "${4:-second}" "$1" + fi +} + +function: splitvar "\ +Découper \$1 de la forme name[=value] entre le nom, qui est placé dans la +variable \$2(=name) et la valeur, qui est placée dans la variable \$3(=value)" +function splitvar() { + splitfsep "$1" = "${2:-name}" "${3:-value}" +} + +function: splitpath "\ +Découper \$1 de la forme [dir/]name entre le répertoire, qui est placé dans la +variable \$2(=dir), et le nom du fichier, qui est placé dans la variable +\$3(=name)" +function splitpath() { + splitlsep2 "$1" / "${2:-dir}" "${3:-name}" +} + +function: splitname "\ +Découper \$1 de la forme basename[.ext] entre le nom de base du fichier, qui +est placé dans la variable \$2(=basename) et l'extension, qui est placée dans +la variable \$3(=ext) + +Attention, si \$1 est un chemin, le résultat risque d'être faussé. Par exemple, +'splitname a.b/c' ne donne pas le résultat escompté." +function splitname() { + splitlsep "$1" . "${2:-basename}" "${3:-ext}" +} + +function: splithost "\ +Découper \$1 de la forme hostname[.domain] entre le nom d'hôte, qui est placé +dans la variable \$2(=hostname) et le domaine, qui est placée dans la variable +\$3(=domain)" +function splithost() { + splitfsep "$1" . "${2:-hostname}" "${3:-domain}" +} + +function: splituserhost "\ +Découper \$1 de la forme [user@]host entre le nom de l'utilisateur, qui est placé +dans la variable \$2(=user) et le nom d'hôte, qui est placée dans la variable +\$3(=host)" +function splituserhost() { + splitfsep2 "$1" @ "${2:-user}" "${3:-host}" +} + +function: splitpair "\ +Découper \$1 de la forme first[:second] entre la première valeur, qui est placé +dans la variable \$2(=src) et la deuxième valeur, qui est placée dans la variable +\$3(=dest)" +function splitpair() { + splitfsep "$1" : "${2:-src}" "${3:-dest}" +} + +function: splitproxy "\ +Découper \$1 de la forme http://[user:password@]host[:port]/ entre les valeurs +\$2(=host), \$3(=port), \$4(=user), \$5(=password) + +S'il n'est pas spécifié, port vaut 3128 par défaut" +function splitproxy() { + local sy__tmp sy__host sy__port sy__creds sy__user sy__password + + sy__tmp="${1#http://}" + if [[ "$sy__tmp" == *@* ]]; then + sy__creds="${sy__tmp%%@*}" + sy__tmp="${sy__tmp#${sy__creds}@}" + splitpair "$sy__creds" sy__user sy__password + fi + sy__tmp="${sy__tmp%%/*}" + splitpair "$sy__tmp" sy__host sy__port + [ -n "$sy__port" ] || sy__port=3128 + + setv "${2:-host}" "$sy__host" + setv "${3:-port}" "$sy__port" + setv "${4:-user}" "$sy__user" + setv "${5:-password}" "$sy__password" +} + +function: spliturl "\ +Découper \$1 de la forme scheme://[user:password@]host[:port]/path entre les +valeurs \$2(=scheme), \$3(=user), \$4(=password), \$5(=host), \$6(=port), \$7(=path) + +S'il n'est pas spécifié, port vaut 80 pour http, 443 pour https, 21 pour ftp" +function spliturl() { + local sl__tmp sl__scheme sl__creds sl__user sl__password sl__host sl__port sl__path + + sl__scheme="${1%%:*}" + sl__tmp="${1#${sl__scheme}://}" + if [[ "$sl__tmp" == */* ]]; then + sl__path="${sl__tmp#*/}" + sl__tmp="${sl__tmp%%/*}" + fi + if [[ "$sl__tmp" == *@* ]]; then + sl__creds="${sl__tmp%%@*}" + sl__tmp="${sl__tmp#${sl__creds}@}" + splitpair "$sl__creds" sl__user sl__password + fi + splitpair "$sl__tmp" sl__host sl__port + if [ -z "$sl__port" ]; then + [ "$sl__scheme" == "http" ] && sl__port=80 + [ "$sl__scheme" == "https" ] && sl__port=443 + [ "$sl__scheme" == "ftp" ] && sl__port=21 + fi + + setv "${2:-scheme}" "$sl__scheme" + setv "${3:-user}" "$sl__user" + setv "${4:-password}" "$sl__password" + setv "${5:-host}" "$sl__host" + setv "${6:-port}" "$sl__port" + setv "${7:-path}" "$sl__path" +} + +function: splitwcs "\ +Découper un nom de chemin \$1 entre la partie sans wildcards, qui est placée dans +la variables \$2(=basedir), et la partie avec wildcards, qui est placée dans la +variable \$3(=filespec)" +function splitwcs() { + local ss__p="$1" + local ss__dd="${2:-basedir}" ss__df="${3:-filespec}" ss__part ss__d ss__f + local -a ss__parts + array_split ss__parts "$ss__p" "/" + for ss__part in "${ss__parts[@]}"; do + if [[ "$ss__part" == *\** ]] || [[ "$ss__part" == *\?* ]] || [ -n "$ss__f" ]; then + ss__f="${ss__f:+$ss__f/}$ss__part" + else + ss__d="${ss__d:+$ss__d/}$ss__part" + fi + done + [ "${ss__p#/}" != "$ss__p" ] && ss__d="/$ss__d" + _setv "$ss__dd" "$ss__d" + _setv "$ss__df" "$ss__f" +} diff --git a/bash/src/base.str.sh b/bash/src/base.str.sh new file mode 100644 index 0000000..e83e714 --- /dev/null +++ b/bash/src/base.str.sh @@ -0,0 +1,140 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.str "Fonctions de base: gestion des valeurs chaines" + +function: strmid "Afficher la plage \$1 de la valeur \$2..* + +La plage peut être d'une des formes 'start', '[start]:length'. Si start est +négatif, le compte est effectué à partir de la fin de la chaine. Si length est +négatif, il est rajouté à la longueur de la chaine à partir de start" +function strmid() { + local range="$1"; shift + local str="$*" + if [[ "$range" == *:-* ]]; then + local max=${#str} + [ $max -eq 0 ] && return + local start="${range%%:*}" + [ -n "$start" ] || start=0 + while [ "$start" -lt 0 ]; do + start=$(($max$start)) + done + max=$(($max-$start)) + local length="${range#*:}" + while [ "$length" -lt 0 ]; do + length=$(($max$length)) + done + range="$start:$length" + fi + eval 'echo "${str:'" $range"'}"' +} + +function: strrepl "Remplacer dans la valeur \$3..* le motif \$1 par la chaine \$2 + +\$1 peut commencer par l'un des caractères /, #, % pour indiquer le type de recherche" +function strrepl() { + local pattern="$1"; shift + local repl="$1"; shift + local str="$*" + local cmd='echo "${str/' + if [ "${pattern#/}" != "$pattern" ]; then + pattern="${pattern#/}" + cmd="$cmd/" + elif [ "${pattern#\#}" != "$pattern" ]; then + pattern="${pattern#\#}" + cmd="$cmd#" + elif [ "${pattern#%}" != "$pattern" ]; then + pattern="${pattern#%}" + cmd="$cmd%" + fi + cmd="$cmd"'$pattern/$repl}"' + eval "$cmd" +} + +function: strlcomp "transformer dans le flux en entrée en UTF-8 certains caractères en leur équivalent transformable en latin1. + +si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée." +function strlcomp() { + if [ $# -gt 0 ]; then strlcomp <<<"$*" + else LANG=fr_FR.UTF-8 sed $' +s/[\xE2\x80\x90\xE2\x80\x91\xE2\x80\x92\xE2\x80\x93\xE2\x80\x94\xE2\x80\x95]/-/g +s/[‘’]/\x27/g +s/[«»“”]/"/g +s/[\xC2\xA0\xE2\x80\x87\xE2\x80\xAF\xE2\x81\xA0]/ /g +s/[\xE2\x80\xA6]/.../g +s/[œ]/oe/g +s/[Œ]/OE/g +s/[æ]/ae/g +s/[Æ]/AE/g +s/a\xCC\x80/à/g +s/e\xCC\x81/é/g; s/e\xCC\x80/è/g; s/e\xCC\x82/ê/g; s/e\xCC\x88/ë/g +s/i\xCC\x88/ï/g; s/i\xCC\x82/î/g +s/o\xCC\x82/ô/g; s/o\xCC\x88/ö/g +s/u\xCC\x88/ü/g; s/u\xCC\x82/û/g +s/c\xCC\xA7/ç/g +s/A\xCC\x80/À/g +s/E\xCC\x81/É/g; s/E\xCC\x80/È/g; s/E\xCC\x82/Ê/g; s/E\xCC\x88/Ë/g +s/I\xCC\x88/Ï/g; s/I\xCC\x82/Î/g +s/O\xCC\x82/Ô/g; s/O\xCC\x88/Ö/g +s/U\xCC\x88/Ü/g; s/U\xCC\x82/Û/g +s/C\xCC\xA7/Ç/g +' + fi +} + +function: strnacc "supprimer les accents dans le flux en entrée en UTF-8 + +si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée." +function strnacc() { + if [ $# -gt 0 ]; then strnacc <<<"$*" + else LANG=fr_FR.UTF-8 sed ' +s/[à]/a/g +s/[éèêë]/e/g +s/[ïî]/i/g +s/[ôö]/o/g +s/[üû]/u/g +s/[ç]/c/g +s/[À]/A/g +s/[ÉÈÊË]/E/g +s/[ÏÎ]/I/g +s/[ÔÖ]/O/g +s/[ÜÛ]/U/g +s/[Ç]/C/g +' + fi +} + +function: stripnl "Supprimer dans le flux en entrée les caractères de fin de ligne + +si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée." +function stripnl() { + if [ $# -gt 0 ]; then stripnl <<<"$*" + else tr -d '\r\n' + fi +} + +function: nl2lf "transformer dans le flux en entrée les fins de ligne en LF + +si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée." +function nl2lf() { + if [ $# -gt 0 ]; then nl2lf <<<"$*" + else lawk 'BEGIN {RS="\r|\r\n|\n"} {print}' + fi +} + +function: nl2crlf "transformer dans le flux en entrée les fins de ligne en CRLF + +si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée." +function nl2crlf() { + if [ $# -gt 0 ]; then nl2crlf <<<"$*" + else lawk 'BEGIN {RS="\r|\r\n|\n"} {print $0 "\r"}' + fi +} + +function: nl2cr "transformer dans le flux en entrée les fins de ligne en CR + +si cette fonction est appelée avec des arguments, prendre \$* comme valeur du flux en entrée." +function nl2cr() { + if [ $# -gt 0 ]; then nl2cr <<<"$*" + else lawk 'BEGIN {RS="\r|\r\n|\n"; ORS=""} {print $0 "\r"}' + fi +} diff --git a/bash/src/base.tools.sh b/bash/src/base.tools.sh new file mode 100644 index 0000000..1a7f800 --- /dev/null +++ b/bash/src/base.tools.sh @@ -0,0 +1,101 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: base.tools "Fonctions de base: outils divers" + +function: mkdirof 'Créer le répertoire correspondant au fichier $1' +function mkdirof() { + mkdir -p "$(dirname -- "$1")" +} + +function __la_cmd() { + [ $# -gt 0 ] || set '*' + local arg + local cmd="/bin/ls -1d" + for arg in "$@"; do + arg="$(qwc "$arg")" + cmd="$cmd $arg" + done + cmd="$cmd 2>/dev/null" + echo "$cmd" +} + +function: ls_all 'Lister les fichiers ou répertoires du répertoire $1, un par ligne +Les répertoires . et .. sont enlevés de la liste +$1=un répertoire dont le contenu doit être listé +$2..@=un ensemble de patterns pour le listage + +Seuls les noms des fichiers sont listés. Utiliser l'\''option -p pour inclure +les chemins' +function ls_all() { + local withp f b + if [ "$1" == -p ]; then withp=1; shift; fi + b="${1:-.}"; shift + + ( + cd "$b" || exit + eval "$(__la_cmd "$@")" | while read f; do + [ "$f" == "." -o "$f" == ".." ] && continue + recho "${withp:+$b/}$f" + done + ) +} + +function: ls_files 'Lister les fichiers du répertoire $1, un par ligne +$1=un répertoire dont le contenu doit être listé. +$2..@=un ensemble de patterns pour le listage + +Seuls les noms des fichiers sont listés. Utiliser l'\''option -p pour inclure +les chemins' +function ls_files() { + local withp f b + if [ "$1" == -p ]; then withp=1; shift; fi + b="${1:-.}"; shift + + ( + cd "$b" || exit + eval "$(__la_cmd "$@")" | while read f; do + [ -f "$f" ] && recho "${withp:+$b/}$f" + done + ) +} + +function: ls_dirs 'Lister les répertoires du répertoire $1, un par ligne +Les répertoires . et .. sont enlevés de la liste +$1=un répertoire dont le contenu doit être listé. +$2..@=un ensemble de patterns pour le listage + +Seuls les noms des répertoires sont listés. Utiliser l'\''option -p pour +inclure les chemins' +function ls_dirs() { + local withp f b + if [ "$1" == -p ]; then withp=1; shift; fi + b="${1:-.}"; shift + + ( + cd "$b" || exit + eval "$(__la_cmd "$@")" | while read f; do + [ "$f" == "." -o "$f" == ".." ] && continue + [ -d "$f" ] && recho "${withp:+$b/}$f" + done + ) +} + +function: quietgrep "tester la présence d'un pattern dans un fichier en mode silencieux" +function quietgrep() { grep -q "$@" 2>/dev/null; } + +function: testsame "tester si deux fichiers sont identiques en mode silencieux" +function testsame() { diff -q "$@" >&/dev/null; } + +function: testdiff "tester si deux fichiers sont différents en mode silencieux" +function testdiff() { ! diff -q "$@" >&/dev/null; } + +function: should_update "faut-il mettre à jour le \$1 qui est construit à partir de \$2..@" +function should_update() { + local dest="$1"; shift + local source + for source in "$@"; do + [ -f "$source" ] || continue + [ "$source" -nt "$dest" ] && return 0 + done + return 1 +} diff --git a/bash/src/donk.build.sh b/bash/src/donk.build.sh new file mode 100644 index 0000000..a88daa0 --- /dev/null +++ b/bash/src/donk.build.sh @@ -0,0 +1,4 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: donk.build "construire des images docker" +require: donk.common diff --git a/bash/src/donk.common.sh b/bash/src/donk.common.sh new file mode 100644 index 0000000..79a5efb --- /dev/null +++ b/bash/src/donk.common.sh @@ -0,0 +1,3 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: donk.common "fonctions communes" diff --git a/bash/src/donk.help.sh b/bash/src/donk.help.sh new file mode 100644 index 0000000..2e0a21b --- /dev/null +++ b/bash/src/donk.help.sh @@ -0,0 +1,41 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: donk.help "aide de donk" + +DONK_VALID_ACTIONS=( + dump:d + build:b + clean:k +) +dump_SUMMARY="afficher les valeurs des variables et ce que ferait l'action build" +build_SUMMARY="construire les images" +clean_SUMMARY="nettoyer le projet des fichiers créés avec 'copy gitignore=', en utilisant la commande 'git clean -dX'" + +DONK_HELP_SECTIONS=( + base:b + reference:ref:r +) + +function donk_help() { + : +} + +function _donk_show_actions() { + local action summary + echo " +ACTIONS" + for action in "${DONK_VALID_ACTIONS[@]}"; do + IFS=: read -a action <<<"$action"; action="${action[0]}" + summary="${action}_SUMMARY"; summary="${!summary}" + echo "\ + $action + $summary" + done +} + +function _donk_show_help() { + if [ -z "$value_" ]; then showhelp@ + else donk_help "$value_" + fi + exit $? +} diff --git a/bash/src/fndate.sh b/bash/src/fndate.sh new file mode 100644 index 0000000..7c94e2b --- /dev/null +++ b/bash/src/fndate.sh @@ -0,0 +1,45 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: fndate "gestion de fichiers dont le nom contient la date" + +function: fndate_verifix "\ +corriger le chemin \$1 pour ajouter le cas échéant une date au nom du fichier +le fichier n'existe peut-être pas au moment où cette fonction est appelée +\$2 est l'extension finale du fichier, à ignorer si elle est présente + (elle n'est cependant pas ajoutée si elle n'est pas présente) +\$3 est la date à sélectionner (par défaut c'est la date du jour) + +XXX à implémenter: +- gestion de la date +- ajout d'un suffixe .N le cas échéant (i.e YYMMDD.NN) +" +function fndate_verifix() { + local dir filename ext date + if [[ "$1" == */* ]]; then + dir="$(dirname -- "$1")" + filename="$(basename -- "$1")" + else + dir= + filename="$1" + fi + ext="$2" + if [ -n "$ext" ]; then + ext=".${2#.}" + if [ "${filename%$ext}" != "$filename" ]; then + filename="${filename%$ext}" + else + ext= + fi + fi + date="$3" + [ -n "$date" ] || date="$(date +%y%m%d)" + + case "$filename" in + ~~-*) filename="$date-${filename#~~-}";; + ~~*) filename="$date-${filename#~~}";; + *-~~) filename="${filename%-~~}-$date";; + *~~) filename="${filename%~~}-$date";; + esac + + echo "${dir:+$dir/}$filename$ext" +} diff --git a/bash/src/git.sh b/bash/src/git.sh new file mode 100644 index 0000000..a879f28 --- /dev/null +++ b/bash/src/git.sh @@ -0,0 +1,217 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +##@require nulib.sh +##@require base +module: git "Fonctions pour faciliter l'utilisation de git" +require: nulib base + +function: git_geturl "" +function git_geturl() { + git config --get remote.origin.url +} + +function: git_have_annex "" +function git_have_annex() { + [ -n "$(git config --get annex.uuid)" ] +} + +NULIB_GIT_FUNCTIONS=( + git_check_gitvcs git_ensure_gitvcs + git_list_branches git_list_rbranches + git_have_branch git_have_rbranch + git_get_branch git_is_branch + git_have_remote git_track_branch + git_check_cleancheckout git_ensure_cleancheckout + git_is_ancestor git_should_ff git_should_push + git_is_merged +) +NULIB_GIT_FUNCTIONS_MAP=( + cg:git_check_gitvcs eg:git_ensure_gitvcs + lbs:git_list_branches rbs:git_list_rbranches + hlb:git_have_branch hrb:git_have_rbranch + gb:git_get_branch ib:git_is_branch + hr:git_have_remote tb:git_track_branch + cc:git_check_cleancheckout ec:git_ensure_cleancheckout + ia:git_is_ancestor sff:git_should_ff spu:git_should_push + im:git_is_merged +) + +function: git_check_gitvcs "" +function git_check_gitvcs() { + git rev-parse --show-toplevel >&/dev/null +} + +function: git_ensure_gitvcs "" +function git_ensure_gitvcs() { + git_check_gitvcs || edie "Ce n'est pas un dépôt git" || return +} + +function: git_list_branches "" +function git_list_branches() { + git for-each-ref refs/heads/ --format='%(refname:short)' | csort +} + +function: git_list_rbranches "" +function git_list_rbranches() { + git for-each-ref "refs/remotes/${1:-origin}/" --format='%(refname:short)' | csort +} + +function: git_list_pbranches "lister les branches locales et celles qui existent dans l'origine \$1(=origin) et qui pourraient devenir une branche locale avec la commande git checkout -b" +function git_list_pbranches() { + local prefix="${1:-origin}/" + { + git for-each-ref refs/heads/ --format='%(refname:short)' + git for-each-ref "refs/remotes/$prefix" --format='%(refname:short)' | grep -F "$prefix" | cut -c $((${#prefix} + 1))- + } | grep -vF HEAD | csort -u +} + +function: git_have_branch "" +function git_have_branch() { + git_list_branches | grep -qF "$1" +} + +function: git_have_rbranch "" +function git_have_rbranch() { + git_list_rbranches "${2:-origin}" | grep -qF "$1" +} + +function: git_get_branch "" +function git_get_branch() { + git rev-parse --abbrev-ref HEAD 2>/dev/null +} + +function: git_get_branch_remote "" +function git_get_branch_remote() { + local branch="$1" + [ -n "$branch" ] || branch="$(git_get_branch)" + [ -n "$branch" ] || return + git config --get "branch.$branch.remote" +} + +function: git_get_branch_merge "" +function git_get_branch_merge() { + local branch="$1" + [ -n "$branch" ] || branch="$(git_get_branch)" + [ -n "$branch" ] || return + git config --get "branch.$branch.merge" +} + +function: git_get_branch_rbranch "" +function git_get_branch_rbranch() { + local branch="$1" remote="$2" merge + [ -n "$branch" ] || branch="$(git_get_branch)" + [ -n "$branch" ] || return + [ -n "$remote" ] || remote="$(git_get_branch_remote "$branch")" + [ -n "$remote" ] || return + merge="$(git_get_branch_merge "$branch")" + [ -n "$merge" ] || return + echo "refs/remotes/$remote/${merge#refs/heads/}" +} + +function: git_is_branch "" +function git_is_branch() { + [ "$(git_get_branch)" == "${1:-master}" ] +} + +function: git_have_remote "" +function git_have_remote() { + [ -n "$(git config --get remote.${1:-origin}.url)" ] +} + +function: git_track_branch "" +function git_track_branch() { + local branch="$1" origin="${2:-origin}" + [ -n "$branch" ] || return + git_have_remote "$origin" || return + [ "$(git config --get branch.$branch.remote)" == "$origin" ] && return + if git_have_rbranch "$branch" "$origin"; then + if git_have_branch "$branch"; then + git branch -u "$origin/$branch" "$branch" + else + git branch -t "$branch" "$origin/$branch" + fi + elif git_have_branch "$branch"; then + git push -u "$origin" "$branch" || return + fi +} + +function: git_ensure_branch " +@return 0 si la branche a été créée, 1 si elle existait déjà, 2 en cas d'erreur" +function git_ensure_branch() { + local branch="$1" source="${2:-master}" origin="${3:-origin}" + [ -n "$branch" ] || return 2 + git_have_branch "$branch" && return 1 + if git_have_rbranch "$branch" "$origin"; then + # une branche du même nom existe dans l'origine. faire une copie de cette branche + git branch -t "$branch" "$origin/$branch" || return 2 + else + # créer une nouvelle branche du nom spécifié + git_have_branch "$source" || return 2 + git branch "$branch" "$source" || return 2 + if [ -z "$NULIB_GIT_OFFLINE" ]; then + git_have_remote "$origin" && git_track_branch "$branch" "$origin" + fi + fi + return 0 +} + +function: git_check_cleancheckout "vérifier qu'il n'y a pas de modification locales dans le dépôt correspondant au répertoire courant." +function git_check_cleancheckout() { + [ -z "$(git status --porcelain 2>/dev/null)" ] +} + +function: git_ensure_cleancheckout "" +function git_ensure_cleancheckout() { + git_check_cleancheckout || + edie "Vous avez des modifications locales. Enregistrez ces modifications avant de continuer" || return +} + +function git__init_ff() { + o="${3:-origin}" + b="$1" s="${2:-refs/remotes/$o/$1}" + b="$(git rev-parse --verify --quiet "$b")" || return 1 + s="$(git rev-parse --verify --quiet "$s")" || return 1 + return 0 +} +function git__can_ff() { + [ "$1" == "$(git merge-base "$1" "$2")" ] +} + +function: git_is_ancestor "vérifier que la branche \$1 est un ancêtre direct de la branche \$2, qui vaut par défaut refs/remotes/\${3:-origin}/\$1 +note: cette fonction retourne vrai si \$1 et \$2 identifient le même commit" +function git_is_ancestor() { + local o b s; git__init_ff "$@" || return + git__can_ff "$b" "$s" +} + +function: git_should_ff "vérifier si la branche \$1 devrait être fast-forwardée à partir de la branche d'origine \$2, qui vaut par défaut refs/remotes/\${3:-origin}/\$1 +note: cette fonction est similaire à git_is_ancestor(), mais retourne false si \$1 et \$2 identifient le même commit" +function git_should_ff() { + local o b s; git__init_ff "$@" || return + [ "$b" != "$s" ] || return 1 + git__can_ff "$b" "$s" +} + +function: git_should_push "vérifier si la branche \$1 devrait être poussée vers la branche de même nom dans l'origine \$2(=origin), parce que l'origin peut-être fast-forwardée à partir de cette branche." +function git_should_push() { + git_should_ff "refs/remotes/${2:-origin}/$1" "$1" +} + +function: git_fast_forward "vérifier que la branche courante est bien \$1, puis tester s'il faut la fast-forwarder à partir de la branche d'origine \$2, puis le faire si c'est nécessaire. la branche d'origine \$2 vaut par défaut refs/remotes/origin/\$1" +function git_fast_forward() { + local o b s; git__init_ff "$@" || return + [ "$b" != "$s" ] || return 1 + local head="$(git rev-parse HEAD)" + [ "$head" == "$b" ] || return 1 + git__can_ff "$b" "$s" || return 1 + git merge --ff-only "$s" +} + +function: git_is_merged "vérifier que les branches \$1 et \$2 ont un ancêtre commun, et que la branche \$1 a été complètement fusionnée dans la branche destination \$2" +function git_is_merged() { + local b="$1" d="$2" + b="$(git rev-parse --verify --quiet "$b")" || return 1 + d="$(git rev-parse --verify --quiet "$d")" || return 1 + [ -n "$(git merge-base "$b" "$d")" ] || return 1 + [ -z "$(git rev-list "$d..$b")" ] +} diff --git a/bash/src/nulib.sh b/bash/src/nulib.sh new file mode 120000 index 0000000..562af8d --- /dev/null +++ b/bash/src/nulib.sh @@ -0,0 +1 @@ +../../load.sh \ No newline at end of file diff --git a/bash/src/pretty.sh b/bash/src/pretty.sh new file mode 100644 index 0000000..1d24a5b --- /dev/null +++ b/bash/src/pretty.sh @@ -0,0 +1,194 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: pretty "Affichage en couleur" +require: base + +################################################################################ +# Gestion des couleurs + +function __get_color() { + [ -z "$*" ] && set RESET + echo_ $'\e[' + local sep + while [ -n "$1" ]; do + [ -n "$sep" ] && echo_ ";" + case "$1" in + z|RESET) echo_ "0";; + o|BLACK) echo_ "30";; + r|RED) echo_ "31";; + g|GREEN) echo_ "32";; + y|YELLOW) echo_ "33";; + b|BLUE) echo_ "34";; + m|MAGENTA) echo_ "35";; + c|CYAN) echo_ "36";; + w|WHITE) echo_ "37";; + DEFAULT) echo_ "39";; + O|BLACK_BG) echo_ "40";; + R|RED_BG) echo_ "41";; + G|GREEN_BG) echo_ "42";; + Y|YELLOW_BG) echo_ "43";; + B|BLUE_BG) echo_ "44";; + M|MAGENTA_BG) echo_ "45";; + C|CYAN_BG) echo_ "46";; + W|WHITE_BG) echo_ "47";; + DEFAULT_BG) echo_ "49";; + @|BOLD) echo_ "1";; + -|FAINT) echo_ "2";; + _|UNDERLINED) echo_ "4";; + ~|REVERSE) echo_ "7";; + n|NORMAL) echo_ "22";; + esac + sep=1 + shift + done + echo_ "m" +} +function get_color() { + [ -n "$NO_COLORS" ] && return + __get_color "$@" +} +function __set_no_colors() { + if [ -z "$1" ]; then + if [ -n "$NULIB_NO_COLORS" ]; then NO_COLORS=1 + elif out_isatty && err_isatty; then NO_COLORS= + else NO_COLORS=1 + fi + else + is_yes "$1" && NO_COLORS=1 || NO_COLORS= + fi + + COULEUR_ROUGE="$(get_color RED BOLD)" + COULEUR_VERTE="$(get_color GREEN BOLD)" + COULEUR_JAUNE="$(get_color YELLOW BOLD)" + COULEUR_BLEUE="$(get_color BLUE BOLD)" + COULEUR_BLANCHE="$(get_color WHITE BOLD)" + COULEUR_NORMALE="$(get_color RESET)" + if [ -n "$NO_COLORS" ]; then + nulib__load: _output_vanilla + else + nulib__load: _output_color + fi +} +__set_no_colors + +# 5=afficher les messages de debug; 4=afficher les message verbeux; +# 3=afficher les message informatifs; 2=afficher les warnings et les notes; +# 1=afficher les erreurs; 0=ne rien afficher +export __verbosity +[ -z "$__verbosity" ] && __verbosity=3 +function set_verbosity() { + [ -z "$__verbosity" ] && __verbosity=3 + case "$1" in + -Q|--very-quiet) __verbosity=0;; + -q|--quiet) [ "$__verbosity" -gt 0 ] && let __verbosity=$__verbosity-1;; + -v|--verbose) [ "$__verbosity" -lt 5 ] && let __verbosity=$__verbosity+1;; + -c|--default) __verbosity=3;; + -D|--debug) __verbosity=5; DEBUG=1;; + *) return 1;; + esac + return 0 +} +# 3=interaction maximale; 2=interaction par défaut +# 1= interaction minimale; 0=pas d'interaction +export __interaction +[ -z "$__interaction" ] && __interaction=2 +function set_interaction() { + [ -z "$__interaction" ] && __interaction=2 + case "$1" in + -b|--batch) __interaction=0;; + -y|--automatic) [ "$__interaction" -gt 0 ] && let __interaction=$__interaction-1;; + -i|--interactive) [ "$__interaction" -lt 3 ] && let __interaction=$__interaction+1;; + -c|--default) __interaction=2;; + *) return 1;; + esac + return 0 +} + +# Variable à inclure pour lancer automatiquement set_verbosity et +# set_interaction en fonction des arguments de la ligne de commande. A utiliser +# de cette manière: +# parse_opts ... "${PRETTYOPTS[@]}" @ args -- ... +PRETTYOPTS=( + -L:,--log-to: '$elogto $value_' + -Q,--very-quiet,-q,--quiet,-v,--verbose,-D,--debug '$set_verbosity $option_' + -b,--batch,-y,--automatic,-i,--interactive '$set_interaction $option_' +) + +function show_error() { [ "$__verbosity" -ge 1 ]; } +function show_warn() { [ "$__verbosity" -ge 2 ]; } +function show_info() { [ "$__verbosity" -ge 3 ]; } +function show_verbose() { [ "$__verbosity" -ge 4 ]; } +function show_debug() { [ -n "$DEBUG" -o "$__verbosity" -ge 5 ]; } + +# Vérifier le niveau de verbosité actuel par rapport à l'argument. $1 peut valoir: +# -Q retourner true si on peut afficher les messages d'erreur +# -q retourner true si on peut afficher les messages d'avertissement +# -c retourner true si on peut afficher les message normaux +# -v retourner true si on peut afficher les messages verbeux +# -D retourner true si on peut afficher les messages de debug +function check_verbosity() { + case "$1" in + -Q|--very-quiet) [ "$__verbosity" -ge 1 ];; + -q|--quiet) [ "$__verbosity" -ge 2 ];; + -c|--default) [ "$__verbosity" -ge 3 ];; + -v|--verbose) [ "$__verbosity" -ge 4 ];; + -D|--debug) [ -n "$DEBUG" -o "$__verbosity" -ge 5 ];; + *) return 0;; + esac +} +# Retourner l'option correspondant au niveau de verbosité actuel +function get_verbosity_option() { + case "$__verbosity" in + 0) echo --very-quiet;; + 1) echo --quiet --quiet;; + 2) echo --quiet;; + 4) echo --verbose;; + 5) echo --debug;; + esac +} + +# Vérifier le niveau d'interaction autorisé par rapport à l'argument. Par +# exemple, 'check_interaction -y' signifie "Il ne faut interagir avec +# l'utilisateur qu'à partir du niveau d'interaction -y. Suis-je dans les +# condition voulues pour autoriser l'interaction?" +# $1 peut valoir: +# -b retourner true +# -y retourner true si on est au moins en interaction minimale +# -c retourner true si on est au moins en interaction normale +# -i retourner true si on est au moins en interaction maximale +function check_interaction() { + case "$1" in + -b|--batch) return 0;; + -y|--automatic) [ -n "$__interaction" -a "$__interaction" -ge 1 ];; + -c|--default) [ -n "$__interaction" -a "$__interaction" -ge 2 ];; + -i|--interactive) [ -n "$__interaction" -a "$__interaction" -ge 3 ];; + *) return 0;; + esac +} +# Vérifier le niveau d'interaction dans lequel on se trouve actuellement. $1 +# peut valoir: +# -b retourner true si l'une des options -b ou -yy a été spécifiée +# -Y retourner true si l'une des options -b, -yy, ou -y a été spécifiée +# -y retourner true si l'option -y a été spécifiée +# -c retourner true si aucune option n'a été spécifiée +# -i retourner true si l'option -i a été spécifiée +# -C retourner true si aucune option ou l'option -i ont été spécifiés +function is_interaction() { + case "$1" in + -b|--batch) [ -n "$__interaction" -a "$__interaction" -eq 0 ];; + -Y) [ -n "$__interaction" -a "$__interaction" -le 1 ];; + -y|--automatic) [ -n "$__interaction" -a "$__interaction" -eq 1 ];; + -c|--default) [ -n "$__interaction" -a "$__interaction" -eq 2 ];; + -i|--interactive) [ -n "$__interaction" -a "$__interaction" -eq 3 ];; + -C) [ -n "$__interaction" -a "$__interaction" -ge 2 ];; + *) return 1;; + esac +} +# Retourner l'option correspondant au niveau d'interaction actuel +function get_interaction_option() { + case "$__interaction" in + 0) echo --batch;; + 1) echo --automatic;; + 3) echo --interactive;; + esac +} diff --git a/bash/src/sysinfos.sh b/bash/src/sysinfos.sh new file mode 100644 index 0000000..341a528 --- /dev/null +++ b/bash/src/sysinfos.sh @@ -0,0 +1,4 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: sysinfos "Informations sur le système courant" +require: base diff --git a/bash/src/template.sh b/bash/src/template.sh new file mode 100644 index 0000000..3201b6a --- /dev/null +++ b/bash/src/template.sh @@ -0,0 +1,225 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: template "Mise à jour de templates à partir de modèles" + +function: template_locals "\ +Afficher les variables qui doivent être locales + +Utiliser de cette façon: +~~~ +eval \$(template_locals) +~~~" +function template_locals() { + echo "local -a userfiles; local updated" +} + +function: template_copy_replace "\ +Copier \$1 vers \$2 de façon inconditionnelle + +Si \$2 n'est pas spécifié, on assume que \$1 est de la forme '.file.ext' +et \$2 vaudra alors 'file' + +si un fichier \${2#.}.local existe, prendre ce fichier à la place comme source + +Ajouter file au tableau userfiles" +function template_copy_replace() { + local src="$1" dest="$2" + local srcdir srcname lsrcname + setx srcdir=dirname "$src" + setx srcname=basename "$src" + if [ -z "$dest" ]; then + dest="${srcname#.}" + dest="${dest%.*}" + dest="$srcdir/$dest" + fi + + lsrcname="${srcname#.}.local" + [ -e "$srcdir/$lsrcname" ] && src="$srcdir/$lsrcname" + + userfiles+=("$dest") + cp -P "$src" "$dest" + return 0 +} + +function: template_copy_missing "\ +Copier \$1 vers \$2 si ce fichier n'existe pas déjà + +Si \$2 n'est pas spécifié, on assume que \$1 est de la forme '.file.ext' +et \$2 vaudra alors 'file' + +si un fichier \${2#.}.local existe, prendre ce fichier à la place comme source + +Ajouter file au tableau userfiles" +function template_copy_missing() { + local src="$1" dest="$2" + local srcdir srcname lsrcname + setx srcdir=dirname "$src" + setx srcname=basename "$src" + if [ -z "$dest" ]; then + dest="${srcname#.}" + dest="${dest%.*}" + dest="$srcdir/$dest" + fi + + userfiles+=("$dest") + if [ ! -e "$dest" ]; then + lsrcname="${srcname#.}.local" + [ -e "$srcdir/$lsrcname" ] && src="$srcdir/$lsrcname" + + cp -P "$src" "$dest" + return 0 + fi + return 1 +} + +function: template_dump_vars "\ +Lister les variables mentionnées dans les fichiers \$@ + +Seules sont prises en compte les variables dont le nom est de la forme +[A-Z][A-Za-z_]* + +Cette fonction est utilisée par template_source_envs(). Elle utilise la +fonction outil _template_dump_vars() qui peut être redéfinie si nécessaire." +function template_dump_vars() { + _template_dump_vars "$@" +} +function _template_dump_vars() { + [ $# -gt 0 ] || return 0 + cat "$@" | + grep -E '^[A-Z][A-Za-z_]*=' | + sed 's/=.*//' | + sort -u +} + +function: template_source_envs "\ +Cette fonction doit être implémentée par l'utilisateur et doit: +- initialiser le tableau template_vars qui donne la liste des variables scalaires +- initialiser te tableau template_lists qui donne la liste des variables listes +- charger ces variables depuis les fichiers \$@ + +Cette fonction utilise la fonction outil _template_source_envs() qui peut être +redéfinie si nécessaire." +function template_source_envs() { + _template_source_envs "$@" +} +function _template_source_envs() { + local e_ + for e_ in "$@"; do + [ -f "$e_" ] && source "$e_" + done + setx -a template_vars=template_dump_vars "$@" + template_lists=() +} + +function: template_resolve_scripts "\ +Générer le script awk \$1 et le script sed \$2 qui remplacent dans les fichiers +destination les marqueurs @@VAR@@ par la valeur de la variable \$VAR +correspondante + +Les fichiers \$3..@ contiennent les valeurs des variables + +Les marqueurs supportés sont les suivants et sont évalués dans cet ordre: +- XXXRANDOMXXX remplacer cette valeur par une chaine de 16 caractères au hasard +- @@FOR:VARS@@ VARS étant une liste de valeurs séparées par des espaces: + dupliquer la ligne autant de fois qu'il y a de valeurs dans \$VARS + dans chaque ligne, remplacer les occurrences de @@VAR@@ par la valeur + de l'itération courante +- #@@IF:VAR@@ afficher la ligne si VAR est non vide, supprimer la ligne sinon +- #@@UL:VAR@@ afficher la ligne si VAR est vide, supprimer la ligne sinon +- #@@if:VAR@@ +- #@@ul:VAR@@ variantes qui ne suppriment pas la ligne mais sont remplacées par # +- @@VAR:-string@@ remplacer par 'string' si VAR a une valeur vide ou n'est pas défini, \$VAR sinon +- @@VAR:+string@@ remplacer par 'string' si VAR est défini a une valeur non vide +" +function template_generate_scripts() { + local awkscript="$1"; shift + local sedscript="$1"; shift + ( + template_source_envs "$@" + + NL=$'\n' + # random, for + exec >"$awkscript" + echo '@include "base.tools.awk"' + echo 'BEGIN {' + for list in "${template_lists[@]}"; do + items="${!list}"; read -a items <<<"${items// +/ }" + let i=0 + echo " $list[0] = 0; delete $list" + for item in "${items[@]}"; do + item="${item//\\/\\\\}" + item="${item//\"/\\\"}" + echo " $list[$i] = \"$item\"" + let i=i+1 + done + done + echo '}' + echo '{ if (should_generate_password()) { generate_password() } }' + for list in "${template_lists[@]}"; do + items="${!list}"; read -a items <<<"${items// +/ }" + echo "/@@FOR:$list@@/ {" + if [ ${#items[*]} -gt 0 ]; then + if [ "${list%S}" != "$list" ]; then item="${list%S}" + elif [ "${list%s}" != "$list" ]; then item="${list%s}" + else item="$list" + fi + echo " sub(/@@FOR:$list@@/, \"\")" + echo " for (i in $list) {" + echo " print gensub(/@@$item@@/, $list[i], \"g\")" + echo " }" + fi + echo " next" + echo "}" + done + echo '{ print }' + + # if, ul, var + exec >"$sedscript" + for var in "${template_vars[@]}"; do + value="${!var}" + value="${value//\//\\\/}" + value="${value//[/\\[}" + value="${value//\*/\\\*}" + value="${value//$NL/\\n}" + if [ -n "$value" ]; then + echo "s/#@@IF:${var}@@//g" + echo "s/#@@if:${var}@@//g" + echo "/#@@UL:${var}@@/d" + echo "s/#@@ul:${var}@@/#/g" + echo "s/@@${var}:-([^@]*)@@/${value}/g" + echo "s/@@${var}:+([^@]*)@@/\\1/g" + else + echo "/#@@IF:${var}@@/d" + echo "s/#@@if:${var}@@/#/g" + echo "s/#@@UL:${var}@@//g" + echo "s/#@@ul:${var}@@//g" + echo "s/@@${var}:-([^@]*)@@/\\1/g" + echo "s/@@${var}:+([^@]*)@@//g" + fi + echo "s/@@${var}@@/${value}/g" + done + ) + #etitle "awkscript" cat "$awkscript" + #etitle "sedscript" cat "$sedscript" +} + +function template_process_userfiles() { + local awkscript sedscript workfile userfile + ac_set_tmpfile awkscript + ac_set_tmpfile sedscript + template_generate_scripts "$awkscript" "$sedscript" "$@" + + ac_set_tmpfile workfile + for userfile in "${userfiles[@]}"; do + if cat "$userfile" | awk -f "$awkscript" | sed -rf "$sedscript" >"$workfile"; then + if testdiff "$workfile" "$userfile"; then + # n'écrire le fichier que s'il a changé + cat "$workfile" >"$userfile" + fi + fi + done + + ac_clean "$awkscript" "$sedscript" "$workfile" +} diff --git a/bash/src/tests.sh b/bash/src/tests.sh new file mode 100644 index 0000000..86f4dfd --- /dev/null +++ b/bash/src/tests.sh @@ -0,0 +1,160 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +##@cooked nocomments +module: tests "tests unitaires" +require: base + +function tests__get_line() { + # obtenir le nom du script depuis lequel les fonctions de ce module ont été + # appelées + local mysource="${BASH_SOURCE[0]}" + local i=1 + while [ "${BASH_SOURCE[$i]}" == "$mysource" ]; do + let i=i+1 + done + echo "${BASH_SOURCE[$i]}:${BASH_LINENO[$((i-1))]}" +} + +function tests__set_message() { + if [ "$1" == -m ]; then + message="$2" + return 2 + elif [[ "$1" == -m* ]]; then + message="${1#-m}" + return 1 + else + return 0 + fi +} + +function: assert_ok "faire un test unitaire. la syntaxe est + assert_ok [-m message] cmd +la commande doit retourner vrai pour que le test soit réussi" +function assert_ok() { + local message; tests__set_message "$@" || shift $? + "$@" && return 0 + + [ -n "$message" ] && message="$message: " + message="${message}test failed at $(tests__get_line)" + die "$message" +} +function assert() { assert_ok "$@"; } + +function: assert_ko "faire un test unitaire. la syntaxe est + assert_ko [-m message] cmd +la commande doit retourner faux pour que le test soit réussi" +function assert_ko() { + local message; tests__set_message "$@" || shift $? + "$@" || return 0 + + [ -n "$message" ] && message="$message: " + message="${message}test failed at $(tests__get_line)" + die "$message" +} + +function tests__assert() { + local message="$1"; shift + "assert_${1#assert_}" -m "$message" "${@:2}" +} + +function assert_n() { + local message; tests__set_message "$@" || shift $? + [ -n "$message" ] || message="$2" + [ -n "$message" ] || message="value is empty" + tests__assert "$message" ok [ -n "$1" ] +} +function assert_z() { + local message; tests__set_message "$@" || shift $? + [ -n "$message" ] || message="$2" + [ -n "$message" ] || message="value is not empty" + tests__assert "$message" ok [ -z "$1" ] +} +function assert_same() { + local message; tests__set_message "$@" || shift $? + [ -n "$message" ] || message="$3" + [ -n "$message" ] || message="'$1' and '$2' are different" + tests__assert "$message" ok [ "$1" == "$2" ] +} +function assert_diff() { + local message; tests__set_message "$@" || shift $? + [ -n "$message" ] || message="$3" + [ -n "$message" ] || message="'$1' and '$2' are the same" + tests__assert "$message" ok [ "$1" != "$2" ] +} + +function assert_eq() { + local message; tests__set_message "$@" || shift $? + [ -n "$message" ] || message="$3" + [ -n "$message" ] || message="'$1' and '$2' are not equals" + tests__assert "'$1' must be a number" ok isnum "$1" + tests__assert "$message" ok [ "$1" -eq "$2" ] +} +function assert_ne() { + local message; tests__set_message "$@" || shift $? + [ -n "$message" ] || message="$3" + [ -n "$message" ] || message="'$1' and '$2' are equals" + tests__assert "'$1' must be a number" ok isnum "$1" + tests__assert "$message" ok [ "$1" -ne "$2" ] +} +function assert_gt() { + local message; tests__set_message "$@" || shift $? + [ -n "$message" ] || message="$3" + [ -n "$message" ] || message="'$1' is not greater than '$2'" + tests__assert "'$1' must be a number" ok isnum "$1" + tests__assert "$message" ok [ "$1" -gt "$2" ] +} +function assert_ge() { + local message; tests__set_message "$@" || shift $? + [ -n "$message" ] || message="$3" + [ -n "$message" ] || message="'$1' is not greater than or equals to '$2'" + tests__assert "'$1' must be a number" ok isnum "$1" + tests__assert "$message" ok [ "$1" -ge "$2" ] +} +function assert_lt() { + local message; tests__set_message "$@" || shift $? + [ -n "$message" ] || message="$3" + [ -n "$message" ] || message="'$1' is not less than '$2'" + tests__assert "'$1' must be a number" ok isnum "$1" + tests__assert "$message" ok [ "$1" -lt "$2" ] +} +function assert_le() { + local message; tests__set_message "$@" || shift $? + [ -n "$message" ] || message="$3" + [ -n "$message" ] || message="'$1' is not less than or equals to '$2'" + tests__assert "'$1' must be a number" ok isnum "$1" + tests__assert "$message" ok [ "$1" -le "$2" ] +} + +function assert_is_defined() { + local message; tests__set_message "$@" || shift $? + [ -n "$message" ] || message="$2" + [ -n "$message" ] || message="'$1' is not defined" + tests__assert "$message" ok is_defined "$1" +} +function assert_not_defined() { + local message; tests__set_message "$@" || shift $? + [ -n "$message" ] || message="$2" + [ -n "$message" ] || message="'$1' is defined" + tests__assert "$message" ko is_defined "$1" +} +function assert_is_array() { + local message; tests__set_message "$@" || shift $? + [ -n "$message" ] || message="$2" + [ -n "$message" ] || message="'$1' is not an array" + tests__assert "$message" ok is_array "$1" +} +function assert_not_array() { + local message; tests__set_message "$@" || shift $? + [ -n "$message" ] || message="$2" + [ -n "$message" ] || message="'$1' is an array" + tests__assert "$message" ko is_array "$1" +} + +function assert_array_same() { + local message; tests__set_message "$@" || shift $? + [ -n "$message" ] || message="'$1' is not an array or not equals to (${*:2})" + + assert_is_array "$1" "$message" + eval "actual=\"\$(qvals \"\${$1[@]}\")\""; shift + eval "expected=\"\$(qvals \"\$@\")\"" + assert_same "$actual" "$expected" "$message" +} diff --git a/bash/tests/.gitignore b/bash/tests/.gitignore new file mode 100644 index 0000000..3ee43c2 --- /dev/null +++ b/bash/tests/.gitignore @@ -0,0 +1 @@ +/template-dest.txt diff --git a/bash/tests/_template-dest.txt b/bash/tests/_template-dest.txt new file mode 100644 index 0000000..8de596b --- /dev/null +++ b/bash/tests/_template-dest.txt @@ -0,0 +1,21 @@ +--- +PROFILES vaut prod test +c'est à dire, si on fait un par ligne: +- prod +- test +--- +PASSWORD is vaeRL6ADYKmWndEA +--- +hosts: +- first +- second +--- +--- +IF valeur +if valeur +#ul valeur +--- +#if +UL +ul +--- diff --git a/bash/tests/_template-source.txt b/bash/tests/_template-source.txt new file mode 100644 index 0000000..d98644e --- /dev/null +++ b/bash/tests/_template-source.txt @@ -0,0 +1,23 @@ +--- +PROFILES vaut @@PROFILES@@ +c'est à dire, si on fait un par ligne: +@@FOR:PROFILES@@- @@PROFILE@@ +--- +PASSWORD is XXXRANDOMXXX +--- +#@@IF:HOSTS@@hosts: +@@FOR:HOSTS@@- @@HOST@@ +--- +#@@IF:VALUES@@values: +@@FOR:VALUES@@- @@VALUE@@ +--- +#@@IF:PLEIN@@IF @@PLEIN@@ +#@@if:PLEIN@@if @@PLEIN@@ +#@@UL:PLEIN@@UL @@PLEIN@@ +#@@ul:PLEIN@@ul @@PLEIN@@ +--- +#@@IF:VIDE@@IF @@VIDE@@ +#@@if:VIDE@@if @@VIDE@@ +#@@UL:VIDE@@UL @@VIDE@@ +#@@ul:VIDE@@ul @@VIDE@@ +--- diff --git a/bash/tests/_template-source_envs b/bash/tests/_template-source_envs new file mode 100755 index 0000000..e2aadd5 --- /dev/null +++ b/bash/tests/_template-source_envs @@ -0,0 +1,19 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +echo "\ +source ./_template-values.env +template_vars=( + PROFILES + PASSWORD + HOSTS + VALUES + PLEIN + VIDE +) +template_lists=( + PROFILES + HOSTS + VALUES +) +" diff --git a/bash/tests/_template-values.env b/bash/tests/_template-values.env new file mode 100644 index 0000000..03af4ac --- /dev/null +++ b/bash/tests/_template-values.env @@ -0,0 +1,12 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +PROFILES="prod test" + +HOSTS=" +first +second +" +VALUES= + +PLEIN=valeur +VIDE= diff --git a/bash/tests/test-args-autodebug.sh b/bash/tests/test-args-autodebug.sh new file mode 100755 index 0000000..e035ffb --- /dev/null +++ b/bash/tests/test-args-autodebug.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../src/nulib.sh" || exit 1 +#NULIB_NO_DISABLE_SET_X=1 + +args=( + "tester autodebug" + #-D x=1 "désactiver l'option automatique -D" + #--debug x=1 "désactiver l'option automatique --debug" +) +parse_args "$@"; set -- "${args[@]}" + +if is_debug; then + echo "on est en mode debug" +else + echo "on n'est pas en mode debug, relancer avec -D ou --debug" +fi diff --git a/bash/tests/test-args-autohelp.sh b/bash/tests/test-args-autohelp.sh new file mode 100755 index 0000000..bd2cf98 --- /dev/null +++ b/bash/tests/test-args-autohelp.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../src/nulib.sh" || exit 1 +#NULIB_NO_DISABLE_SET_X=1 + +args=("tester l'affichage de l'aide") + +case "$1" in +s|std) + # NB: seul l'affichage standard est disponible... + args+=( + -h,--help,--hstd '$showhelp@' "afficher l'aide de base" + ) + shift + ;; +a|adv) + # NB: seul l'affichage avancé est disponible... + args+=( + -H,--help++,--hadv '$showhelp@ ++' "afficher l'aide avancée" + ) + shift + ;; +sa|std+adv) + args+=( + -h,--help,--hstd '$showhelp@' "afficher l'aide de base" + -H,--help++,--hadv '$showhelp@ ++' "afficher l'aide avancée" + ) + shift + ;; +esac + +args+=( + -a,--std . "cette option apparait dans les options standards" + -b,--adv . "++cette option apparait dans les options avancées" +) +parse_args "$@"; set -- "${args[@]}" + +enote "lancer le script +- avec --help pour afficher les options standards uniquement +- avec --help++ pour afficher toutes les options" diff --git a/bash/tests/test-args-base.sh b/bash/tests/test-args-base.sh new file mode 100755 index 0000000..b6b45da --- /dev/null +++ b/bash/tests/test-args-base.sh @@ -0,0 +1,187 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../src/nulib.sh" || exit 1 +require: tests +#NULIB_NO_DISABLE_SET_X=1 + +function pa() { + unset count fixed mopt dmopt oopt doopt autoinc autoval a1 a2 a3 a4 + count= + fixed= + efixed=1 + mopt= + dmopt= + oopt= + doopt= + autoinc= + autoval= + unset a1 + a2=() + a3= + a4=x + args=( + "tester la gestion des arguments" + -o,--eopt count "incrémenter count" + -f,--fixed fixed=42 "spécifier fixed" + -e,--efixed efixed= "spécifier efixed" + -a:,--mopt mopt= "spécifier mopt" + -A:,--dmopt dmopt=default "spécifier dmopt" + -b::,--oopt oopt= "spécifier oopt" + -B::,--doopt doopt=default "spécifier doopt" + -n,--autoinc . "incrémenter autoinc" + -N,--no-autoinc . "décrémenter autoinc" + -v:,--autoval . "spécifier autoval" + -x: a1 "autoadd a1 qui n'est pas défini" + -y: a2 "autoadd a2 qui est défini à ()" + -z: a3 "autoadd a3 qui est défini à vide" + -t: a4 "autoadd a4 qui est défini à une valeur non vide" + -s,--sans-arg '$echo "sans_arg option=$option_, name=$name_, value=$value_"' + -S::,--avec-arg '$echo "avec_arg option=$option_, name=$name_, value=$value_"' + ) + parse_args "$@" +} + +pa +assert_z "$count" +assert_z "$fixed" +assert_eq "$efixed" 1 +assert_z "$mopt" +assert_z "$dmopt" +assert_z "$oopt" +assert_z "$doopt" +assert_z "$autoinc" +assert_z "$autoval" +assert_not_defined a1 +assert_is_array a2 +assert_not_array a3 +assert_z "$a3" +assert_not_array a4 +assert_same x "$a4" +assert_eq "${#args[*]}" 0 + +pa -o +assert_eq "$count" 1 +pa -oo +assert_eq "$count" 2 +pa -ooo +assert_eq "$count" 3 + +pa -f +assert_eq "$fixed" 42 +pa -ff +assert_eq "$fixed" 42 +pa -fff +assert_eq "$fixed" 42 + +assert_same "$efixed" "1" +pa -e +assert_same "$efixed" "" +pa -ee +assert_same "$efixed" "" +pa -eee +assert_same "$efixed" "" + +pa -a "" +assert_not_array mopt +assert_same "$mopt" "" +pa -a abc +assert_not_array mopt +assert_same "$mopt" abc +pa -a abc -a xyz +assert_not_array mopt +assert_same "$mopt" xyz + +pa -A "" +assert_not_array dmopt +assert_same "$dmopt" default +pa -A abc +assert_not_array dmopt +assert_same "$dmopt" abc +pa -A abc -A xyz +assert_not_array dmopt +assert_same "$dmopt" xyz + +pa -b +assert_not_array oopt +assert_same "$oopt" "" +pa -babc +assert_not_array oopt +assert_same "$oopt" abc +pa -babc -bxyz +assert_not_array oopt +assert_same "$oopt" xyz + +pa -B +assert_not_array doopt +assert_same "$doopt" default +pa -Babc +assert_not_array doopt +assert_same "$doopt" abc +pa -Babc -Bxyz +assert_not_array doopt +assert_same "$doopt" xyz + +pa -n +assert_eq "$autoinc" 1 +pa -nn +assert_eq "$autoinc" 2 +pa -nnn +assert_eq "$autoinc" 3 + +pa -nN +assert_z "$autoinc" +pa -nnN +assert_eq "$autoinc" 1 +pa -nnnNN +assert_eq "$autoinc" 1 + +pa -v "" +assert_is_array autoval +assert_array_same autoval "" +pa -v abc +assert_is_array autoval +assert_array_same autoval abc +pa -v abc -v xyz +assert_is_array autoval +assert_array_same autoval abc xyz + +pa -xa +assert_not_array a1 +assert_same "$a1" a +pa -xa -xb +assert_is_array a1 +assert_array_same a1 a b + +pa -ya +assert_is_array a2 +assert_array_same a2 a +pa -ya -yb +assert_is_array a2 +assert_array_same a2 a b + +pa -za +assert_is_array a3 +assert_array_same a3 a +pa -za -zb +assert_is_array a3 +assert_array_same a3 a b + +pa -ta +assert_is_array a4 +assert_array_same a4 x a +pa -ta -tb +assert_is_array a4 +assert_array_same a4 x a b + +assert_same "$(pa -s)" "sans_arg option=-s, name=sans_arg, value=" +assert_same "$(pa --sans-arg)" "sans_arg option=--sans-arg, name=sans_arg, value=" + +assert_same "$(pa -S)" "avec_arg option=-S, name=avec_arg, value=" +assert_same "$(pa -Sx)" "avec_arg option=-S, name=avec_arg, value=x" +assert_same "$(pa --avec-arg)" "avec_arg option=--avec-arg, name=avec_arg, value=" +assert_same "$(pa --avec-arg=x)" "avec_arg option=--avec-arg, name=avec_arg, value=x" + +pa x +assert_array_same args x + +enote "tests successful" diff --git a/bash/tests/test-args-help.sh b/bash/tests/test-args-help.sh new file mode 100755 index 0000000..646c392 --- /dev/null +++ b/bash/tests/test-args-help.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../src/nulib.sh" || exit 1 +#NULIB_NO_DISABLE_SET_X=1 + +args=( + "tester l'affichage de l'aide" + -f:,--input input= "spécifier le fichier en entrée +il est possible de spécifier aussi un répertoire auquel cas un fichier par défaut est chargé +nb: l'aide pour cette option doit faire 3 lignes indentées" + -a,--std . "cette option apparait dans les options standards" + -b,--adv . "++cette option apparait dans les options avancées" +) +parse_args "$@"; set -- "${args[@]}" + +enote "lancer le script +- avec --help pour afficher les options standards uniquement +- avec --help++ pour afficher toutes les options" diff --git a/bash/tests/test-input.sh b/bash/tests/test-input.sh new file mode 100755 index 0000000..5263dc5 --- /dev/null +++ b/bash/tests/test-input.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../src/nulib.sh" || exit 1 +#NULIB_NO_DISABLE_SET_X=1 + +args=( + "tester diverses fonctions de saisie" +) +parse_args "$@"; set -- "${args[@]}" + +choices=(first second third) +choice= +simple_menu -t "choix sans valeur par défaut" -m "le prompt" choice choices +enote "vous avez choisi choice=$choice" + +choice=second +simple_menu -t "choix avec valeur par défaut" -m "le prompt" choice choices +enote "vous avez choisi choice=$choice" diff --git a/bash/tests/test-output.sh b/bash/tests/test-output.sh new file mode 100755 index 0000000..56a5812 --- /dev/null +++ b/bash/tests/test-output.sh @@ -0,0 +1,169 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../src/nulib.sh" || exit 1 +#NULIB_NO_DISABLE_SET_X=1 + +Multiline= +Banner= +args=( + "afficher divers messages avec les fonctions e*" + -D,--debug '$set_debug' + -d,--date NULIB_ELOG_DATE=1 + -m,--myname NULIB_ELOG_MYNAME=1 + -n,--nc,--no-color '$__set_no_colors 1' + --ml Multiline=1 + -b Banner=1 +) +parse_args "$@"; set -- "${args[@]}" + +if [ -n "$Multiline" ]; then + ############################################################################ + [ -n "$Banner" ] && ebanner $'multi-line\nbanner' + + esection $'multi-line\nsection' + etitle $'multi-line\ntitle' + etitle $'another\ntitle' + edesc $'multi-line\ndesc' + + [ -n "$Banner" ] && ebanner $'multi-line\nbanner' + eimportant $'multi-line\nimportant' + eattention $'multi-line\nattention' + eerror $'multi-line\nerror' + ewarn $'multi-line\nwarn' + enote $'multi-line\nnote' + einfo $'multi-line\ninfo' + eecho $'multi-line\necho' + edebug $'multi-line\ndebug' + + action $'multi-line\naction' + asuccess + + action $'multi-line\naction' + estep $'multi-line\nstep' + afailure + + action $'multi-line\naction' + estep $'multi-line\nstep' + asuccess $'multi-line\nsuccess' + + action $'multi-line\naction' + estep $'multi-line\nstep' + adone $'multi-line\nneutral' + + eend + eend + +else + ############################################################################ + [ -n "$Banner" ] && ebanner "banner" + eimportant "important" + eattention "attention" + eerror "error" + ewarn "warn" + enote "note" + einfo "info" + eecho "echo" + edebug "debug" + + estep "step" + estepe "stepe" + estepw "stepw" + estepn "stepn" + estepi "stepi" + + esection "section" + eecho "content" + + etitle "title0" + etitle "title1" + eecho "print under title1" + eend + eecho "print under title0" + eend + + edesc "action avec step" + action "action avec step" + estep "step" + asuccess "action success" + + action "action avec step" + estep "step" + afailure "action failure" + + action "action avec step" + estep "step" + adone "action neutral" + + edesc "actions sans step" + action "action sans step" + asuccess "action success" + + action "action sans step" + afailure "action failure" + + action "action sans step" + adone "action neutral" + + edesc "actions imbriquées" + action "action0" + action "action1" + action "action2" + asuccess "action2 success" + asuccess "action1 success" + asuccess "action0 success" + + edesc "action avec step, sans messages" + action "action avec step, sans messages, success" + estep "step" + asuccess + + action "action avec step, sans messages, failure" + estep "step" + afailure + + action "action avec step, sans messages, done" + estep "step" + adone + + edesc "action sans step, sans messages" + action "action sans step, sans messages, success" + asuccess + + action "action sans step, sans messages, failure" + afailure + + action "action sans step, sans messages, done" + adone + + edesc "actions imbriquées, sans messages" + action "action0" + action "action1" + action "action2" + asuccess + asuccess + asuccess + + function vtrue() { + echo "commande qui se termine avec succès" + } + function vfalse() { + echo "commande qui se termine en échec" + return 1 + } + + edesc "action avec commande" + action "commande true" vtrue + action "commande false" vfalse + + edesc "action avec commande et aresult sans message" + action "commande true" + vtrue; aresult $? + action "commande false" + vfalse; aresult $? + + edesc "action avec commande et aresult" + action "commande true" + vtrue; aresult $? "résultat de la commande" + action "commande false" + vfalse; aresult $? "résultat de la commande" +fi diff --git a/bash/tests/test-template.sh b/bash/tests/test-template.sh new file mode 100755 index 0000000..ca297c8 --- /dev/null +++ b/bash/tests/test-template.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../src/nulib.sh" || exit 1 +require: template +#NULIB_NO_DISABLE_SET_X=1 + +args=( + "description" + #"usage" +) +parse_args "$@"; set -- "${args[@]}" + +function template__source_envs() { + eval "$("$MYDIR/_template-source_envs")" +} + +cd "$MYDIR" +#template_generate_scripts \ +# /tmp/awkscript /tmp/sedscript \ +# template_values.env +# +#for i in awk sed; do +# etitle "$i" cat "/tmp/${i}script" +#done + +template_copy_replace _template-source.txt _template-dest.txt +template_process_userfiles _template_values.env + +cat _template-dest.txt diff --git a/bin/_runphp_build-all b/bin/_runphp_build-all new file mode 100755 index 0000000..c6d0f4f --- /dev/null +++ b/bin/_runphp_build-all @@ -0,0 +1,30 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 + +force= +args=( + "Construire toutes les images supportées de runphp" + #"usage" + -f,--force force=1 "Créer les images même si elles existent déjà" +) +parse_args "$@"; set -- "${args[@]}" + +export RUNPHP_STANDALONE="$NULIBDIR" +export RUNPHP_PROJDIR= +export RUNPHP_REGISTRY= +export RUNPHP_DIST= +export RUNPHP_BUILD_FLAVOUR= + +runphp=("$MYDIR/../runphp/runphp" --bs) +[ -z "$force" ] && runphp+=(--ue) + +for RUNPHP_DIST in d12 d11; do + for RUNPHP_BUILD_FLAVOUR in +ic none; do + flavour="$RUNPHP_BUILD_FLAVOUR" + [ "$flavour" == none ] && flavour= + etitle "$RUNPHP_DIST$flavour" + "${runphp[@]}" + eend + done +done diff --git a/bin/nlman b/bin/nlman new file mode 100755 index 0000000..a6845bd --- /dev/null +++ b/bin/nlman @@ -0,0 +1,69 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +NULIB_NO_IMPORT_DEFAULTS=1 +source "$(dirname -- "$0")/../load.sh" || exit 1 + +LIST_FUNCS= +SHOW_MODULE= +SHOW_FUNCS=() +function nulib__define_functions() { + function nulib_check_loaded() { + local module + for module in "${NULIB_LOADED_MODULES[@]}"; do + [ "$module" == "$1" ] && return 0 + done + return 1 + } + function module:() { + if [ -n "$LIST_FUNCS" ]; then + esection "@$1: $2" + fi + local module + SHOW_MODULE= + for module in "${SHOW_FUNCS[@]}"; do + if [ "$module" == "@$1" ]; then + SHOW_MODULE=1 + fi + done + NULIB_MODULE="$1" + if ! nulib_check_loaded "$1"; then + NULIB_LOADED_MODULES+=("$1") + fi + } + function function:() { + if [ -n "$LIST_FUNCS" ]; then + eecho "$1" + elif [ -n "$SHOW_MODULE" ]; then + eecho "$COULEUR_BLEUE>>> $1 <<<$COULEUR_NORMALE" + eecho "$2" + else + local func + for func in "${SHOW_FUNCS[@]}"; do + if [ "$func" == "$1" ]; then + esection "$1" + eecho "$2" + fi + done + fi + } +} +require: DEFAULTS + +modules=() +args=( + "afficher l'aide d'une fonction nulib" + "FUNCTIONS|@MODULES..." + -m:,--module: modules "charger des modules supplémentaires" + -l,--list LIST_FUNCS=1 "lister les fonctions pour lesquelles une aide existe" +) +parse_args "$@"; set -- "${args[@]}" + +for func in "$@"; do + SHOW_FUNCS+=("$func") +done + +NULIB_LOADED_MODULES=(nulib) +require: DEFAULTS +for module in "${modules[@]}"; do + require: "$module" +done diff --git a/bin/nlshell b/bin/nlshell new file mode 100755 index 0000000..f5f64f6 --- /dev/null +++ b/bin/nlshell @@ -0,0 +1,38 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 + +force_reload= +args=( + "lancer un shell avec les fonctions de nulib préchargées" + -r,--force-reload force_reload=1 "forcer le rechargement des modules" +) +parse_args "$@"; set -- "${args[@]}" + +ac_set_tmpfile bashrc +echo >"$bashrc" "\ +if ! grep -q '/etc/bash.bashrc' /etc/profile; then + [ -f /etc/bash.bashrc ] && source /etc/bash.bashrc +fi +if ! grep -q '~/.bashrc' ~/.bash_profile; then + [ -f ~/.bashrc ] && source ~/.bashrc +fi +[ -f /etc/profile ] && source /etc/profile +[ -f ~/.bash_profile ] && source ~/.bash_profile + +# Modifier le PATH. Ajouter aussi le chemin vers les uapps python +PATH=$(qval "$NULIBDIR/bin:$PATH") + +if [ -n '$DEFAULT_PS1' ]; then + DEFAULT_PS1=$(qval "[nlshell] $DEFAULT_PS1") +else + if [ -z '$PS1' ]; then + PS1='\\u@\\h \\w \\$ ' + fi + PS1=\"[nlshell] \$PS1\" +fi + +$(qvals source "$NULIBDIR/load.sh") +NULIB_FORCE_RELOAD=$(qval "$force_reload")" + +"$SHELL" --rcfile "$bashrc" -i -- "$@" diff --git a/bin/np b/bin/np new file mode 100755 index 0000000..e567f2b --- /dev/null +++ b/bin/np @@ -0,0 +1,61 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 +require: git + +function git_status() { + local status r cwd + status="$(git status "$@" 2>&1)"; r=$? + if [ -n "$status" ]; then + setx cwd=ppath2 "$(pwd)" "$OrigCwd" + etitle "$cwd" + if [ $r -eq 0 ]; then + echo "$status" + else + eerror "$status" + fi + eend + fi +} + +chdir= +all= +args=( + "afficher l'état du dépôt" + "[-d chdir] [-a patterns...] + +Si l'option -a est utilisée, ce script accepte comme arguments une liste de patterns permettant de filtrer les répertoires concernés" + -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations" + -a,--all all=1 "faire l'opération sur tous les sous-répertoires de BASEDIR qui sont des dépôts git" +) +parse_args "$@"; set -- "${args[@]}" + +setx OrigCwd=pwd +if [ -n "$chdir" ]; then + cd "$chdir" || die +fi + +if [ -n "$all" ]; then + # liste de sous répertoires + if [ $# -gt 0 ]; then + # si on a une liste de patterns, l'utiliser + setx -a dirs=ls_dirs . "$@" + else + dirs=() + for dir in */.git; do + [ -d "$dir" ] || continue + dirs+=("${dir%/.git}") + done + fi + setx cwd=pwd + for dir in "${dirs[@]}"; do + cd "$dir" || die + git_status --porcelain + cd "$cwd" + done +else + # répertoire courant uniquement + args=() + isatty || args+=(--porcelain) + git_status "${args[@]}" +fi diff --git a/bin/runphp b/bin/runphp index a3aa6dc..a938ba3 100755 --- a/bin/runphp +++ b/bin/runphp @@ -1,52 +1,51 @@ #!/bin/bash # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 -MYDIR="$(dirname -- "$0")"; MYNAME="$(basename -- "$0")" -function die() { echo 1>&2 "ERROR: $*"; exit 1; } +source "$(dirname -- "$0")/../load.sh" || exit 1 -case "$MYNAME" in -runphp) ;; -composer) - if [ -f "$MYDIR/composer.phar" ]; then - set -- "$MYDIR/composer.phar" "$@" - elif [ -f "$MYDIR/../sbin/composer.phar" ]; then - set -- "$MYDIR/../sbin/composer.phar" "$@" - elif [ -f "/usr/bin/composer" ]; then - set -- "/usr/bin/composer" "$@" - else - set -- "" "$@" +owd="$(pwd)" +PROJDIR= +while true; do + cwd="$(pwd)" + if [ -f .runphp.conf ]; then + PROJDIR="$cwd" + break + elif [ -f composer.json ]; then + PROJDIR="$cwd" + break fi - ;; -*) die "$MYNAME: nom de script invalide";; -esac - -function runphp_help() { - echo "$MYNAME: lance un programme PHP en sélectionnant une version en particulier - -USAGE - $MYNAME [options] [args...] - -OPTIONS - -s, --min PHP_MIN - -m, --max PHP_MAX - -i, --image IMAGE" -} - -SOPTS=+smi -LOPTS=help,php-min,min,php-max,max,image -args="$(getopt -n runphp -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args" - -while [ $# -gt 0 ]; do - case "$1" in - --) shift; break;; - --help) runphp_help; exit 0;; - *) die "$1: option non configurée";; - esac - shift + if [ "$cwd" == "$HOME" -o "$cwd" == / ]; then + cd "$owd" + break + fi + cd .. done -script="$1"; shift -[ -n "$script" ] || die "vous devez spécifier le script à lancer" -[ -f "$script" ] || die "$script: script introuvable" +if [ -z "$PROJDIR" ]; then + # s'il n'y a pas de projet, --bs est l'action par défaut + [ $# -gt 0 ] || set -- --bs +elif [ "$MYNAME" == composer ]; then + set -- composer "$@" +else + case "$1" in + *.php|*.phar) set -- php "$@";; + esac +fi -scriptdir="$(dirname -- "$script")" -scritname="$(basename -- "$script")" +if [ -n "$PROJDIR" ]; then + export RUNPHP_STANDALONE= + RUNPHP=; DIST=; REGISTRY= + if [ -f "$PROJDIR/.runphp.conf" ]; then + source "$PROJDIR/.runphp.conf" + [ -n "$RUNPHP" ] && exec "$PROJDIR/$RUNPHP" "$@" + elif [ -f "$PROJDIR/sbin/runphp" ]; then + exec "$PROJDIR/sbin/runphp" "$@" + elif [ -f "$PROJDIR/runphp" ]; then + exec "$PROJDIR/runphp" "$@" + fi +fi + +export RUNPHP_STANDALONE="$NULIBDIR" +export RUNPHP_PROJDIR="$PROJDIR" +export RUNPHP_REGISTRY="$REGISTRY" +export RUNPHP_DIST="$DIST" +exec "$MYDIR/../runphp/runphp" "$@" diff --git a/bin/templ.md b/bin/templ.md new file mode 100755 index 0000000..a7b4934 --- /dev/null +++ b/bin/templ.md @@ -0,0 +1,38 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 +require: fndate + +: ${EDITOR:=vim} + +autoext=1 +args=( + "créer un nouveau fichier .markdown" + "" + -j,--no-autoext autoext= "ne pas rajouter l'extension .yaml ni .yml" +) +parse_args "$@"; set -- "${args[@]}" + +[ $# -gt 0 ] || die "vous devez spécifier les noms des fichiers à créer" + +for file in "$@"; do + setx file=fndate_verifix "$file" .md + setx filename=basename -- "$file" + if [[ "$filename" == *.* ]]; then + : # y'a déjà une extension, ne rien faire + elif [ -z "$autoext" ]; then + : # ne pas rajouter d'extension + elif [ -f "$file.markdown" ]; then + file="$file.markdown" + else + file="$file.md" + fi + [ -e "$file" ] && die "$file: fichier existant" + estep "Création de $file" + + echo -n >"$file" "\ + + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary" + "$EDITOR" "$file" +done diff --git a/bin/templ.sh b/bin/templ.sh new file mode 100755 index 0000000..e17b6c5 --- /dev/null +++ b/bin/templ.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 +require: fndate + +: ${EDITOR:=vim} + +executable=1 +autoext=1 +args=( + "créer un nouveau fichier .sh" + "" + -x,--exec executable=1 "créer un script exécutable" + -n,--no-exec executable= "créer un fichier non exécutable" + -j,--no-autoext autoext= "ne pas rajouter l'extension .sh" +) +parse_args "$@"; set -- "${args[@]}" + +[ $# -gt 0 ] || die "vous devez spécifier les noms des fichiers à créer" + +for file in "$@"; do + setx file=fndate_verifix "$file" .sh + setx filename=basename -- "$file" + if [[ "$filename" == *.* ]]; then + : # y'a déjà une extension, ne rien faire + elif [ -z "$autoext" ]; then + : # ne pas rajouter d'extension + else + file="$file.sh" + fi + [ -e "$file" ] && die "$file: fichier existant" + estep "Création de $file" + + if [ -n "$executable" ]; then + cat >"$file" <"$file" <" + -j,--no-autoext autoext= "ne pas rajouter l'extension .sql" +) +parse_args "$@"; set -- "${args[@]}" + +[ $# -gt 0 ] || die "vous devez spécifier les noms des fichiers à créer" + +for file in "$@"; do + setx file=fndate_verifix "$file" .sql + setx filename=basename -- "$file" + if [[ "$filename" == *.* ]]; then + : # y'a déjà une extension, ne rien faire + elif [ -z "$autoext" ]; then + : # ne pas rajouter d'extension + else + file="$file.sql" + fi + [ -e "$file" ] && die "$file: fichier existant" + estep "Création de $file" + + cat >"$file" <" + -j,--no-autoext autoext= "ne pas rajouter l'extension .yaml ni .yml" +) +parse_args "$@"; set -- "${args[@]}" + +[ $# -gt 0 ] || die "vous devez spécifier les noms des fichiers à créer" + +for file in "$@"; do + setx file=fndate_verifix "$file" .yml + setx filename=basename -- "$file" + if [[ "$filename" == *.* ]]; then + : # y'a déjà une extension, ne rien faire + elif [ -z "$autoext" ]; then + : # ne pas rajouter d'extension + elif [ -f "$file.yaml" ]; then + file="$file.yaml" + else + file="$file.yml" + fi + [ -e "$file" ] && die "$file: fichier existant" + estep "Création de $file" + + echo >"$file" "\ +# -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8 + +" + "$EDITOR" +3 "$file" +done diff --git a/bin_wip/donk b/bin_wip/donk new file mode 100755 index 0000000..b93ad81 --- /dev/null +++ b/bin_wip/donk @@ -0,0 +1,25 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 +require: donk.help + +# par défaut, c'est l'action build +case "$1" in +-h*|--help|--help=*|--help++) ;; +-*) set -- build "$@";; +esac + +args=( + "construire des images docker" + "ACTION [options] +$(_donk_show_actions)" + + + -h::section,--help '$_donk_show_help' "Afficher l'aide de la section spécifiée. +Les sections valides sont: ${DONK_HELP_SECTIONS[*]%%:*}" + --help++ '$_donk_show_help' "++Afficher l'aide" +) +parse_args "$@"; set -- "${args[@]}" + +action="$1"; shift +require: "donk.$action" || die +"donk_$action" "$@" diff --git a/bin_wip/npci b/bin_wip/npci new file mode 100755 index 0000000..bc02cc3 --- /dev/null +++ b/bin_wip/npci @@ -0,0 +1,30 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 +require: git + +projdir= +remote= +what=auto +push=auto +clobber=ask +args=( + "\ +valider les modifications locales + +si la branche courante est une branche wip, écraser les modifications distantes éventuelles après un avertissement. +sinon, ne mettre à jour la branche locale qu'en mode fast-forward" + "MESSAGE [FILES...]" + -d:,--projdir projdir= "spécifier le projet dans lequel faire la mise à jour" + -o:,--remote remote= "spécifier le remote depuis lequel faire le fetch et vers lequel pousser les modifications" + --auto what=auto "calculer les modifications à valider: soit les fichiers mentionnés, soit ceux de l'index, soit les fichiers modifiés. c'est l'option par défaut" + -a,--all what=all "valider les modifications sur les fichiers modifiés uniquement" + -A,--all-new what=new "valider les modifications sur les fichiers modifiés et rajouter aussi les nouveaux fichiers" + --current push=auto "pousser les modifications sur la branche courante après validation. c'est l'option par défaut" + -p,--push push=1 "pousser les modifications de toutes les branches après la validation" + -l,--no-push push= "ne pas pousser les modifications après la validation" + --clobber clobber=1 "écraser les modifications distantes si la branche courante est une branche wip" + -n,--no-clobber clobber= "ne jamais écraser les modifications distantes, même si la branche courante est une branche wip" +) +parse_args "$@"; set -- "${args[@]}" + diff --git a/bin_wip/npp b/bin_wip/npp new file mode 100755 index 0000000..4023184 --- /dev/null +++ b/bin_wip/npp @@ -0,0 +1,22 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 +require: git + +projdir= +remote= +clobber=ask +args=( + "\ +pousser les modifications locales + +si la branche courante est une branche wip, écraser les modifications distantes éventuelles après un avertissement. +sinon, ne mettre à jour la branche locale qu'en mode fast-forward" + "MESSAGE [FILES...]" + -d:,--projdir projdir= "spécifier le projet dans lequel faire la mise à jour" + -o:,--remote remote= "spécifier le remote depuis lequel faire le fetch et vers lequel pousser les modifications" + --clobber clobber=1 "écraser les modifications distantes si la branche courante est une branche wip" + -n,--no-clobber clobber= "ne jamais écraser les modifications distantes, même si la branche courante est une branche wip" +) +parse_args "$@"; set -- "${args[@]}" + diff --git a/bin_wip/npu b/bin_wip/npu new file mode 100755 index 0000000..7c227b6 --- /dev/null +++ b/bin_wip/npu @@ -0,0 +1,147 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 +require: git + +function _git_update() { + local branch + local -a prbranches crbranches dbranches + + setx -a prbranches=git_list_rbranches + git fetch -p "$@" || return + setx -a crbranches=git_list_rbranches + + # vérifier s'il y a des branches distantes qui ont été supprimées + for branch in "${prbranches[@]}"; do + if ! array_contains crbranches "$branch"; then + array_add dbranches "${branch#*/}" + fi + done + if [ ${#dbranches[*]} -gt 0 ]; then + setx -a branches=git_list_branches + setx branch=git_get_branch + + eimportant "One or more distant branches where deleted" + if git_check_cleancheckout; then + einfo "Delete the obsolete local branches with these commands:" + else + ewarn "Take care of uncommitted local changes first" + einfo "Then delete the obsolete local branches with these commands:" + fi + if array_contains dbranches "$branch"; then + # si la branche courante est l'une des branches à supprimer, il faut + # basculer vers develop ou master + local swto + if [ -z "$swto" ] && array_contains branches develop && ! array_contains dbranches develop; then + swto=develop + fi + if [ -z "$swto" ] && array_contains branches master && ! array_contains dbranches master; then + swto=master + fi + [ -n "$swto" ] && qvals git checkout "$swto" + fi + qvals git branch -D "${dbranches[@]}" + return 1 + fi + + # intégrer les modifications des branches locales + if ! git_check_cleancheckout; then + setx branch=git_get_branch + setx remote=git_get_branch_remote "$branch" + setx rbranch=git_get_branch_rbranch "$branch" "$remote" + pbranch="${rbranch#refs/remotes/}" + if git merge -q --ff-only "$rbranch"; then + enote "There are uncommitted local changes: only CURRENT branch were updated" + fi + return 0 + fi + + setx -a branches=git_list_branches + restore_branch= + for branch in "${branches[@]}"; do + setx remote=git_get_branch_remote "$branch" + setx rbranch=git_get_branch_rbranch "$branch" "$remote" + pbranch="${rbranch#refs/remotes/}" + [ -n "$remote" -a -n "$rbranch" ] || continue + if git_is_ancestor "$branch" "$rbranch"; then + if git_should_ff "$branch" "$rbranch"; then + einfo "Fast-forwarding $branch -> $pbranch" + git checkout -q "$branch" + git merge -q --ff-only "$rbranch" + restore_branch=1 + fi + else + if [ "$branch" == "$orig_branch" ]; then + echo "* Cannot fast-forward CURRENT branch $branch from $pbranch +Try to merge manually with: git merge $pbranch" + else + echo "* Cannot fast-forward local branch $branch from $pbranch +You can merge manually with: git checkout $branch; git merge $pbranch" + fi + fi + done + [ -n "$restore_branch" ] && git checkout -q "$orig_branch" + return 0 +} + +function git_update() { + local cwd r + setx cwd=ppath2 "$(pwd)" "$OrigCwd" + etitle "$cwd" + _git_update "$@"; r=$? + eend + return $r +} + +chdir= +all= +Remote= +Autoff=1 +Reset=ask +args=( + "\ +mettre à jour les branches locales + +si la branche courante est une branche wip, écraser les modifications locales éventuelles après un avertissement. +sinon, ne mettre à jour la branche locale qu'en mode fast-forward" + #"usage" + -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations" + -a,--all all=1 "faire l'opération sur tous les sous-répertoires de BASEDIR qui sont des dépôts git" + -o:,--remote Remote= "spécifier le remote depuis lequel faire le fetch" + --autoff Autoff=1 "s'il n'y a pas de modifications locales, faire un fast-forward de toutes les branches traquées. c'est l'option par défaut." + -l,--no-autoff Autoff= "ne pas faire de fast-forward automatique des branches traquées. seule la branche courante est mise à jour" + --reset Reset=1 "écraser les modifications locales si la branche courante est une branche wip" + -n,--no-reset Reset= "ne jamais écraser les modifications locales, même si la branche courante est une branche wip" +) +parse_args "$@"; set -- "${args[@]}" + + +setx OrigCwd=pwd +if [ -n "$chdir" ]; then + cd "$chdir" || die +fi + +if [ -n "$all" ]; then + # liste de sous répertoires + if [ $# -gt 0 ]; then + # si on a une liste de patterns, l'utiliser + setx -a dirs=ls_dirs . "$@" + else + dirs=() + for dir in */.git; do + [ -d "$dir" ] || continue + dirs+=("${dir%/.git}") + done + fi + setx cwd=pwd + for dir in "${dirs[@]}"; do + cd "$dir" || die + git_update || die + cd "$cwd" + done +else + # répertoire courant uniquement + args=() + isatty || args+=(--porcelain) + git_update "${args[@]}" +fi diff --git a/composer.json b/composer.json index a075909..530fed3 100644 --- a/composer.json +++ b/composer.json @@ -9,14 +9,17 @@ } ], "require": { - "php": ">=7.3" + "php": "^7.4" }, "require-dev": { - "nulib/tests": "7.3" + "nulib/tests": "7.4", + "ext-posix": "*", + "ext-pcntl": "*", + "ext-curl": "*" }, "autoload": { "psr-4": { - "nulib\\": "php/src_base" + "nulib\\": "php/src" } }, "autoload-dev": { diff --git a/composer.lock b/composer.lock index ba7dfde..bf3295e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a83db90dff9c8a1e44abc608738042c3", + "content-hash": "356c1dcfe9eee39e9e6eadff4f63cdfe", "packages": [], "packages-dev": [ { @@ -79,16 +79,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -96,11 +96,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -126,7 +127,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" }, "funding": [ { @@ -134,29 +135,31 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "nikic/php-parser", - "version": "v4.17.1", + "version": "v5.3.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -164,7 +167,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -188,17 +191,17 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" }, - "time": "2023-08-13T19:53:39+00:00" + "time": "2024-10-08T18:51:32+00:00" }, { "name": "nulib/tests", - "version": "7.3", + "version": "7.4", "source": { "type": "git", "url": "https://git.univ-reunion.fr/sda-php/nulib-tests.git", - "reference": "8902035bef6ddfe9864675a00844dd14872f6d13" + "reference": "6ce8257560b42e8fb3eea03eba84d3877c9648ca" }, "require": { "php": ">=7.3", @@ -207,12 +210,12 @@ "type": "library", "autoload": { "psr-4": { - "mur\\tests\\": "src" + "nulib\\tests\\": "src" } }, "autoload-dev": { "psr-4": { - "mur\\tests\\": "tests" + "nulib\\tests\\": "tests" } }, "authors": [ @@ -222,24 +225,25 @@ } ], "description": "fonctions et classes pour les tests", - "time": "2023-10-01T11:57:55+00:00" + "time": "2024-03-26T10:56:17+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -280,9 +284,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -337,35 +347,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.29", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -374,7 +384,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -403,7 +413,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -411,7 +421,7 @@ "type": "github" } ], - "time": "2023-09-19T04:57:46+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -656,45 +666,45 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.13", + "version": "9.6.21", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be" + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3d767f7f9e191eab4189abe41ab37797e30b1be", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1 || ^2", + "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.28", - "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.5", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.2", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.6", + "sebastian/global-state": "^5.0.7", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", "sebastian/version": "^3.0.2" }, "suggest": { @@ -739,7 +749,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.13" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" }, "funding": [ { @@ -755,20 +765,20 @@ "type": "tidelift" } ], - "time": "2023-09-19T05:39:22+00:00" + "time": "2024-09-19T10:50:18+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { @@ -803,7 +813,7 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" }, "funding": [ { @@ -811,7 +821,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { "name": "sebastian/code-unit", @@ -1000,20 +1010,20 @@ }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -1045,7 +1055,7 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" }, "funding": [ { @@ -1053,20 +1063,20 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { "name": "sebastian/diff", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { @@ -1111,7 +1121,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -1119,7 +1129,7 @@ "type": "github" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", @@ -1186,16 +1196,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", "shasum": "" }, "require": { @@ -1251,7 +1261,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" }, "funding": [ { @@ -1259,20 +1269,20 @@ "type": "github" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2024-03-02T06:33:00+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.6", + "version": "5.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bde739e7565280bda77be70044ac1047bc007e34" + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", - "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", "shasum": "" }, "require": { @@ -1315,7 +1325,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" }, "funding": [ { @@ -1323,24 +1333,24 @@ "type": "github" } ], - "time": "2023-08-02T09:26:13+00:00" + "time": "2024-03-02T06:35:11+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -1372,7 +1382,7 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" }, "funding": [ { @@ -1380,7 +1390,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", @@ -1559,16 +1569,16 @@ }, { "name": "sebastian/resource-operations", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { @@ -1580,7 +1590,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -1601,8 +1611,7 @@ "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" }, "funding": [ { @@ -1610,7 +1619,7 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { "name": "sebastian/type", @@ -1723,16 +1732,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -1761,7 +1770,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -1769,7 +1778,7 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], @@ -1778,8 +1787,12 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=7.3" + "php": "^7.4" }, - "platform-dev": [], - "plugin-api-version": "2.6.0" + "platform-dev": { + "ext-posix": "*", + "ext-pcntl": "*", + "ext-curl": "*" + }, + "plugin-api-version": "2.2.0" } diff --git a/dockerfiles/Dockerfile.adminer b/dockerfiles/Dockerfile.adminer new file mode 100644 index 0000000..96df10b --- /dev/null +++ b/dockerfiles/Dockerfile.adminer @@ -0,0 +1,31 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=php /g/ /g/ +RUN /g/build -a @adminer + +EXPOSE 80 +ENTRYPOINT ["/g/entrypoint"] diff --git a/dockerfiles/Dockerfile.adminer+ic b/dockerfiles/Dockerfile.adminer+ic new file mode 100644 index 0000000..609ee69 --- /dev/null +++ b/dockerfiles/Dockerfile.adminer+ic @@ -0,0 +1,40 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/instantclient AS instantclient +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +COPY --from=instantclient /g/ /g/ +COPY --from=instantclient /src/ /src/ +RUN /g/build _instantclient_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=php /g/ /g/ +RUN /g/build -a @adminer + +COPY --from=instantclient /g/ /g/ +COPY --from=builder /opt/oracle/ /opt/oracle/ +RUN /g/build instantclient + +EXPOSE 80 +ENTRYPOINT ["/g/entrypoint"] diff --git a/dockerfiles/Dockerfile.mariadb10 b/dockerfiles/Dockerfile.mariadb10 new file mode 100644 index 0000000..f26efd5 --- /dev/null +++ b/dockerfiles/Dockerfile.mariadb10 @@ -0,0 +1,19 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG REGISTRY=pubdocker.univ-reunion.fr +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/mariadb AS mariadb +FROM $REGISTRY/src/legacytools AS legacytools + +FROM mariadb:10 +ARG APT_PROXY TIMEZONE +ENV APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=mariadb /g/ /g/ +RUN /g/build -a @base @mariadb + +COPY --from=legacytools /g/ /g/ +RUN /g/build nutools servertools + +EXPOSE 3306 +ENTRYPOINT ["/g/entrypoint"] diff --git a/dockerfiles/Dockerfile.php-apache b/dockerfiles/Dockerfile.php-apache new file mode 100644 index 0000000..3d5adf4 --- /dev/null +++ b/dockerfiles/Dockerfile.php-apache @@ -0,0 +1,31 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=php /g/ /g/ +RUN /g/build -a @apache-php-cas php-utils + +EXPOSE 80 443 +ENTRYPOINT ["/g/entrypoint"] diff --git a/dockerfiles/Dockerfile.php-apache+ic b/dockerfiles/Dockerfile.php-apache+ic new file mode 100644 index 0000000..9c3cd80 --- /dev/null +++ b/dockerfiles/Dockerfile.php-apache+ic @@ -0,0 +1,44 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/legacytools AS legacytools +FROM $REGISTRY/src/instantclient AS instantclient +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +COPY --from=instantclient /g/ /g/ +COPY --from=instantclient /src/ /src/ +RUN /g/build _instantclient_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=legacytools /g/ /g/ +RUN /g/build nutools + +COPY --from=php /g/ /g/ +RUN /g/build -a @apache-php-cas php-utils + +COPY --from=instantclient /g/ /g/ +COPY --from=builder /opt/oracle/ /opt/oracle/ +RUN /g/build instantclient + +EXPOSE 80 443 +ENTRYPOINT ["/g/entrypoint"] diff --git a/dockerfiles/Dockerfile.php-cli b/dockerfiles/Dockerfile.php-cli new file mode 100644 index 0000000..ef17f83 --- /dev/null +++ b/dockerfiles/Dockerfile.php-cli @@ -0,0 +1,30 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=php /g/ /g/ +RUN /g/build @php-cli php-utils + +ENTRYPOINT ["/g/entrypoint"] diff --git a/dockerfiles/Dockerfile.php-cli+ic b/dockerfiles/Dockerfile.php-cli+ic new file mode 100644 index 0000000..b380090 --- /dev/null +++ b/dockerfiles/Dockerfile.php-cli+ic @@ -0,0 +1,43 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/legacytools AS legacytools +FROM $REGISTRY/src/instantclient AS instantclient +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +COPY --from=instantclient /g/ /g/ +COPY --from=instantclient /src/ /src/ +RUN /g/build _instantclient_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=legacytools /g/ /g/ +RUN /g/build nutools + +COPY --from=php /g/ /g/ +RUN /g/build @php-cli php-utils + +COPY --from=instantclient /g/ /g/ +COPY --from=builder /opt/oracle/ /opt/oracle/ +RUN /g/build instantclient + +ENTRYPOINT ["/g/entrypoint"] diff --git a/dockerfiles/Dockerfile.postgres15 b/dockerfiles/Dockerfile.postgres15 new file mode 100644 index 0000000..f9b4db9 --- /dev/null +++ b/dockerfiles/Dockerfile.postgres15 @@ -0,0 +1,16 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG REGISTRY=pubdocker.univ-reunion.fr +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/postgres AS postgres + +FROM postgres:15-bookworm +ARG APT_PROXY TIMEZONE +ENV APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=postgres /g/ /g/ +RUN /g/build -a @base @postgres +RUN /g/pkg i @ssl @git + +EXPOSE 5432 +ENTRYPOINT ["/g/entrypoint"] diff --git a/lib/profile.d/nulib b/lib/profile.d/nulib new file mode 100644 index 0000000..542e828 --- /dev/null +++ b/lib/profile.d/nulib @@ -0,0 +1,2 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +__uaddpath "@@dest@@/bin" PATH diff --git a/lib/setup.sh b/lib/setup.sh new file mode 100755 index 0000000..64bc396 --- /dev/null +++ b/lib/setup.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname -- "$0")/../load.sh" || exit 1 + +[ "$(id -u)" -eq 0 ] || die "Ce script doit être lancé avec les droits root" + +cd "$MYDIR/.." +[ -n "$1" ] && dest="$1" || dest="$(pwd)" + +estep "Maj /etc/nulib.sh" +sed "s|@@""dest""@@|$dest|g" load.sh >/etc/nulib.sh diff --git a/lib/uinst/conf b/lib/uinst/conf new file mode 100644 index 0000000..6f4d212 --- /dev/null +++ b/lib/uinst/conf @@ -0,0 +1,6 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +source "$@" || exit 1 + +# supprimer les fichiers de VCS +rm -rf "$srcdir/.git" diff --git a/lib/uinst/rootconf b/lib/uinst/rootconf new file mode 100644 index 0000000..bdc0ed3 --- /dev/null +++ b/lib/uinst/rootconf @@ -0,0 +1,5 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +source "$@" || exit 1 + +"$srcdir/lib/setup.sh" "$dest" diff --git a/load.sh b/load.sh new file mode 100644 index 0000000..fa544c9 --- /dev/null +++ b/load.sh @@ -0,0 +1,182 @@ +##@cooked comments # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +## Charger nulib et rendre disponible les modules bash, awk, php et python +##@cooked nocomments +# Ce fichier doit être sourcé en premier. Si ce fichier n'est pas sourcé, alors +# le répertoire nulib doit être disponible dans le répertoire du script qui +# inclue ce fichier. +# Une fois ce fichier sourcé, les autres modules peuvent être importés avec +# require:() e.g. +# source /etc/nulib.sh || exit 1 +# require: other_modules +# ou pour une copie locale de nulib: +# source "$(dirname "$0")/nulib/load.sh" || exit 1 +# require: other_modules + +# vérifier version minimum de bash +if [ "x$BASH" = "x" ]; then + echo "ERROR: nulib: this script requires bash" + exit 1 +fi + +function eerror() { echo "ERROR: $*" 1>&2; } +function die() { [ $# -gt 0 ] && eerror "$*"; exit 1; } +function edie() { [ $# -gt 0 ] && eerror "$*"; return 1; } +function delpath() { local _qdir="${1//\//\\/}"; eval "export ${2:-PATH}; ${2:-PATH}"'="${'"${2:-PATH}"'#$1:}"; '"${2:-PATH}"'="${'"${2:-PATH}"'%:$1}"; '"${2:-PATH}"'="${'"${2:-PATH}"'//:$_qdir:/:}"; [ "$'"${2:-PATH}"'" == "$1" ] && '"${2:-PATH}"'='; } +function addpath() { local _qdir="${1//\//\\/}"; eval "export ${2:-PATH}; "'[ "${'"${2:-PATH}"'#$1:}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'%:$1}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'//:$_qdir:/:}" == "$'"${2:-PATH}"'" -a "$'"${2:-PATH}"'" != "$1" ] && '"${2:-PATH}"'="${'"${2:-PATH}"':+$'"${2:-PATH}"':}$1"'; } +function inspathm() { local _qdir="${1//\//\\/}"; eval "export ${2:-PATH}; "'[ "${'"${2:-PATH}"'#$1:}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'%:$1}" == "$'"${2:-PATH}"'" -a "${'"${2:-PATH}"'//:$_qdir:/:}" == "$'"${2:-PATH}"'" -a "$'"${2:-PATH}"'" != "$1" ] && '"${2:-PATH}"'="$1${'"${2:-PATH}"':+:$'"${2:-PATH}"'}"'; } +function inspath() { delpath "$@"; inspathm "$@"; } + +if [ ${BASH_VERSINFO[0]} -ge 5 -o \( ${BASH_VERSINFO[0]} -eq 4 -a ${BASH_VERSINFO[1]} -ge 1 \) ]; then : +elif [ -n "$NULIB_IGNORE_BASH_VERSION" ]; then : +else die "nulib: bash 4.1+ is required" +fi + +# Calculer emplacement de nulib +NULIBDIR="@@dest@@" +if [ "$NULIBDIR" = "@@""dest""@@" ]; then + # La valeur "@@"dest"@@" n'est remplacée que dans la copie de ce script + # faite dans /etc. Sinon, il faut toujours faire le calcul. Cela permet de + # déplacer la librairie n'importe où sur le disque, ce qui est + # particulièrement intéressant quand on fait du déploiement. + NULIBDIR="${BASH_SOURCE[0]}" + if [ -f "$NULIBDIR" -a "$(basename -- "$NULIBDIR")" == load.sh ]; then + # Fichier sourcé depuis nulib/ + NULIB_SOURCED=1 + NULIBDIR="$(dirname -- "$NULIBDIR")" + elif [ -f "$NULIBDIR" -a "$(basename -- "$NULIBDIR")" == nulib.sh ]; then + # Fichier sourcé depuis nulib/bash/src + NULIB_SOURCED=1 + NULIBDIR="$(dirname -- "$NULIBDIR")/../.." + else + # Fichier non sourcé. Tout exprimer par rapport au script courant + NULIB_SOURCED= + NULIBDIR="$(dirname -- "$0")" + if [ -d "$NULIBDIR/nulib" ]; then + NULIBDIR="$NULIBDIR/nulib" + elif [ -d "$NULIBDIR/lib/nulib" ]; then + NULIBDIR="$NULIBDIR/lib/nulib" + fi + fi +elif [ "${BASH_SOURCE[0]}" = /etc/nulib.sh ]; then + # Fichier chargé depuis /etc/nulib.sh + NULIB_SOURCED=1 +fi +NULIBDIR="$(cd "$NULIBDIR" 2>/dev/null; pwd)" +NULIBDIRS=("$NULIBDIR/bash/src") + +# marqueur pour vérifier que nulib a réellement été chargé. il faut avoir $NULIBINIT == $NULIBDIR +# utilisé par le module base qui doit pouvoir être inclus indépendamment +NULIBINIT="$NULIBDIR" + +## Modules bash +NULIB_LOADED_MODULES=(nulib) +NULIB_DEFAULT_MODULES=(base pretty sysinfos) + +# Si cette variable est non vide, require: recharge toujours le module, même +# s'il a déjà été chargé. Cette valeur n'est pas transitive: il faut toujours +# recharger explicitement tous les modules désirés +NULIB_FORCE_RELOAD= + +function nulib__define_functions() { + function nulib_check_loaded() { + local module + for module in "${NULIB_LOADED_MODULES[@]}"; do + [ "$module" == "$1" ] && return 0 + done + return 1 + } + function module:() { + NULIB_MODULE="$1" + if ! nulib_check_loaded "$1"; then + NULIB_LOADED_MODULES+=("$1") + fi + } + function function:() { + : + } +} + +function nulib__load:() { + local nl__module nl__nulibdir nl__found + [ $# -gt 0 ] || set DEFAULTS + + for nl__module in "$@"; do + nl__found= + for nl__nulibdir in "${NULIBDIRS[@]}"; do + if [ -f "$nl__nulibdir/$nl__module.sh" ]; then + source "$nl__nulibdir/$nl__module.sh" || die + nl__found=1 + break + fi + done + [ -n "$nl__found" ] || die "nulib: unable to find module $nl__module in (${NULIBDIRS[*]})" + done +} +function nulib__require:() { + local nr__module nr__nulibdir nr__found + [ $# -gt 0 ] || set DEFAULTS + + # sauvegarder valeurs globales + local nr__orig_module="$NULIB_MODULE" + NULIB_MODULE= + + # garder une copie de la valeur originale et casser la transitivité + local nr__force_reload="$NULIB_FORCE_RELOAD" + local NULIB_FORCE_RELOAD + + for nr__module in "$@"; do + nr__found= + for nr__nulibdir in "${NULIBDIRS[@]}"; do + if [ -f "$nr__nulibdir/$nr__module.sh" ]; then + nr__found=1 + if [ -n "$nr__force_reload" ] || ! nulib_check_loaded "$nr__module"; then + NULIB_LOADED_MODULES+=("$nr__module") + source "$nr__nulibdir/$nr__module.sh" || die + fi + break + fi + done + if [ -z "$nr__found" -a "$nr__module" == DEFAULTS ]; then + for nr__module in "${NULIB_DEFAULT_MODULES[@]}"; do + if [ -f "$nr__nulibdir/$nr__module.sh" ]; then + nr__found=1 + if [ -n "$nr__force_reload" ] || ! nulib_check_loaded "$nr__module"; then + NULIB_LOADED_MODULES+=("$nr__module") + source "$nr__nulibdir/$nr__module.sh" || die + fi + else + break + fi + done + fi + [ -n "$nr__found" ] || die "nulib: unable to find module $nr__module in (${NULIBDIRS[*]})" + done + + # restaurer valeurs globales + NULIB_MODULE="$nr__orig_module" +} + +# désactiver set -x +NULIB__DISABLE_SET_X='local NULIB__SET_X; [ -z "$NULIB_NO_DISABLE_SET_X" ] && [[ $- == *x* ]] && { set +x; NULIB__SET_X=1; }' +NULIB__ENABLE_SET_X='[ -n "$NULIB__SET_X" ] && set -x' +# désactiver set -x de manière réentrante +NULIB__RDISABLE_SET_X='[ -z "$NULIB_NO_DISABLE_SET_X" ] && [[ $- == *x* ]] && { set +x; local NULIB_REQUIRE_SET_X=1; }; if [ -n "$NULIB_REQUIRE_SET_X" ]; then [ -n "$NULIB_REQUIRE_SET_X_RL1" ] || local NULIB_REQUIRE_SET_X_RL1; local NULIB_REQUIRE_SET_X_RL2=$RANDOM; [ -n "$NULIB_REQUIRE_SET_X_RL1" ] || NULIB_REQUIRE_SET_X_RL1=$NULIB_REQUIRE_SET_X_RL2; fi' +NULIB__RENABLE_SET_X='[ -n "$NULIB_REQUIRE_SET_X" -a "$NULIB_REQUIRE_SET_X_RL1" == "$NULIB_REQUIRE_SET_X_RL2" ] && set -x' + +function require:() { + eval "$NULIB__RDISABLE_SET_X" + nulib__define_functions + nulib__require: "$@" + eval "$NULIB__RENABLE_SET_X" + return 0 +} + +## Autres modules +[ -d "$NULIBDIR/awk/src" ] && inspath "$NULIBDIR/awk/src" AWKPATH; export AWKPATH +[ -d "$NULIBDIR/python3/src" ] && inspath "$NULIBDIR/python3/src" PYTHONPATH; export PYTHONPATH + +## Auto import DEFAULTS +nulib__define_functions +if [ -n "$NULIB_SOURCED" -a -z "$NULIB_NO_IMPORT_DEFAULTS" ]; then + require: DEFAULTS +fi diff --git a/php/src/A.php b/php/src/A.php new file mode 100644 index 0000000..c7d982b --- /dev/null +++ b/php/src/A.php @@ -0,0 +1,235 @@ +wrappedArray(); + if ($array === null || $array === false) $array = []; + elseif ($array instanceof Traversable) $array = cl::all($array); + else $array = [$array]; + return false; + } + + /** + * s'assurer que $array est un array s'il est non null. retourner true si + * $array n'a pas été modifié (s'il était déjà un array ou s'il valait null). + */ + static final function ensure_narray(&$array): bool { + if ($array instanceof IArrayWrapper) $array = $array->wrappedArray(); + if ($array === null || is_array($array)) return true; + if ($array === false) $array = []; + elseif ($array instanceof Traversable) $array = cl::all($array); + else $array = [$array]; + return false; + } + + /** + * s'assurer que $array est un tableau de $size éléments, en complétant avec + * des occurrences de $default si nécessaire + * + * @return bool true si le tableau a été modifié, false sinon + */ + static final function ensure_size(?array &$array, int $size, $default=null): bool { + $modified = false; + if ($array === null) { + $array = []; + $modified = true; + } + if ($size < 0) return $modified; + $count = count($array); + if ($count == $size) return $modified; + if ($count < $size) { + # agrandir le tableau + while ($count++ < $size) { + $array[] = $default; + } + return true; + } + # rétrécir le tableau + $tmparray = []; + foreach ($array as $key => $value) { + if ($size-- == 0) break; + $tmparray[$key] = $value; + } + $array = $tmparray; + return true; + } + + static function merge(&$dest, ...$merges): void { + self::ensure_narray($dest); + $dest = cl::merge($dest, ...$merges); + } + + static function merge2(&$dest, ...$merges): void { + self::ensure_narray($dest); + $dest = cl::merge2($dest, ...$merges); + } + + static final function select(&$dest, ?array $mappings, bool $inverse=false): void { + self::ensure_narray($dest); + $dest = cl::select($dest, $mappings, $inverse); + } + + static final function selectm(&$dest, ?array $mappings, ?array $merge=null): void { + self::ensure_narray($dest); + $dest = cl::selectm($dest, $mappings, $merge); + } + + static final function mselect(&$dest, ?array $merge, ?array $mappings): void { + self::ensure_narray($dest); + $dest = cl::mselect($dest, $merge, $mappings); + } + + static final function pselect(&$dest, ?array $pkeys): void { + self::ensure_narray($dest); + $dest = cl::pselect($dest, $pkeys); + } + + static final function pselectm(&$dest, ?array $pkeys, ?array $merge=null): void { + self::ensure_narray($dest); + $dest = cl::pselectm($dest, $pkeys, $merge); + } + + static final function mpselect(&$dest, ?array $merge, ?array $pkeys): void { + self::ensure_narray($dest); + $dest = cl::mpselect($dest, $merge, $pkeys); + } + + static final function set_nn(&$dest, $key, $value) { + self::ensure_narray($dest); + if ($value !== null) { + if ($key === null) $dest[] = $value; + else $dest[$key] = $value; + } + return $value; + } + + static final function append_nn(&$dest, $value) { + return self::set_nn($dest, null, $value); + } + + static final function set_nz(&$dest, $key, $value) { + self::ensure_narray($dest); + if ($value !== null && $value !== false) { + if ($key === null) $dest[] = $value; + else $dest[$key] = $value; + } + return $value; + } + + static final function append_nz(&$dest, $value) { + self::ensure_narray($dest); + return self::set_nz($dest, null, $value); + } + + static final function prepend_nn(&$dest, $value) { + self::ensure_narray($dest); + if ($value !== null) { + if ($dest === null) $dest = []; + array_unshift($dest, $value); + } + return $value; + } + + static final function prepend_nz(&$dest, $value) { + self::ensure_narray($dest); + if ($value !== null && $value !== false) { + if ($dest === null) $dest = []; + array_unshift($dest, $value); + } + return $value; + } + + static final function replace_nx(&$dest, $key, $value) { + self::ensure_narray($dest); + if ($dest !== null && !array_key_exists($key, $dest)) { + return $dest[$key] = $value; + } else { + return $dest[$key] ?? null; + } + } + + static final function replace_n(&$dest, $key, $value) { + self::ensure_narray($dest); + $pvalue = $dest[$key] ?? null; + if ($pvalue === null) $dest[$key] = $value; + return $pvalue; + } + + static final function replace_z(&$dest, $key, $value) { + self::ensure_narray($dest); + $pvalue = $dest[$key] ?? null; + if ($pvalue === null || $pvalue === false) $dest[$key] = $value; + return $pvalue; + } + + static final function pop(&$dest, $key, $default=null) { + if ($dest === null) return $default; + self::ensure_narray($dest); + if ($key === null) return array_pop($dest); + $value = $dest[$key] ?? $default; + unset($dest[$key]); + return $value; + } + + static final function popx(&$dest, ?array $keys): array { + $values = []; + if ($dest === null) return $values; + self::ensure_narray($dest); + if ($keys === null) return $values; + foreach ($keys as $key) { + $values[$key] = self::pop($dest, $key); + } + return $values; + } + + static final function filter_if(&$dest, callable $cond): void { + self::ensure_narray($dest); + $dest = cl::filter_if($dest, $cond); + } + + static final function filter_z($dest): void { self::filter_if($dest, [cv::class, "z"]);} + static final function filter_nz($dest): void { self::filter_if($dest, [cv::class, "nz"]);} + static final function filter_n($dest): void { self::filter_if($dest, [cv::class, "n"]);} + static final function filter_nn($dest): void { self::filter_if($dest, [cv::class, "nn"]);} + static final function filter_t($dest): void { self::filter_if($dest, [cv::class, "t"]);} + static final function filter_f($dest): void { self::filter_if($dest, [cv::class, "f"]);} + static final function filter_pt($dest): void { self::filter_if($dest, [cv::class, "pt"]);} + static final function filter_pf($dest): void { self::filter_if($dest, [cv::class, "pf"]);} + static final function filter_equals($dest, $value): void { self::filter_if($dest, cv::equals($value)); } + static final function filter_not_equals($dest, $value): void { self::filter_if($dest, cv::not_equals($value)); } + static final function filter_same($dest, $value): void { self::filter_if($dest, cv::same($value)); } + static final function filter_not_same($dest, $value): void { self::filter_if($dest, cv::not_same($value)); } + + ############################################################################# + + static final function sort(?array &$array, int $flags=SORT_REGULAR, bool $assoc=false): void { + if ($array === null) return; + if ($assoc) asort($array, $flags); + else sort($array, $flags); + } + + static final function ksort(?array &$array, int $flags=SORT_REGULAR): void { + if ($array === null) return; + ksort($array, $flags); + } + + static final function usort(?array &$array, array $keys, bool $assoc=false): void { + if ($array === null) return; + if ($assoc) uasort($array, cl::compare($keys)); + else usort($array, cl::compare($keys)); + } +} diff --git a/php/src_base/AccessException.php b/php/src/AccessException.php similarity index 76% rename from php/src_base/AccessException.php rename to php/src/AccessException.php index 0fe1c6d..6996667 100644 --- a/php/src_base/AccessException.php +++ b/php/src/AccessException.php @@ -1,13 +1,18 @@ $file, + "line" => $line, + "class" => $class, + "object" => null, + "type" => $type, + "function" => $function, + "args" => [], + ]; + } + return $frames; + } + + function __construct(Throwable $exception) { + $this->class = get_class($exception); + $this->message = $exception->getMessage(); + $this->code = $exception->getCode(); + $this->file = $exception->getFile(); + $this->line = $exception->getLine(); + $this->trace = self::extract_trace($exception->getTrace()); + $previous = $exception->getPrevious(); + if ($previous !== null) $this->previous = new static($previous); + } + + /** @var string */ + protected $class; + + function getClass(): string { + return $this->class; + } + + /** @var string */ + protected $message; + + function getMessage(): string { + return $this->message; + } + + /** @var mixed */ + protected $code; + + function getCode() { + return $this->code; + } + + /** @var string */ + protected $file; + + function getFile(): string { + return $this->file; + } + + /** @var int */ + protected $line; + + function getLine(): int { + return $this->line; + } + + /** @var array */ + protected $trace; + + function getTrace(): array { + return $this->trace; + } + + function getTraceAsString(): string { + $lines = []; + foreach ($this->trace as $index => $frame) { + $lines[] = "#$index $frame[file]($frame[line]): $frame[class]$frame[type]$frame[function]()"; + } + $index++; + $lines[] = "#$index {main}"; + return implode("\n", $lines); + } + + /** @var ExceptionShadow */ + protected $previous; + + function getPrevious(): ?ExceptionShadow { + return $this->previous; + } +} diff --git a/php/src/ExitError.php b/php/src/ExitError.php new file mode 100644 index 0000000..a14c3a8 --- /dev/null +++ b/php/src/ExitError.php @@ -0,0 +1,31 @@ +userMessage = $userMessage; + } + + function isError(): bool { + return $this->getCode() !== 0; + } + + /** @var ?string */ + protected $userMessage; + + function haveUserMessage(): bool { + return $this->userMessage !== null; + } + + function getUserMessage(): ?string { + return $this->userMessage; + } +} diff --git a/php/src/IArrayWrapper.php b/php/src/IArrayWrapper.php new file mode 100644 index 0000000..1509129 --- /dev/null +++ b/php/src/IArrayWrapper.php @@ -0,0 +1,11 @@ +getUserMessage(); + else return null; + } + + /** @param Throwable|ExceptionShadow $e */ + static final function get_user_summary($e): string { + $parts = []; + $first = true; + while ($e !== null) { + $message = self::get_user_message($e); + if (!$message) $message = "(no message)"; + if ($first) $first = false; + else $parts[] = "caused by "; + $parts[] = get_class($e) . ": " . $message; + $e = $e->getPrevious(); + } + return implode(", ", $parts); + } + + /** @param Throwable|ExceptionShadow $e */ + static function get_message($e): ?string { + $message = $e->getMessage(); + if (!$message && $e instanceof self) $message = $e->getUserMessage(); + return $message; + } + + /** @param Throwable|ExceptionShadow $e */ + static final function get_summary($e): string { + $parts = []; + $first = true; + while ($e !== null) { + $message = self::get_message($e); + if (!$message) $message = "(no message)"; + if ($first) $first = false; + else $parts[] = "caused by "; + if ($e instanceof ExceptionShadow) $class = $e->getClass(); + else $class = get_class($e); + $parts[] = "$class: $message"; + $e = $e->getPrevious(); + } + return implode(", ", $parts); + } + + /** @param Throwable|ExceptionShadow $e */ + static final function get_traceback($e): string { + $tbs = []; + $previous = false; + while ($e !== null) { + if (!$previous) { + $efile = $e->getFile(); + $eline = $e->getLine(); + $tbs[] = "at $efile($eline)"; + } else { + $tbs[] = "~~ caused by: " . self::get_summary($e); + } + $tbs[] = $e->getTraceAsString(); + $e = $e->getPrevious(); + $previous = true; + #XXX il faudrait ne pas réinclure les lignes communes aux exceptions qui + # ont déjà été affichées + } + return implode("\n", $tbs); + } + + function __construct($userMessage, $techMessage=null, $code=0, ?Throwable $previous=null) { + $this->userMessage = $userMessage; + if ($techMessage === null) $techMessage = $userMessage; + parent::__construct($techMessage, $code, $previous); + } + + /** @var ?string */ + protected $userMessage; + + function getUserMessage(): ?string { + return $this->userMessage; + } +} diff --git a/php/src/ValueException.php b/php/src/ValueException.php new file mode 100644 index 0000000..12813d2 --- /dev/null +++ b/php/src/ValueException.php @@ -0,0 +1,76 @@ +"; + } elseif (is_array($value)) { + $values = $value; + $parts = []; + $index = 0; + foreach ($values as $key => $value) { + if ($key === $index) { + $index++; + $parts[] = self::value($value); + } else { + $parts[] = "$key=>".self::value($value); + } + } + return "[" . implode(", ", $parts) . "]"; + } elseif (is_string($value)) { + return $value; + } else { + return var_export($value, true); + } + } + + private static function message($value, ?string $message, ?string $kind, ?string $prefix, ?string $suffix): string { + if ($kind === null) $kind = "value"; + if ($message === null) $message = "$kind$suffix"; + if ($value !== null) { + $value = self::value($value); + if ($prefix) $prefix = "$prefix: $value"; + else $prefix = $value; + } + if ($prefix) $prefix = "$prefix: "; + return $prefix.$message; + } + + static final function null(?string $kind=null, ?string $prefix=null, ?string $message=null): self { + return new static(self::message(null, $message, $kind, $prefix, " should not be null")); + } + + static final function check_null($value, ?string $kind=null, ?string $prefix=null, ?string $message=null) { + if ($value === null) throw static::null($kind, $prefix, $message); + return $value; + } + + static final function invalid_kind($value=null, ?string $kind=null, ?string $prefix=null, ?string $message=null): self { + return new static(self::message($value, $message, $kind, $prefix, " is invalid")); + } + + static final function invalid_key($value, ?string $prefix=null, ?string $message=null): self { + return self::invalid_kind($value, "key", $prefix, $message); + } + + static final function invalid_value($value, ?string $prefix=null, ?string $message=null): self { + return self::invalid_kind($value, "value", $prefix, $message); + } + + static final function invalid_type($value, string $expected_type): self { + return new static(self::message($value, null, "type", null, " is invalid, expected $expected_type")); + } + + static final function invalid_class($class, string $expected_class): self { + if (is_object($class)) $class = get_class($class); + return new static(self::message($class, null, "class", null, " is invalid, expected $expected_class")); + } + + static final function forbidden($value=null, ?string $kind=null, ?string $prefix=null, ?string $message=null): self { + return new static(self::message($value, $message, $kind, $prefix, " is forbidden")); + } +} diff --git a/php/src/app/LockFile.php b/php/src/app/LockFile.php new file mode 100644 index 0000000..d32bd2d --- /dev/null +++ b/php/src/app/LockFile.php @@ -0,0 +1,89 @@ +file = new SharedFile($file); + $this->name = $name ?? static::NAME; + $this->title = $title ?? static::TITLE; + } + + /** @var SharedFile */ + protected $file; + + /** @var ?string */ + protected $name; + + /** @var ?string */ + protected $title; + + protected function initData(): array { + return [ + "name" => $this->name, + "title" => $this->title, + "locked" => false, + "date_lock" => null, + "date_release" => null, + ]; + } + + function read(bool $close=true): array { + $data = $this->file->unserialize(null, $close); + if (!is_array($data)) $data = $this->initData(); + return $data; + } + + function isLocked(?array &$data=null): bool { + $data = $this->read(); + return $data["locked"]; + } + + function warnIfLocked(?array $data=null): bool { + if ($data === null) $data = $this->read(); + if ($data["locked"]) { + msg::warning("$data[name]: possède le verrou depuis $data[date_lock] -- $data[title]"); + return true; + } + return false; + } + + function lock(?array &$data=null): bool { + $file = $this->file; + $data = $this->read(false); + if ($data["locked"]) { + $file->close(); + return false; + } else { + $file->ftruncate(); + $file->serialize(cl::merge($data, [ + "locked" => true, + "date_lock" => new DateTime(), + "date_release" => null, + ])); + return true; + } + } + + function release(?array &$data=null): void { + $file = $this->file; + $data = $this->read(false); + $file->ftruncate(); + $file->serialize(cl::merge($data, [ + "locked" => false, + "date_release" => new DateTime(), + ])); + } +} diff --git a/php/src/app/RunFile.php b/php/src/app/RunFile.php new file mode 100644 index 0000000..d76beb9 --- /dev/null +++ b/php/src/app/RunFile.php @@ -0,0 +1,496 @@ +name = $name ?? static::NAME; + $this->file = new SharedFile($file); + $this->outfile = $outfile; + } + + protected ?string $name; + + protected SharedFile $file; + + protected ?string $outfile; + + function getOutfile(): ?string { + return $this->outfile; + } + + protected static function merge(array $data, array $merge): array { + return cl::merge($data, [ + "serial" => $data["serial"] + 1, + ], $merge); + } + + protected function initData(): array { + return [ + "name" => $this->name, + "pgid" => null, + "pid" => null, + "serial" => 0, + # lock + "locked" => false, + "date_lock" => null, + "date_release" => null, + # run + "logfile" => $this->outfile, + "date_start" => null, + "date_stop" => null, + "exitcode" => null, + "is_reaped" => null, + "is_ack_done" => null, + # action + "action" => null, + "action_date_start" => null, + "action_current_step" => null, + "action_max_step" => null, + "action_date_step" => null, + ]; + } + + function reset(bool $delete=false) { + $file = $this->file; + if ($delete) { + $file->close(); + unlink($file->getFile()); + } else { + $file->ftruncate(); + } + } + + function read(): array { + $data = $this->file->unserialize(); + if (!is_array($data)) $data = $this->initData(); + return $data; + } + + protected function willWrite(): array { + $file = $this->file; + $file->lockWrite(); + $data = $file->unserialize(null, false, true); + if (!is_array($data)) { + $data = $this->initData(); + $file->ftruncate(); + $file->serialize($data, false, true); + } + return [$file, $data]; + } + + protected function serialize(SharedFile $file, array $data, ?array $merge=null): void { + $file->ftruncate(); + $file->serialize(self::merge($data, $merge), true, true); + } + + protected function update(callable $func): void { + /** @var SharedFile$file */ + [$file, $data] = $this->willWrite(); + $merge = call_user_func($func, $data); + if ($merge !== null && $merge !== false) { + $this->serialize($file, $data, $merge); + } else { + $file->cancelWrite(); + } + } + + function haveWorked(int $serial, ?int &$currentSerial=null, ?array $data=null): bool { + $data ??= $this->read(); + $currentSerial = $data["serial"]; + return $serial !== $currentSerial; + } + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # verrouillage par défaut + + function isLocked(?array &$data=null): bool { + $data = $this->read(); + return $data["locked"]; + } + + function warnIfLocked(?array $data=null): bool { + $data ??= $this->read(); + if ($data["locked"]) { + msg::warning("$data[name]: possède le verrou depuis $data[date_lock]"); + return true; + } + return false; + } + + function lock(): bool { + $this->update(function ($data) use (&$locked) { + if ($data["locked"]) { + $locked = false; + return null; + } else { + $locked = true; + return [ + "locked" => true, + "date_lock" => new DateTime(), + "date_release" => null, + ]; + } + }); + return $locked; + } + + function release(): void { + $this->update(function ($data) { + return [ + "locked" => false, + "date_release" => new DateTime(), + ]; + }); + } + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # cycle de vie de l'application + + /** + * Préparer le démarrage de l'application. Cette méhode est appelée par un + * script externe qui doit préparer le démarrage du script + * + * - démarrer un groupe de process dont le process courant est le leader + */ + function wfPrepare(?int &$pgid=null): void { + $this->update(function (array $data) use (&$pgid) { + posix_setsid(); + $pgid = posix_getpid(); + return cl::merge($this->initData(), [ + "pgid" => $pgid, + "pid" => null, + "date_start" => new DateTime(), + ]); + }); + } + + /** indiquer que l'application démarre. */ + function wfStart(): void { + $this->update(function (array $data) { + $pid = posix_getpid(); + if ($data["pgid"] !== null) { + A::merge($data, [ + "pid" => $pid, + ]); + } else { + $data = cl::merge($this->initData(), [ + "pid" => $pid, + "date_start" => new DateTime(), + ]); + } + return $data; + }); + } + + /** tester si l'application a déjà été démarrée au moins une fois */ + function wasStarted(?array $data=null): bool { + $data ??= $this->read(); + return $data["date_start"] !== null; + } + + /** tester si l'application est démarrée et non arrêtée */ + function isStarted(?array $data=null): bool { + $data ??= $this->read(); + return $data["date_start"] !== null && $data["date_stop"] === null; + } + + function _getCid(array $data=null): int { + if ($data["pgid"] !== null) return -$data["pgid"]; + else return $data["pid"]; + } + + function _isRunning(array $data=null): bool { + if (!posix_kill($data["pid"], 0)) { + switch (posix_get_last_error()) { + case 1: #PCNTL_EPERM: + # process auquel on n'a pas accès?! est-ce un autre process qui a + # réutilisé le PID? + return false; + case 3: #PCNTL_ESRCH: + # process inexistant + return false; + case 22: #PCNTL_EINVAL: + # ne devrait pas se produire + return false; + } + } + # process existant auquel on a accès + return true; + } + + /** + * vérifier si l'application marquée comme démarrée tourne réellement + */ + function isRunning(?array $data=null): bool { + $data ??= $this->read(); + if ($data["date_start"] === null) return false; + if ($data["date_stop"] !== null) return false; + return $this->_isRunning($data); + } + + /** indiquer que l'application s'arrête */ + function wfStop(): void { + $this->update(function (array $data) { + return [ + "date_stop" => new DateTime(), + ]; + }); + } + + /** tester si l'application est déjà été stoppée au moins une fois */ + function wasStopped(?array $data=null): bool { + $data ??= $this->read(); + return $data["date_stop"] !== null; + } + + /** tester si l'application a été démarrée puis arrêtée */ + function isStopped(?array $data=null): bool { + $data ??= $this->read(); + return $data["date_start"] !== null && $data["date_stop"] !== null; + } + + /** après l'arrêt de l'application, mettre à jour le code de retour */ + function wfReaped(int $exitcode): void { + $this->update(function (array $data) use ($exitcode) { + return [ + "pgid" => null, + "date_stop" => $data["date_stop"] ?? new DateTime(), + "exitcode" => $exitcode, + "is_reaped" => true, + ]; + }); + } + + private static function kill(int $pid, int $signal, ?string &$reason=null): bool { + if (!posix_kill($pid, $signal)) { + switch (posix_get_last_error()) { + case PCNTL_ESRCH: + $reason = "process inexistant"; + break; + case PCNTL_EPERM: + $reason = "process non accessible"; + break; + case PCNTL_EINVAL: + $reason = "signal invalide"; + break; + } + return false; + } + return true; + } + + function wfKill(?string &$reason=null): bool { + $data = $this->read(); + $pid = $this->_getCid($data); + $stopped = false; + $timeout = 10; + $delay = 300000; + while (--$timeout >= 0) { + if (!self::kill($pid, SIGTERM, $reason)) return false; + usleep($delay); + $delay = 1000000; // attendre 1 seconde à partir de la deuxième fois + if (!$this->_isRunning($data)) { + $stopped = true; + break; + } + } + if (!$stopped) { + $timeout = 3; + $delay = 300000; + while (--$timeout >= 0) { + if (!self::kill($pid, SIGKILL, $reason)) return false; + usleep($delay); + $delay = 1000000; // attendre 1 seconde à partir de la deuxième fois + if (!$this->_isRunning($data)) { + $stopped = true; + break; + } + } + } + if ($stopped) { + sh::_waitpid($pid, $exitcode); + $this->wfReaped($exitcode); + } + return $stopped; + } + + /** + * vérifier si on est dans le cas où la tâche devrait tourner mais en réalité + * ce n'est pas le cas + */ + function _isUndead(?int $pid=null): bool { + $data = $this->read(); + if ($data["date_start"] === null) return false; + if ($data["date_stop"] !== null) return false; + $pid ??= $data["pid"]; + if (!posix_kill($pid, 0)) { + switch (posix_get_last_error()) { + case 1: #PCNTL_EPERM: + # process auquel on n'a pas accès?! est-ce un autre process qui a + # réutilisé le PID? + return false; + case 3: #PCNTL_ESRCH: + # process inexistant + return true; + case 22: #PCNTL_EINVAL: + # ne devrait pas se produire + return false; + } + } + # process existant auquel on a accès + return false; + } + + /** + * comme {@link self::isStopped()} mais ne renvoie true qu'une seule fois si + * $updateDone==true + */ + function isDone(?array &$data=null, bool $updateDone=true): bool { + $done = false; + $this->update(function (array $ldata) use (&$done, &$data, $updateDone) { + $data = $ldata; + if ($data["date_start"] === null || $data["date_stop"] === null || $data["is_ack_done"]) { + return false; + } + $done = true; + if ($updateDone) return ["is_ack_done" => $done]; + else return null; + }); + return $done; + } + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # gestion des actions + + /** indiquer le début d'une action */ + function action(?string $title, ?int $maxSteps=null): void { + $this->update(function (array $data) use ($title, $maxSteps) { + return [ + "action" => $title, + "action_date_start" => new DateTime(), + "action_max_step" => $maxSteps, + "action_current_step" => 0, + ]; + }); + app::_dispatch_signals(); + } + + /** indiquer qu'une étape est franchie dans l'action en cours */ + function step(int $nbSteps=1): void { + $this->update(function (array $data) use ($nbSteps) { + return [ + "action_date_step" => new DateTime(), + "action_current_step" => $data["action_current_step"] + $nbSteps, + ]; + }); + app::_dispatch_signals(); + } + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Divers + + function getLockFile(?string $name=null, ?string $title=null): LockFile { + $ext = self::LOCK_EXT; + if ($name !== null) $ext = ".$name$ext"; + $file = path::ensure_ext($this->file->getFile(), $ext, self::RUN_EXT); + $name = str::join("/", [$this->name, $name]); + return new LockFile($file, $name, $title); + } + + function getDesc(?array $data=null, bool $withAction=true): ?array { + $data ??= $this->read(); + $desc = $data["name"]; + $dateStart = $data["date_start"]; + $action = $withAction? $data["action"]: null; + $dateStop = $data["date_stop"]; + $exitcode = $data["exitcode"]; + if ($action !== null) { + $date ??= $data["action_date_step"]; + $date ??= $data["action_date_start"]; + if ($date !== null) $action = "$date $action"; + $action = "Etape en cours: $action"; + $current = $data["action_current_step"]; + $max = $data["action_max_step"]; + if ($current !== null && $max !== null) { + $action .= " ($current / $max)"; + } elseif ($current !== null) { + $action .= " ($current)"; + } + } + if ($exitcode !== null) { + $result = ["Code de retour $exitcode"]; + if ($data["is_reaped"]) $result[] = "reaped"; + if ($data["is_ack_done"]) $result[] = "acknowledged"; + $result = join(", ", $result); + } else { + $result = null; + } + if (!$this->wasStarted($data)) { + $type = "neutral"; + $haveLog = false; + $exitcode = null; + $message = [ + "status" => "$desc: pas encore démarré", + ]; + } elseif ($this->isRunning($data)) { + $sinceStart = Elapsed::format_since($dateStart); + $type = "info"; + $haveLog = true; + $exitcode = null; + $message = [ + "status" => "$desc: EN COURS pid $data[pid]", + "started" => "Démarrée depuis $dateStart ($sinceStart)", + "action" => $action, + ]; + } elseif ($this->isStopped($data)) { + $duration = "\nDurée ".Elapsed::format_delay($dateStart, $dateStop); + $sinceStop = Elapsed::format_since($dateStop); + $haveLog = true; + if ($exitcode === null) $type = "warning"; + elseif ($exitcode === 0) $type = "success"; + else $type = "danger"; + $message = [ + "status" => "$desc: TERMINEE$duration", + "stopped" => "Arrêtée $sinceStop le $dateStop", + "result" => $result, + ]; + } else { + $type = "warning"; + $haveLog = true; + $exitcode = null; + $message = [ + "status" => "$desc: ETAT INCONNU", + "started" => "Commencée le $dateStart", + "stopped" => $dateStop? "Arrêtée le $dateStop": null, + "exitcode" => $result !== null? "Code de retour $result": null, + ]; + } + return [ + "type" => $type, + "have_log" => $haveLog, + "exitcode" => $exitcode, + "message" => array_filter($message), + ]; + } +} diff --git a/php/src/app/args.php b/php/src/app/args.php new file mode 100644 index 0000000..90e24c7 --- /dev/null +++ b/php/src/app/args.php @@ -0,0 +1,39 @@ + $value] devient ["--my-arg", "$value"] + * - ["myOpt" => true] devient ["--my-opt"] + * - ["myOpt" => false] est omis + * - les autres valeurs sont prises telles quelles + */ + static function from_array(?array $array): array { + $args = []; + if ($array === null) return $args; + $index = 0; + foreach ($array as $arg => $value) { + if ($value === false) continue; + if ($arg === $index) { + $index++; + } else { + $arg = str::us2camel($arg); + $arg = str::camel2us($arg, false, "-"); + $arg = str_replace("_", "-", $arg); + $args[] = "--$arg"; + if (is_array($value)) $value[] = "--"; + elseif ($value === true) $value = null; + } + if (is_array($value)) { + A::merge($args, array_map("strval", $value)); + } elseif ($value !== null) { + $args[] = "$value"; + } + } + return $args; + } +} diff --git a/php/src/app/cli/include-launcher.php b/php/src/app/cli/include-launcher.php new file mode 100644 index 0000000..99ebabf --- /dev/null +++ b/php/src/app/cli/include-launcher.php @@ -0,0 +1,29 @@ + $name, + ]); + require $app; +} diff --git a/php/src_base/cl.php b/php/src/cl.php similarity index 53% rename from php/src_base/cl.php rename to php/src/cl.php index 960abde..8bb3b37 100644 --- a/php/src_base/cl.php +++ b/php/src/cl.php @@ -2,50 +2,125 @@ namespace nulib; use ArrayAccess; +use nulib\php\nur_func; use Traversable; /** - * Class cl: gestion de tableau de valeurs scalaires + * Class cl: gestion de tableaux ou d'instances de {@link ArrayAccess} le cas + * échéant + * + * contrairement à {@link A}, les méthodes de cette classes sont plutôt conçues + * pour retourner un nouveau tableau */ class cl { + /** + * retourner un array avec les éléments retournés par l'itérateur. les clés + * numériques sont réordonnées, les clés chaine sont laissées en l'état + */ + static final function all(?iterable $iterable): array { + if ($iterable === null) return []; + if (is_array($iterable)) return $iterable; + $array = []; + foreach ($iterable as $key => $value) { + if (is_int($key)) $array[] = $value; + else $array[$key] = $value; + } + return $array; + } + + /** + * construire un tableau avec le résultat de $row[$key] pour chaque élément + * de $rows + */ + static function all_get($key, ?iterable $rows): array { + $array = []; + if ($rows !== null) { + foreach ($rows as $row) { + $array[] = self::get($row, $key); + } + } + return $array; + } + + /** + * retourner la première valeur de $array ou $default si le tableau est null + * ou vide + */ + static final function first(?iterable $iterable, $default=null) { + if (is_array($iterable)) { + $key = array_key_first($iterable); + if ($key === null) return $default; + return $iterable[$key]; + } + if (is_iterable($iterable)) { + foreach ($iterable as $value) { + return $value; + } + } + return $default; + } + + /** + * retourner la dernière valeur de $array ou $default si le tableau est null + * ou vide + */ + static final function last(?iterable $iterable, $default=null) { + if (is_array($iterable)) { + $key = array_key_last($iterable); + if ($key === null) return $default; + return $iterable[$key]; + } + $value = $default; + if (is_iterable($iterable)) { + foreach ($iterable as $value) { + # parcourir tout l'iterateur pour avoir le dernier élément + } + } + return $value; + } + /** retourner un array non null à partir de $array */ static final function with($array): array { + if ($array instanceof IArrayWrapper) $array = $array->wrappedArray(); if (is_array($array)) return $array; elseif ($array === null || $array === false) return []; - elseif ($array instanceof Traversable) return iterator_to_array($array); + elseif ($array instanceof Traversable) return self::all($array); else return [$array]; } /** retourner un array à partir de $array, ou null */ static final function withn($array): ?array { + if ($array instanceof IArrayWrapper) $array = $array->wrappedArray(); if (is_array($array)) return $array; elseif ($array === null || $array === false) return null; - elseif ($array instanceof Traversable) return iterator_to_array($array); + elseif ($array instanceof Traversable) return self::all($array); else return [$array]; } - /** - * s'assurer que $array est un array non null. retourner true si $array n'a - * pas été modifié (s'il était déjà un array), false sinon. - */ - static final function ensure_array(&$array): bool { - if (is_array($array)) return true; - elseif ($array === null || $array === false) $array = []; - elseif ($array instanceof Traversable) $array = iterator_to_array($array); - else $array = [$array]; + /** tester si $array a au moins une clé numérique */ + static final function have_num_keys(?array $array): bool { + if ($array === null) return false; + foreach ($array as $key => $value) { + if (is_int($key)) return true; + } return false; } /** - * s'assurer que $array est un array s'il est non null. retourner true si - * $array n'a pas été modifié (s'il était déjà un array ou s'il valait null). + * tester si $array est une liste, c'est à dire un tableau non null avec + * uniquement des clés numériques séquentielles commençant à zéro + * + * NB: is_list(null) === false + * et is_list([]) === true */ - static final function ensure_narray(&$array): bool { - if ($array === null || is_array($array)) return true; - elseif ($array === false) $array = []; - elseif ($array instanceof Traversable) $array = iterator_to_array($array); - else $array = [$array]; - return false; + static final function is_list(?array $array): bool { + if ($array === null) return false; + $index = -1; + foreach ($array as $key => $value) { + ++$index; + if ($key !== $index) return false; + } + return true; } /** @@ -76,6 +151,128 @@ class cl { return $default; } + /** + * retourner un tableau construit à partir des clés de $keys + * - [$to => $from] --> $dest[$to] = self::get($array, $from) + * - [$to => null] --> $dest[$to] = null + * - [$to => false] --> NOP + * - [$to] --> $dest[$to] = self::get($array, $to) + * - [null] --> $dest[] = null + * - [false] --> NOP + * + * Si $inverse===true, le mapping est inversé: + * - [$to => $from] --> $dest[$from] = self::get($array, $to) + * - [$to => null] --> $dest[$to] = self::get($array, $to) + * - [$to => false] --> NOP + * - [$to] --> $dest[$to] = self::get($array, $to) + * - [null] --> NOP (XXX que faire dans ce cas?) + * - [false] --> NOP + * + * notez que l'ordre est inversé par rapport à {@link self::rekey()} qui + * attend des mappings [$from => $to], alors que cette méthode attend des + * mappings [$to => $from] + */ + static final function select($array, ?array $mappings, bool $inverse=false): array { + $dest = []; + $index = 0; + if (!$inverse) { + foreach ($mappings as $to => $from) { + if ($to === $index) { + $index++; + $to = $from; + if ($to === false) continue; + elseif ($to === null) $dest[] = null; + else $dest[$to] = self::get($array, $to); + } elseif ($from === false) { + continue; + } elseif ($from === null) { + $dest[$to] = null; + } else { + $dest[$to] = self::get($array, $from); + } + } + } else { + foreach ($mappings as $to => $from) { + if ($to === $index) { + $index++; + $to = $from; + if ($to === false) continue; + elseif ($to === null) continue; + else $dest[$to] = self::get($array, $to); + } elseif ($from === false) { + continue; + } elseif ($from === null) { + $dest[$to] = self::get($array, $to); + } else { + $dest[$from] = self::get($array, $to); + } + } + } + return $dest; + } + + /** + * obtenir la liste des clés finalement obtenues après l'appel à + * {@link self::select()} avec le mapping spécifié + */ + static final function selected_keys(?array $mappings): array { + if ($mappings === null) return []; + $keys = []; + $index = 0; + foreach ($mappings as $to => $from) { + if ($to === $index) { + if ($from === false) continue; + elseif ($from === null) $keys[] = $index; + else $keys[] = $from; + $index++; + } elseif ($from === false) { + continue; + } else { + $keys[] = $to; + } + } + return $keys; + } + + /** + * méthode de convenance qui sélectionne certaines clés de $array avec + * {@link self::select()} puis merge le tableau $merge au résultat. + */ + static final function selectm($array, ?array $mappings, ?array $merge=null): array { + return cl::merge(self::select($array, $mappings), $merge); + } + + /** + * méthode de convenance qui merge $merge dans $array puis sélectionne + * certaines clés avec {@link self::select()} + */ + static final function mselect($array, ?array $merge, ?array $mappings): array { + return self::select(cl::merge($array, $merge), $mappings); + } + + /** + * construire un sous-ensemble du tableau $array en sélectionnant les clés de + * $includes qui ne sont pas mentionnées dans $excludes. + * + * - si $includes===null && $excludes===null, retourner le tableau inchangé + * - si $includes vaut null, prendre toutes les clés + * + */ + static final function xselect($array, ?array $includes, ?array $excludes=null): ?array { + if ($array === null) return null; + $array = self::withn($array); + if ($includes === null && $excludes === null) return $array; + if ($includes === null) $includes = array_keys($array); + if ($excludes === null) $excludes = []; + $result = []; + foreach ($array as $key => $value) { + if (!in_array($key, $includes)) continue; + if (in_array($key, $excludes)) continue; + $result[$key] = $value; + } + return $result; + } + /** * si $array est un array ou une instance de ArrayAccess, créer ou modifier * l'élément dont la clé est $key @@ -119,29 +316,66 @@ class cl { /** * Fusionner tous les tableaux spécifiés. Les valeurs null sont ignorées. + * IMPORTANT: les clés numériques sont réordonnées. * Retourner null si aucun tableau n'est fourni ou s'ils étaient tous null. */ static final function merge(...$arrays): ?array { $merges = []; foreach ($arrays as $array) { - self::ensure_narray($array); + A::ensure_narray($array); if ($array !== null) $merges[] = $array; } return $merges? array_merge(...$merges): null; } + /** + * Fusionner tous les tableaux spécifiés. Les valeurs null sont ignorées. + * IMPORTANT: les clés numériques NE SONT PAS réordonnées. + * Retourner null si aucun tableau n'est fourni ou s'ils étaient tous null. + */ + static final function merge2(...$arrays): ?array { + $merged = null; + foreach ($arrays as $array) { + $array = self::withn($array); + if ($array === null) continue; + $merged ??= []; + foreach ($array as $key => $value) { + $merged[$key] = $value; + } + } + return $merged; + } + + ############################################################################# + + static final function map(callable $callback, ?iterable $array): array { + $result = []; + if ($array !== null) { + $ctx = nur_func::_prepare($callback); + foreach ($array as $key => $value) { + $result[$key] = nur_func::_call($ctx, [$value, $key]); + } + } + return $result; + } + ############################################################################# /** * vérifier que le chemin $keys existe dans le tableau $array * - * si $keys est vide ou null, retourner true + * si $pkey est vide ou null, retourner true */ static final function phas($array, $pkey): bool { - if ($pkey !== null && !is_array($pkey)) { + # optimisations + if ($pkey === null || $pkey === []) { + return true; + } elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) { + return self::has($array, $pkey); + } elseif (!is_array($pkey)) { $pkey = explode(".", strval($pkey)); } - if ($pkey === null || $pkey === []) return true; + # phas $first = true; foreach($pkey as $key) { if ($key === "" && $first) { @@ -174,13 +408,18 @@ class cl { /** * obtenir la valeur correspondant au chemin $keys dans $array * - * si $keys est vide ou null, retourner $default + * si $pkey est vide ou null, retourner $default */ static final function pget($array, $pkey, $default=null) { - if ($pkey !== null && !is_array($pkey)) { + # optimisations + if ($pkey === null || $pkey === []) return $default; + elseif ($pkey === "") return $array; + elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) { + return self::get($array, $pkey, $default); + } elseif (!is_array($pkey)) { $pkey = explode(".", strval($pkey)); } - if ($pkey === null || $pkey === []) return true; + # pget $value = $array; $first = true; foreach($pkey as $key) { @@ -211,25 +450,78 @@ class cl { return $result; } + /** + * retourner un tableau construit à partir des chemins de clé de $pkeys + * ces chemins peuvent être exprimés de plusieurs façon: + * - [$key => $pkey] --> $dest[$key] = self::pget($array, $pkey) + * - [$key => null] --> $dest[$key] = null + * - [$pkey] --> $dest[$key] = self::pget($array, $pkey) + * avec $key = implode("__", $pkey)) + * - [null] --> $dest[] = null + * - [false] --> NOP + */ + static final function pselect($array, ?array $pkeys): array { + $dest = []; + $index = 0; + foreach ($pkeys as $key => $pkey) { + if ($key === $index) { + $index++; + if ($pkey === null) continue; + $value = self::pget($array, $pkey); + if (!is_array($pkey)) $pkey = explode(".", strval($pkey)); + $key = implode("__", $pkey); + } elseif ($pkey === null) { + $value = null; + } else { + $value = self::pget($array, $pkey); + } + $dest[$key] = $value; + } + return $dest; + } + + /** + * méthode de convenance qui sélectionne certaines clés de $array avec + * {@link self::pselect()} puis merge le tableau $merge au résultat. + */ + static final function pselectm($array, ?array $pkeys, ?array $merge=null): array { + return cl::merge(self::pselect($array, $pkeys), $merge); + } + + /** + * méthode de convenance qui merge $merge dans $array puis sélectionne + * certaines clés avec {@link self::pselect()} + */ + static final function mpselect($array, ?array $merge, ?array $mappings): array { + return self::pselect(cl::merge($array, $merge), $mappings); + } + /** * modifier la valeur au chemin de clé $keys dans le tableau $array * * utiliser la clé "" (chaine vide) en dernière position pour rajouter à la fin, e.g - * - _pset($array, [""], $value) est équivalent à $array[] = $value - * - _pset($array, ["a", "b", ""], $value) est équivalent à $array["a"]["b"][] = $value + * - pset($array, [""], $value) est équivalent à $array[] = $value + * - pset($array, ["a", "b", ""], $value) est équivalent à $array["a"]["b"][] = $value * la clé "" n'a pas de propriété particulière quand elle n'est pas en dernière position * - * si $keys est vide ou null, $array est remplacé par $value + * si $pkey est vide ou null, $array est remplacé par $value */ static final function pset(&$array, $pkey, $value): void { - if ($pkey !== null && !is_array($pkey)) { - $pkey = explode(".", strval($pkey)); - } + # optimisations if ($pkey === null || $pkey === []) { $array = $value; return; + } elseif ($pkey === "") { + $array[] = $value; + return; + } elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) { + self::set($array, $pkey, $value); + return; + } elseif (!is_array($pkey)) { + $pkey = explode(".", strval($pkey)); } - self::ensure_array($array); + # pset + A::ensure_array($array); $current =& $array; $key = null; $last = count($pkey) - 1; @@ -245,7 +537,7 @@ class cl { $current = [$current]; } } else { - self::ensure_array($current[$key]); + A::ensure_array($current[$key]); $current =& $current[$key]; } $i++; @@ -262,28 +554,27 @@ class cl { } } - /** - * supprimer la valeur au chemin $keys fourni sous forme de tableau - */ - static final function pdel_a(&$array, ?array $pkey): void { - } - /** * supprimer la valeur au chemin de clé $keys dans $array * * si $array vaut null ou false, sa valeur est inchangée. - * si $keys est vide ou null, $array devient null + * si $pkey est vide ou null, $array devient null */ static final function pdel(&$array, $pkey): void { - if ($array === false || $array === null) return; - if ($pkey !== null && !is_array($pkey)) { - $pkey = explode(".", strval($pkey)); - } - if ($pkey === null || $pkey === []) { + # optimisations + if ($array === null || $array === false) { + return; + } elseif ($pkey === null || $pkey === []) { $array = null; return; + } elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) { + self::del($array, $pkey); + return; + } elseif (!is_array($pkey)) { + $pkey = explode(".", strval($pkey)); } - self::ensure_array($array); + # pdel + A::ensure_array($array); $current =& $array; $key = null; $last = count($pkey) - 1; @@ -322,9 +613,12 @@ class cl { /** * retourner le tableau $array en "renommant" les clés selon le tableau * $mappings qui contient des associations de la forme [$from => $to] + * + * Si $inverse===true, renommer dans le sens $to => $from */ - static function rekey(?array $array, ?array $mappings): ?array { + static function rekey(?array $array, ?array $mappings, bool $inverse=false): ?array { if ($array === null || $mappings === null) return $array; + if ($inverse) $mappings = array_flip($mappings); $mapped = []; foreach ($array as $key => $value) { if (array_key_exists($key, $mappings)) $key = $mappings[$key]; @@ -333,6 +627,19 @@ class cl { return $mapped; } + /** + * indiquer si {@link self::rekey()} modifierai le tableau indiqué (s'il y a + * des modifications à faire) + */ + static function would_rekey(?array $array, ?array $mappings, bool $inverse=false): bool { + if ($array === null || $mappings === null) return false; + if ($inverse) $mappings = array_flip($mappings); + foreach ($array as $key => $value) { + if (array_key_exists($key, $mappings)) return true; + } + return false; + } + ############################################################################# /** tester si tous les éléments du tableau satisfont la condition */ @@ -420,15 +727,12 @@ class cl { ############################################################################# static final function sorted(?array $array, int $flags=SORT_REGULAR, bool $assoc=false): ?array { - if ($array === null) return null; - if ($assoc) asort($array, $flags); - else sort($array, $flags); + A::sort($array, $flags, $assoc); return $array; } static final function ksorted(?array $array, int $flags=SORT_REGULAR): ?array { - if ($array === null) return null; - ksort($array, $flags); + A::ksort($array, $flags); return $array; } @@ -445,10 +749,10 @@ class cl { static final function compare(array $keys): callable { return function ($a, $b) use ($keys) { foreach ($keys as $key) { - if (cstr::del_prefix($key, "+")) $w = 1; - elseif (cstr::del_prefix($key, "-")) $w = -1; - elseif (cstr::del_suffix($key, "|asc")) $w = 1; - elseif (cstr::del_suffix($key, "|desc")) $w = -1; + if (str::del_prefix($key, "+")) $w = 1; + elseif (str::del_prefix($key, "-")) $w = -1; + elseif (str::del_suffix($key, "|asc")) $w = 1; + elseif (str::del_suffix($key, "|desc")) $w = -1; else $w = 1; if ($c = $w * cv::compare(cl::get($a, $key), cl::get($b, $key))) { return $c; @@ -459,9 +763,7 @@ class cl { } static final function usorted(?array $array, array $keys, bool $assoc=false): ?array { - if ($array === null) return null; - if ($assoc) uasort($array, self::compare($keys)); - else usort($array, self::compare($keys)); + A::usort($array, $keys, $assoc); return $array; } } diff --git a/php/src_base/cv.php b/php/src/cv.php similarity index 95% rename from php/src_base/cv.php rename to php/src/cv.php index 42b5eb5..8fdcc4b 100644 --- a/php/src_base/cv.php +++ b/php/src/cv.php @@ -79,6 +79,15 @@ class cv { ############################################################################# + /** échanger les deux valeurs */ + static final function swap(&$a, &$b): void { + $tmp = $a; + $a = $b; + $b = $tmp; + } + + ############################################################################# + /** mettre à jour $dest avec $value si $cond($value) est vrai */ static final function set_if(&$dest, $value, callable $cond) { if ($cond($value)) $dest = $value; @@ -181,7 +190,7 @@ class cv { $index = is_int($value)? $value : null; $key = is_string($value)? $value : null; if ($index === null && $key === null && $throw_exception) { - throw ValueException::invalid($value, "key", $prefix); + throw ValueException::invalid_kind($value, "key", $prefix); } else { return [$index, $key]; } @@ -198,7 +207,7 @@ class cv { $string = is_string($value)? $value : null; $array = is_array($value)? $value : null; if ($bool === null && $string === null && $array === null && $throw_exception) { - throw ValueException::invalid($value, "value", $prefix); + throw ValueException::invalid_kind($value, "value", $prefix); } else { return [$bool, $string, $array]; } diff --git a/php/src/db/Capacitor.php b/php/src/db/Capacitor.php new file mode 100644 index 0000000..90c3c9b --- /dev/null +++ b/php/src/db/Capacitor.php @@ -0,0 +1,173 @@ +storage = $storage; + $this->channel = $channel; + $this->channel->setCapacitor($this); + if ($ensureExists) $this->ensureExists(); + } + + /** @var CapacitorStorage */ + protected $storage; + + function getStorage(): CapacitorStorage { + return $this->storage; + } + + function db(): IDatabase { + return $this->getStorage()->db(); + } + + /** @var CapacitorChannel */ + protected $channel; + + function getChannel(): CapacitorChannel { + return $this->channel; + } + + function getTableName(): string { + return $this->getChannel()->getTableName(); + } + + /** @var CapacitorChannel[] */ + protected ?array $subChannels = null; + + protected ?array $subManageTransactions = null; + + function willUpdate(...$channels): self { + if ($this->subChannels === null) { + # désactiver la gestion des transaction sur le channel local aussi + $this->subChannels[] = $this->channel; + } + if ($channels) { + foreach ($channels as $channel) { + if ($channel instanceof Capacitor) $channel = $channel->getChannel(); + if ($channel instanceof CapacitorChannel) { + $this->subChannels[] = $channel; + } else { + throw ValueException::invalid_type($channel, CapacitorChannel::class); + } + } + } + return $this; + } + + function inTransaction(): bool { + return $this->db()->inTransaction(); + } + + function beginTransaction(?callable $func=null, bool $commit=true): void { + $db = $this->db(); + if ($this->subChannels !== null) { + # on gère des subchannels: ne débuter la transaction que si ce n'est déjà fait + if ($this->subManageTransactions === null) { + foreach ($this->subChannels as $channel) { + $name = $channel->getName(); + $this->subManageTransactions ??= []; + if (!array_key_exists($name, $this->subManageTransactions)) { + $this->subManageTransactions[$name] = $channel->isManageTransactions(); + } + $channel->setManageTransactions(false); + } + if (!$db->inTransaction()) $db->beginTransaction(); + } + } elseif (!$db->inTransaction()) { + $db->beginTransaction(); + } + if ($func !== null) { + $commited = false; + try { + nur_func::call($func, $this); + if ($commit) { + $this->commit(); + $commited = true; + } + } finally { + if ($commit && !$commited) $this->rollback(); + } + } + } + + protected function beforeEndTransaction(): void { + if ($this->subManageTransactions !== null) { + foreach ($this->subChannels as $channel) { + $name = $channel->getName(); + $channel->setManageTransactions($this->subManageTransactions[$name]); + } + $this->subManageTransactions = null; + } + } + + function commit(): void { + $this->beforeEndTransaction(); + $db = $this->db(); + if ($db->inTransaction()) $db->commit(); + } + + function rollback(): void { + $this->beforeEndTransaction(); + $db = $this->db(); + if ($db->inTransaction()) $db->rollback(); + } + + function getCreateSql(): string { + return $this->storage->_getCreateSql($this->channel); + } + + function exists(): bool { + return $this->storage->_exists($this->channel); + } + + function ensureExists(): void { + $this->storage->_ensureExists($this->channel); + } + + function reset(bool $recreate=false): void { + $this->storage->_reset($this->channel, $recreate); + } + + function charge($item, $func=null, ?array $args=null, ?array &$values=null): int { + if ($this->subChannels !== null) $this->beginTransaction(); + return $this->storage->_charge($this->channel, $item, $func, $args, $values); + } + + function discharge(bool $reset=true): Traversable { + return $this->storage->_discharge($this->channel, $reset); + } + + function count($filter=null): int { + return $this->storage->_count($this->channel, $filter); + } + + function one($filter, ?array $mergeQuery=null): ?array { + return $this->storage->_one($this->channel, $filter, $mergeQuery); + } + + function all($filter, ?array $mergeQuery=null): Traversable { + return $this->storage->_all($this->channel, $filter, $mergeQuery); + } + + function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int { + if ($this->subChannels !== null) $this->beginTransaction(); + return $this->storage->_each($this->channel, $filter, $func, $args, $mergeQuery, $nbUpdated); + } + + function delete($filter, $func=null, ?array $args=null): int { + if ($this->subChannels !== null) $this->beginTransaction(); + return $this->storage->_delete($this->channel, $filter, $func, $args); + } + + function close(): void { + $this->storage->close(); + } +} diff --git a/php/src/db/CapacitorChannel.php b/php/src/db/CapacitorChannel.php new file mode 100644 index 0000000..4495074 --- /dev/null +++ b/php/src/db/CapacitorChannel.php @@ -0,0 +1,407 @@ +name = $name; + $this->tableName = $tableName; + $this->manageTransactions = $manageTransactions ?? static::MANAGE_TRANSACTIONS; + $this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold); + $this->useCache = static::USE_CACHE; + $this->setup = false; + $this->created = false; + $columnDefinitions = cl::withn(static::COLUMN_DEFINITIONS); + $primaryKeys = cl::withn(static::PRIMARY_KEYS); + if ($primaryKeys === null && $columnDefinitions !== null) { + $index = 0; + foreach ($columnDefinitions as $col => $def) { + if ($col === $index) { + $index++; + if (preg_match('/\bprimary\s+key\s+\((.+)\)/i', $def, $ms)) { + $primaryKeys = preg_split('/\s*,\s*/', trim($ms[1])); + } + } else { + if (preg_match('/\bprimary\s+key\b/i', $def)) { + $primaryKeys[] = $col; + } + } + } + } + $this->columnDefinitions = $columnDefinitions; + $this->primaryKeys = $primaryKeys; + } + + protected string $name; + + function getName(): string { + return $this->name; + } + + protected string $tableName; + + function getTableName(): string { + return $this->tableName; + } + + /** + * @var bool indiquer si les modifications de each doivent être gérées dans + * une transaction. si false, l'utilisateur doit lui même gérer la + * transaction. + */ + protected bool $manageTransactions; + + function isManageTransactions(): bool { + return $this->manageTransactions; + } + + function setManageTransactions(bool $manageTransactions=true): self { + $this->manageTransactions = $manageTransactions; + return $this; + } + + /** + * @var ?int nombre maximum de modifications dans une transaction avant un + * commit automatique dans {@link Capacitor::each()}. Utiliser null pour + * désactiver la fonctionnalité. + * + * ce paramètre n'a d'effet que si $manageTransactions==true + */ + protected ?int $eachCommitThreshold; + + function getEachCommitThreshold(): ?int { + return $this->eachCommitThreshold; + } + + function setEachCommitThreshold(?int $eachCommitThreshold=null): self { + $this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold); + return $this; + } + + /** + * @var bool faut-il passer par le cache pour les requêtes de all(), each() + * et delete()? + * ça peut être nécessaire avec MySQL/MariaDB si on utilise les requêtes non + * bufférisées, et que la fonction manipule la base de données + */ + protected bool $useCache; + + function isUseCache(): bool { + return $this->useCache; + } + + function setUseCache(bool $useCache=true): self { + $this->useCache = $useCache; + return $this; + } + + /** + * initialiser ce channel avant sa première utilisation. + */ + protected function setup(): void { + } + + protected bool $setup; + + function ensureSetup() { + if (!$this->setup) { + $this->setup(); + $this->setup = true; + } + } + + protected bool $created; + + function isCreated(): bool { + return $this->created; + } + + function setCreated(bool $created=true): void { + $this->created = $created; + } + + protected ?array $columnDefinitions; + + /** + * retourner un ensemble de définitions pour des colonnes supplémentaires à + * insérer lors du chargement d'une valeur + * + * la clé primaire "id_" a pour définition "integer primary key autoincrement". + * elle peut être redéfinie, et dans ce cas la valeur à utiliser doit être + * retournée par {@link getItemValues()} + * + * la colonne "item__" contient la valeur sérialisée de l'élément chargé. bien + * que ce soit possible techniquement, cette colonne n'a pas à être redéfinie + * + * les colonnes dont le nom se termine par "_" sont réservées. + * les colonnes dont le nom se termine par "__" sont automatiquement sérialisées + * lors de l'insertion dans la base de données, et automatiquement désérialisées + * avant d'être retournées à l'utilisateur (sans le suffixe "__") + */ + function getColumnDefinitions(): ?array { + return $this->columnDefinitions; + } + + protected ?array $primaryKeys; + + function getPrimaryKeys(): ?array { + return $this->primaryKeys; + } + + /** + * calculer les valeurs des colonnes supplémentaires à insérer pour le + * chargement de $item. pour une même valeur de $item, la valeur de retour + * doit toujours être la même. pour rajouter des valeurs supplémentaires qui + * dépendent de l'environnement, il faut plutôt les retournner dans + * {@link self::onCreate()} ou {@link self::onUpdate()} + * + * Cette méthode est utilisée par {@link Capacitor::charge()}. Si la clé + * primaire est incluse (il s'agit généralement de "id_"), la ligne + * correspondate est mise à jour si elle existe. + * Retourner la clé primaire par cette méthode est l'unique moyen de + * déclencher une mise à jour plutôt qu'une nouvelle création. + * + * Retourner [false] pour annuler le chargement + */ + function getItemValues($item): ?array { + return null; + } + + /** + * Avant d'utiliser un id pour rechercher dans la base de donnée, corriger sa + * valeur le cas échéant. + * + * Cette fonction assume que la clé primaire n'est pas multiple. Elle n'est + * pas utilisée si une clé primaire multiple est définie. + */ + function verifixId(string &$id): void { + } + + /** + * retourne true si un nouvel élément ou un élément mis à jour a été chargé. + * false si l'élément chargé est identique au précédent. + * + * cette méthode doit être utilisée dans {@link self::onUpdate()} + */ + function wasRowModified(array $values, array $pvalues): bool { + return $values["item__sum_"] !== $pvalues["item__sum_"]; + } + + final function serialize($item): ?string { + return $item !== null? serialize($item): null; + } + + final function unserialize(?string $serial) { + return $serial !== null? unserialize($serial): null; + } + + const SERIAL_DEFINITION = "mediumtext"; + const SUM_DEFINITION = "varchar(40)"; + + final function sum(?string $serial, $value=null): ?string { + if ($serial === null) $serial = $this->serialize($value); + return $serial !== null? sha1($serial): null; + } + + final function isSerialCol(string &$key): bool { + return str::del_suffix($key, "__"); + } + + final function getSumCols(string $key): array { + return ["${key}__", "${key}__sum_"]; + } + + function getSum(string $key, $value): array { + $sumCols = $this->getSumCols($key); + $serial = $this->serialize($value); + $sum = $this->sum($serial, $value); + return array_combine($sumCols, [$serial, $sum]); + } + + function wasSumModified(string $key, $value, array $pvalues): bool { + $sumCol = $this->getSumCols($key)[1]; + $sum = $this->sum(null, $value); + $psum = $pvalues[$sumCol] ?? $this->sum(null, $pvalues[$key] ?? null); + return $sum !== $psum; + } + + function _wasSumModified(string $key, array $row, array $prow): bool { + $sumCol = $this->getSumCols($key)[1]; + $sum = $row[$sumCol] ?? null; + $psum = $prow[$sumCol] ?? null; + return $sum !== $psum; + } + + /** + * méthode appelée lors du chargement avec {@link Capacitor::charge()} pour + * créer un nouvel élément + * + * @param mixed $item l'élément à charger + * @param array $values la ligne à créer, calculée à partir de $item et des + * valeurs retournées par {@link getItemValues()} + * @return ?array le cas échéant, un tableau non null à merger dans $values et + * utilisé pour provisionner la ligne nouvellement créée. + * Retourner [false] pour annuler le chargement (la ligne n'est pas créée) + * + * Si $item est modifié dans cette méthode, il est possible de le retourner + * avec la clé "item" pour mettre à jour la ligne correspondante. + * + * la création ou la mise à jour est uniquement décidée en fonction des + * valeurs calculées par {@link self::getItemValues()}. Bien que cette méthode + * peut techniquement retourner de nouvelles valeurs pour la clé primaire, ça + * risque de créer des doublons + */ + function onCreate($item, array $values, ?array $alwaysNull): ?array { + return null; + } + + /** + * méthode appelée lors du chargement avec {@link Capacitor::charge()} pour + * mettre à jour un élément existant + * + * @param mixed $item l'élément à charger + * @param array $values la nouvelle ligne, calculée à partir de $item et + * des valeurs retournées par {@link getItemValues()} + * @param array $pvalues la précédente ligne, chargée depuis la base de + * données + * @return ?array null s'il ne faut pas mettre à jour la ligne. sinon, ce + * tableau est mergé dans $values puis utilisé pour mettre à jour la ligne + * existante + * Retourner [false] pour annuler le chargement (la ligne n'est pas mise à + * jour) + * + * - Il est possible de mettre à jour $item en le retourant avec la clé "item" + * - La clé primaire (il s'agit généralement de "id_") ne peut pas être + * modifiée. si elle est retournée, elle est ignorée + */ + function onUpdate($item, array $values, array $pvalues): ?array { + return null; + } + + /** + * méthode appelée lors du parcours des éléments avec + * {@link Capacitor::each()} + * + * @param mixed $item l'élément courant + * @param ?array $values la ligne courante + * @return ?array le cas échéant, un tableau non null utilisé pour mettre à + * jour la ligne courante + * + * - Il est possible de mettre à jour $item en le retourant avec la clé "item" + * - La clé primaire (il s'agit généralement de "id_") ne peut pas être + * modifiée. si elle est retournée, elle est ignorée + */ + function onEach($item, array $values): ?array { + return null; + } + const onEach = "->".[self::class, "onEach"][1]; + + /** + * méthode appelée lors du parcours des éléments avec + * {@link Capacitor::delete()} + * + * @param mixed $item l'élément courant + * @param ?array $values la ligne courante + * @return bool true s'il faut supprimer la ligne, false sinon + */ + function onDelete($item, array $values): bool { + return true; + } + const onDelete = "->".[self::class, "onDelete"][1]; + + ############################################################################# + # Méthodes déléguées pour des workflows centrés sur le channel + + /** + * @var Capacitor|null instance de Capacitor par laquelle cette instance est + * utilisée + */ + protected ?Capacitor $capacitor; + + function getCapacitor(): ?Capacitor { + return $this->capacitor; + } + + function setCapacitor(Capacitor $capacitor): self { + $this->capacitor = $capacitor; + return $this; + } + + function charge($item, $func=null, ?array $args=null, ?array &$values=null): int { + return $this->capacitor->charge($item, $func, $args, $values); + } + + function discharge(bool $reset=true): Traversable { + return $this->capacitor->discharge($reset); + } + + function count($filter=null): int { + return $this->capacitor->count($filter); + } + + function one($filter, ?array $mergeQuery=null): ?array { + return $this->capacitor->one($filter, $mergeQuery); + } + + function all($filter, ?array $mergeQuery=null): Traversable { + return $this->capacitor->all($filter, $mergeQuery); + } + + function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int { + return $this->capacitor->each($filter, $func, $args, $mergeQuery, $nbUpdated); + } + + function delete($filter, $func=null, ?array $args=null): int { + return $this->capacitor->delete($filter, $func, $args); + } +} diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php new file mode 100644 index 0000000..ec27fae --- /dev/null +++ b/php/src/db/CapacitorStorage.php @@ -0,0 +1,632 @@ +_create($channel); + $this->channels[$channel->getName()] = $channel; + return $channel; + } + + protected function getChannel(?string $name): CapacitorChannel { + CapacitorChannel::verifix_name($name); + $channel = $this->channels[$name] ?? null; + if ($channel === null) { + $channel = $this->addChannel(new CapacitorChannel($name)); + } + return $channel; + } + + /** DOIT être défini dans les classes dérivées */ + const PRIMARY_KEY_DEFINITION = null; + + const COLUMN_DEFINITIONS = [ + "item__" => CapacitorChannel::SERIAL_DEFINITION, + "item__sum_" => CapacitorChannel::SUM_DEFINITION, + "created_" => "datetime", + "modified_" => "datetime", + ]; + + protected function ColumnDefinitions(CapacitorChannel $channel): array { + $definitions = []; + if ($channel->getPrimaryKeys() === null) { + $definitions[] = static::PRIMARY_KEY_DEFINITION; + } + $definitions[] = $channel->getColumnDefinitions(); + $definitions[] = static::COLUMN_DEFINITIONS; + # forcer les définitions sans clé à la fin (sqlite requière par exemple que + # primary key (columns) soit à la fin) + $tmp = cl::merge(...$definitions); + $definitions = []; + $constraints = []; + $index = 0; + foreach ($tmp as $col => $def) { + if ($col === $index) { + $index++; + $constraints[] = $def; + } else { + $definitions[$col] = $def; + } + } + return cl::merge($definitions, $constraints); + } + + /** sérialiser les valeurs qui doivent l'être dans $values */ + protected function serialize(CapacitorChannel $channel, ?array $values): ?array { + if ($values === null) return null; + $cols = $this->ColumnDefinitions($channel); + $index = 0; + $row = []; + foreach (array_keys($cols) as $col) { + $key = $col; + if ($key === $index) { + $index++; + } elseif ($channel->isSerialCol($key)) { + [$serialCol, $sumCol] = $channel->getSumCols($key); + if (array_key_exists($key, $values)) { + $sum = $channel->getSum($key, $values[$key]); + $row[$serialCol] = $sum[$serialCol]; + if (array_key_exists($sumCol, $cols)) { + $row[$sumCol] = $sum[$sumCol]; + } + } + } elseif (array_key_exists($key, $values)) { + $row[$col] = $values[$key]; + } + } + return $row; + } + + /** désérialiser les valeurs qui doivent l'être dans $values */ + protected function unserialize(CapacitorChannel $channel, ?array $row): ?array { + if ($row === null) return null; + $cols = $this->ColumnDefinitions($channel); + $index = 0; + $values = []; + foreach (array_keys($cols) as $col) { + $key = $col; + if ($key === $index) { + $index++; + } elseif (!array_key_exists($col, $row)) { + } elseif ($channel->isSerialCol($key)) { + $value = $row[$col]; + if ($value !== null) $value = $channel->unserialize($value); + $values[$key] = $value; + } else { + $values[$key] = $row[$col]; + } + } + return $values; + } + + function getPrimaryKeys(CapacitorChannel $channel): array { + $primaryKeys = $channel->getPrimaryKeys(); + if ($primaryKeys === null) $primaryKeys = ["id_"]; + return $primaryKeys; + } + + function getRowIds(CapacitorChannel $channel, ?array $row, ?array &$primaryKeys=null): ?array { + $primaryKeys = $this->getPrimaryKeys($channel); + $rowIds = cl::select($row, $primaryKeys); + if (cl::all_n($rowIds)) return null; + else return $rowIds; + } + + protected function _createSql(CapacitorChannel $channel): array { + $cols = $this->ColumnDefinitions($channel); + return [ + "create table if not exists", + "table" => $channel->getTableName(), + "cols" => $cols, + ]; + } + + protected static function format_sql(CapacitorChannel $channel, string $sql): string { + $class = get_class($channel); + return <<_getCreateSql($this->getChannel($channel)); + } + + protected function _afterCreate(CapacitorChannel $channel): void { + } + + protected function _create(CapacitorChannel $channel): void { + $channel->ensureSetup(); + if (!$channel->isCreated()) { + $this->db->exec($this->_createSql($channel)); + $this->_afterCreate($channel); + $channel->setCreated(); + } + } + + /** tester si le canal spécifié existe */ + abstract function _exists(CapacitorChannel $channel): bool; + + function exists(?string $channel): bool { + return $this->_exists($this->getChannel($channel)); + } + + /** s'assurer que le canal spécifié existe */ + function _ensureExists(CapacitorChannel $channel): void { + $this->_create($channel); + } + + function ensureExists(?string $channel): void { + $this->_ensureExists($this->getChannel($channel)); + } + + protected function _beforeReset(CapacitorChannel $channel): void { + } + + /** supprimer le canal spécifié */ + function _reset(CapacitorChannel $channel, bool $recreate=false): void { + $this->_beforeReset($channel); + $this->db->exec([ + "drop table if exists", + $channel->getTableName(), + ]); + $channel->setCreated(false); + if ($recreate) $this->_ensureExists($channel); + } + + function reset(?string $channel, bool $recreate=false): void { + $this->_reset($this->getChannel($channel), $recreate); + } + + /** + * charger une valeur dans le canal + * + * Après avoir calculé les valeurs des clés supplémentaires + * avec {@link CapacitorChannel::getItemValues()}, l'une des deux fonctions + * {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()} + * est appelée en fonction du type d'opération: création ou mise à jour + * + * Ensuite, si $func !== null, la fonction est appelée avec la signature de + * {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()} + * en fonction du type d'opération: création ou mise à jour + * + * Dans les deux cas, si la fonction retourne un tableau, il est utilisé pour + * modifier les valeurs insérées/mises à jour. De plus, $values obtient la + * valeur finale des données insérées/mises à jour + * + * Si $args est renseigné, il est ajouté aux arguments utilisés pour appeler + * les méthodes {@link CapacitorChannel::getItemValues()}, + * {@link CapacitorChannel::onCreate()} et/ou + * {@link CapacitorChannel::onUpdate()} + * + * @return int 1 si l'objet a été chargé ou mis à jour, 0 s'il existait + * déjà à l'identique dans le canal + */ + function _charge(CapacitorChannel $channel, $item, $func, ?array $args, ?array &$values=null): int { + $this->_create($channel); + $tableName = $channel->getTableName(); + $db = $this->db(); + $args ??= []; + + $initFunc = [$channel, "getItemValues"]; + $initArgs = $args; + nur_func::ensure_func($initFunc, null, $initArgs); + $values = nur_func::call($initFunc, $item, ...$initArgs); + if ($values === [false]) return 0; + + $row = cl::merge( + $channel->getSum("item", $item), + $this->serialize($channel, $values)); + $prow = null; + $rowIds = $this->getRowIds($channel, $row, $primaryKeys); + if ($rowIds !== null) { + # modification + $prow = $db->one([ + "select", + "from" => $tableName, + "where" => $rowIds, + ]); + } + + $now = date("Y-m-d H:i:s"); + $insert = null; + if ($prow === null) { + # création + $row = cl::merge($row, [ + "created_" => $now, + "modified_" => $now, + ]); + $insert = true; + $initFunc = [$channel, "onCreate"]; + $initArgs = $args; + nur_func::ensure_func($initFunc, null, $initArgs); + $values = $this->unserialize($channel, $row); + $pvalues = null; + } else { + # modification + # intégrer autant que possible les valeurs de prow dans row, de façon que + # l'utilisateur puisse voir clairement ce qui a été modifié + if ($channel->_wasSumModified("item", $row, $prow)) { + $insert = false; + $row = cl::merge($prow, $row, [ + "modified_" => $now, + ]); + } else { + $row = cl::merge($prow, $row); + } + $initFunc = [$channel, "onUpdate"]; + $initArgs = $args; + nur_func::ensure_func($initFunc, null, $initArgs); + $values = $this->unserialize($channel, $row); + $pvalues = $this->unserialize($channel, $prow); + } + + $updates = nur_func::call($initFunc, $item, $values, $pvalues, ...$initArgs); + if ($updates === [false]) return 0; + if (is_array($updates) && $updates) { + if ($insert === null) $insert = false; + if (!array_key_exists("modified_", $updates)) { + $updates["modified_"] = $now; + } + $values = cl::merge($values, $updates); + $row = cl::merge($row, $this->serialize($channel, $updates)); + } + + if ($func !== null) { + nur_func::ensure_func($func, $channel, $args); + $updates = nur_func::call($func, $item, $values, $pvalues, ...$args); + if ($updates === [false]) return 0; + if (is_array($updates) && $updates) { + if ($insert === null) $insert = false; + if (!array_key_exists("modified_", $updates)) { + $updates["modified_"] = $now; + } + $values = cl::merge($values, $updates); + $row = cl::merge($row, $this->serialize($channel, $updates)); + } + } + + # aucune modification + if ($insert === null) return 0; + + # si on est déjà dans une transaction, désactiver la gestion des transactions + $manageTransactions = $channel->isManageTransactions() && !$db->inTransaction(); + if ($manageTransactions) { + $commited = false; + $db->beginTransaction(); + } + $nbModified = 0; + try { + if ($insert) { + $id = $db->exec([ + "insert", + "into" => $tableName, + "values" => $row, + ]); + if (count($primaryKeys) == 1 && $rowIds === null) { + # mettre à jour avec l'id généré + $values[$primaryKeys[0]] = $id; + } + $nbModified = 1; + } else { + # calculer ce qui a changé pour ne mettre à jour que le nécessaire + $updates = []; + foreach ($row as $col => $value) { + if (array_key_exists($col, $rowIds)) { + # ne jamais mettre à jour la clé primaire + continue; + } + $pvalue = $prow[$col] ?? null; + if ($value !== ($pvalue)) { + $updates[$col] = $value; + } + } + if (count($updates) == 1 && array_key_first($updates) == "modified_") { + # si l'unique modification porte sur la date de modification, alors + # la ligne n'est pas modifiée. ce cas se présente quand on altère la + # valeur de $item + $updates = null; + } + if ($updates) { + $db->exec([ + "update", + "table" => $tableName, + "values" => $updates, + "where" => $rowIds, + ]); + $nbModified = 1; + } + } + if ($manageTransactions) { + $db->commit(); + $commited = true; + } + return $nbModified; + } finally { + if ($manageTransactions && !$commited) $db->rollback(); + } + } + + function charge(?string $channel, $item, $func=null, ?array $args=null, ?array &$values=null): int { + return $this->_charge($this->getChannel($channel), $item, $func, $args, $values); + } + + /** décharger les données du canal spécifié */ + function _discharge(CapacitorChannel $channel, bool $reset=true): Traversable { + $this->_create($channel); + $rows = $this->db()->all([ + "select item__", + "from" => $channel->getTableName(), + ]); + foreach ($rows as $row) { + yield unserialize($row['item__']); + } + if ($reset) $this->_reset($channel); + } + + function discharge(?string $channel, bool $reset=true): Traversable { + return $this->_discharge($this->getChannel($channel), $reset); + } + + protected function _convertValue2row(CapacitorChannel $channel, array $filter, array $cols): array { + $index = 0; + $fixed = []; + foreach ($filter as $key => $value) { + if ($key === $index) { + $index++; + if (is_array($value)) { + $value = $this->_convertValue2row($channel, $value, $cols); + } + $fixed[] = $value; + } else { + $col = "${key}__"; + if (array_key_exists($col, $cols)) { + # colonne sérialisée + $fixed[$col] = $channel->serialize($value); + } else { + $fixed[$key] = $value; + } + } + } + return $fixed; + } + + protected function verifixFilter(CapacitorChannel $channel, &$filter): void { + if ($filter !== null && !is_array($filter)) { + $primaryKeys = $this->getPrimaryKeys($channel); + $id = $filter; + $channel->verifixId($id); + $filter = [$primaryKeys[0] => $id]; + } + $cols = $this->ColumnDefinitions($channel); + if ($filter !== null) { + $filter = $this->_convertValue2row($channel, $filter, $cols); + } + } + + /** indiquer le nombre d'éléments du canal spécifié */ + function _count(CapacitorChannel $channel, $filter): int { + $this->_create($channel); + $this->verifixFilter($channel, $filter); + return $this->db()->get([ + "select count(*)", + "from" => $channel->getTableName(), + "where" => $filter, + ]); + } + + function count(?string $channel, $filter=null): int { + return $this->_count($this->getChannel($channel), $filter); + } + + /** + * obtenir la ligne correspondant au filtre sur le canal spécifié + * + * si $filter n'est pas un tableau, il est transformé en ["id_" => $filter] + */ + function _one(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): ?array { + if ($filter === null) throw ValueException::null("filter"); + $this->_create($channel); + $this->verifixFilter($channel, $filter); + $row = $this->db()->one(cl::merge([ + "select", + "from" => $channel->getTableName(), + "where" => $filter, + ], $mergeQuery)); + return $this->unserialize($channel, $row); + } + + function one(?string $channel, $filter, ?array $mergeQuery=null): ?array { + return $this->_one($this->getChannel($channel), $filter, $mergeQuery); + } + + private function _allCached(string $id, CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable { + $this->_create($channel); + $this->verifixFilter($channel, $filter); + $rows = $this->db()->all(cl::merge([ + "select", + "from" => $channel->getTableName(), + "where" => $filter, + ], $mergeQuery), null, $this->getPrimaryKeys($channel)); + if ($channel->isUseCache()) { + $cacheIds = [$id, get_class($channel)]; + cache::get()->resetCached($cacheIds); + $rows = cache::new(null, $cacheIds, function() use ($rows) { + yield from $rows; + }); + } + foreach ($rows as $key => $row) { + yield $key => $this->unserialize($channel, $row); + } + } + + /** + * obtenir les lignes correspondant au filtre sur le canal spécifié + * + * si $filter n'est pas un tableau, il est transformé en ["id_" => $filter] + */ + function _all(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable { + return $this->_allCached("all", $channel, $filter, $mergeQuery); + } + + function all(?string $channel, $filter, $mergeQuery=null): Traversable { + return $this->_all($this->getChannel($channel), $filter, $mergeQuery); + } + + /** + * appeler une fonction pour chaque élément du canal spécifié. + * + * $filter permet de filtrer parmi les élements chargés + * + * $func est appelé avec la signature de {@link CapacitorChannel::onEach()} + * si la fonction retourne un tableau, il est utilisé pour mettre à jour la + * ligne + * + * @param int $nbUpdated reçoit le nombre de lignes mises à jour + * @return int le nombre de lignes parcourues + */ + function _each(CapacitorChannel $channel, $filter, $func, ?array $args, ?array $mergeQuery=null, ?int &$nbUpdated=null): int { + $this->_create($channel); + if ($func === null) $func = CapacitorChannel::onEach; + nur_func::ensure_func($func, $channel, $args); + $onEach = nur_func::_prepare($func); + $db = $this->db(); + # si on est déjà dans une transaction, désactiver la gestion des transactions + $manageTransactions = $channel->isManageTransactions() && !$db->inTransaction(); + if ($manageTransactions) { + $commited = false; + $db->beginTransaction(); + $commitThreshold = $channel->getEachCommitThreshold(); + } + $count = 0; + $nbUpdated = 0; + $tableName = $channel->getTableName(); + try { + $args ??= []; + $all = $this->_allCached("each", $channel, $filter, $mergeQuery); + foreach ($all as $values) { + $rowIds = $this->getRowIds($channel, $values); + $updates = nur_func::_call($onEach, [$values["item"], $values, ...$args]); + if (is_array($updates) && $updates) { + if (!array_key_exists("modified_", $updates)) { + $updates["modified_"] = date("Y-m-d H:i:s"); + } + $nbUpdated += $db->exec([ + "update", + "table" => $tableName, + "values" => $this->serialize($channel, $updates), + "where" => $rowIds, + ]); + if ($manageTransactions && $commitThreshold !== null) { + $commitThreshold--; + if ($commitThreshold <= 0) { + $db->commit(); + $db->beginTransaction(); + $commitThreshold = $channel->getEachCommitThreshold(); + } + } + } + $count++; + } + if ($manageTransactions) { + $db->commit(); + $commited = true; + } + return $count; + } finally { + if ($manageTransactions && !$commited) $db->rollback(); + } + } + + function each(?string $channel, $filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int { + return $this->_each($this->getChannel($channel), $filter, $func, $args, $mergeQuery, $nbUpdated); + } + + /** + * supprimer tous les éléments correspondant au filtre et pour lesquels la + * fonction retourne une valeur vraie si elle est spécifiée + * + * $filter permet de filtrer parmi les élements chargés + * + * $func est appelé avec la signature de {@link CapacitorChannel::onDelete()} + * si la fonction retourne un tableau, il est utilisé pour mettre à jour la + * ligne + * + * @return int le nombre de lignes parcourues + */ + function _delete(CapacitorChannel $channel, $filter, $func, ?array $args): int { + $this->_create($channel); + if ($func === null) $func = CapacitorChannel::onDelete; + nur_func::ensure_func($func, $channel, $args); + $onEach = nur_func::_prepare($func); + $db = $this->db(); + # si on est déjà dans une transaction, désactiver la gestion des transactions + $manageTransactions = $channel->isManageTransactions() && !$db->inTransaction(); + if ($manageTransactions) { + $commited = false; + $db->beginTransaction(); + $commitThreshold = $channel->getEachCommitThreshold(); + } + $count = 0; + $tableName = $channel->getTableName(); + try { + $args ??= []; + $all = $this->_allCached("delete", $channel, $filter); + foreach ($all as $values) { + $rowIds = $this->getRowIds($channel, $values); + $delete = boolval(nur_func::_call($onEach, [$values["item"], $values, ...$args])); + if ($delete) { + $db->exec([ + "delete", + "from" => $tableName, + "where" => $rowIds, + ]); + if ($manageTransactions && $commitThreshold !== null) { + $commitThreshold--; + if ($commitThreshold <= 0) { + $db->commit(); + $db->beginTransaction(); + $commitThreshold = $channel->getEachCommitThreshold(); + } + } + } + $count++; + } + if ($manageTransactions) { + $db->commit(); + $commited = true; + } + return $count; + } finally { + if ($manageTransactions && !$commited) $db->rollback(); + } + } + + function delete(?string $channel, $filter, $func=null, ?array $args=null): int { + return $this->_delete($this->getChannel($channel), $filter, $func, $args); + } + + abstract function close(): void; +} diff --git a/php/src/db/IDatabase.php b/php/src/db/IDatabase.php new file mode 100644 index 0000000..497e5be --- /dev/null +++ b/php/src/db/IDatabase.php @@ -0,0 +1,19 @@ +format('Y-m-d'); + } elseif ($value instanceof DateTime) { + $value = $value->format('Y-m-d H:i:s'); + } elseif ($value instanceof DateTimeInterface) { + $value = $value->format('Y-m-d H:i:s'); + str::del_suffix($value, " 00:00:00"); + } elseif (is_string($value)) { + if (self::is_sqldate($value)) { + # déjà dans le bon format + } elseif (Date::isa_date($value, true)) { + $value = new Date($value); + $value = $value->format('Y-m-d'); + } elseif (DateTime::isa_datetime($value, true)) { + $value = new DateTime($value); + $value = $value->format('Y-m-d H:i:s'); + } + } elseif (is_bool($value)) { + $value = $value? 1: 0; + } + } +} diff --git a/php/src/db/_private/Tcreate.php b/php/src/db/_private/Tcreate.php new file mode 100644 index 0000000..a85d118 --- /dev/null +++ b/php/src/db/_private/Tcreate.php @@ -0,0 +1,39 @@ + &$definition) { + if ($col === $index) { + $index++; + } else { + $definition = "$col $definition"; + } + }; unset($definition); + $sql[] = "(\n ".implode("\n, ", $cols)."\n)"; + + ## suffixe + if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + + ## fin de la requête + return implode(" ", $sql); + } +} diff --git a/php/src/db/_private/Tdelete.php b/php/src/db/_private/Tdelete.php new file mode 100644 index 0000000..0eeb91a --- /dev/null +++ b/php/src/db/_private/Tdelete.php @@ -0,0 +1,38 @@ + $col) { + if ($key === $index) { + $index++; + $cols[] = $col; + $usercols[] = self::add_prefix($col, $colPrefix); + } else { + $cols[] = $key; + $usercols[] = self::add_prefix($col, $colPrefix)." as $key"; + } + } + } else { + $cols = null; + if ($schema && is_array($schema) && !in_array("*", $usercols)) { + $cols = array_keys($schema); + foreach ($cols as $col) { + $usercols[] = self::add_prefix($col, $colPrefix); + } + } + } + if (!$usercols && !$cols) $usercols = [self::add_prefix("*", $colPrefix)]; + $sql[] = implode(", ", $usercols); + + ## from + $from = $query["from"] ?? null; + if (self::consume('from\s+([a-z_][a-z0-9_]*)\s*(?=;?\s*$|\bwhere\b)', $tmpsql, $ms)) { + if ($from === null) $from = $ms[1]; + $sql[] = "from"; + $sql[] = $from; + } elseif ($from !== null) { + $sql[] = "from"; + $sql[] = $from; + } else { + throw new ValueException("expected table name: $usersql"); + } + + ## where + $userwhere = []; + if (self::consume('where\b\s*(.*?)(?=;?\s*$|\border\s+by\b)', $tmpsql, $ms)) { + if ($ms[1]) $userwhere[] = $ms[1]; + } + $where = cl::withn($query["where"] ?? null); + if ($where !== null) self::parse_conds($where, $userwhere, $bindings); + if ($userwhere) { + $sql[] = "where"; + $sql[] = implode(" and ", $userwhere); + } + + ## order by + $userorderby = []; + if (self::consume('order\s+by\b\s*(.*?)(?=;?\s*$|\bgroup\s+by\b)', $tmpsql, $ms)) { + if ($ms[1]) $userorderby[] = $ms[1]; + } + $orderby = cl::withn($query["order by"] ?? null); + if ($orderby !== null) { + $index = 0; + foreach ($orderby as $key => $value) { + if ($key === $index) { + $userorderby[] = $value; + $index++; + } else { + if ($value === null) $value = false; + if (!is_bool($value)) { + $userorderby[] = "$key $value"; + } elseif ($value) { + $userorderby[] = $key; + } + } + } + } + if ($userorderby) { + $sql[] = "order by"; + $sql[] = implode(", ", $userorderby); + } + ## group by + $usergroupby = []; + if (self::consume('group\s+by\b\s*(.*?)(?=;?\s*$|\bhaving\b)', $tmpsql, $ms)) { + if ($ms[1]) $usergroupby[] = $ms[1]; + } + $groupby = cl::withn($query["group by"] ?? null); + if ($groupby !== null) { + $index = 0; + foreach ($groupby as $key => $value) { + if ($key === $index) { + $usergroupby[] = $value; + $index++; + } else { + if ($value === null) $value = false; + if (!is_bool($value)) { + $usergroupby[] = "$key $value"; + } elseif ($value) { + $usergroupby[] = $key; + } + } + } + } + if ($usergroupby) { + $sql[] = "group by"; + $sql[] = implode(", ", $usergroupby); + } + + ## having + $userhaving = []; + if (self::consume('having\b\s*(.*?)(?=;?\s*$)', $tmpsql, $ms)) { + if ($ms[1]) $userhaving[] = $ms[1]; + } + $having = cl::withn($query["having"] ?? null); + if ($having !== null) self::parse_conds($having, $userhaving, $bindings); + if ($userhaving) { + $sql[] = "having"; + $sql[] = implode(" and ", $userhaving); + } + + ## suffixe + if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + + ## fin de la requête + self::check_eof($tmpsql, $usersql); + return implode(" ", $sql); + } +} diff --git a/php/src/db/_private/Tupdate.php b/php/src/db/_private/Tupdate.php new file mode 100644 index 0000000..4e1de5b --- /dev/null +++ b/php/src/db/_private/Tupdate.php @@ -0,0 +1,40 @@ + $value) { + if ($key === $index) { + $index++; + if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) { + $sql .= " "; + } + $sql .= $value; + } + } + return $sql; + } + + protected static function is_sep(&$cond): bool { + if (!is_string($cond)) return false; + if (!preg_match('/^\s*(and|or|not)\s*$/i', $cond, $ms)) return false; + $cond = $ms[1]; + return true; + } + + static function parse_conds(?array $conds, ?array &$sql, ?array &$bindings): void { + if (!$conds) return; + $sep = null; + $index = 0; + $condsql = []; + foreach ($conds as $key => $cond) { + if ($key === $index) { + ## séquentiel + if ($index === 0 && self::is_sep($cond)) { + $sep = $cond; + } elseif (is_bool($cond)) { + # ignorer les valeurs true et false + } elseif (is_array($cond)) { + # condition récursive + self::parse_conds($cond, $condsql, $bindings); + } else { + # condition litérale + $condsql[] = strval($cond); + } + $index++; + } elseif ($cond === false) { + ## associatif + # condition litérale ignorée car condition false + } elseif ($cond === true) { + # condition litérale sélectionnée car condition true + $condsql[] = strval($key); + } else { + ## associatif + # paramètre + $param0 = preg_replace('/^.+\./', "", $key); + $i = false; + if ($bindings !== null && array_key_exists($param0, $bindings)) { + $i = 2; + while (array_key_exists("$param0$i", $bindings)) { + $i++; + } + } + # value ou [operator, value] + $condprefix = $condsep = $condsuffix = null; + if (is_array($cond)) { + $condkey = 0; + $condkeys = array_keys($cond); + $op = null; + if (array_key_exists("op", $cond)) { + $op = $cond["op"]; + } elseif (array_key_exists($condkey, $condkeys)) { + $op = $cond[$condkeys[$condkey]]; + $condkey++; + } + $op = strtolower($op); + $condvalues = null; + switch ($op) { + case "between": + # ["between", $upper, $lower] + $condsep = " and "; + if (array_key_exists("lower", $cond)) { + $condvalues[] = $cond["lower"]; + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues[] = $cond[$condkeys[$condkey]]; + $condkey++; + } + if (array_key_exists("upper", $cond)) { + $condvalues[] = $cond["upper"]; + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues[] = $cond[$condkeys[$condkey]]; + $condkey++; + } + break; + case "in": + # ["in", $values] + $condprefix = "("; + $condsep = ", "; + $condsuffix = ")"; + $condvalues = null; + if (array_key_exists("values", $cond)) { + $condvalues = cl::with($cond["values"]); + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues = cl::with($cond[$condkeys[$condkey]]); + $condkey++; + } + break; + case "null": + case "is null": + $op = "is null"; + break; + case "not null": + case "is not null": + $op = "is not null"; + break; + default: + if (array_key_exists("value", $cond)) { + $condvalues = [$cond["value"]]; + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues = [$cond[$condkeys[$condkey]]]; + $condkey++; + } + } + } elseif ($cond !== null) { + $op = "="; + $condvalues = [$cond]; + } else { + $op = "is null"; + $condvalues = null; + } + $cond = [$key, $op]; + if ($condvalues !== null) { + $parts = []; + foreach ($condvalues as $condvalue) { + if (is_array($condvalue)) { + $first = true; + foreach ($condvalue as $value) { + if ($first) { + $first = false; + } else { + if ($sep === null) $sep = "and"; + $parts[] = " $sep "; + $parts[] = $key; + $parts[] = " $op "; + } + $param = "$param0$i"; + $parts[] = ":$param"; + $bindings[$param] = $value; + if ($i === false) $i = 2; + else $i++; + } + } else { + $param = "$param0$i"; + $parts[] = ":$param"; + $bindings[$param] = $condvalue; + if ($i === false) $i = 2; + else $i++; + } + } + $cond[] = $condprefix.implode($condsep, $parts).$condsuffix; + } + $condsql[] = implode(" ", $cond); + } + } + if ($sep === null) $sep = "and"; + $count = count($condsql); + if ($count > 1) { + $sql[] = "(" . implode(" $sep ", $condsql) . ")"; + } elseif ($count == 1) { + $sql[] = $condsql[0]; + } + } + + static function parse_set_values(?array $values, ?array &$sql, ?array &$bindings): void { + if (!$values) return; + $index = 0; + $parts = []; + foreach ($values as $key => $part) { + if ($key === $index) { + ## séquentiel + if (is_array($part)) { + # paramètres récursifs + self::parse_set_values($part, $parts, $bindings); + } else { + # paramètre litéral + $parts[] = strval($part); + } + $index++; + } else { + ## associatif + # paramètre + $param = $param0 = preg_replace('/^.+\./', "", $key); + if ($bindings !== null && array_key_exists($param0, $bindings)) { + $i = 2; + while (array_key_exists("$param0$i", $bindings)) { + $i++; + } + $param = "$param0$i"; + } + # value + $value = $part; + $part = [$key, "="]; + if ($value === null) { + $part[] = "null"; + } else { + $part[] = ":$param"; + $bindings[$param] = $value; + } + $parts[] = implode(" ", $part); + } + } + $sql = cl::merge($sql, $parts); + } + + protected static function check_eof(string $tmpsql, string $usersql): void { + self::consume(';\s*', $tmpsql); + if ($tmpsql) { + throw new ValueException("unexpected value at end: $usersql"); + } + } + + abstract protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void; + + function __construct($sql, ?array $bindings=null) { + static::verifix($sql, $bindings, $meta); + $this->sql = $sql; + $this->bindings = $bindings; + $this->meta = $meta; + } + + /** @var string */ + protected $sql; + + function getSql(): string { + return $this->sql; + } + + /** @var ?array */ + protected $bindings; + + function getBindings(): ?array { + return $this->bindings; + } + + /** @var ?array */ + protected $meta; + + function isInsert(): bool { + return ($this->meta["isa"] ?? null) === "insert"; + } +} diff --git a/php/src/db/_private/_create.php b/php/src/db/_private/_create.php new file mode 100644 index 0000000..64c29a5 --- /dev/null +++ b/php/src/db/_private/_create.php @@ -0,0 +1,12 @@ + "?string", + "table" => "string", + "schema" => "?array", + "cols" => "?array", + "suffix" => "?string", + ]; +} diff --git a/php/src/db/_private/_delete.php b/php/src/db/_private/_delete.php new file mode 100644 index 0000000..e79ec34 --- /dev/null +++ b/php/src/db/_private/_delete.php @@ -0,0 +1,11 @@ + "?string", + "from" => "?string", + "where" => "?array", + "suffix" => "?string", + ]; +} diff --git a/php/src/db/_private/_generic.php b/php/src/db/_private/_generic.php new file mode 100644 index 0000000..97d4b51 --- /dev/null +++ b/php/src/db/_private/_generic.php @@ -0,0 +1,7 @@ + "?string", + "into" => "?string", + "schema" => "?array", + "cols" => "?array", + "values" => "?array", + "suffix" => "?string", + ]; +} diff --git a/php/src/db/_private/_select.php b/php/src/db/_private/_select.php new file mode 100644 index 0000000..ee2bdbc --- /dev/null +++ b/php/src/db/_private/_select.php @@ -0,0 +1,17 @@ + "?string", + "schema" => "?array", + "cols" => "?array", + "col_prefix" => "?string", + "from" => "?string", + "where" => "?array", + "order by" => "?array", + "group by" => "?array", + "having" => "?array", + "suffix" => "?string", + ]; +} diff --git a/php/src/db/_private/_update.php b/php/src/db/_private/_update.php new file mode 100644 index 0000000..b5b2dc6 --- /dev/null +++ b/php/src/db/_private/_update.php @@ -0,0 +1,14 @@ + "?string", + "table" => "?string", + "schema" => "?array", + "cols" => "?array", + "values" => "?array", + "where" => "?array", + "suffix" => "?string", + ]; +} diff --git a/php/src/db/cache/CacheChannel.php b/php/src/db/cache/CacheChannel.php new file mode 100644 index 0000000..b1f8619 --- /dev/null +++ b/php/src/db/cache/CacheChannel.php @@ -0,0 +1,116 @@ + "varchar(64) not null", + "id" => "varchar(64) not null", + "date_start" => "datetime", + "duration_" => "text", + "primary key (group_id, id)", + ]; + + static function get_cache_ids($id): array { + if (is_array($id)) { + $keys = array_keys($id); + if (array_key_exists("group_id", $id)) $groupIdKey = "group_id"; + else $groupIdKey = $keys[1] ?? null; + $groupId = $id[$groupIdKey] ?? ""; + if (array_key_exists("id", $id)) $idKey = "id"; + else $idKey = $keys[0] ?? null; + $id = $id[$idKey] ?? ""; + } else { + $groupId = ""; + } + if (preg_match('/^(.*\\\\)?([^\\\\]+)$/', $groupId, $ms)) { + # si le groupe est une classe, faire un hash du package pour limiter la + # longueur du groupe + [$package, $groupId] = [$ms[1], $ms[2]]; + $package = substr(md5($package), 0, 4); + $groupId = "${groupId}_$package"; + } + return ["group_id" => $groupId, "id" => $id]; + } + + function __construct(?string $duration=null, ?string $name=null) { + parent::__construct($name); + $this->duration = $duration ?? static::DURATION; + $this->includes = static::INCLUDES; + $this->excludes = static::EXCLUDES; + } + + protected string $duration; + + protected ?array $includes; + + protected ?array $excludes; + + function getItemValues($item): ?array { + return cl::merge(self::get_cache_ids($item), [ + "item" => null, + ]); + } + + function onCreate($item, array $values, ?array $alwaysNull, ?string $duration=null): ?array { + $now = new DateTime(); + $duration ??= $this->duration; + return [ + "date_start" => $now, + "duration" => new Delay($duration, $now), + ]; + } + + function onUpdate($item, array $values, array $pvalues, ?string $duration=null): ?array { + $now = new DateTime(); + $duration ??= $this->duration; + return [ + "date_start" => $now, + "duration" => new Delay($duration, $now), + ]; + } + + function shouldUpdate($id, bool $noCache=false): bool { + if ($noCache) return true; + + $cacheIds = self::get_cache_ids($id); + $groupId = $cacheIds["group_id"]; + if ($groupId) { + $includes = $this->includes; + $shouldInclude = $includes !== null && in_array($groupId, $includes); + $excludes = $this->excludes; + $shouldExclude = $excludes !== null && in_array($groupId, $excludes); + if (!$shouldInclude || $shouldExclude) return true; + } + + $found = false; + $expired = false; + $this->each($cacheIds, + function($item, $values) use (&$found, &$expired) { + $found = true; + $expired = $values["duration"]->isElapsed(); + }); + return !$found || $expired; + } + + function setCached($id, ?string $duration=null): void { + $cacheIds = self::get_cache_ids($id); + $this->charge($cacheIds, null, [$duration]); + } + + function resetCached($id) { + $cacheIds = self::get_cache_ids($id); + $this->delete($cacheIds); + } +} diff --git a/php/src/db/cache/RowsChannel.php b/php/src/db/cache/RowsChannel.php new file mode 100644 index 0000000..a3f7055 --- /dev/null +++ b/php/src/db/cache/RowsChannel.php @@ -0,0 +1,51 @@ + "varchar(128) primary key not null", + "all_values" => "mediumtext", + ]; + + function __construct($id, callable $builder, ?string $duration=null) { + $this->cacheIds = $cacheIds = CacheChannel::get_cache_ids($id); + $this->builder = Closure::fromCallable($builder); + $this->duration = $duration; + $name = "{$cacheIds["group_id"]}-{$cacheIds["id"]}"; + parent::__construct($name); + } + + protected array $cacheIds; + + protected Closure $builder; + + protected ?string $duration = null; + + function getItemValues($item): ?array { + $key = array_keys($item)[0]; + $row = $item[$key]; + return [ + "key" => $key, + "item" => $row, + "all_values" => implode(" ", cl::filter_n(cl::with($row))), + ]; + } + + function getIterator(): Traversable { + $cm = cache::get(); + if ($cm->shouldUpdate($this->cacheIds)) { + $this->capacitor->reset(); + foreach (($this->builder)() as $key => $row) { + $this->charge([$key => $row]); + } + $cm->setCached($this->cacheIds, $this->duration); + } + return $this->discharge(false); + } +} diff --git a/php/src/db/cache/cache.php b/php/src/db/cache/cache.php new file mode 100644 index 0000000..401fb19 --- /dev/null +++ b/php/src/db/cache/cache.php @@ -0,0 +1,37 @@ +dbconn["name"] ?? null; + if ($url !== null && preg_match('/^mysql(?::|.*;)dbname=([^;]+)/i', $url, $ms)) { + return $ms[1]; + } + return null; + } +} diff --git a/php/src/db/mysql/MysqlStorage.php b/php/src/db/mysql/MysqlStorage.php new file mode 100644 index 0000000..50b09d2 --- /dev/null +++ b/php/src/db/mysql/MysqlStorage.php @@ -0,0 +1,46 @@ +db = Mysql::with($mysql); + } + + /** @var Mysql */ + protected $db; + + function db(): Mysql { + return $this->db; + } + + const PRIMARY_KEY_DEFINITION = [ + "id_" => "integer primary key auto_increment", + ]; + + function _getCreateSql(CapacitorChannel $channel): string { + $query = new _query_base($this->_createSql($channel)); + return self::format_sql($channel, $query->getSql()); + } + + function _exists(CapacitorChannel $channel): bool { + $db = $this->db; + $tableName = $db->get([ + "select table_name from information_schema.tables", + "where" => [ + "table_schema" => $db->getDbname(), + "table_name" => $channel->getTableName(), + ], + ]); + return $tableName !== null; + } + + function close(): void { + $this->db->close(); + } +} diff --git a/php/src/db/mysql/_query_base.php b/php/src/db/mysql/_query_base.php new file mode 100644 index 0000000..614ec06 --- /dev/null +++ b/php/src/db/mysql/_query_base.php @@ -0,0 +1,52 @@ + "create", "type" => "ddl"]; + } elseif (_query_select::isa($prefix)) { + $sql = _query_select::parse($sql, $bindinds); + $meta = ["isa" => "select", "type" => "dql"]; + } elseif (_query_insert::isa($prefix)) { + $sql = _query_insert::parse($sql, $bindinds); + $meta = ["isa" => "insert", "type" => "dml"]; + } elseif (_query_update::isa($prefix)) { + $sql = _query_update::parse($sql, $bindinds); + $meta = ["isa" => "update", "type" => "dml"]; + } elseif (_query_delete::isa($prefix)) { + $sql = _query_delete::parse($sql, $bindinds); + $meta = ["isa" => "delete", "type" => "dml"]; + } elseif (_query_generic::isa($prefix)) { + $sql = _query_generic::parse($sql, $bindinds); + $meta = ["isa" => "generic", "type" => null]; + } else { + throw ValueException::invalid_kind($sql, "query"); + } + } else { + if (!is_string($sql)) $sql = strval($sql); + if (_query_create::isa($sql)) { + $meta = ["isa" => "create", "type" => "ddl"]; + } elseif (_query_select::isa($sql)) { + $meta = ["isa" => "select", "type" => "dql"]; + } elseif (_query_insert::isa($sql)) { + $meta = ["isa" => "insert", "type" => "dml"]; + } elseif (_query_update::isa($sql)) { + $meta = ["isa" => "update", "type" => "dml"]; + } elseif (_query_delete::isa($sql)) { + $meta = ["isa" => "delete", "type" => "dml"]; + } elseif (_query_generic::isa($sql)) { + $meta = ["isa" => "generic", "type" => null]; + } else { + $meta = ["isa" => "generic", "type" => null]; + } + } + } +} diff --git a/php/src/db/mysql/_query_create.php b/php/src/db/mysql/_query_create.php new file mode 100644 index 0000000..11f6602 --- /dev/null +++ b/php/src/db/mysql/_query_create.php @@ -0,0 +1,10 @@ + $pdo->dbconn, + "options" => $pdo->options, + "config" => $pdo->config, + "migrate" => $pdo->migration, + ], $params)); + } else { + return new static($pdo, $params); + } + } + + static function config_errmodeException_lowerCase(self $pdo): void { + $pdo->db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $pdo->db->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_LOWER); + } + const CONFIG_errmodeException_lowerCase = [self::class, "config_errmodeException_lowerCase"]; + + static function config_unbufferedQueries(self $pdo): void { + $pdo->db->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + } + const CONFIG_unbufferedQueries = [self::class, "config_unbufferedQueries"]; + + protected const OPTIONS = [ + \PDO::ATTR_PERSISTENT => true, + ]; + + protected const DEFAULT_CONFIG = [ + self::CONFIG_errmodeException_lowerCase, + ]; + + protected const CONFIG = null; + + protected const MIGRATE = null; + + const dbconn_SCHEMA = [ + "name" => "string", + "user" => "?string", + "pass" => "?string", + ]; + + const params_SCHEMA = [ + "dbconn" => ["array"], + "options" => ["?array|callable"], + "replace_config" => ["?array|callable"], + "config" => ["?array|callable"], + "migrate" => ["?array|string|callable"], + "auto_open" => ["bool", true], + ]; + + function __construct($dbconn=null, ?array $params=null) { + if ($dbconn !== null) { + if (!is_array($dbconn)) { + $dbconn = ["name" => $dbconn]; + #XXX à terme, il faudra interroger config + #$tmp = config::db($dbconn); + #if ($tmp !== null) $dbconn = $tmp; + #else $dbconn = ["name" => $dbconn]; + } + $params["dbconn"] = $dbconn; + } + # dbconn + $this->dbconn = $params["dbconn"] ?? null; + $this->dbconn["name"] ??= null; + $this->dbconn["user"] ??= null; + $this->dbconn["pass"] ??= null; + # options + $this->options = $params["options"] ?? static::OPTIONS; + # configuration + $config = $params["replace_config"] ?? null; + if ($config === null) { + $config = $params["config"] ?? static::CONFIG; + if (is_callable($config)) $config = [$config]; + $config = cl::merge(static::DEFAULT_CONFIG, $config); + } + $this->config = $config; + # migrations + $this->migration = $params["migrate"] ?? static::MIGRATE; + # + $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; + if ($params["auto_open"] ?? $defaultAutoOpen) { + $this->open(); + } + } + + protected ?array $dbconn; + + /** @var array|callable */ + protected array $options; + + /** @var array|string|callable */ + protected $config; + + /** @var array|string|callable */ + protected $migration; + + protected ?\PDO $db = null; + + function open(): self { + if ($this->db === null) { + $dbconn = $this->dbconn; + $options = $this->options; + if (is_callable($options)) { + nur_func::ensure_func($options, $this, $args); + $options = nur_func::call($options, ...$args); + } + $this->db = new \PDO($dbconn["name"], $dbconn["user"], $dbconn["pass"], $options); + _config::with($this->config)->configure($this); + //_migration::with($this->migration)->migrate($this); + } + return $this; + } + + function close(): void { + $this->db = null; + } + + protected function db(): \PDO { + $this->open(); + return $this->db; + } + + /** @return int|false */ + function _exec(string $query) { + return $this->db()->exec($query); + } + + private static function is_insert(?string $sql): bool { + if ($sql === null) return false; + return preg_match('/^\s*insert\b/i', $sql); + } + + function exec($query, ?array $params=null) { + $db = $this->db(); + $query = new _query_base($query, $params); + if ($query->useStmt($db, $stmt, $sql)) { + if ($stmt->execute() === false) return false; + if ($query->isInsert()) return $db->lastInsertId(); + else return $stmt->rowCount(); + } else { + $rowCount = $db->exec($sql); + if (self::is_insert($sql)) return $db->lastInsertId(); + else return $rowCount; + } + } + + /** @var ITransactor[] */ + protected ?array $transactors = null; + + function willUpdate(...$transactors): self { + foreach ($transactors as $transactor) { + if ($transactor instanceof ITransactor) { + $this->transactors[] = $transactor; + $transactor->willUpdate(); + } else { + throw ValueException::invalid_type($transactor, ITransactor::class); + } + } + return $this; + } + + function inTransaction(): bool { + return $this->db()->inTransaction(); + } + + function beginTransaction(?callable $func=null, bool $commit=true): void { + $this->db()->beginTransaction(); + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->beginTransaction(); + } + } + if ($func !== null) { + $commited = false; + try { + nur_func::call($func, $this); + if ($commit) { + $this->commit(); + $commited = true; + } + } finally { + if ($commit && !$commited) $this->rollback(); + } + } + } + + function commit(): void { + $this->db()->commit(); + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->commit(); + } + } + } + + function rollback(): void { + $this->db()->rollBack(); + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->rollback(); + } + } + } + + function get($query, ?array $params=null, bool $entireRow=false) { + $db = $this->db(); + $query = new _query_base($query, $params); + $stmt = null; + try { + /** @var \PDOStatement $stmt */ + if ($query->useStmt($db, $stmt, $sql)) { + if ($stmt->execute() === false) return null; + } else { + $stmt = $db->query($sql); + } + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + if ($row === false) return null; + $this->verifixRow($row); + if ($entireRow) return $row; + else return cl::first($row); + } finally { + if ($stmt instanceof \PDOStatement) $stmt->closeCursor(); + } + } + + function one($query, ?array $params=null): ?array { + return $this->get($query, $params, true); + } + + /** + * si $primaryKeys est fourni, le résultat est indexé sur la(es) colonne(s) + * spécifiée(s) + */ + function all($query, ?array $params=null, $primaryKeys=null): Generator { + $db = $this->db(); + $query = new _query_base($query, $params); + $stmt = null; + try { + /** @var \PDOStatement $stmt */ + if ($query->useStmt($db, $stmt, $sql)) { + if ($stmt->execute() === false) return; + } else { + $stmt = $db->query($sql); + } + if ($primaryKeys !== null) $primaryKeys = cl::with($primaryKeys); + while (($row = $stmt->fetch(\PDO::FETCH_ASSOC)) !== false) { + $this->verifixRow($row); + if ($primaryKeys !== null) { + $key = implode("-", cl::select($row, $primaryKeys)); + yield $key => $row; + } else { + yield $row; + } + } + } finally { + if ($stmt instanceof \PDOStatement) $stmt->closeCursor(); + } + } +} diff --git a/php/src/db/pdo/_config.php b/php/src/db/pdo/_config.php new file mode 100644 index 0000000..5055d6f --- /dev/null +++ b/php/src/db/pdo/_config.php @@ -0,0 +1,36 @@ +configs = $configs; + } + + /** @var array */ + protected $configs; + + function configure(Pdo $pdo): void { + foreach ($this->configs as $key => $config) { + if (is_string($config) && !nur_func::is_method($config)) { + $pdo->exec($config); + } else { + nur_func::ensure_func($config, $this, $args); + nur_func::call($config, $pdo, $key, ...$args); + } + } + } +} diff --git a/php/src/db/pdo/_query_base.php b/php/src/db/pdo/_query_base.php new file mode 100644 index 0000000..921704d --- /dev/null +++ b/php/src/db/pdo/_query_base.php @@ -0,0 +1,76 @@ + "create", "type" => "ddl"]; + } elseif (_query_select::isa($prefix)) { + $sql = _query_select::parse($sql, $bindinds); + $meta = ["isa" => "select", "type" => "dql"]; + } elseif (_query_insert::isa($prefix)) { + $sql = _query_insert::parse($sql, $bindinds); + $meta = ["isa" => "insert", "type" => "dml"]; + } elseif (_query_update::isa($prefix)) { + $sql = _query_update::parse($sql, $bindinds); + $meta = ["isa" => "update", "type" => "dml"]; + } elseif (_query_delete::isa($prefix)) { + $sql = _query_delete::parse($sql, $bindinds); + $meta = ["isa" => "delete", "type" => "dml"]; + } elseif (_query_generic::isa($prefix)) { + $sql = _query_generic::parse($sql, $bindinds); + $meta = ["isa" => "generic", "type" => null]; + } else { + throw ValueException::invalid_kind($sql, "query"); + } + } else { + if (!is_string($sql)) $sql = strval($sql); + if (_query_create::isa($sql)) { + $meta = ["isa" => "create", "type" => "ddl"]; + } elseif (_query_select::isa($sql)) { + $meta = ["isa" => "select", "type" => "dql"]; + } elseif (_query_insert::isa($sql)) { + $meta = ["isa" => "insert", "type" => "dml"]; + } elseif (_query_update::isa($sql)) { + $meta = ["isa" => "update", "type" => "dml"]; + } elseif (_query_delete::isa($sql)) { + $meta = ["isa" => "delete", "type" => "dml"]; + } elseif (_query_generic::isa($sql)) { + $meta = ["isa" => "generic", "type" => null]; + } else { + $meta = ["isa" => "generic", "type" => null]; + } + } + } + + const DEBUG_QUERIES = false; + + function useStmt(\PDO $db, ?\PDOStatement &$stmt=null, ?string &$sql=null): bool { + if (static::DEBUG_QUERIES) { #XXX + error_log($this->sql); + //error_log(var_export($this->bindings, true)); + } + if ($this->bindings !== null) { + $stmt = $db->prepare($this->sql); + foreach ($this->bindings as $name => $value) { + $this->verifixBindings($value); + $stmt->bindValue($name, $value); + } + return true; + } else { + $sql = $this->sql; + return false; + } + } +} diff --git a/php/src/db/pdo/_query_create.php b/php/src/db/pdo/_query_create.php new file mode 100644 index 0000000..997349a --- /dev/null +++ b/php/src/db/pdo/_query_create.php @@ -0,0 +1,10 @@ + $sqlite->file, + "flags" => $sqlite->flags, + "encryption_key" => $sqlite->encryptionKey, + "allow_wal" => $sqlite->allowWal, + "config" => $sqlite->config, + "migrate" => $sqlite->migration, + ], $params)); + } elseif (is_array($sqlite)) { + return new static(null, cl::merge($sqlite, $params)); + } else { + return new static($sqlite, $params); + } + } + + static function config_enableExceptions(self $sqlite): void { + $sqlite->db->enableExceptions(true); + } + const CONFIG_enableExceptions = [self::class, "config_enableExceptions"]; + + /** + * @var int temps maximum à attendre que la base soit accessible si elle est + * verrouillée + */ + protected const BUSY_TIMEOUT = 30 * 1000; + + static function config_busyTimeout(self $sqlite): void { + $sqlite->db->busyTimeout(static::BUSY_TIMEOUT); + } + const CONFIG_busyTimeout = [self::class, "config_busyTimeout"]; + + static function config_enableWalIfAllowed(self $sqlite): void { + if ($sqlite->isWalAllowed()) { + $sqlite->db->exec("PRAGMA journal_mode=WAL"); + } + } + const CONFIG_enableWalIfAllowed = [self::class, "config_enableWalIfAllowed"]; + + const ALLOW_WAL = null; + + const DEFAULT_CONFIG = [ + self::CONFIG_enableExceptions, + self::CONFIG_busyTimeout, + self::CONFIG_enableWalIfAllowed, + ]; + + const CONFIG = null; + + const MIGRATE = null; + + const params_SCHEMA = [ + "file" => ["string", ""], + "flags" => ["int", SQLITE3_OPEN_READWRITE + SQLITE3_OPEN_CREATE], + "encryption_key" => ["string", ""], + "allow_wal" => ["?bool"], + "replace_config" => ["?array|callable"], + "config" => ["?array|callable"], + "migrate" => ["?array|string|callable"], + "auto_open" => ["bool", true], + ]; + + function __construct(?string $file=null, ?array $params=null) { + if ($file !== null) $params["file"] = $file; + ##schéma + $defaultFile = self::params_SCHEMA["file"][1]; + $this->file = $file = strval($params["file"] ?? $defaultFile); + $inMemory = $file === ":memory:" || $file === ""; + # + $defaultFlags = self::params_SCHEMA["flags"][1]; + $this->flags = intval($params["flags"] ?? $defaultFlags); + # + $defaultEncryptionKey = self::params_SCHEMA["encryption_key"][1]; + $this->encryptionKey = strval($params["encryption_key"] ?? $defaultEncryptionKey); + # + $defaultAllowWal = static::ALLOW_WAL ?? !$inMemory; + $this->allowWal = $params["allow_wal"] ?? $defaultAllowWal; + # configuration + $config = $params["replace_config"] ?? null; + if ($config === null) { + $config = $params["config"] ?? static::CONFIG; + if (is_callable($config)) $config = [$config]; + $config = cl::merge(static::DEFAULT_CONFIG, $config); + } + $this->config = $config; + # migrations + $this->migration = $params["migrate"] ?? static::MIGRATE; + # + $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; + $this->inTransaction = false; + if ($params["auto_open"] ?? $defaultAutoOpen) { + $this->open(); + } + } + + /** @var string */ + protected $file; + + /** @var int */ + protected $flags; + + /** @var string */ + protected $encryptionKey; + + /** @var bool */ + protected $allowWal; + + /** vérifier s'il est autorisé de configurer le mode WAL */ + function isWalAllowed(): bool { + return $this->allowWal; + } + + /** @var array|string|callable */ + protected $config; + + /** @var array|string|callable */ + protected $migration; + + /** @var SQLite3 */ + protected $db; + + protected bool $inTransaction; + + function open(): self { + if ($this->db === null) { + $this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey); + _config::with($this->config)->configure($this); + _migration::with($this->migration)->migrate($this); + $this->inTransaction = false; + } + return $this; + } + + function close(): void { + if ($this->db !== null) { + $this->db->close(); + $this->db = null; + $this->inTransaction = false; + } + } + + protected function checkStmt($stmt): SQLite3Stmt { + return SqliteException::check($this->db, $stmt); + } + + protected function checkResult($result): SQLite3Result { + return SqliteException::check($this->db, $result); + } + + protected function db(): SQLite3 { + $this->open(); + return $this->db; + } + + function _exec(string $query): bool { + return $this->db()->exec($query); + } + + private static function is_insert(?string $sql): bool { + if ($sql === null) return false; + return preg_match('/^\s*insert\b/i', $sql); + } + + function exec($query, ?array $params=null) { + $db = $this->db(); + $query = new _query_base($query, $params); + if ($query->useStmt($db, $stmt, $sql)) { + try { + $result = $stmt->execute(); + if ($result === false) return false; + $result->finalize(); + if ($query->isInsert()) return $db->lastInsertRowID(); + else return $db->changes(); + } finally { + $stmt->close(); + } + } else { + $result = $db->exec($sql); + if ($result === false) return false; + if (self::is_insert($sql)) return $db->lastInsertRowID(); + else return $db->changes(); + } + } + + /** @var ITransactor[] */ + protected ?array $transactors = null; + + function willUpdate(...$transactors): self { + foreach ($transactors as $transactor) { + if ($transactor instanceof ITransactor) { + $this->transactors[] = $transactor; + $transactor->willUpdate(); + } else { + throw ValueException::invalid_type($transactor, ITransactor::class); + } + } + return $this; + } + + function inTransaction(): bool { + #XXX très imparfait, mais y'a rien de mieux pour le moment :-( + return $this->inTransaction; + } + + function beginTransaction(?callable $func=null, bool $commit=true): void { + $this->db()->exec("begin"); + $this->inTransaction = true; + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->beginTransaction(); + } + } + if ($func !== null) { + $commited = false; + try { + nur_func::call($func, $this); + if ($commit) { + $this->commit(); + $commited = true; + } + } finally { + if ($commit && !$commited) $this->rollback(); + } + } + } + + function commit(): void { + $this->inTransaction = false; + $this->db()->exec("commit"); + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->commit(); + } + } + } + + function rollback(): void { + $this->inTransaction = false; + $this->db()->exec("rollback"); + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->rollback(); + } + } + } + + function _get(string $query, bool $entireRow=false) { + return $this->db()->querySingle($query, $entireRow); + } + + function get($query, ?array $params=null, bool $entireRow=false) { + $db = $this->db(); + $query = new _query_base($query, $params); + if ($query->useStmt($db, $stmt, $sql)) { + try { + $result = $this->checkResult($stmt->execute()); + try { + $row = $result->fetchArray(SQLITE3_ASSOC); + if ($row === false) return null; + $this->verifixRow($row); + if ($entireRow) return $row; + else return cl::first($row); + } finally { + $result->finalize(); + } + } finally { + $stmt->close(); + } + } else { + return $db->querySingle($sql, $entireRow); + } + } + + function one($query, ?array $params=null): ?array { + return $this->get($query, $params, true); + } + + protected function _fetchResult(SQLite3Result $result, ?SQLite3Stmt $stmt=null, $primaryKeys=null): Generator { + if ($primaryKeys !== null) $primaryKeys = cl::with($primaryKeys); + try { + while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) { + $this->verifixRow($row); + if ($primaryKeys !== null) { + $key = implode("-", cl::select($row, $primaryKeys)); + yield $key => $row; + } else { + yield $row; + } + } + } finally { + $result->finalize(); + if ($stmt !== null) $stmt->close(); + } + } + + /** + * si $primaryKeys est fourni, le résultat est indexé sur la(es) colonne(s) + * spécifiée(s) + */ + function all($query, ?array $params=null, $primaryKeys=null): iterable { + $db = $this->db(); + $query = new _query_base($query, $params); + if ($query->useStmt($db, $stmt, $sql)) { + $result = $this->checkResult($stmt->execute()); + return $this->_fetchResult($result, $stmt, $primaryKeys); + } else { + $result = $this->checkResult($db->query($sql)); + return $this->_fetchResult($result, null, $primaryKeys); + } + } +} diff --git a/php/src/db/sqlite/SqliteException.php b/php/src/db/sqlite/SqliteException.php new file mode 100644 index 0000000..b71fc38 --- /dev/null +++ b/php/src/db/sqlite/SqliteException.php @@ -0,0 +1,18 @@ +lastErrorMsg(), $db->lastErrorCode()); + } + + static final function wrap(Exception $e): self{ + return new static($e->getMessage(), $e->getCode(), $e); + } +} diff --git a/php/src/db/sqlite/SqliteStorage.php b/php/src/db/sqlite/SqliteStorage.php new file mode 100644 index 0000000..287a9f7 --- /dev/null +++ b/php/src/db/sqlite/SqliteStorage.php @@ -0,0 +1,97 @@ +db = Sqlite::with($sqlite); + } + + /** @var Sqlite */ + protected $db; + + function db(): Sqlite { + return $this->db; + } + + const PRIMARY_KEY_DEFINITION = [ + "id_" => "integer primary key autoincrement", + ]; + + function _getCreateSql(CapacitorChannel $channel): string { + $query = new _query_base($this->_createSql($channel)); + return self::format_sql($channel, $query->getSql()); + } + + function tableExists(string $tableName): bool { + $name = $this->db->get([ + # depuis la version 3.33.0 le nom officiel de la table est sqlite_schema, + # mais le nom sqlite_master est toujours valable pour le moment + "select name from sqlite_master ", + "where" => ["name" => $tableName], + ]); + return $name !== null; + } + + function channelExists(string $name): bool { + $name = $this->db->get([ + "select name from _channels", + "where" => ["name" => $name], + ]); + return $name !== null; + } + + protected function _afterCreate(CapacitorChannel $channel): void { + $db = $this->db; + if (!$this->tableExists("_channels")) { + # ne pas créer si la table existe déjà, pour éviter d'avoir besoin d'un + # verrou en écriture + $db->exec([ + "create table if not exists", + "table" => "_channels", + "cols" => [ + "name" => "varchar primary key", + "table_name" => "varchar", + "class" => "varchar", + ], + ]); + } + if (!$this->channelExists($channel->getName())) { + # ne pas insérer si la ligne existe déjà, pour éviter d'avoir besoin d'un + # verrou en écriture + $db->exec([ + "insert", + "into" => "_channels", + "values" => [ + "name" => $channel->getName(), + "table_name" => $channel->getTableName(), + "class" => get_class($channel), + ], + "suffix" => "on conflict do nothing", + ]); + } + } + + protected function _beforeReset(CapacitorChannel $channel): void { + $this->db->exec([ + "delete", + "from" => "_channels", + "where" => [ + "name" => $channel->getName(), + ], + ]); + } + + function _exists(CapacitorChannel $channel): bool { + return $this->tableExists($channel->getTableName()); + } + + function close(): void { + $this->db->close(); + } +} diff --git a/php/src/db/sqlite/_config.php b/php/src/db/sqlite/_config.php new file mode 100644 index 0000000..ea7553a --- /dev/null +++ b/php/src/db/sqlite/_config.php @@ -0,0 +1,36 @@ +configs = $configs; + } + + /** @var array */ + protected $configs; + + function configure(Sqlite $sqlite): void { + foreach ($this->configs as $key => $config) { + if (is_string($config) && !nur_func::is_method($config)) { + $sqlite->exec($config); + } else { + nur_func::ensure_func($config, $this, $args); + nur_func::call($config, $sqlite, $key, ...$args); + } + } + } +} diff --git a/php/src/db/sqlite/_migration.php b/php/src/db/sqlite/_migration.php new file mode 100644 index 0000000..d2adf93 --- /dev/null +++ b/php/src/db/sqlite/_migration.php @@ -0,0 +1,55 @@ +migrations); + } else { + return new static($migrations); + } + } + + const MIGRATE = null; + + function __construct($migrations) { + if ($migrations === null) $migrations = static::MIGRATE; + if ($migrations === null) $migrations = []; + elseif (is_string($migrations)) $migrations = [$migrations]; + elseif (is_callable($migrations)) $migrations = [$migrations]; + elseif (!is_array($migrations)) $migrations = [strval($migrations)]; + $this->migrations = $migrations; + } + + /** @var callable[]|string[] */ + protected $migrations; + + function migrate(Sqlite $sqlite): void { + $sqlite->exec("create table if not exists _migration(key varchar primary key, value varchar not null, done integer default 0)"); + foreach ($this->migrations as $key => $migration) { + $exists = $sqlite->get("select 1 from _migration where key = :key and done = 1", [ + "key" => $key, + ]); + if (!$exists) { + $sqlite->exec("insert or replace into _migration(key, value, done) values(:key, :value, :done)", [ + "key" => $key, + "value" => $migration, + "done" => 0, + ]); + if (is_string($migration) && !nur_func::is_method($migration)) { + $sqlite->exec($migration); + } else { + nur_func::ensure_func($migration, $this, $args); + nur_func::call($migration, $sqlite, $key, ...$args); + } + $sqlite->exec("update _migration set done = 1 where key = :key", [ + "key" => $key, + ]); + } + } + } +} diff --git a/php/src/db/sqlite/_query_base.php b/php/src/db/sqlite/_query_base.php new file mode 100644 index 0000000..9545077 --- /dev/null +++ b/php/src/db/sqlite/_query_base.php @@ -0,0 +1,61 @@ +sql); #XXX + if ($this->bindings !== null) { + /** @var SQLite3Stmt $stmt */ + $stmt = SqliteException::check($db, $db->prepare($this->sql)); + $close = true; + try { + foreach ($this->bindings as $param => $value) { + $this->verifixBindings($value); + SqliteException::check($db, $stmt->bindValue($param, $value)); + } + $close = false; + return true; + } finally { + if ($close) $stmt->close(); + } + } else { + $sql = $this->sql; + return false; + } + } +} diff --git a/php/src/db/sqlite/_query_create.php b/php/src/db/sqlite/_query_create.php new file mode 100644 index 0000000..5aa7aa1 --- /dev/null +++ b/php/src/db/sqlite/_query_create.php @@ -0,0 +1,10 @@ +getContents(); + return JsonException::ensure_json_value(self::decode($contents)); + } + + /** obtenir la valeur JSON correspondant au corps de la requête POST */ + static final function post_data() { + $content = file_get_contents("php://input"); + return JsonException::ensure_json_value(self::decode($content)); + } + + /** envoyer $data au format JSON */ + static final function send($data, bool $exit=true): void { + header("Content-Type: application/json"); + print self::encode($data); + if ($exit) exit; + } + + const INDENT_TABS = "\t"; + + static final function with($data, ?string $indent=null): string { + $json = self::encode($data, JSON_PRETTY_PRINT); + if ($indent !== null) { + $json = preg_replace_callback('/^(?: {4})+/m', function (array $ms) use ($indent) { + return str_repeat($indent, strlen($ms[0]) / 4); + }, $json); + } + return $json; + } + + static final function dump($data, $output=null): void { + file::writer($output)->putContents(self::with($data)); + } +} diff --git a/php/src/ext/json/JsonException.php b/php/src/ext/json/JsonException.php new file mode 100644 index 0000000..a534188 --- /dev/null +++ b/php/src/ext/json/JsonException.php @@ -0,0 +1,20 @@ +close(); + } + } + return $file; + } + + static function writer($output, ?string $mode="w+b", ?callable $func=null): FileWriter { + $file = new FileWriter(self::fix_dash($output), $mode); + if ($func !== null) { + try { + $func($file); + } finally { + $file->close(); + } + } + return $file; + } + + static function shared($file, ?callable $func=null): SharedFile { + $file = new SharedFile($file); + if ($func !== null) { + try { + $func($file); + } finally { + $file ->close(); + } + } + return $file; + } + + static function tmpwriter($destdir=null, ?callable $func=null): TmpfileWriter { + $file = new TmpfileWriter($destdir); + if ($func !== null) { + try { + $func($file); + } finally { + $file->close(); + } + } + return $file; + } + + static function memory(?callable $func=null): MemoryStream { + $file = new MemoryStream(); + if ($func !== null) { + try { + $func($file); + } finally { + $file->close(); + } + } + return $file; + } + + static function temp(?callable $func=null): TempStream { + $file = new TempStream(); + if ($func !== null) { + try { + $func($file); + } finally { + $file->close(); + } + } + return $file; + } +} diff --git a/php/src/file/FileReader.php b/php/src/file/FileReader.php new file mode 100644 index 0000000..d663296 --- /dev/null +++ b/php/src/file/FileReader.php @@ -0,0 +1,51 @@ +ignoreBom = $ignoreBom; + if ($input === null) { + $fd = STDIN; + $close = false; + } elseif (is_resource($input)) { + $fd = $input; + $close = false; + } else { + $file = $input; + if ($mode === null) $mode = static::DEFAULT_MODE; + $this->file = $file; + $this->mode = $mode; + $fd = $this->open(); + $close = true; + } + parent::__construct($fd, $close, $throwOnError, $allowLocking); + } + + /** @return resource */ + protected function open() { + $fd = parent::open(); + $this->haveBom = false; + if ($this->ignoreBom) { + $bom = fread($fd, 3); + if ($bom === "\xEF\xBB\xBF") $this->seekOffset = 3; + else rewind($fd); + } + return $fd; + } + + function haveBom(): bool { + return $this->seekOffset !== 0; + } +} diff --git a/php/src/file/FileWriter.php b/php/src/file/FileWriter.php new file mode 100644 index 0000000..b3fdfc9 --- /dev/null +++ b/php/src/file/FileWriter.php @@ -0,0 +1,31 @@ +file = $file; + $this->mode = $mode; + $fd = $this->open(); + $close = true; + } + parent::__construct($fd, $close, $throwOnError, $allowLocking); + } +} diff --git a/php/src/file/IReader.php b/php/src/file/IReader.php new file mode 100644 index 0000000..36a351b --- /dev/null +++ b/php/src/file/IReader.php @@ -0,0 +1,43 @@ +fd === null) $this->fd = self::memory_fd(); + return parent::getResource(); + } +} diff --git a/php/src/file/SharedFile.php b/php/src/file/SharedFile.php new file mode 100644 index 0000000..c14001e --- /dev/null +++ b/php/src/file/SharedFile.php @@ -0,0 +1,15 @@ +fd = $fd; + $this->close = $close; + $this->throwOnError = $throwOnError ?? static::THROW_ON_ERROR; + $this->useLocking = $useLocking ?? static::USE_LOCKING; + } + + ############################################################################# + # File + + /** + * @return resource|null retourner la resource associée à ce fichier si cela + * a du sens + */ + function getResource() { + IOException::ensure_open($this->fd === null); + $this->_streamAppendFilters($this->fd); + return $this->fd; + } + + protected function lock(int $operation, ?int &$wouldBlock=null): bool { + $locked = flock($this->getResource(), $operation, $wouldBlock); + if ($locked) return true; + if ($operation & LOCK_NB) return false; + else throw IOException::error(); + } + + protected function unlock(bool $close=false): void { + if ($this->fd !== null) { + flock($this->fd, LOCK_UN); + if ($close) $this->close(); + } + } + + function isatty(): bool { + return stream_isatty($this->getResource()); + } + + /** obtenir des informations sur le fichier */ + function fstat(bool $reload=false): array { + if ($this->stat === null || $reload) { + $fd = $this->getResource(); + $this->stat = IOException::ensure_valid(fstat($fd), $this->throwOnError); + } + return $this->stat; + } + + function getSize(?int $seekOffset=null): int { + if ($seekOffset === null) $seekOffset = $this->seekOffset; + return $this->fstat()["size"] - $seekOffset; + } + + /** @throws IOException */ + function ftell(?int $seekOffset=null): int { + $fd = $this->getResource(); + if ($seekOffset === null) $seekOffset = $this->seekOffset; + return IOException::ensure_valid(ftell($fd), $this->throwOnError) - $seekOffset; + } + + /** + * @return int la position après avoir déplacé le pointeur + * @throws IOException + */ + function fseek(int $offset, int $whence=SEEK_SET, ?int $seekOffset=null): int { + $fd = $this->getResource(); + if ($seekOffset === null) $seekOffset = $this->seekOffset; + if ($whence === SEEK_SET) $offset += $seekOffset; + IOException::ensure_valid(fseek($fd, $offset, $whence), $this->throwOnError, -1); + return $this->ftell($seekOffset); + } + + function seek(int $offset, int $whence=SEEK_SET): self { + $this->fseek($offset, $whence); + return $this; + } + + /** fermer le fichier si c'est nécessaire */ + function close(bool $close=true, ?int $ifSerial=null): void { + AbstractIterator::rewind(); + if ($this->fd !== null && $close && $this->close && ($ifSerial === null || $this->serial === $ifSerial)) { + fclose($this->fd); + $this->fd = null; + } + } + + function copyTo(IWriter $dest, bool $closeWriter=false, bool $closeReader=true): void { + $srcr = $this->getResource(); + $destr = $dest->getResource(); + if ($srcr !== null && $destr !== null) { + while (!feof($srcr)) { + fwrite($destr, fread($srcr, 8192)); + } + } else { + $dest->fwrite($this->getContents(false)); + } + if ($closeWriter) $dest->close(); + if ($closeReader) $this->close(); + } + + const DEFAULT_CSV_FLAVOUR = ref_csv::OO_FLAVOUR; + + /** @var string paramètres pour la lecture et l'écriture de flux au format CSV */ + protected $csvFlavour; + + function setCsvFlavour(?string $flavour): void { + $this->csvFlavour = csv_flavours::verifix($flavour); + } + + protected function getCsvParams($fd): array { + $flavour = $this->csvFlavour; + if ($flavour === null) { + self::probe_fd($fd, $seekable, $readable); + if (!$seekable || !$readable) $fd = null; + if ($fd === null) { + # utiliser la valeur par défaut + $flavour = static::DEFAULT_CSV_FLAVOUR; + } else { + # il faut déterminer le type de fichier CSV en lisant la première ligne + $pos = IOException::ensure_valid(ftell($fd)); + $line = fgets($fd); + if ($line !== false) $line = strpbrk($line, ",;\t"); + if ($line === false) { + # aucun séparateur trouvé, prender la valeur par défaut + $flavour = static::DEFAULT_CSV_FLAVOUR; + } else { + $flavour = substr($line, 0, 1); + $flavour = csv_flavours::verifix($flavour); + } + IOException::ensure_valid(fseek($fd, $pos), true, -1); + } + $this->csvFlavour = $flavour; + } + return csv_flavours::get_params($flavour); + } + + ############################################################################# + # Reader + + /** @throws IOException */ + function fread(int $length): string { + $fd = $this->getResource(); + return IOException::ensure_valid(fread($fd, $length), $this->throwOnError); + } + + /** + * lire la prochaine ligne. la ligne est retournée avec le caractère de fin + * de ligne[\r]\n + * + * @throws EOFException si plus aucune ligne n'est disponible + * @throws IOException si une erreur se produit + */ + function fgets(?int $length=null): string { + $fd = $this->getResource(); + if ($length === null) $r = fgets($fd); + else $r = fgets($fd, $length); + return EOFException::ensure_not_eof($r, $this->throwOnError); + } + + /** @throws IOException */ + function fpassthru(): int { + $fd = $this->getResource(); + return IOException::ensure_valid(fpassthru($fd), $this->throwOnError); + } + + /** + * retourner la prochaine ligne au format CSV ou null si le fichier est arrivé + * à sa fin + */ + function fgetcsv(): ?array { + $fd = $this->getResource(); + $params = $this->getCsvParams($fd); + $row = fgetcsv($fd, 0, $params[0], $params[1], $params[2]); + if ($row === false && feof($fd)) return null; + return IOException::ensure_valid($row, $this->throwOnError); + } + + /** + * lire la prochaine ligne. la ligne est retournée *sans* le caractère de fin + * de ligne [\r]\n + * + * @throws EOFException si plus aucune ligne n'est disponible + * @throws IOException si une erreur se produit + */ + function readLine(): ?string { + return str::strip_nl($this->fgets()); + } + + /** lire et retourner toutes les lignes */ + function readLines(): array { + return iterator_to_array($this); + } + + /** + * verrouiller le fichier en lecture de façon inconditionelle (ignorer la + * valeur de $useLocking). bloquer jusqu'à ce que le verrou soit disponible + */ + function lockRead(): void { + $this->lock(LOCK_SH); + } + + /** + * essayer de verrouiller le fichier en lecture. retourner true si l'opération + * réussit. dans ce cas, il faut appeler {@link getReader()} avec l'argument + * true + */ + function canRead(): bool { + if ($this->useLocking) return $this->lock(LOCK_SH + LOCK_NB); + else return true; + } + + /** + * verrouiller en mode partagé puis retourner un objet permettant de lire le + * fichier. + */ + function getReader(bool $alreadyLocked=false): IReader { + if ($this->useLocking && !$alreadyLocked) $this->lock(LOCK_SH); + return new class($this->fd, ++$this->serial, $this) extends Stream { + function __construct($fd, int $serial, Stream $parent) { + $this->parent = $parent; + $this->serial = $serial; + parent::__construct($fd); + } + + /** @var Stream */ + private $parent; + + function close(bool $close=true, ?int $ifSerial=null): void { + if ($this->parent !== null && $close) { + $this->parent->close(true, $this->serial); + $this->fd = null; + $this->parent = null; + } + } + }; + } + + /** retourner le contenu du fichier sous forme de chaine */ + function getContents(bool $close=true, bool $alreadyLocked=false): string { + $useLocking = $this->useLocking; + if ($useLocking && !$alreadyLocked) $this->lock(LOCK_SH); + try { + return IOException::ensure_valid(stream_get_contents($this->fd), $this->throwOnError); + } finally { + if ($useLocking) $this->unlock($close); + elseif ($close) $this->close(); + } + } + + function unserialize(?array $options=null, bool $close=true, bool $alreadyLocked=false) { + $args = [$this->getContents($close, $alreadyLocked)]; + if ($options !== null) $args[] = $options; + return unserialize(...$args); + } + + function decodeJson(bool $close=true, bool $alreadyLocked=false) { + $contents = $this->getContents($close, $alreadyLocked); + return json_decode($contents, true, 512, JSON_THROW_ON_ERROR); + } + + ############################################################################# + # Iterator + + protected function iter_setup(): void { + } + + protected function iter_next(&$key) { + try { + return $this->fgets(); + } catch (EOFException $e) { + throw new NoMoreDataException(); + } + } + + private function _rewindFd(): void { + self::probe_fd($this->fd, $seekable); + if ($seekable) $this->fseek(0); + } + + protected function iter_teardown(): void { + $this->_rewindFd(); + } + + function rewind(): void { + # il faut toujours faire un rewind sur la resource, que l'itérateur aie été + # initialisé ou non + if ($this->_hasIteratorBeenSetup()) parent::rewind(); + else $this->_rewindFd(); + } + + ############################################################################# + # Writer + + /** @throws IOException */ + function ftruncate(int $size=0, bool $rewind=true): self { + $fd = $this->getResource(); + IOException::ensure_valid(ftruncate($fd, $size), $this->throwOnError); + if ($rewind) rewind($fd); + return $this; + } + + /** @throws IOException */ + function fwrite(string $data, ?int $length=null): int { + $fd = $this->getResource(); + if ($length === null) $r = fwrite($fd, $data); + else $r = fwrite($fd, $data, $length); + return IOException::ensure_valid($r, $this->throwOnError); + } + + /** @throws IOException */ + function fputcsv(array $row): void { + $fd = $this->getResource(); + $params = $this->getCsvParams($fd); + if (csv_flavours::is_dumb($this->csvFlavour, $sep)) { + $line = []; + foreach ($row as $col) { + $line[] = strval($col); + } + $line = implode($sep, $line); + IOException::ensure_valid(fwrite($fd, "$line\n"), $this->throwOnError); + } else { + IOException::ensure_valid(fputcsv($fd, $row, $params[0], $params[1], $params[2]), $this->throwOnError); + } + } + + /** @throws IOException */ + function fflush(): self { + $fd = $this->getResource(); + IOException::ensure_valid(fflush($fd), $this->throwOnError); + return $this; + } + + function writeLines(?iterable $lines): IWriter { + if ($lines !== null) { + foreach ($lines as $line) { + $this->fwrite($line); + $this->fwrite("\n"); + } + } + return $this; + } + + /** + * verrouiller le fichier en écriture de façon inconditionelle (ignorer la + * valeur de $useLocking). bloquer jusqu'à ce que le verrou soit disponible + */ + function lockWrite(): void { + $this->lock(LOCK_EX); + } + + /** + * essayer de verrouiller le fichier en écriture. retourner true si l'opération + * réussit. dans ce cas, il faut appeler {@link getWriter()} avec l'argument + * true + */ + function canWrite(): bool { + if ($this->useLocking) return $this->lock(LOCK_EX + LOCK_NB); + else return true; + } + + /** + * verrouiller en mode exclusif puis retourner un objet permettant d'écrire + * dans le fichier + */ + function getWriter(bool $alreadyLocked=false): IWriter { + if ($this->useLocking && !$alreadyLocked) $this->lock(LOCK_EX); + return new class($this->fd, ++$this->serial, $this) extends Stream { + function __construct($fd, int $serial, Stream $parent) { + $this->parent = $parent; + $this->serial = $serial; + parent::__construct($fd); + } + + /** @var Stream */ + private $parent; + + function close(bool $close=true, ?int $ifSerial=null): void { + if ($this->parent !== null && $close) { + $this->parent->close(true, $this->serial); + $this->fd = null; + $this->parent = null; + } + } + }; + } + + function putContents(string $contents, bool $close=true, bool $alreadyLocked=false): void { + $useLocking = $this->useLocking; + if ($useLocking && !$alreadyLocked) $this->lock(LOCK_EX); + try { + $this->fwrite($contents); + } finally { + if ($useLocking) $this->unlock($close); + elseif ($close) $this->close(); + } + } + + function serialize($object, bool $close=true, bool $alreadyLocked=false): void { + $this->putContents(serialize($object), $close, $alreadyLocked); + } + + function encodeJson($data, bool $close=true, bool $alreadyLocked=false): void { + $contents = json_encode($data, JSON_UNESCAPED_SLASHES + JSON_UNESCAPED_UNICODE); + $this->putContents($contents, $close, $alreadyLocked); + } + + /** + * annuler une tentative d'écriture commencée avec {@link self::canWrite()} + */ + function cancelWrite(bool $close=true): void { + if ($this->useLocking) $this->unlock($close); + elseif ($close) $this->close(); + } +} diff --git a/php/src/file/TStreamFilter.php b/php/src/file/TStreamFilter.php new file mode 100644 index 0000000..93063ae --- /dev/null +++ b/php/src/file/TStreamFilter.php @@ -0,0 +1,49 @@ +filters[] = [$filterName, $readWrite, $params]; + } + + function prependFilter(string $filterName, ?int $readWrite=null, $params=null): void { + if ($this->filters === null) $this->filters = []; + array_unshift($this->filters, [$filterName, $readWrite, $params]); + } + + function setEncodingFilter(string $from, string $to): void { + if ($to !== $from) { + $this->appendFilter("convert.iconv.$from.$to"); + } + } + + /** + * @param $fd resource + * @throws IOException + */ + protected function _streamAppendFilters($fd): void { + if ($this->filters !== null) { + foreach ($this->filters as [$filterName, $readWrite, $params]) { + if (stream_filter_append($fd, $filterName, $readWrite, $params) === false) { + throw new IOException("unable to add filter $filterName"); + } + } + $this->filters = null; + } + } + + /** + * @param $file _IFile + */ + protected function _appendFilters($file): void { + if ($this->filters !== null) { + foreach ($this->filters as [$filterName, $readWrite, $params]) { + $file->appendFilter($filterName, $readWrite, $params); + } + } + } +} diff --git a/php/src/file/TempStream.php b/php/src/file/TempStream.php new file mode 100644 index 0000000..28961c5 --- /dev/null +++ b/php/src/file/TempStream.php @@ -0,0 +1,28 @@ +maxMemory = $maxMemory ?? static::MAX_MEMORY; + parent::__construct($this->tempFd(), true, $throwOnError); + } + + /** @var int */ + protected $maxMemory; + + protected function tempFd() { + return fopen("php://temp/maxmemory:$this->maxMemory", "w+b"); + } + + function getResource() { + if ($this->fd === null) $this->fd = $this->tempFd(); + return parent::getResource(); + } +} diff --git a/php/src/file/TmpfileWriter.php b/php/src/file/TmpfileWriter.php new file mode 100644 index 0000000..2292235 --- /dev/null +++ b/php/src/file/TmpfileWriter.php @@ -0,0 +1,99 @@ +delete = true; + } elseif (is_file($destdir)) { + # si on spécifie un fichier qui existe le prendre comme "fichier + # temporaire" mais ne pas le supprimer automatiquement + $file = $destdir; + if (!path::is_qualified($file)) $file = path::join($tmpDir, $file); + $this->delete = false; + } else { + # un chemin qui n'existe pas: ne le sélectionner que si le répertoire + # existe. dans ce cas, le fichier sera créé automatiquement, mais pas + # supprimé + if (!is_dir(dirname($destdir))) { + throw new IOException("$destdir: no such file or directory"); + } + $file = $destdir; + $this->delete = false; + } + parent::__construct($file, $mode, $throwOnError, $allowLocking); + } + + /** @var bool */ + protected $delete; + + /** désactiver la suppression automatique du fichier temporaire */ + function keep(): self { + $this->delete = false; + return $this; + } + + function __destruct() { + $this->close(); + if ($this->delete) $this->delete(); + } + + /** supprimer le fichier. NB: le flux **n'est pas** fermé au préalable */ + function delete(): self { + if (file_exists($this->file)) unlink($this->file); + return $this; + } + + /** + * renommer le fichier. le flux est fermé d'abord + * + * @param int|null $defaultMode mode par défaut si le fichier destination + * n'existe pas. sinon, changer le mode du fichier temporaire à la valeur du + * fichier destination après renommage + * @param bool $setOwner si le propriétaire et/ou le groupe du fichier + * temporaire ne sont pas les mêmes que le fichier destination, tenter de + * changer le propriétaire et le groupe du fichier temporaire à la valeur + * du fichier destination après le renommage (nécessite les droits de root) + * @throws IOException + */ + function rename(string $dest, ?int $defaultMode=0644, bool $setOwner=true): void { + $this->close(); + $file = $this->file; + if (file_exists($dest)) { + $mode = fileperms($dest); + if ($setOwner) { + $tmpowner = fileowner($file); + $owner = fileowner($dest); + $tmpgroup = filegroup($file); + $group = filegroup($dest); + } else { + $owner = $group = null; + } + } else { + $mode = $defaultMode; + $owner = $group = null; + } + if (!rename($file, $dest)) { + throw new IOException("$file: unable to rename to $dest"); + } + $this->file = $dest; + if ($mode !== null) chmod($dest, $mode); + if ($owner !== null) { + if ($owner !== $tmpowner) chown($dest, $owner); + if ($group !== $tmpgroup) chgrp($dest, $group); + } + if ($mode !== null || $owner !== null) clearstatcache(true, $file); + } +} diff --git a/php/src/file/_File.php b/php/src/file/_File.php new file mode 100644 index 0000000..80a50f2 --- /dev/null +++ b/php/src/file/_File.php @@ -0,0 +1,39 @@ +file; + } + + /** @var string */ + protected $mode; + + function getMode(): string { + return $this->mode; + } + + /** @return resource */ + protected function open() { + return IOException::ensure_valid(@fopen($this->file, $this->mode)); + } + + /** @return resource */ + function getResource() { + if ($this->fd === null && $this->file !== null) $this->fd = $this->open(); + return parent::getResource(); + } + + /** streamer le contenu du fichier en sortie */ + function readfile(?string $contentType=null, ?string $charset=null, ?string $filename=null, string $disposition=null): bool { + if ($contentType !== null) http::content_type($contentType, $charset); + if ($filename !== null) http::download_as($filename, $disposition); + return readfile($this->file) !== false; + } +} diff --git a/php/src/file/_IFile.php b/php/src/file/_IFile.php new file mode 100644 index 0000000..1c599e5 --- /dev/null +++ b/php/src/file/_IFile.php @@ -0,0 +1,56 @@ +schema = $params["schema"] ?? static::SCHEMA; + $this->headers = $params["headers"] ?? static::HEADERS; + $this->useHeaders = $params["use_headers"] ?? static::USE_HEADERS; + $rows = $params["rows"] ?? null; + if (is_callable($rows)) $rows = $rows(); + $this->rows = $rows; + $cookFunc = $params["cook_func"] ?? null; + $cookCtx = $cookArgs = null; + if ($cookFunc !== null) { + nur_func::ensure_func($cookFunc, $this, $cookArgs); + $cookCtx = nur_func::_prepare($cookFunc); + } + $this->cookCtx = $cookCtx; + $this->cookArgs = $cookArgs; + $this->output = $params["output"] ?? static::OUTPUT; + $maxMemory = $params["max_memory"] ?? null; + $throwOnError = $params["throw_on_error"] ?? null; + parent::__construct($maxMemory, $throwOnError); + } + + protected ?array $schema; + + protected ?array $headers; + + protected bool $useHeaders; + + protected ?iterable $rows; + + protected ?string $output; + + protected ?array $cookCtx; + + protected ?array $cookArgs; + + protected function ensureHeaders(?array $row=null): void { + if ($this->headers !== null || !$this->useHeaders) return; + if ($this->schema === null) $headers = null; + else $headers = array_keys($this->schema); + if ($headers === null && $row !== null) $headers = array_keys($row); + $this->headers = $headers; + } + + protected abstract function _write(array $row): void; + + protected bool $wroteHeaders = false; + + function writeHeaders(?array $headers=null): void { + if ($this->wroteHeaders) return; + if ($this->useHeaders) { + if ($headers !== null) $this->headers = $headers; + else $this->ensureHeaders(); + if ($this->headers !== null) $this->_write($this->headers); + } + $this->wroteHeaders = true; + } + + protected function cookRow(?array $row): ?array { + if ($this->cookCtx !== null) { + $args = cl::merge([$row], $this->cookArgs); + $row = nur_func::_call($this->cookCtx, $args); + } + if ($row !== null) { + foreach ($row as &$value) { + # formatter les dates + if ($value instanceof DateTime) { + $value = $value->format(); + } elseif ($value instanceof DateTimeInterface) { + $value = DateTime::with($value)->format(); + } + }; unset($value); + } + return $row; + } + + function write(?array $row): void { + $row = $this->cookRow($row); + if ($row === null) return; + $this->writeHeaders(array_keys($row)); + $this->_write($row); + } + + function writeAll(?iterable $rows=null): void { + $unsetRows = false; + if ($rows === null) { + $rows = $this->rows; + $unsetRows = true; + } + if ($rows !== null) { + foreach ($rows as $row) { + $this->write(cl::with($row)); + } + } + if ($unsetRows) $this->rows = null; + } + + abstract protected function _sendContentType(): void; + + protected bool $sentHeaders = false; + + function sendHeaders(): void { + if ($this->sentHeaders) return; + $this->_sendContentType(); + $output = $this->output; + if ($output !== null) { + http::download_as(path::filename($output)); + } + $this->sentHeaders = true; + } + + protected function _build(?iterable $rows=null): void { + $this->writeAll($rows); + $this->writeHeaders(); + } + + abstract protected function _checkOk(): bool; + + protected bool $built = false, $closed = false; + + function build(?iterable $rows=null, bool $close=true): bool { + $ok = true; + if (!$this->built) { + $this->_build($rows); + $this->built = true; + } + if ($close && !$this->closed) { + $ok = $this->_checkOk(); + $this->closed = true; + } + return $ok; + } + + function sendFile(?iterable $rows=null): int { + if (!$this->built) { + $this->_build($rows); + $this->built = true; + } + if (!$this->closed) { + if (!$this->_checkOk()) return 0; + $this->closed = true; + } + $this->sendHeaders(); + return $this->fpassthru(); + } +} diff --git a/php/src/file/csv/AbstractReader.php b/php/src/file/csv/AbstractReader.php new file mode 100644 index 0000000..cf6221a --- /dev/null +++ b/php/src/file/csv/AbstractReader.php @@ -0,0 +1,129 @@ +schema = $params["schema"] ?? static::SCHEMA; + $this->headers = $params["headers"] ?? static::HEADERS; + $this->useHeaders = $params["use_headers"] ?? static::USE_HEADERS; + $this->input = $params["input"] ?? static::INPUT; + $this->trim = boolval($params["trim"] ?? static::TRIM); + $this->emptyAsNull = boolval($params["empty_as_null"] ?? static::EMPTY_AS_NULL); + $this->parseNone = boolval($params["parse_none"] ?? static::PARSE_NONE); + $this->parseNumeric = boolval($params["parse_numeric"] ?? static::PARSE_NUMERIC); + $this->parseDate = boolval($params["parse_date"] ?? static::PARSE_DATE); + } + + protected ?array $schema; + + protected ?array $headers; + + protected bool $useHeaders; + + protected $input; + + protected bool $trim; + + protected bool $emptyAsNull; + + protected bool $parseNone; + + protected bool $parseNumeric; + + protected bool $parseDate; + + protected int $isrc = 0, $idest = 0; + + protected function cookRow(array &$row): bool { + if (!$this->useHeaders) return true; + if ($this->isrc == 0) { + # ligne d'en-tête + $headers = $this->headers; + if ($headers === null) { + if ($this->schema === null) $headers = null; + else $headers = array_keys($this->schema); + if ($headers === null) $headers = $row; + $this->headers = $headers; + } + return false; + } + A::ensure_size($row, count($this->headers)); + $row = array_combine($this->headers, $row); + return true; + } + + protected function verifixCol(&$col): void { + if ($this->trim && is_string($col)) { + $col = trim($col); + } + if ($this->emptyAsNull && $col === "") { + # valeur vide --> null + $col = null; + } + if (!is_string($col) || $this->parseNone) return; + if ($this->parseDate) { + if (DateTime::isa_datetime($col, true)) { + # datetime + $col = new DateTime($col); + } elseif (DateTime::isa_date($col, true)) { + # date + $col = new Date($col); + } + if (!is_string($col)) return; + } + $parseNumeric = $this->parseNumeric || substr($col, 0, 1) !== "0"; + if ($parseNumeric) { + $tmp = str_replace(",", ".", $col); + $float = strpos($tmp, ".") !== false; + if (is_numeric($tmp)) { + if ($float) $col = floatval($tmp); + else $col = intval($tmp); + } + } + } +} diff --git a/php/src/file/csv/CsvBuilder.php b/php/src/file/csv/CsvBuilder.php new file mode 100644 index 0000000..c8f6947 --- /dev/null +++ b/php/src/file/csv/CsvBuilder.php @@ -0,0 +1,32 @@ +csvFlavour = csv_flavours::verifix($csvFlavour); + parent::__construct($output, $params); + } + + protected function _write(array $row): void { + $this->fputcsv($row); + } + + function _sendContentType(): void { + http::content_type("text/csv"); + } + + protected function _checkOk(): bool { + $size = $this->ftell(); + if ($size === 0) return false; + $this->rewind(); + return true; + } +} diff --git a/php/src/file/csv/CsvReader.php b/php/src/file/csv/CsvReader.php new file mode 100644 index 0000000..d05b2e0 --- /dev/null +++ b/php/src/file/csv/CsvReader.php @@ -0,0 +1,39 @@ +csvFlavour = $params["csv_flavour"] ?? null; + $this->inputEncoding = $params["input_encoding"] ?? null; + } + + protected ?string $csvFlavour; + + protected ?string $inputEncoding; + + function getIterator() { + $reader = new FileReader(file::fix_dash($this->input)); + $inputEncoding = $this->inputEncoding; + if ($inputEncoding !== null) { + $reader->appendFilter("convert.iconv.$inputEncoding.utf-8"); + } + $reader->setCsvFlavour($this->csvFlavour); + while (($row = $reader->fgetcsv()) !== null) { + foreach ($row as &$col) { + $this->verifixCol($col); + }; unset($col); + if ($this->cookRow($row)) { + yield $row; + $this->idest++; + } + $this->isrc++; + } + $reader->close(); + } +} diff --git a/php/src/file/csv/IBuilder.php b/php/src/file/csv/IBuilder.php new file mode 100644 index 0000000..fae647a --- /dev/null +++ b/php/src/file/csv/IBuilder.php @@ -0,0 +1,14 @@ +isExt(".csv")) { + $class = CsvBuilder::class; + } else { + $class = static::class; + if ($builder->isExt(".ods")) { + $params["ss_type"] = "ods"; + } else { + $params["ss_type"] = "xlsx"; + } + } + return new $class($builder->name, $params); + } + + if (is_string($builder)) { + $params["output"] = $builder; + } elseif (is_array($builder)) { + $params = cl::merge($builder, $params); + } elseif ($builder !== null) { + throw ValueException::invalid_type($builder, self::class); + } + + $output = $params["output"] ?? null; + $ssType = null; + if (is_string($output)) { + $ext = path::ext($output); + if ($output === "-" || $ext === ".csv") { + $class = CsvBuilder::class; + } elseif ($ext === ".ods") { + $ssType = "ods"; + } elseif ($ext === ".xlsx") { + $ssType = "xlsx"; + } else { + $ssType = "xlsx"; + } + } + $params["ss_type"] = $ssType; + if ($class === null) $class = static::class; + return new $class(null, $params); + } +} diff --git a/php/src/file/csv/TAbstractReader.php b/php/src/file/csv/TAbstractReader.php new file mode 100644 index 0000000..ed59776 --- /dev/null +++ b/php/src/file/csv/TAbstractReader.php @@ -0,0 +1,54 @@ +isExt(".csv")) { + $class = CsvReader::class; + } else { + $class = static::class; + if ($reader->isExt(".ods")) { + $params["ss_type"] = "ods"; + } else { + $params["ss_type"] = "xlsx"; + } + } + return new $class($reader->getFile(), $params); + } + + if (is_string($reader)) { + $params["input"] = $reader; + } elseif (is_array($reader)) { + $params = cl::merge($reader, $params); + } elseif ($reader !== null) { + throw ValueException::invalid_type($reader, self::class); + } + + $input = $params["input"] ?? null; + $ssType = null; + if (is_string($input)) { + $ext = path::ext($input); + if ($input === "-" || $ext === ".csv") { + $class = CsvReader::class; + } elseif ($ext === ".ods") { + $ssType = "ods"; + } elseif ($ext === ".xlsx") { + $ssType = "xlsx"; + } else { + $ssType = "xlsx"; + } + } + $params["ss_type"] = $ssType; + if ($class === null) $class = static::class; + return new $class(null, $params); + } +} diff --git a/php/src/file/csv/csv_flavours.php b/php/src/file/csv/csv_flavours.php new file mode 100644 index 0000000..4bc7bd9 --- /dev/null +++ b/php/src/file/csv/csv_flavours.php @@ -0,0 +1,59 @@ + ref_csv::OO_FLAVOUR, + "ooffice" => ref_csv::OO_FLAVOUR, + ref_csv::OOCALC => ref_csv::OO_FLAVOUR, + "xl" => ref_csv::XL_FLAVOUR, + "excel" => ref_csv::XL_FLAVOUR, + ref_csv::MSEXCEL => ref_csv::XL_FLAVOUR, + "dumb;" => ref_csv::DUMB_XL_FLAVOUR, + "dumb," => ref_csv::DUMB_OO_FLAVOUR, + "dumb" => ref_csv::DUMB_FLAVOUR, + ]; + + const ENCODINGS = [ + ref_csv::OO_FLAVOUR => ref_csv::OO_ENCODING, + ref_csv::XL_FLAVOUR => ref_csv::XL_ENCODING, + ref_csv::DUMB_FLAVOUR => ref_csv::DUMB_ENCODING, + ]; + + static final function verifix(?string $flavour): ?string { + if ($flavour === null) return null; + $lflavour = strtolower($flavour); + if (array_key_exists($lflavour, self::MAP)) { + $flavour = self::MAP[$lflavour]; + } + if (strlen($flavour) < 1) $flavour .= ","; + if (strlen($flavour) < 2) $flavour .= "\""; + if (strlen($flavour) < 3) $flavour .= "\\"; + return $flavour; + } + + static final function get_name(string $flavour): string { + if ($flavour == ref_csv::OO_FLAVOUR) return ref_csv::OOCALC; + elseif ($flavour == ref_csv::XL_FLAVOUR) return ref_csv::MSEXCEL; + else return $flavour; + } + + static final function get_params(string $flavour): array { + return [$flavour[0], $flavour[1], $flavour[2]]; + } + + static final function get_encoding(string $flavour): ?string { + return cl::get(self::ENCODINGS, $flavour); + } + + static final function is_dumb(string $flavour, ?string &$sep): bool { + if (!str::del_prefix($flavour, "xxx")) return false; + $sep = $flavour; + if (!$sep) $sep = ";"; + return true; + } +} diff --git a/php/src/file/web/Upload.php b/php/src/file/web/Upload.php new file mode 100644 index 0000000..ce4d19b --- /dev/null +++ b/php/src/file/web/Upload.php @@ -0,0 +1,123 @@ + "Ceci n'est pas un fichier téléversé", + "nofile" => "Aucun fichier n'a été fourni", + "toobig" => "Le fichier que vous avez fourni est trop volumineux.", + "unknown" => "Une erreur s'est produite pendant le transfert du fichier. Veuillez réessayer.", + ]; + + protected static function error(string $message) { + return new ValueException(static::MESSAGES[$message]); + } + + function __construct(?array $file, bool $required=true, bool $check=true) { + parent::__construct($file); + if ($check) $this->check($required); + } + + function check(bool $required=true, bool $throw=true): bool { + $file = $this->data; + if ($file) { + $name = $file["name"] ?? null; + $type = $file["type"] ?? null; + $error = $file["error"] ?? null; + if (!is_scalar($name) || !is_scalar($type) || !is_scalar($error)) { + if ($throw) throw static::error("invalid"); + else return false; + } + switch ($error) { + case UPLOAD_ERR_OK: + break; + case UPLOAD_ERR_NO_FILE: + if ($required) { + if ($throw) throw self::error("nofile"); + else return false; + } + break; + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + if ($throw) throw self::error("toobig"); + else return false; + default: + if ($throw) self::error("unknown"); + else return false; + } + } elseif ($required) { + if ($throw) throw static::error("nofile"); + else return false; + } + return true; + } + + const _AUTO_PROPERTIES = [ + "tmpName" => "tmp_name", + "fullPath" => "full_path", + ]; + function &__get($name) { + $name = static::_AUTO_PROPERTIES[$name] ?? $name; + return parent::__get($name); + } + + function isError(): bool { + $error = $this->error; + return $error !== UPLOAD_ERR_OK && $error !== UPLOAD_ERR_NO_FILE; + } + + function isValid(): bool { + return $this->error === UPLOAD_ERR_OK; + } + + /** + * retourner true si le nom du fichier a une des extensions de $exts + * + * @param string|array $exts une ou plusieurs extensions qui sont vérifiées + */ + function isExt($exts): bool { + if ($exts === null) return false; + $ext = path::ext($this->name); + $exts = cl::with($exts); + return in_array($ext, $exts); + } + + /** @var ?string chemin du fichier, s'il a été déplacé */ + protected $file; + + function moveTo(string $dest): bool { + if ($this->file === null) { + sh::mkdirof($dest); + $moved = move_uploaded_file($this->tmpName, $dest); + if ($moved) $this->file = $dest; + } else { + $moved = false; + } + return $moved; + } + + function getFile(): string { + return $this->file ?? $this->tmpName; + } + + function getReader(): FileReader { + return new FileReader($this->getFile(), "r+b"); + } +} diff --git a/php/src/os/EOFException.php b/php/src/os/EOFException.php new file mode 100644 index 0000000..c7af63d --- /dev/null +++ b/php/src/os/EOFException.php @@ -0,0 +1,14 @@ +needsStdin = true; + $this->needsTty = true; + $this->sources = null; + $this->vars = null; + $this->cmds = []; + } + + function then($cmd, ?string $input=null, ?string $output=null): Cmd { + if ($this instanceof Cmd) { + $this->add($cmd, $input, $output); + return $this; + } else { + return (new Cmd($this))->add($cmd, $input, $output); + } + } + + function or($cmd, ?string $input=null, ?string $output=null): CmdOr { + if ($this instanceof CmdOr) { + $this->add($cmd, $input, $output); + return $this; + } else { + return (new CmdOr($this))->add($cmd, $input, $output); + } + } + + function and($cmd, ?string $input=null, ?string $output=null): CmdAnd { + if ($this instanceof CmdAnd) { + $this->add($cmd, $input, $output); + return $this; + } else { + return (new CmdAnd($this))->add($cmd, $input, $output); + } + } + + function pipe($cmd): CmdPipe { + if ($this instanceof CmdPipe) { + $this->add($cmd); + return $this; + } else { + return new CmdPipe([$this, $cmd]); + } + } + + function isNeedsStdin(): bool { + return $this->needsStdin; + } + + function setNeedsStdin(bool $needsStdin): void { + $this->needsStdin = $needsStdin; + } + + function isNeedsTty(): bool { + return $this->needsTty; + } + + function setNeedsTty(bool $needsTty): void { + $this->needsTty = $needsTty; + } + + function addSource(?string $source, bool $onlyIfExists=true): void { + if ($source === null) return; + if (!$onlyIfExists || file_exists($source)) { + $source = implode(" ", [".", sh::quote($source)]); + $this->sources[] = $source; + } + } + + function getSources(?string $sep=null): ?string { + if ($this->sources === null) return null; + if ($sep === null) $sep = "\n"; + return implode($sep, $this->sources); + } + + function addLiteralVars($vars, ?string $sep=null): void { + if (cv::z($vars)) return; + if (is_array($vars)) { + if ($sep === null) $sep = "\n"; + $vars = implode($sep, $vars); + } + $this->vars[] = strval($vars); + } + + function addVars(?array $vars): void { + if ($vars === null) return; + foreach ($vars as $name => $value) { + $var = []; + if (!is_array($value)) $var[] = "export "; + A::merge($var, [$name, "=", sh::quote($value)]); + $this->vars[] = implode("", $var); + } + } + + function getVars(?string $sep=null): ?string { + if ($this->vars === null) return null; + if ($sep === null) $sep = "\n"; + return implode($sep, $this->vars); + } + + function addPrefix($prefix): void { + $count = count($this->cmds); + if ($count == 0) return; + $cmd =& $this->cmds[$count - 1]; + if ($cmd instanceof ICmd) { + $cmd->addPrefix($prefix); + } elseif (is_array($prefix)) { + $prefix = sh::join($prefix); + $cmd = "$prefix $cmd"; + } else { + $cmd = "$prefix $cmd"; + } + } + + function addRedir(?string $redir, $output=null, bool $append=false, $input=null): void { + $count = count($this->cmds); + if ($count == 0) return; + + if ($output !== null) $output = escapeshellarg($output); + if ($input !== null) $input = escapeshellarg($input); + if ($redir === "default") $redir = null; + $gt = $append? ">>": ">"; + if ($redir === null) { + $redirs = []; + if ($input !== null) $redirs[] = "<$input"; + if ($output !== null) $redirs[] = "$gt$output"; + if ($redirs) $redir = implode(" ", $redir); + } else { + switch ($redir) { + case "outonly": + case "noerr": + if ($output !== null) $redir = "$gt$output 2>/dev/null"; + else $redir = "2>/dev/null"; + break; + case "erronly": + case "noout": + if ($output !== null) $redir = "2$gt$output >/dev/null"; + else $redir = "2>&1 >/dev/null"; + break; + case "both": + case "err2out": + if ($output !== null) $redir = "$gt$output 2>&1"; + else $redir = "2>&1"; + break; + case "none": + case "null": + $redir = ">/dev/null 2>&1"; + break; + } + } + if ($redir !== null) { + $cmd =& $this->cmds[$count - 1]; + if ($cmd instanceof ICmd) { + $cmd->addRedir($redir); + } else { + $cmd = "$cmd $redir"; + } + } + } + + abstract function getCmd(?string $sep=null, bool $exec=false): string; + + function passthru(int &$retcode=null): bool { + passthru($this->getCmd(), $retcode); + return $retcode == 0; + } + + function system(string &$output=null, int &$retcode=null): bool { + $lastLine = system($this->getCmd(), $retcode); + if ($lastLine !== false) $output = $lastLine; + return $retcode == 0; + } + + function exec(array &$output=null, int &$retcode=null): bool { + exec($this->getCmd(), $output, $retcode); + return $retcode == 0; + } + + function fork_exec(?int &$retcode=null, bool $wait=true): bool { + $cmd = $this->getCmd(null, true); + sh::_fork_exec($cmd, $retcode, $wait); + return $retcode == 0; + } +} diff --git a/php/src/os/proc/AbstractCmdList.php b/php/src/os/proc/AbstractCmdList.php new file mode 100644 index 0000000..25de171 --- /dev/null +++ b/php/src/os/proc/AbstractCmdList.php @@ -0,0 +1,53 @@ +sep = $sep; + $this->add($cmd, $input, $output); + } + + function addLiteral($cmd): self { + A::append_nn($this->cmds, $cmd); + return $this; + } + + function add($cmd, ?string $input=null, ?string $output=null): self { + if ($cmd !== null) { + if (!($cmd instanceof ICmd)) { + sh::verifix_cmd($cmd, null, $input, $output); + } + $this->cmds[] = $cmd; + } + return $this; + } + + function getCmd(?string $sep=null, bool $exec=false): string { + if ($sep === null) $sep = "\n"; + + $actualCmd = []; + A::append_nn($actualCmd, $this->getSources($sep)); + A::append_nn($actualCmd, $this->getVars($sep)); + + $parts = []; + foreach ($this->cmds as $cmd) { + if ($cmd instanceof ICmd) { + $cmd = "(".$cmd->getCmd($sep).")"; + } + $parts[] = $cmd; + } + if (count($parts) == 1 && $exec) $parts[0] = "exec $parts[0]"; + $actualCmd[] = implode($this->sep ?? $sep, $parts); + + return implode($sep, $actualCmd); + } +} diff --git a/php/src/os/proc/Cmd.php b/php/src/os/proc/Cmd.php new file mode 100644 index 0000000..c5d6634 --- /dev/null +++ b/php/src/os/proc/Cmd.php @@ -0,0 +1,19 @@ +add($command); + } + } + $this->input = $input; + $this->output = $output; + } + + function addLiteral($cmd): self { + A::append_nn($this->cmds, $cmd); + return $this; + } + + function add($cmd): self { + if ($cmd !== null) { + if (!($cmd instanceof ICmd)) { + sh::verifix_cmd($cmd); + } + $this->cmds[] = $cmd; + } + return $this; + } + + function setInput(?string $input=null): self { + $this->input = $input; + return $this; + } + + function setOutput(?string $output=null): self { + $this->output = $output; + return $this; + } + + function getCmd(?string $sep=null, bool $exec=false): string { + if ($sep === null) $sep = "\n"; + + $actualCmd = []; + A::append_nn($actualCmd, $this->getSources($sep)); + A::append_nn($actualCmd, $this->getVars($sep)); + + $parts = []; + foreach ($this->cmds as $cmd) { + if ($cmd instanceof ICmd) { + $cmd = "(".$cmd->getCmd($sep).")"; + } + $parts[] = $cmd; + } + $cmd = implode(" | ", $parts); + + $input = $this->input; + $output = $this->output; + if ($input !== null || $output !== null) { + $parts = []; + if ($input !== null) $parts[] = "<".escapeshellarg($input); + $parts[] = $cmd; + if ($output !== null) $parts[] = ">".escapeshellarg($output); + $cmd = implode(" ", $parts); + } + $actualCmd[] = $cmd; + + return implode($sep, $actualCmd); + } +} diff --git a/php/src/os/proc/ICmd.php b/php/src/os/proc/ICmd.php new file mode 100644 index 0000000..2659f66 --- /dev/null +++ b/php/src/os/proc/ICmd.php @@ -0,0 +1,82 @@ + $part) { + $key = self::_quote($key); + $val = self::_quote($part); + $parts[] = "[$key]=$val"; + } + } + return "(".implode(" ", $parts).")"; + } else { + return self::_quote(strval($value)); + } + } + + /** + * obtenir une commande shell à partir du tableau des arguments. + * à utiliser avec exec() + */ + static final function join(array $parts): string { + $count = count($parts); + for($i = 0; $i < $count; $i++) { + $parts[$i] = self::_quote(strval($parts[$i])); + } + return implode(" ", $parts); + } + + private static function add_redir(string &$cmd, ?string $redir, ?string $input, ?string $output): void { + if ($redir !== null) { + switch ($redir) { + case "outonly": + case "noerr": + $redir = "2>/dev/null"; + break; + case "erronly": + case "noout": + $redir = "2>&1 >/dev/null"; + break; + case "both": + case "err2out": + $redir = "2>&1"; + break; + case "none": + case "null": + $redir = ">/dev/null 2>&1"; + break; + case "default": + $redir = null; + break; + } + } + if ($input !== null) { + $redir = $redir !== null? "$redir ": ""; + $redir .= "<".escapeshellarg($input); + } + if ($output !== null) { + $redir = $redir !== null? "$redir ": ""; + $redir .= ">".escapeshellarg($output); + } + if ($redir !== null) $cmd .= " $redir"; + } + + /** + * Corriger la commande $cmd: + * - si c'est tableau, joindre les arguments avec {@link join()} + * - sinon, mettre les caractères en échappement avec {@link escapeshellarg()} + */ + static final function verifix_cmd(&$cmd, ?string $redir=null, ?string $input=null, ?string $output=null): void { + if (is_array($cmd)) $cmd = self::join($cmd); + else $cmd = escapeshellcmd(strval($cmd)); + self::add_redir($cmd, $redir, $input, $output); + } + + /** + * Lancer la commande spécifiée avec passthru() et retourner le code de retour + * dans la variable $retcode. $cmd doit déjà être formaté comme il convient + * + * voici la différence entre system(), passthru() et exec() + * +----------------+-----------------+----------------+----------------+ + * | Command | Displays Output | Can Get Output | Gets Exit Code | + * +----------------+-----------------+----------------+----------------+ + * | passthru() | Yes (raw) | No | Yes | + * | system() | Yes (as text) | Last line only | Yes | + * | exec() | No | Yes (array) | Yes | + * +----------------+-----------------+----------------+----------------+ + * + * @return bool true si la commande s'est lancée sans erreur, false sinon + */ + static final function _passthru(string $cmd, int &$retcode=null): bool { + passthru($cmd, $retcode); + return $retcode == 0; + } + + /** + * Comme {@link _passthru()} mais lancer la commande spécifiée avec system(). + * cf la doc de {@link _passthru()} pour les autres détails + */ + static final function _system(string $cmd, string &$output=null, int &$retcode=null): bool { + $last_line = system($cmd, $retcode); + if ($last_line !== false) $output = $last_line; + return $retcode == 0; + } + + /** + * Comme {@link _passthru()} mais lancer la commande spécifiée avec exec(). + * cf la doc de {@link _passthru()} pour les autres détails + */ + static final function _exec(string $cmd, array &$output=null, int &$retcode=null): bool { + exec($cmd, $output, $retcode); + return $retcode == 0; + } + + static function _waitpid(int $pid, ?int &$retcode=null): bool { + pcntl_waitpid($pid, $status); + if (pcntl_wifexited($status)) $retcode = pcntl_wexitstatus($status); + elseif (pcntl_wifsignaled($status)) $retcode = -pcntl_wtermsig($status); + else $retcode = app::EC_FORK_CHILD; + return $retcode == 0; + } + + /** + * Lancer la commande $cmd dans un processus fils via un shell et attendre la + * fin de son exécution. + * + * $cmd doit déjà être formaté comme il convient + */ + static final function _fork_exec(string $cmd, ?int &$retcode=null, bool $wait=true): bool { + $pid = pcntl_fork(); + if ($pid == -1) { + // parent, impossible de forker + throw new ExitError(app::EC_FORK_PARENT, "unable to fork"); + } elseif ($pid) { + // parent, fork ok + if ($wait) return self::_waitpid($pid, $retcode); + $retcode = null; + return true; + } + // child, fork ok + pcntl_exec("/bin/sh", ["-c", $cmd]); + throw StateException::unexpected_state(); + } + + /** + * Corriger la commande spécifiée avec {@link verifix_cmd()} puis la lancer + * avec passthru() et retourner le code de retour dans la variable $retcode + * + * $redir spécifie le type de redirection demandée: + * - "default" | null: $output reçoit STDOUT et STDERR n'est pas redirigé + * - "outonly" | "noerr": $output ne reçoit que STDOUT et STDERR est perdu + * - "erronly" | "noout": $output ne reçoit que STDERR et STDOUT est perdu + * - "both" | "err2out": $output reçoit STDOUT et STDERR + * - "none" | "null": STDOUT et STDERR sont perdus + * - sinon c'est une redirection spécifique, et la valeur est rajoutée telle + * quelle à la ligne de commande + * + * @return bool true si la commande s'est lancée sans erreur, false sinon + */ + static final function passthru($cmd, int &$retcode=null, ?string $redir=null): bool { + self::verifix_cmd($cmd, $redir); + return self::_passthru($cmd, $retcode); + } + + /** + * Comme {@link passthru()} mais lancer la commande spécifiée avec system(). + * Cf la doc de {@link passthru()} pour les autres détails + */ + static final function system($cmd, string &$output=null, int &$retcode=null, ?string $redir=null): bool { + self::verifix_cmd($cmd, $redir); + return self::_system($cmd, $output, $retcode); + } + + /** + * Comme {@link passthru()} mais lancer la commande spécifiée avec exec(). + * Cf la doc de {@link passthru()} pour les autres détails + */ + static final function exec($cmd, array &$output=null, int &$retcode=null, ?string $redir=null): bool { + self::verifix_cmd($cmd, $redir); + return self::_exec($cmd, $output, $retcode); + } + + /** + * Corriger la commande spécifiée avec {@link verifix_cmd()}, la préfixer de + * "exec" puis la lancer avec {@link _fork_exec()} + */ + static final function fork_exec($cmd, int &$retcode=null, ?string $redir=null): bool { + self::verifix_cmd($cmd, $redir); + return self::_fork_exec("exec $cmd", $retcode); + } + + ############################################################################# + + /** retourner le répertoire $HOME */ + static final function homedir(): string { + $homedir = getenv("HOME"); + if ($homedir === false) { + $homedir = posix_getpwuid(posix_getuid())["dir"]; + } + return path::abspath($homedir); + } + + /** s'assurer que le répertoire $dir existe */ + static final function mkdirp(string $dir): bool { + if (is_dir($dir)) return true; + return mkdir($dir, 0777, true); + } + + /** créer le répertoire qui va contenir le fichier $file */ + static final function mkdirof(string $file): bool { + if (file_exists($file)) return true; + $dir = path::dirname($file); + if (file_exists($dir)) return true; + return mkdir($dir, 0777, true); + } + + /** + * créer un répertoire avec un nom unique. ce répertoire doit être supprimé + * manuellement quand il n'est plus utilisé. + * + * @return string le chemin du répertoire + * @throws IOException si une erreur se produit (impossible de créer un + * répertoire unique après 2560 essais) + */ + static final function mktempdir(?string $prefix=null, ?string $basedir=null): string { + if ($basedir === null) $basedir = sys_get_temp_dir(); + if ($prefix !== null) $prefix .= "-"; + $max = 2560; + do { + $dir = "$basedir/$prefix".uniqid(); + $r = @mkdir($dir); + $max--; + } while ($r === false && $max > 0); + if ($r === false) { + throw IOException::last_error("$dir: unable to create directory"); + } + return $dir; + } + + /** + * Supprimer un répertoire créé avec mktempdir + * + * un minimum de vérification est effectué qu'il s'agit bien d'un répertoire + * généré par mktempdir + */ + static final function rmtempdir(string $tmpdir, ?string $prefix=null, ?string $basedir=null): void { + if ($basedir === null) $basedir = sys_get_temp_dir(); + if ($prefix !== null) $prefix .= "-"; + // 13 '?' parce que c'est la taille d'une chaine générée par uniqid() + if (fnmatch("$basedir/$prefix?????????????", $tmpdir)) { + self::exec(["rm", "-rf", $tmpdir]); + } else { + throw new IOException("$tmpdir: n'est pas un répertoire temporaire"); + } + } + + /** + * supprimer tous les répertoires temporaires qui ont été créés avec le + * suffixe spécifié dans le répertoire $basedir + */ + static final function cleantempdirs(string $prefix, ?string $basedir=null): void { + if ($basedir === null) $basedir = sys_get_temp_dir(); + $prefix .= "-"; + // 13 '?' parce que c'est la taille d'une chaine générée par uniqid() + $tmpdirs = glob("$basedir/$prefix?????????????", GLOB_ONLYDIR); + if ($tmpdirs) { + self::exec(["rm", "-rf", ...$tmpdirs]); + } + } + + static final function is_diff_file(string $f, string $g): bool { + if (!is_file($f) || !is_file($g)) return true; + self::exec(array("diff", "-q", $f, $g), $output, $retcode); + return $retcode !== 0; + } + + static final function is_same_file(string $f, string $g): bool { + if (!is_file($f) || !is_file($g)) return false; + self::exec(array("diff", "-q", $f, $g), $output, $retcode); + return $retcode === 0; + } + + static final function is_diff_link(string $f, string $g): bool { + if (!is_link($f) || !is_link($g)) return true; + return @readlink($f) !== @readlink($g); + } + + static final function is_same_link(string $f, string $g): bool { + if (!is_link($f) || !is_link($g)) return false; + return @readlink($f) === @readlink($g); + } + + ############################################################################# + + static function ls_all(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array { + $all = scandir($dir, $sorting_order); + if ($all === false) return []; + return array_values(array_filter($all, + function ($file) use ($pattern) { + if ($file === "." || $file === "..") return false; + return $pattern === null || fnmatch($pattern, $file); + } + )); + } + + static function ls_dirs(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array { + return array_values(array_filter(self::ls_all($dir, $pattern, $sorting_order), + function ($file) use ($dir) { + return path::is_dir(path::join($dir, $file)); + } + )); + } + + static function ls_files(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array { + return array_values(array_filter(self::ls_all($dir, $pattern, $sorting_order), + function ($file) use ($dir) { + return path::is_file(path::join($dir, $file)); + } + )); + } + + static function ls_pall(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array { + return array_map(function(string $name) use ($dir) { + return path::join($dir, $name); + }, self::ls_all($dir, $pattern, $sorting_order)); + } + + static function ls_pdirs(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array { + return array_map(function(string $name) use ($dir) { + return path::join($dir, $name); + }, self::ls_dirs($dir, $pattern, $sorting_order)); + } + + static function ls_pfiles(string $dir, ?string $pattern=null, int $sorting_order=SCANDIR_SORT_ASCENDING): array { + return array_map(function(string $name) use ($dir) { + return path::join($dir, $name); + }, self::ls_files($dir, $pattern, $sorting_order)); + } +} diff --git a/php/src/output/IMessenger.php b/php/src/output/IMessenger.php new file mode 100644 index 0000000..7b8fdec --- /dev/null +++ b/php/src/output/IMessenger.php @@ -0,0 +1,107 @@ + ou renommer `say` en `console`, et `ui` en `say` + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/php/src/output/_messenger.php b/php/src/output/_messenger.php new file mode 100644 index 0000000..1226c24 --- /dev/null +++ b/php/src/output/_messenger.php @@ -0,0 +1,74 @@ +clone($params); + } + + static final function __callStatic($name, $args) { + $name = str::us2camel($name); + call_user_func_array([static::get(), $name], $args); + } + + ############################################################################# + + const DEBUG = IMessenger::DEBUG; + const MINOR = IMessenger::MINOR; + const NORMAL = IMessenger::NORMAL; + const MAJOR = IMessenger::MAJOR; + const NONE = IMessenger::NONE; + + static function reset_params(?array $params=null): void { static::get()->resetParams($params); } + static function section($content, ?callable $func=null, ?int $level=null): void { static::get()->section($content, $func, $level); } + static function title($content, ?callable $func=null, ?int $level=null): void { static::get()->title($content, $func, $level); } + static function desc($content, ?int $level=null): void { static::get()->desc($content, $level); } + static function action($content, ?callable $func=null, ?int $level=null): void { static::get()->action($content, $func, $level); } + static function step($content, ?int $level=null): void { static::get()->step($content, $level); } + static function asuccess($content=null, ?int $override_level=null): void { static::get()->asuccess($content, $override_level); } + static function afailure($content=null, ?int $override_level=null): void { static::get()->afailure($content, $override_level); } + static function adone($content=null, ?int $override_level=null): void { static::get()->adone($content, $override_level); } + static function aresult($result=null, ?int $override_level=null): void { static::get()->aresult($result, $override_level); } + static function print($content, ?int $level=null): void { static::get()->print($content, $level); } + static function info($content, ?int $level=null): void { static::get()->info($content, $level); } + static function note($content, ?int $level=null): void { static::get()->note($content, $level); } + static function warning($content, ?int $level=null): void { static::get()->warning($content, $level); } + static function error($content, ?int $level=null): void { static::get()->error($content, $level); } + static function end(bool $all=false): void { static::get()->end($all); } + + static function debug($content): void { self::info($content, self::DEBUG);} + static function normal($content): void { self::info($content, self::NORMAL);} + static function minor($content): void { self::info($content, self::MINOR);} + static function important($content): void { self::info($content, self::MAJOR);} + static function attention($content): void { self::note($content, self::MAJOR);} + static function critwarning($content): void { self::warning($content, self::MAJOR);} + static function criterror($content): void { self::error($content, self::MAJOR);} +} diff --git a/php/src/output/console.php b/php/src/output/console.php new file mode 100644 index 0000000..0ef8c81 --- /dev/null +++ b/php/src/output/console.php @@ -0,0 +1,28 @@ +resetParams($params); + return self::$out; + } + + static function write(...$values): void { self::$out->write(...$values); } + static function print(...$values): void { self::$out->print(...$values); } + + static function iwrite(int $indentLevel, ...$values): void { self::$out->iwrite($indentLevel, ...$values); } + static function iprint(int $indentLevel, ...$values): void { self::$out->iprint($indentLevel, ...$values); } +} +out::reset(); diff --git a/php/src/output/say.php b/php/src/output/say.php new file mode 100644 index 0000000..9d8b6d0 --- /dev/null +++ b/php/src/output/say.php @@ -0,0 +1,28 @@ +msgs = []; + foreach ($msgs as $msg) { + if ($msg !== null) $this->msgs[] = $msg; + } + } + + /** @var IMessenger[] */ + protected $msgs; + + function resetParams(?array $params=null): void { foreach ($this->msgs as $msg) { $msg->resetParams($params); } } + function clone(?array $params=null): self { + $clone = clone $this; + foreach ($clone->msgs as &$msg) { + $msg = $msg->clone($params); + }; unset($msg); + return $clone; + } + function section($content, ?callable $func=null, ?int $level=null): void { + $useFunc = false; + foreach ($this->msgs as $msg) { + $msg->section($content, null, $level); + if ($msg instanceof _IMessenger) $useFunc = true; + } + if ($useFunc && $func !== null) { + try { + $func($this); + } finally { + /** @var _IMessenger $msg */ + foreach ($this->msgs as $msg) { + $msg->_endSection(); + } + } + } + } + function title($content, ?callable $func=null, ?int $level=null): void { + $useFunc = false; + $untils = []; + foreach ($this->msgs as $msg) { + if ($msg instanceof _IMessenger) { + $useFunc = true; + $untils[] = $msg->_getTitleMark(); + } + $msg->title($content, null, $level); + } + if ($useFunc && $func !== null) { + try { + $func($this); + } finally { + /** @var _IMessenger $msg */ + $index = 0; + foreach ($this->msgs as $msg) { + if ($msg instanceof _IMessenger) { + $msg->_endTitle($untils[$index++]); + } + } + } + } + } + function desc($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->desc($content, $level); } } + function action($content, ?callable $func=null, ?int $level=null): void { + $useFunc = false; + $untils = []; + foreach ($this->msgs as $msg) { + if ($msg instanceof _IMessenger) { + $useFunc = true; + $untils[] = $msg->_getActionMark(); + } + $msg->action($content, null, $level); + } + if ($useFunc && $func !== null) { + try { + $result = $func($this); + /** @var _IMessenger $msg */ + $index = 0; + foreach ($this->msgs as $msg) { + if ($msg->_getActionMark() > $untils[$index++]) { + $msg->aresult($result); + } + } + } catch (Exception $e) { + /** @var _IMessenger $msg */ + foreach ($this->msgs as $msg) { + $msg->afailure($e); + } + throw $e; + } finally { + /** @var _IMessenger $msg */ + $index = 0; + foreach ($this->msgs as $msg) { + $msg->_endAction($untils[$index++]); + } + } + } + } + function step($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->step($content, $level); } } + function asuccess($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->asuccess($content, $overrideLevel); } } + function afailure($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->afailure($content, $overrideLevel); } } + function adone($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->adone($content, $overrideLevel); } } + function aresult($result=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->aresult($result, $overrideLevel); } } + function print($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->print($content, $level); } } + function info($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->info($content, $level); } } + function note($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->note($content, $level); } } + function warning($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->warning($content, $level); } } + function error($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->error($content, $level); } } + function end(bool $all=false): void { foreach ($this->msgs as $msg) { $msg->end($all); } } +} diff --git a/php/src/output/std/StdMessenger.php b/php/src/output/std/StdMessenger.php new file mode 100644 index 0000000..3892a0d --- /dev/null +++ b/php/src/output/std/StdMessenger.php @@ -0,0 +1,722 @@ + self::DEBUG, + "minor" => self::MINOR, "verbose" => self::MINOR, + "normal" => self::NORMAL, + "major" => self::MAJOR, "quiet" => self::MAJOR, + "none" => self::NONE, "silent" => self::NONE, + ]; + + protected static function verifix_level($level, int $max_level=self::MAX_LEVEL): int { + if (!in_array($level, self::VALID_LEVELS, true)) { + $level = cl::get(self::LEVEL_MAP, $level, $level); + } + if (!in_array($level, self::VALID_LEVELS, true)) { + throw new Exception("$level: invalid level"); + } + if ($level > $max_level) { + throw new Exception("$level: level not allowed here"); + } + return $level; + } + + const GENERIC_PREFIXES = [ + self::MAJOR => [ + "section" => [true, "SECTION!", "===", "=", "=", "==="], + "title" => [false, "TITLE!", null, "T", "", "==="], + "desc" => ["DESC!", ">", ""], + "error" => ["CRIT.ERROR!", "E!", ""], + "warning" => ["CRIT.WARNING!", "W!", ""], + "note" => ["ATTENTION!", "N!", ""], + "info" => ["IMPORTANT!", "N!", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + self::NORMAL => [ + "section" => [true, "SECTION:", "---", "-", "-", "---"], + "title" => [false, "TITLE:", null, "T", "", "---"], + "desc" => ["DESC:", ">", ""], + "error" => ["ERROR:", "E", ""], + "warning" => ["WARNING:", "W", ""], + "note" => ["NOTE:", "N", ""], + "info" => ["INFO:", "I", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + self::MINOR => [ + "section" => [true, "section", null, ">>", "<<", null], + "title" => [false, "title", null, "t", "", null], + "desc" => ["desc", ">", ""], + "error" => ["error", "E", ""], + "warning" => ["warning", "W", ""], + "note" => ["note", "N", ""], + "info" => ["info", "I", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + self::DEBUG => [ + "section" => [true, "section", null, ">>", "<<", null], + "title" => [false, "title", null, "t", "", null], + "desc" => ["desc", ">", ""], + "error" => ["debugE", "e", ""], + "warning" => ["debugW", "w", ""], + "note" => ["debugN", "i", ""], + "info" => ["debug", "D", ""], + "step" => ["*", ".", ""], + "print" => [null, null, null], + ], + ]; + + const RESULT_PREFIXES = [ + "failure" => ["(FAILURE)", ""], + "success" => ["(SUCCESS)", ""], + "done" => [null, null], + ]; + + function __construct(?array $params=null) { + $output = cl::get($params, "output"); + $color = cl::get($params, "color"); + $indent = cl::get($params, "indent", static::INDENT); + + $defaultLevel = cl::get($params, "default_level"); + if ($defaultLevel === null) $defaultLevel = self::NORMAL; + $defaultLevel = self::verifix_level($defaultLevel); + + $debug = boolval(cl::get($params, "debug")); + $minLevel = cl::get($params, "min_level"); + if ($minLevel === null && $debug) $minLevel = self::DEBUG; + if ($minLevel === null) $minLevel = cl::get($params, "verbosity"); # alias + if ($minLevel === null) $minLevel = self::NORMAL; + $minLevel = self::verifix_level($minLevel, self::NONE); + + $addDate = boolval(cl::get($params, "add_date")); + $dateFormat = cl::get($params, "date_format", static::DATE_FORMAT); + $id = cl::get($params, "id"); + + $params = [ + "color" => $color, + "indent" => $indent, + ]; + if ($output !== null) { + $this->err = $this->out = new StdOutput($output, $params); + } else { + $this->out = new StdOutput(STDOUT, $params); + $this->err = new StdOutput(STDERR, $params); + } + $this->defaultLevel = $defaultLevel; + $this->minLevel = $minLevel; + $this->addDate = $addDate; + $this->dateFormat = $dateFormat; + $this->id = $id; + $this->inSection = false; + $this->titles = []; + $this->actions = []; + } + + function resetParams(?array $params=null): void { + $output = cl::get($params, "output"); + $color = cl::get($params, "color"); + $indent = cl::get($params, "indent"); + + $defaultLevel = cl::get($params, "default_level"); + if ($defaultLevel !== null) $defaultLevel = self::verifix_level($defaultLevel); + + $debug = cl::get($params, "debug"); + $minLevel = cl::get($params, "min_level"); + if ($minLevel === null && $debug !== null) $minLevel = $debug? self::DEBUG: self::NORMAL; + if ($minLevel === null) $minLevel = cl::get($params, "verbosity"); # alias + if ($minLevel !== null) $minLevel = self::verifix_level($minLevel, self::NONE); + + $addDate = cl::get($params, "add_date"); + $dateFormat = cl::get($params, "date_format"); + $id = cl::get($params, "id"); + + $params = [ + "output" => $output, + "color" => $color, + "indent" => $indent, + ]; + if ($this->out === $this->err) { + $this->out->resetParams($params); + } else { + # NB: si initialement [output] était null, et qu'on spécifie une valeur + # [output], alors les deux instances $out et $err sont mis à jour + # séparément avec la même valeur de output + # de plus, on ne peut plus revenir à la situation initiale avec une + # destination différente pour $out et $err + $this->out->resetParams($params); + $this->err->resetParams($params); + } + if ($defaultLevel !== null) $this->defaultLevel = $defaultLevel; + if ($minLevel !== null) $this->minLevel = $minLevel; + if ($addDate !== null) $this->addDate = boolval($addDate); + if ($dateFormat !== null) $this->dateFormat = $dateFormat; + if ($id !== null) $this->id = $id; + } + + function clone(?array $params=null): IMessenger { + $clone = clone $this; + if ($params !== null) $clone->resetParams($params); + #XXX faut-il marquer la section et les titres du clone à "print" => false? + # ou en faire des références au parent? + # dans tous les cas, on considère qu'il n'y a pas d'actions en cours, et on + # ne doit pas dépiler avec end() plus que l'état que l'on a eu lors du clone + return $clone; + } + + /** @var StdOutput la sortie standard */ + protected $out; + + /** @var StdOutput la sortie d'erreur */ + protected $err; + + /** @var int level par défaut dans lequel les messages sont affichés */ + protected $defaultLevel; + + /** @var int level minimum que doivent avoir les messages pour être affichés */ + protected $minLevel; + + /** @var bool faut-il ajouter la date à chaque ligne? */ + protected $addDate; + + /** @var string format de la date */ + protected $dateFormat; + + /** @var ?string identifiant de ce messenger, à ajouter à chaque ligne */ + protected $id; + + protected function getLinePrefix(): ?string { + $linePrefix = null; + if ($this->addDate) { + $date = date_create()->format($this->dateFormat); + $linePrefix .= "$date "; + } + if ($this->id !== null) { + $linePrefix .= "$this->id "; + } + return $linePrefix; + } + + protected function decrLevel(int $level, int $amount=-1): int { + $level += $amount; + if ($level < self::MIN_LEVEL) $level = self::MIN_LEVEL; + return $level; + } + + protected function checkLevel(?int &$level): bool { + if ($level === null) $level = $this->defaultLevel; + elseif ($level < 0) $level = $this->decrLevel($this->defaultLevel, $level); + return $level >= $this->minLevel; + } + + protected function getIndentLevel(bool $withActions=true): int { + $indentLevel = count($this->titles) - 1; + if ($indentLevel < 0) $indentLevel = 0; + if ($withActions) { + foreach ($this->actions as $action) { + if ($action["level"] < $this->minLevel) continue; + $indentLevel++; + } + } + return $indentLevel; + } + + protected function _printTitle(?string $linePrefix, int $level, + string $type, $content, + int $indentLevel, StdOutput $out): void { + $prefixes = self::GENERIC_PREFIXES[$level][$type]; + if ($prefixes[0]) $out->print(); + $content = cl::with($content); + if ($out->isColor()) { + $before = $prefixes[2]; + $prefix = $prefixes[3]; + $prefix2 = $prefix !== null? "$prefix ": null; + $suffix = $prefixes[4]; + $suffix2 = $suffix !== null? " $suffix": null; + $after = $prefixes[5]; + + $lines = $out->getLines(false, ...$content); + $maxlen = 0; + foreach ($lines as &$content) { + $line = $out->filterColors($content); + $len = mb_strlen($line); + if ($len > $maxlen) $maxlen = $len; + $content = [$content, $len]; + }; unset($content); + if ($before !== null) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, substr($before, 1), str_repeat($before[0], $maxlen), $suffix); + } + foreach ($lines as [$content, $len]) { + if ($linePrefix !== null) $out->write($linePrefix); + $padding = $len < $maxlen? str_repeat(" ", $maxlen - $len): null; + $out->iprint($indentLevel, $prefix2, $content, $padding, $suffix2); + } + if ($after !== null) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, substr($after, 1), str_repeat($after[0], $maxlen), $suffix); + } + } else { + $prefix = $prefixes[1]; + if ($prefix !== null) $prefix .= " "; + $prefix2 = str_repeat(" ", mb_strlen($prefix)); + $lines = $out->getLines(false, ...$content); + foreach ($lines as $content) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, $content); + $prefix = $prefix2; + } + } + } + + protected function _printAction(?string $linePrefix, int $level, + bool $printContent, $content, + bool $printResult, ?bool $rsuccess, $rcontent, + int $indentLevel, StdOutput $out): void { + $color = $out->isColor(); + if ($rsuccess === true) $type = "success"; + elseif ($rsuccess === false) $type = "failure"; + else $type = "done"; + $rprefixes = self::RESULT_PREFIXES[$type]; + if ($color) { + $rprefix = $rprefixes[1]; + $rprefix2 = null; + if ($rprefix !== null) { + $rprefix .= " "; + $rprefix2 = $out->filterColors($out->filterContent($rprefix)); + $rprefix2 = str_repeat(" ", mb_strlen($rprefix2)); + } + } else { + $rprefix = $rprefixes[0]; + if ($rprefix !== null) $rprefix .= " "; + $rprefix2 = str_repeat(" ", mb_strlen($rprefix)); + } + if ($printContent && $printResult) { + A::ensure_array($content); + if ($rcontent) { + $content[] = ": "; + $content[] = $rcontent; + } + $lines = $out->getLines(false, ...$content); + foreach ($lines as $content) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $rprefix, $content); + $rprefix = $rprefix2; + } + } elseif ($printContent) { + $prefixes = self::GENERIC_PREFIXES[$level]["step"]; + if ($color) { + $prefix = $prefixes[1]; + if ($prefix !== null) $prefix .= " "; + $prefix2 = $out->filterColors($out->filterContent($prefix)); + $prefix2 = str_repeat(" ", mb_strlen($prefix2)); + $suffix = $prefixes[2]; + } else { + $prefix = $prefixes[0]; + if ($prefix !== null) $prefix .= " "; + $prefix2 = str_repeat(" ", mb_strlen($prefix)); + $suffix = null; + } + A::ensure_array($content); + $content[] = ":"; + $lines = $out->getLines(false, ...$content); + foreach ($lines as $content) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, $content, $suffix); + $prefix = $prefix2; + } + } elseif ($printResult) { + if (!$rcontent) { + if ($type === "success") $rcontent = $color? "succès": ""; + elseif ($type === "failure") $rcontent = $color? "échec": ""; + elseif ($type === "done") $rcontent = "fait"; + } + $rprefix = " $rprefix"; + $rprefix2 = " $rprefix2"; + $lines = $out->getLines(false, $rcontent); + foreach ($lines as $rcontent) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $rprefix, $rcontent); + $rprefix = $rprefix2; + } + } + } + + protected function _printGeneric(?string $linePrefix, int $level, + string $type, $content, + int $indentLevel, StdOutput $out): void { + $prefixes = self::GENERIC_PREFIXES[$level][$type]; + $content = cl::with($content); + if ($out->isColor()) { + $prefix = $prefixes[1]; + $prefix2 = null; + if ($prefix !== null) { + $prefix .= " "; + $prefix2 = $out->filterColors($out->filterContent($prefix)); + $prefix2 = str_repeat(" ", mb_strlen($prefix2)); + } + $suffix = $prefixes[2]; + $lines = $out->getLines(false, ...$content); + foreach ($lines as $content) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, $content, $suffix); + $prefix = $prefix2; + } + } else { + $prefix = $prefixes[0]; + if ($prefix !== null) $prefix .= " "; + $prefix2 = str_repeat(" ", mb_strlen($prefix)); + $lines = $out->getLines(false, ...$content); + foreach ($lines as $content) { + if ($linePrefix !== null) $out->write($linePrefix); + $out->iprint($indentLevel, $prefix, $content); + $prefix = $prefix2; + } + } + } + + protected function _printGenericOrException(?int $level, string $type, $content, int $indentLevel, StdOutput $out): void { + $linePrefix = $this->getLinePrefix(); + # si $content contient des exceptions, les afficher avec un level moindre + $exceptions = null; + if (is_array($content)) { + $valueContent = null; + foreach ($content as $value) { + if ($value instanceof Throwable || $value instanceof ExceptionShadow) { + $exceptions[] = $value; + } else { + $valueContent[] = $value; + } + } + if ($valueContent === null) $content = null; + elseif (count($valueContent) == 1) $content = $valueContent[0]; + else $content = $valueContent; + } elseif ($content instanceof Throwable || $content instanceof ExceptionShadow) { + $exceptions[] = $content; + $content = null; + } + + $printActions = true; + $showContent = $this->checkLevel($level); + if ($content !== null && $showContent) { + $this->printActions(); $printActions = false; + $this->_printGeneric($linePrefix, $level, $type, $content, $indentLevel, $out); + } + if ($exceptions !== null) { + $level1 = $this->decrLevel($level); + $showTraceback = $this->checkLevel($level1); + foreach ($exceptions as $exception) { + # tout d'abord userMessage + if ($exception instanceof UserException) { + $userMessage = UserException::get_user_message($exception); + $showSummary = true; + } else { + $userMessage = UserException::get_summary($exception); + $showSummary = false; + } + if ($userMessage !== null && $showContent) { + if ($printActions) { $this->printActions(); $printActions = false; } + $this->_printGeneric($linePrefix, $level, $type, $userMessage, $indentLevel, $out); + } + # puis summary et traceback + if ($showTraceback) { + if ($printActions) { $this->printActions(); $printActions = false; } + if ($showSummary) { + $summary = UserException::get_summary($exception); + $this->_printGeneric($linePrefix, $level1, $type, $summary, $indentLevel, $out); + } + $traceback = UserException::get_traceback($exception); + $this->_printGeneric($linePrefix, $level1, $type, $traceback, $indentLevel, $out); + } + } + } + } + + /** @var bool est-on dans une section? */ + protected $inSection; + + /** @var array section qui est en attente d'affichage */ + protected $section; + + function section($content, ?callable $func=null, ?int $level=null): void { + $this->_endSection(); + $this->inSection = true; + if (!$this->checkLevel($level)) return; + $this->section = [ + "line_prefix" => $this->getLinePrefix(), + "level" => $level, + "content" => $content, + "print_content" => true, + ]; + if ($func !== null) { + try { + $func($this); + } finally { + $this->_endSection(); + } + } + } + + protected function printSection() { + $section =& $this->section; + if ($section !== null && $section["print_content"]) { + $this->_printTitle( + $section["line_prefix"], $section["level"], + "section", $section["content"], + 0, $this->err); + $section["print_content"] = false; + } + } + + function _endSection(): void { + $this->inSection = false; + $this->section = null; + } + + /** @var array */ + protected $titles; + + /** @var array */ + protected $title; + + function _getTitleMark(): int { + return count($this->titles); + } + + function title($content, ?callable $func=null, ?int $level=null): void { + if (!$this->checkLevel($level)) return; + $until = $this->_getTitleMark(); + $this->titles[] = [ + "line_prefix" => $this->getLinePrefix(), + "level" => $level, + "content" => $content, + "print_content" => true, + "descs" => [], + "print_descs" => false, + ]; + $this->title =& $this->titles[$until]; + if ($func !== null) { + try { + $func($this); + } finally { + $this->_endTitle($until); + } + } + } + + function desc($content, ?int $level=null): void { + if (!$this->checkLevel($level)) return; + $title =& $this->title; + $title["descs"][] = [ + "line_prefix" => $this->getLinePrefix(), + "level" => $level, + "content" => $content, + ]; + $title["print_descs"] = true; + } + + protected function printTitles(): void { + $this->printSection(); + $err = $this->err; + $indentLevel = 0; + foreach ($this->titles as &$title) { + if ($title["print_content"]) { + $this->_printTitle( + $title["line_prefix"], $title["level"], + "title", $title["content"], + $indentLevel, $err); + $title["print_content"] = false; + } + if ($title["print_descs"]) { + foreach ($title["descs"] as $desc) { + $this->_printGeneric( + $desc["line_prefix"], $desc["level"], + "desc", $desc["content"], + $indentLevel, $err); + } + $title["descs"] = []; + $title["print_descs"] = false; + } + $indentLevel++; + }; unset($title); + } + + function _endTitle(?int $until=null): void { + if ($until === null) $until = $this->_getTitleMark() - 1; + while (count($this->titles) > $until) { + array_pop($this->titles); + } + if ($this->titles) { + $this->title =& $this->titles[count($this->titles) - 1]; + } else { + $this->titles = []; + unset($this->title); + } + } + + /** @var array */ + protected $actions; + + /** @var array */ + protected $action; + + function _getActionMark(): int { + return count($this->actions); + } + + function action($content, ?callable $func=null, ?int $level=null): void { + $this->checkLevel($level); + $until = $this->_getActionMark(); + $this->actions[] = [ + "line_prefix" => $this->getLinePrefix(), + "level" => $level, + "content" => $content, + "print_content" => true, + "result_success" => null, + "result_content" => null, + ]; + $this->action =& $this->actions[$until]; + if ($func !== null) { + try { + $result = $func($this); + if ($this->_getActionMark() > $until) { + $this->aresult($result); + } + } catch (Exception $e) { + $this->afailure($e); + throw $e; + } finally { + $this->_endAction($until); + } + } + } + + function printActions(bool $endAction=false, ?int $overrideLevel=null): void { + $this->printTitles(); + $err = $this->err; + $indentLevel = $this->getIndentLevel(false); + $lastIndex = count($this->actions) - 1; + $index = 0; + foreach ($this->actions as &$action) { + $mergeResult = $index++ == $lastIndex && $endAction; + $linePrefix = $action["line_prefix"]; + $level = $overrideLevel?? $action["level"]; + $content = $action["content"]; + $printContent = $action["print_content"]; + $rsuccess = $action["result_success"]; + $rcontent = $action["result_content"]; + if ($level < $this->minLevel) continue; + if ($mergeResult) { + $this->_printAction( + $linePrefix, $level, + $printContent, $content, + true, $rsuccess, $rcontent, + $indentLevel, $err); + } elseif ($printContent) { + $this->_printAction( + $linePrefix, $level, + $printContent, $content, + false, $rsuccess, $rcontent, + $indentLevel, $err); + $action["print_content"] = false; + } + $indentLevel++; + }; unset($action); + if ($endAction) $this->_endAction(); + } + + function step($content, ?int $level=null): void { + $this->_printGenericOrException($level, "step", $content, $this->getIndentLevel(), $this->err); + } + + function asuccess($content=null, ?int $overrideLevel=null): void { + if (!$this->actions) $this->action(null); + $this->action["result_success"] = true; + $this->action["result_content"] = $content; + $this->printActions(true, $overrideLevel); + } + + function afailure($content=null, ?int $overrideLevel=null): void { + if (!$this->actions) $this->action(null); + $this->action["result_success"] = false; + $this->action["result_content"] = $content; + $this->printActions(true, $overrideLevel); + } + + function adone($content=null, ?int $overrideLevel=null): void { + if (!$this->actions) $this->action(null); + $this->action["result_success"] = null; + $this->action["result_content"] = $content; + $this->printActions(true, $overrideLevel); + } + + function aresult($result=null, ?int $overrideLevel=null): void { + if (!$this->actions) $this->action(null); + if ($result === true) $this->asuccess(null, $overrideLevel); + elseif ($result === false) $this->afailure(null, $overrideLevel); + elseif ($result instanceof Exception) $this->afailure($result, $overrideLevel); + else $this->adone($result, $overrideLevel); + } + + function _endAction(?int $until=null): void { + if ($until === null) $until = $this->_getActionMark() - 1; + while (count($this->actions) > $until) { + array_pop($this->actions); + } + if ($this->actions) { + $this->action =& $this->actions[count($this->actions) - 1]; + } else { + $this->actions = []; + unset($this->action); + } + } + + function print($content, ?int $level=null): void { + $this->_printGenericOrException($level, "print", $content, $this->getIndentLevel(), $this->out); + } + + function info($content, ?int $level=null): void { + $this->_printGenericOrException($level, "info", $content, $this->getIndentLevel(), $this->err); + } + + function note($content, ?int $level=null): void { + $this->_printGenericOrException($level, "note", $content, $this->getIndentLevel(), $this->err); + } + + function warning($content, ?int $level=null): void { + $this->_printGenericOrException($level, "warning", $content, $this->getIndentLevel(), $this->err); + } + + function error($content, ?int $level=null): void { + $this->_printGenericOrException($level, "error", $content, $this->getIndentLevel(), $this->err); + } + + function end(bool $all=false): void { + if ($all) { + while ($this->actions) $this->adone(); + while ($this->titles) $this->_endTitle(); + $this->_endSection(); + } elseif ($this->actions) { + $this->_endAction(); + } elseif ($this->titles) { + $this->_endTitle(); + } else { + $this->_endSection(); + } + } +} diff --git a/php/src/output/std/StdOutput.php b/php/src/output/std/StdOutput.php new file mode 100644 index 0000000..c30c466 --- /dev/null +++ b/php/src/output/std/StdOutput.php @@ -0,0 +1,248 @@ + "0", + "bold" => "1", + "faint" => "2", + "underlined" => "4", + "reverse" => "7", + "normal" => "22", + "black" => "30", + "red" => "31", + "green" => "32", + "yellow" => "33", + "blue" => "34", + "magenta" => "35", + "cyan" => "36", + "white" => "37", + "default" => "39", + "black-bg" => "40", + "red-bg" => "41", + "green-bg" => "42", + "yellow-bg" => "43", + "blue-bg" => "44", + "magenta-bg" => "45", + "cyan-bg" => "46", + "white-bg" => "47", + "default-bg" => "49", + ]; + + const COLOR_MAP = [ + "z" => "reset", + "o" => "black", + "r" => "red", + "g" => "green", + "y" => "yellow", + "b" => "blue", + "m" => "magenta", + "c" => "cyan", + "w" => "white", + "O" => "black_bg", + "R" => "red_bg", + "G" => "green_bg", + "Y" => "yellow_bg", + "B" => "blue_bg", + "M" => "magenta_bg", + "C" => "cyan_bg", + "W" => "white_bg", + "@" => "bold", + "-" => "faint", + "_" => "underlined", + "~" => "reverse", + "n" => "normal", + ]; + + /** + * @param resource|null $outf + * @throws Exception si la destination est un fichier et que son ouverture a + * échoué + */ + function __construct($output=null, ?array $params=null) { + if ($output !== null) $params["output"] = $output; + elseif (!isset($params["output"])) $params["output"] = STDOUT; + if (!isset($params["filter_tags"])) $params["filter_tags"] = true; + if (!isset($params["indent"])) $params["indent"] = " "; + $this->resetParams($params); + } + + function resetParams(?array $params=null): void { + $output = cl::get($params, "output"); + $maskErrors = null; + $color = cl::get($params, "color"); + $filterTags = cl::get($params, "filter_tags"); + $indent = cl::get($params, "indent"); + $flush = cl::get($params, "flush"); + + if ($output instanceof Stream) $output = $output->getResource(); + if ($output !== null) { + if ($output === "php://stdout") { + $outf = STDOUT; + } elseif ($output === "php://stderr") { + $outf = STDERR; + } elseif (!is_resource($output)) { + # si $outf est un nom de fichier, vérifier que l'ouverture se fait sans + # erreur. à partir de là, plus aucune gestion d'erreur n'est faite, à + # part afficher les erreurs d'écriture la première fois qu'elles se + # produisent + $maskErrors = false; + $outf = @fopen($output, "ab"); + if ($outf === false) { + $error = error_get_last(); + if ($error !== null) $message = $error["message"]; + else $message = "$output: open error"; + throw new Exception($message); + } + if ($flush === null) $flush = true; + } else { + $outf = $output; + } + $this->outf = $outf; + $this->maskErrors = $maskErrors; + if ($color === null) $color = stream_isatty($outf); + if ($flush === null) $flush = false; + } + if ($color !== null) $this->color = boolval($color); + if ($filterTags !== null) $this->filterTags = boolval($filterTags); + if ($indent !== null) $this->indent = strval($indent); + if ($flush !== null) $this->flush = boolval($flush); + } + + /** @var resource */ + protected $outf; + + /** @var bool faut-il masquer les erreurs d'écriture? */ + protected $maskErrors; + + /** @var bool faut-il autoriser la sortie en couleur? */ + protected $color; + + function isColor(): bool { + return $this->color; + } + + /** @var bool faut-il enlever les tags dans la sortie? */ + protected $filterTags; + + /** @var string indentation unitaire */ + protected $indent; + + /** @var bool faut-il flush le fichier après l'écriture de chaque ligne */ + protected $flush; + + function isatty(): bool { + return stream_isatty($this->outf); + } + + private static function replace_colors(array $ms): string { + $colors = []; + foreach (preg_split('/\s+/', $ms[1]) as $color) { + while ($color && !cl::has(self::COLORS, $color)) { + $alias = substr($color, 0, 1); + $colors[] = self::COLOR_MAP[$alias]; + $color = substr($color, 1); + } + if ($color) $colors[] = $color; + } + $text = "\x1B["; + $first = true; + foreach ($colors as $color) { + if (!$color) continue; + if ($first) $first = false; + else $text .= ";"; + $text .= self::COLORS[$color]; + } + $text .= "m"; + return $text; + } + function filterContent(string $text): string { + # couleur au début + $text = preg_replace_callback('/]*)>/', [self::class, "replace_colors"], $text); + # reset à la fin + $text = preg_replace('/<\/color>/', "\x1B[0m", $text); + # enlever les tags classiques + if ($this->filterTags) { + $text = preg_replace('/<[^>]*>/', "", $text); + } + return $text; + } + function filterColors(string $text): string { + return preg_replace('/\x1B\[.*?m/', "", $text); + } + + function getIndent(int $indentLevel): string { + return str_repeat($this->indent, $indentLevel); + } + + function getLines(bool $withNl, ...$values): array { + $values = c::resolve($values, null, false); + if (!$values) return []; + $text = c::to_string($values, false); + if ($text === "") return [""]; + $text = $this->filterContent($text); + if (!$this->color) $text = $this->filterColors($text); + $lines = explode("\n", $text); + $max = count($lines) - 1; + if ($withNl) { + for ($i = 0; $i < $max; $i++) { + $lines[$i] .= "\n"; + } + } + if ($lines[$max] === "") unset($lines[$max]); + return $lines; + } + + private function _fwrite($outf, string $data): void { + if ($this->maskErrors === null) { + # masquer les erreurs d'écriture en permanence + @fwrite($outf, $data); + return; + } + # masquer uniquement la première erreur, jusqu'à ce que l'erreur disparaisse + if ($this->maskErrors) $r = @fwrite($outf, $data); + else $r = fwrite($outf, $data); + $this->maskErrors = $r === false; + } + + function writeLines($indent, array $lines, bool $addNl=false): void { + $outf = $this->outf; + foreach ($lines as $line) { + if ($indent !== null) $this->_fwrite($outf, $indent); + $this->_fwrite($outf, $line); + if ($addNl) $this->_fwrite($outf, "\n"); + } + if ($this->flush) @fflush($outf); + } + + function write(...$values): void { + $this->writeLines(null, $this->getLines(true, ...$values)); + } + + function print(...$values): void { + $values[] = "\n"; + $this->writeLines(null, $this->getLines(true, ...$values)); + } + + function iwrite(int $indentLevel, ...$values): void { + $indent = $this->getIndent($indentLevel); + $this->writeLines($indent, $this->getLines(true, ...$values)); + } + + function iprint(int $indentLevel, ...$values): void { + $values[] = "\n"; + $indent = $this->getIndent($indentLevel); + $this->writeLines($indent, $this->getLines(true, ...$values)); + } +} diff --git a/php/src/output/std/_IMessenger.php b/php/src/output/std/_IMessenger.php new file mode 100644 index 0000000..9b54b59 --- /dev/null +++ b/php/src/output/std/_IMessenger.php @@ -0,0 +1,19 @@ +offsetExists($key)) return $array->offsetGet($key); + else return $default; + } else { + if (!is_array($array)) $array = cl::with($array); + return cl::get($array, $key, $default); + } + } + + /** spécifier la valeur d'une clé */ + static final function set(&$array, $key, $value) { + if ($array instanceof ArrayAccess) { + $array->offsetSet($key, $value); + } else { + cl::set($array, $key, $value); + } + return $value; + } + + /** initialiser $dest avec les valeurs de $values */ + static final function set_values(&$array, ?array $values): void { + if ($values === null) return; + foreach ($values as $key => $value) { + self::set($array, $key, $value); + } + } + + /** incrémenter la valeur de la clé */ + static final function inc(&$array, $key): int { + if ($array instanceof ArrayAccess) { + $value = (int)$array->offsetGet($key); + $array->offsetSet($key, ++$value); + return $value; + } else { + A::ensure_array($array); + $value = (int)cl::get($array, $key); + return $array[$key] = ++$value; + } + } + + /** décrémenter la valeur de la clé */ + static final function dec(&$array, $key, bool $allow_negative=false): int { + if ($array instanceof ArrayAccess) { + $value = (int)$array->offsetGet($key); + if ($allow_negative || $value > 0) $array->offsetSet($key, --$value); + return $value; + } else { + A::ensure_array($array); + $value = (int)cl::get($array, $key); + if ($allow_negative || $value > 0) $array[$key] = --$value; + return $value; + } + } + + /** + * fusionner $merge dans la valeur de la clé, qui est d'abord transformé en + * tableau si nécessaire + */ + static final function merge(&$array, $key, $merge): void { + if ($array instanceof ArrayAccess) { + $value = $array->offsetGet($key); + $value = cl::merge($value, $merge); + $array->offsetSet($key, $value); + } else { + A::ensure_array($array); + $array[$key] = cl::merge($array[$key], $merge); + } + } + + /** + * ajouter $value à la valeur de la clé, qui est d'abord transformé en + * tableau si nécessaire + */ + static final function append(&$array, $key, $value): void { + if ($array instanceof ArrayAccess) { + $value = $array->offsetGet($key); + cl::set($value, null, $value); + $array->offsetSet($key, $value); + } else { + A::ensure_array($array); + cl::set($array[$key], null, $value); + } + } +} diff --git a/php/src/php/coll/AutoArray.php b/php/src/php/coll/AutoArray.php new file mode 100644 index 0000000..82d1904 --- /dev/null +++ b/php/src/php/coll/AutoArray.php @@ -0,0 +1,44 @@ +has($name)) return true; + $properties = self::_AUTO_PROPERTIES(); + if ($properties === null) return false; + return array_key_exists($name, $properties); + } + function __get($name) { + $properties = self::_AUTO_PROPERTIES(); + if ($this->has($name)) return $this->get($name); + $pkey = cl::get($properties, $name, $name); + return cl::pget($this->data, $pkey); + } + function __set($name, $value) { + $properties = self::_AUTO_PROPERTIES(); + if ($this->has($name)) { + $this->set($name, $value); + } else { + $pkey = cl::get($properties, $name, $name); + cl::pset($this->data, $pkey, $value); + } + } + function __unset($name) { + $properties = self::_AUTO_PROPERTIES(); + if ($this->has($name)) { + $this->del($name); + } else { + $pkey = cl::get($properties, $name, $name); + cl::pdel($this->data, $pkey); + } + } +} diff --git a/php/src/php/coll/BaseArray.php b/php/src/php/coll/BaseArray.php new file mode 100644 index 0000000..ff809fc --- /dev/null +++ b/php/src/php/coll/BaseArray.php @@ -0,0 +1,117 @@ +reset($data); + } + + /** @var array */ + protected $data; + + function &wrappedArray(): ?array { return $this->data; } + + function __toString(): string { return var_export($this->data, true); } + #function __debugInfo() { return $this->data; } + function reset(?array &$data): void { $this->data =& $data; } + function count(): int { return $this->data !== null? count($this->data): 0; } + function keys(): array { return $this->data !== null? array_keys($this->data): []; } + + ############################################################################# + # base + + function has($key): bool { + return $this->data !== null && array_key_exists($key, $this->data); + } + function &get($key, $default=null) { + if ($this->data !== null && array_key_exists($key, $this->data)) { + return $this->data[$key]; + } else return $default; + } + function set($key, $value): void { + if ($key === null) $this->data[] = $value; + else $this->data[$key] = $value; + } + function del($key): void { + unset($this->data[$key]); + } + + function offsetExists($offset): bool { return $this->has($offset); } + function &offsetGet($offset) { return $this->get($offset); } + function offsetSet($offset, $value) { $this->set($offset, $value); } + function offsetUnset($offset) { $this->del($offset); } + + function __isset($name) { return $this->has($name); } + function &__get($name) { return $this->get($name); } + function __set($name, $value) { $this->set($name, $value); } + function __unset($name) { $this->del($name); } + + ############################################################################# + # iterator + + /** @var bool */ + private $valid = false; + + function rewind() { + if ($this->data !== null) { + $first = reset($this->data); + $this->valid = $first !== false || key($this->data) !== null; + } else { + $this->valid = false; + } + } + function valid(): bool { return $this->valid; } + function key() { return key($this->data); } + function current() { return current($this->data); } + function next() { + $next = next($this->data); + $this->valid = $next !== false || key($this->data) !== null; + } + + ############################################################################# + # divers + + function phas($pkey): bool { return cl::phas($this->data, $pkey); } + function pget($pkey, $default=null): bool { return cl::pget($this->data, $pkey, $default); } + function pset($pkey, $value): void { cl::pset($this->data, $pkey, $value); } + function pdel($pkey): void { cl::pdel($this->data, $pkey); } + + function contains($value, bool $strict=false): bool { + if ($value === null || $this->data === null) return false; + return in_array($value, $this->data, $strict); + } + + function add($value, bool $unique=true, bool $strict=false): bool { + if ($unique && $this->contains($value, $strict)) return false; + $this->set(null, $value); + return true; + } + + function addAll(?array $values, bool $unique=true, bool $strict=false): void { + if ($values === null) return; + $index = 0; + foreach ($values as $key => $value) { + if ($key === $index) { + $this->add($value, $unique, $strict); + $index++; + } else { + $this->set($key, $value); + } + } + } + + function resetAll(?array $values): void { + $this->data = null; + $this->addAll($values); + } +} diff --git a/php/src/php/content/IContent.php b/php/src/php/content/IContent.php new file mode 100644 index 0000000..7fd4386 --- /dev/null +++ b/php/src/php/content/IContent.php @@ -0,0 +1,11 @@ +content = $content; + } + + protected IContent $content; + + function print(): void { + $content = $this->content->getContent(); + c::write($content); + } + + function __call($name, $args) { + $content = func::call([$this->content, $name], ...$args); + c::write($content); + } +} diff --git a/php/src/php/content/README.md b/php/src/php/content/README.md new file mode 100644 index 0000000..0e7eb5a --- /dev/null +++ b/php/src/php/content/README.md @@ -0,0 +1,77 @@ +# nulib\php\content + +un contenu (ou "content") est une liste de valeurs, avec une syntaxe pour que +certains éléments soient dynamiquement calculés. + +le contenu final est résolu selon les règles suivantes: +- Si le contenu n'est pas un tableau: + - une chaine est quotée avec `htmlspecialchars()` + - un scalaire ou une instance d'objet sont pris tels quels +- Sinon, le contenu doit être un tableau, séquentiel ou associatif, ça n'a pas + d'incidence + - les éléments scalaires ou instance d'objets sont pris tels quels + - les Closure sont appelés dès la résolution, et leur valeur de retour est + considéré comme un contenu *statique* inséré tel quel dans le flux i.e dans + l'exemple suivant $c1 et $c2 sont globalement équivalents: + ~~~php + $closure = function() { ... } + $c1 = [...$before, $closure, ...$after]; + $c2 = [...$before, ...c::q($closure()), ...$after]; + # $c1 == $c2, sauf si $closure() retourne des valeurs qui peuvent être + # considérées comme du contenu dynamique + ~~~ + - les tableaux représentent un traitement dynamique: appel de fonction, + instanciation, etc. le contenu effectif n'est évalué que lors de l'affichage + +Les syntaxes possibles sont: + +`[[], $args...]` +: contenu statique: les valeurs $args... sont insérées dans le flux du contenu + sans modification. c'est la seule façon d'insérer un tableau dans la liste des + valeurs (on peut aussi utiliser une Closure, mais ce n'est pas toujours + possible, notamment si le contenu est une constante) + +`["class_or_function", $args...]` +`[["class_or_function"], $args...]` +`[["function", $args0...], $args1...]` +`[["class", null, $args0...], $args1...]` +: instantiation ou appel de fonction + +`["->method", $args...]` +`[["->method"], $args...]` +`[[null, "method"], $args...]` +`[[null, "method", $args0...], $args1...]` +: appel de méthode sur l'objet contexte spécifié lors de la résolution du contenu + +`[[$object, "method"], $args...]` +`[[$object, "method", $args0...], $args1...]` +: appel de méthode sur l'objet spécifié + +`[["class", "method"], $args...]` +`[["class", "method", $args0...], $args1...]` +: appel de méthode statique de la classe spécifiée + +Le fait de transformer un contenu en une liste de valeurs statiques s'appelle +la résolution. la résolution se fait par rapport à un objet contexte, qui est +utilisé lors des appels de méthodes. + +Lors des appels de fonctions ou des instanciations, les $arguments sont tous des +contenus: +- une valeur scalaire ou une instance est passée inchangée +- un tableau est traité comme un contenu avec les règles ci-dessus + +## Affichage d'un contenu + +Deux interfaces sont utilisées pour modéliser un élément de contenu à afficher: +- IContent: objet capable de produire du contenu +- IPrintable: objet capable d'afficher un contenu + +Tous les autres éléments de contenus sont transformés en string avant affichage. +Un système de formatters permet de définir des fonctions ou méthodes à utiliser +pour formatter des objets de certains types. + +Lors de l'affichage du contenu, deux éléments contigûs $a et $b sont affichés +séparés par un espace si $a se termine par un mot (éventuellement terminé par +un point '.') et $b commence par un mot. + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/php/src/php/content/c.php b/php/src/php/content/c.php new file mode 100644 index 0000000..9506835 --- /dev/null +++ b/php/src/php/content/c.php @@ -0,0 +1,178 @@ + $svalue) { + if ($skey === $sindex) { + $sindex++; + if ($seq) { + $dest[] = $svalue; + } else { + # la première sous-clé séquentielle est ajoutée avec la clé du + # merge statique + $dest[$key] = $svalue; + $seq = true; + } + } else { + $dest[$skey] = $svalue; + } + } + + } + + /** résoudre le contenu, et retourner la liste des valeurs */ + static final function resolve($content, $object_or_class=null, bool $quote=true, ?array &$dest=null): array { + if ($dest === null) $dest = []; + $content = $quote? self::q($content): self::nq($content); + $index = 0; + foreach ($content as $key => $value) { + if ($key === $index) { + $index++; + $seq = true; + } else { + $seq = false; + } + if ($value instanceof Closure) { + # contenu dynamique: le contenu est la valeur de retour de la fonction + # ce contenu est rajouté à la suite après avoir été quoté avec self::q() + $func = $value; + nur_func::ensure_func($func, $object_or_class, $args); + $values = self::q(nur_func::call($func, ...$args)); + self::add_static_content($dest, $values, $key, $seq); + continue; + } + if (is_array($value)) { + # contenu dynamique + if (count($value) == 0) continue; + $func = cl::first($value); + $args = array_slice($value, 1); + if ($func === []) { + # merge statique + self::add_static_content($dest, $args, $key, $seq); + continue; + } else { + # chaque argument de la fonction à appeler est aussi un contenu + foreach ($args as &$arg) { + $array = is_array($arg); + $arg = self::resolve($arg, $object_or_class, false); + if (!$array) $arg = $arg[0]; + }; unset($arg); + if (nur_func::is_static($func)) { + nur_func::ensure_func($func, $object_or_class, $args); + $value = nur_func::call($func, ...$args); + } elseif (nur_func::is_class($func)) { + nur_func::fix_class_args($func, $args); + $value = nur_func::cons($func, ...$args); + } else { + nur_func::ensure_func($func, $object_or_class, $args); + $value = nur_func::call($func, ...$args); + } + } + } + if ($seq) $dest[] = $value; + else $dest[$key] = $value; + } + return $dest; + } + const resolve = [self::class, "resolve"]; + + private static function wend(?string $value): bool { + return $value !== null && preg_match('/(\w|\w\.)$/', $value); + } + private static function startw(?string $value): bool { + return $value !== null && preg_match('/^\w/', $value); + } + + private static function to_values($content, ?array &$values=null): void { + $pvalue = cl::last($values); + $wend = self::wend($pvalue); + foreach ($content as $value) { + if ($value === null || $value === false) { + continue; + } elseif ($value instanceof IContent) { + self::to_values($value->getContent(), $values); + continue; + } elseif ($value instanceof IPrintable) { + ob_start(null, 0, PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE); + $value->print(); + $value = ob_get_clean(); + } else { + $value = strval($value); + #XXX rendre paramétrable le formatage de $value + } + if ($value !== "") { + $startw = self::startw($value); + if ($wend && $startw) $values[] = " "; + $values[] = $value; + $wend = self::wend($value); + } + } + } + + /** écrire le contenu sur la resource spécifiée, qui vaut STDOUT par défaut */ + static final function write($content, $fd=null, bool $resolve=true): void { + if ($resolve) $content = self::resolve($content); + $wend = false; + foreach ($content as $value) { + if ($value === null || $value === false) { + continue; + } elseif ($value instanceof IPrintable) { + if ($fd === null) { + $value->print(); + } else { + ob_start(null, 0, PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE); + $value->print(); + fwrite($fd, ob_get_clean()); + } + $wend = false; + continue; + } elseif ($value instanceof IContent) { + $values = []; + self::to_values($content, $values); + $value = implode("", $values); + } else { + $value = strval($value); + #XXX rendre paramétrable le formattage de $value + } + $startw = self::startw($value); + if (!$wend && !$startw) $value = " $value"; + if ($fd === null) echo $value; + else fwrite($fd, $value); + $wend = self::wend($value); + } + } + + /** retourner le contenu sous forme de chaine */ + static final function to_string($content, bool $resolve=true): string { + if ($resolve) $content = self::resolve($content); + $values = []; + self::to_values($content, $values); + return implode("", $values); + } +} diff --git a/php/src/php/func.php b/php/src/php/func.php new file mode 100644 index 0000000..63ea334 --- /dev/null +++ b/php/src/php/func.php @@ -0,0 +1,646 @@ +"; + } + + private static function _is_nfunction(?string $f): bool { + return strpos($f, "\\") !== false; + } + + private static function _parse_static(?string &$m): bool { + $pos = strpos($m, "::"); + if ($pos === false) return false; + $m = substr($m, $pos + 2); + return true; + } + + private static function _parse_method(?string &$m): bool { + $pos = strpos($m, "->"); + if ($pos === false) return false; + $m = substr($m, $pos + 2); + return true; + } + + ############################################################################# + # Fonctions + + /** + * vérifier que $func est une fonction et la normaliser le cas échéant. + * retourner true si c'est une fonction, false sinon + * + * les formes suivantes sont supportées: + * - "function" si une classe du même nom n'existe pas déjà + * - [false, "function", ...$args] c'est la forme normalisée + * + * @param bool $strict vérifier l'inexistence de la classe et l'existence de + * la fonction (ne pas uniquement faire une vérification syntaxique) + */ + static function verifix_function(&$func, bool $strict=true, ?string &$reason=null): bool { + if ($strict) { + $msg = var_export($func, true); + $reason = null; + } + if ($func instanceof ReflectionFunction) return true; + if (is_string($func)) { + $c = false; + $f = $func; + } elseif (is_array($func)) { + if (!array_key_exists(0, $func)) return false; + $c = $func[0]; + if (!array_key_exists(1, $func)) return false; + $f = $func[1]; + } else { + return false; + } + if ($c !== false) return false; + if (!is_string($f)) return false; + if (self::_is_invalid($f)) return false; + if (self::_parse_static($f)) return false; + if (self::_parse_method($f)) return false; + if ($strict) { + $reason = null; + if (class_exists($f)) { + $reason = "$msg: is a class"; + return false; + } + if (!function_exists($f)) { + $reason = "$msg: function not found"; + return false; + } + } + $func = [false, $f]; + return true; + } + + /** + * vérifier que $func est une fonction avec les règles de + * {@link self::verifix_function()} + */ + static function is_function($func, bool $strict=true, ?string &$reason=null): bool { + return self::verifix_function($func, $strict, $reason); + } + + ############################################################################# + # Classes + + /** + * vérifier que $func est une classe et la normaliser le cas échéant. + * retourner true si c'est une classe, false sinon + * + * les formes suivantes sont supportées: + * - "class" + * - ["class", false, ...$args] c'est la forme normalisée + * + * @param bool $strict vérifier l'existence de la classe (ne pas uniquement + * faire une vérification syntaxique) + */ + static function verifix_class(&$func, bool $strict=true, ?string &$reason=null): bool { + if ($strict) { + $msg = var_export($func, true); + $reason = null; + } + if ($func instanceof ReflectionClass) return true; + if (is_string($func)) { + $c = $func; + $f = false; + } elseif (is_array($func)) { + if (!array_key_exists(0, $func)) return false; + $c = $func[0]; + if (!array_key_exists(1, $func)) return false; + $f = $func[1]; + } else { + return false; + } + if (!is_string($c)) return false; + if (self::_is_invalid($c)) return false; + if (self::_parse_static($c)) return false; + if (self::_parse_method($c)) return false; + if ($f !== false) return false; + if ($strict) { + if (!class_exists($c)) { + $reason = "$msg: class not found"; + return false; + } + } + $func = [$c, false]; + return true; + } + + /** + * vérifier que $func est une classe avec les règles de + * {@link self::verifix_class()} + */ + static function is_class($func, bool $strict=true, ?string &$reason=null): bool { + return self::verifix_class($func, $strict, $reason); + } + + ############################################################################# + # Méthodes statiques + + private static function _parse_class_s(?string $cs, ?string &$c, ?string &$s): bool { + if (self::_is_invalid($cs) || self::_parse_method($cs)) return false; + $pos = strpos($cs, "::"); + if ($pos === false) return false; + if ($pos === 0) return false; + $tmpc = substr($cs, 0, $pos); + $cs = substr($cs, $pos + 2); + if (self::_is_nfunction($cs)) return false; + [$c, $s] = [$tmpc, cv::vn($cs)]; + return true; + } + + private static function _parse_c_static(?string $cs, ?string &$c, ?string &$s, ?bool &$bound): bool { + if (self::_is_invalid($cs) || self::_parse_method($cs)) return false; + $pos = strpos($cs, "::"); + if ($pos === false) return false; + if ($pos == strlen($cs) - 2) return false; + if ($pos > 0) { + $tmpc = substr($cs, 0, $pos); + $bound = true; + } else { + $tmpc = null; + $bound = false; + } + $cs = substr($cs, $pos + 2); + if (self::_is_nfunction($cs)) return false; + [$c, $s] = [$tmpc, cv::vn($cs)]; + return true; + } + + /** + * vérifier que $func est une méthode statique, et la normaliser le cas + * échéant. retourner true si c'est une méthode statique, false sinon + * + * les formes suivantes sont supportées (XXX étant null ou n'importe quelle + * valeur scalaire de n'importe quel type sauf false) + * - "XXX::function" + * - ["XXX::function", ...$args] + * - [XXX, "::function", ...$args] + * - [XXX, "function", ...$args] c'est la forme normalisée + * + * Si XXX est une classe, la méthode statique est liée. sinon, elle doit être + * liée à une classe avant d'être utilisée + * + * @param bool $strict vérifier l'existence de la classe et de la méthode si + * la méthode est liée (ne pas uniquement faire une vérification syntaxique) + */ + static function verifix_static(&$func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool { + if ($strict) { + $msg = var_export($func, true); + $reason = null; + } + if ($func instanceof ReflectionMethod) { + $bound = false; + return true; + } + if (is_string($func)) { + if (!self::_parse_c_static($func, $c, $f, $bound)) return false; + $cf = [$c, $f]; + } elseif (is_array($func)) { + $cf = $func; + if (!array_key_exists(0, $cf)) return false; + $c = $cf[0]; + if ($c === false) return false; + if (is_object($c)) $c = get_class($c); + if (is_string($c)) { + if (self::_is_invalid($c)) return false; + if (self::_parse_class_s($c, $c, $f)) { + $cf[0] = $c; + if ($f !== null) { + # ["class::method"] --> ["class", "method"] + array_splice($cf, 1, 0, [$f]); + } + $bound = true; + } elseif (self::_parse_c_static($c, $c, $f, $bound)) { + # ["::method"] --> [null, "method"] + array_splice($cf, 0, 0, [null]); + $cf[1] = $f; + } else { + $cf[0] = $c; + $bound = is_string($c); + } + } else { + $cf[0] = null; + $bound = false; + } + # + if (!array_key_exists(1, $cf)) return false; + $f = $cf[1]; + if (!is_string($f)) return false; + if (self::_parse_c_static($f, $rc, $f, $rbound)) { + if ($rc !== null && $c === null) { + $c = $rc; + $bound = $rbound; + } + } else { + if (self::_is_invalid($f)) return false; + if (self::_is_nfunction($f)) return false; + if (self::_parse_method($f)) return false; + self::_parse_static($f); + } + $cf[1] = $f; + } else { + return false; + } + if ($strict) { + $reason = null; + if ($bound) { + if (!class_exists($c)) { + $reason = "$msg: class not found"; + return false; + } + if (!method_exists($c, $f)) { + $reason = "$msg: method not found"; + return false; + } + } else { + $reason = "$msg: not bound"; + } + } + $func = $cf; + return true; + } + + /** + * vérifier que $func est une méthode statique avec les règles de + * {@link self::verifix_static()} + */ + static function is_static($func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool { + return self::verifix_static($func, $strict, $bound, $reason); + } + + ############################################################################# + # Méthodes non statiques + + private static function _parse_class_m(?string $cm, ?string &$c, ?string &$m): bool { + if (self::_is_invalid($cm) || self::_parse_static($cm)) return false; + $pos = strpos($cm, "->"); + if ($pos === false) return false; + if ($pos === 0) return false; + $tmpc = substr($cm, 0, $pos); + $cm = substr($cm, $pos + 2); + if (self::_is_nfunction($cm)) return false; + [$c, $m] = [$tmpc, cv::vn($cm)]; + return true; + } + + private static function _parse_c_method(?string $cm, ?string &$c, ?string &$m, ?bool &$bound): bool { + if (self::_is_invalid($cm) || self::_parse_static($cm)) return false; + $pos = strpos($cm, "->"); + if ($pos === false) return false; + if ($pos == strlen($cm) - 2) return false; + if ($pos > 0) { + $tmpc = substr($cm, 0, $pos); + $bound = true; + } else { + $tmpc = null; + $bound = false; + } + $cm = substr($cm, $pos + 2); + if (self::_is_nfunction($cm)) return false; + [$c, $m] = [$tmpc, cv::vn($cm)]; + return true; + } + + /** + * vérifier que $func est une méthode non statique, et la normaliser le cas + * échéant. retourner true si c'est une méthode non statique, false sinon + * + * les formes suivantes sont supportées (XXX étant null ou n'importe quelle + * valeur scalaire de n'importe quel type sauf false) + * - "XXX->function" + * - ["XXX->function", ...$args] + * - [XXX, "->function", ...$args] + * - [XXX, "function", ...$args] c'est la forme normalisée + * + * Si XXX est une classe ou un objet, la méthode est liée. dans tous les cas, + * elle doit être liée à un objet avant d'être utilisée + * + * @param bool $strict vérifier l'existence de la classe et de la méthode si + * la méthode est liée (ne pas uniquement faire une vérification syntaxique) + */ + static function verifix_method(&$func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool { + if ($strict) { + $msg = var_export($func, true); + $reason = null; + } + if ($func instanceof ReflectionMethod) { + $bound = false; + return true; + } + if (is_string($func)) { + if (!self::_parse_c_method($func, $c, $f, $bound)) return false; + $cf = [$c, $f]; + } elseif (is_array($func)) { + $cf = $func; + if (!array_key_exists(0, $cf)) return false; + $c = $cf[0]; + if ($c === false) return false; + if (is_object($c)) { + $bound = true; + } elseif (is_string($c)) { + if (self::_is_invalid($c)) return false; + if (self::_parse_class_m($c, $c, $f)) { + $cf[0] = $c; + if ($f !== null) { + # ["class->method"] --> ["class", "method"] + array_splice($cf, 1, 0, [$f]); + } + $bound = true; + } elseif (self::_parse_c_method($c, $c, $f, $bound)) { + # ["->method"] --> [null, "method"] + array_splice($cf, 0, 0, [null]); + $cf[1] = $f; + } else { + $cf[0] = $c; + $bound = is_string($c); + } + } else { + $cf[0] = null; + $bound = false; + } + # + if (!array_key_exists(1, $cf)) return false; + $f = $cf[1]; + if (!is_string($f)) return false; + if (self::_parse_c_method($f, $rc, $f, $rbound)) { + if ($rc !== null && $c === null) { + $c = $rc; + $bound = $rbound; + } + } else { + if (self::_is_invalid($f)) return false; + if (self::_is_nfunction($f)) return false; + if (self::_parse_static($f)) return false; + self::_parse_method($f); + } + $cf[1] = $f; + } else { + return false; + } + if ($strict) { + $reason = null; + if ($bound) { + if (!is_object($c) && !class_exists($c)) { + $reason = "$msg: class not found"; + return false; + } + if (!method_exists($c, $f)) { + $reason = "$msg: method not found"; + return false; + } + } else { + $reason = "$msg: not bound"; + } + } + $func = $cf; + return true; + } + + /** + * vérifier que $func est une méthode non statique avec les règles de + * {@link self::verifix_method()} + */ + static function is_method($func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool { + return self::verifix_method($func, $strict, $bound, $reason); + } + + ############################################################################# + # func + + const TYPE_MASK = 0b11; + + const FLAG_STATIC = 0b100; + + const TYPE_CLOSURE = 0, TYPE_FUNCTION = 1, TYPE_CLASS = 2, TYPE_METHOD = 3; + + const TYPE_STATIC = self::TYPE_METHOD | self::FLAG_STATIC; + + protected static function not_a_callable($func, ?string $reason) { + if ($reason === null) { + $msg = var_export($func, true); + $reason = "$msg: not a callable"; + } + return new ValueException($reason); + } + + static function with($func, ?array $args=null, bool $strict=true): self { + if (!is_array($func)) { + if ($func instanceof Closure) { + return new self(self::TYPE_CLOSURE, $func, $args); + } elseif ($func instanceof ReflectionFunction) { + return new self(self::TYPE_FUNCTION, $func, $args); + } elseif ($func instanceof ReflectionClass) { + return new self(self::TYPE_CLASS, $func, $args); + } elseif ($func instanceof ReflectionMethod) { + return new self(self::TYPE_METHOD, $func, $args, false); + } + } + if (self::verifix_function($func, $strict, $reason)) { + return new self(self::TYPE_FUNCTION, $func, $args, false, $reason); + } elseif (self::verifix_class($func, $strict, $reason)) { + return new self(self::TYPE_CLASS, $func, $args, false, $reason); + } elseif (self::verifix_method($func, $strict, $bound, $reason)) { + return new self(self::TYPE_METHOD, $func, $args, $bound, $reason); + } elseif (self::verifix_static($func, $strict, $bound, $reason)) { + return new self(self::TYPE_STATIC, $func, $args, $bound, $reason); + } + throw self::not_a_callable($func, $reason); + } + + static function ensure($func, ?array $args=null, bool $strict=true): self { + $func = self::with($func, $args, $strict); + if (!$func->isBound()) { + throw self::not_a_callable($func->func, $func->reason); + } + return $func; + } + + static function check($func, ?array $args=null, bool $strict=true): bool { + try { + self::ensure($func, $args, $strict); + return true; + } catch (Exception $e) { + return false; + } + } + + static function call($func, ...$args) { + return self::with($func)->invoke($args); + } + + ############################################################################# + + protected function __construct(int $type, $func, ?array $args=null, bool $bound=false, ?string $reason=null) { + $flags = $type & ~self::TYPE_MASK; + $type = $type & self::TYPE_MASK; + $object = null; + $prefixArgs = []; + if (!is_array($func)) { + $reflection = $func; + $func = null; + } else { + if (count($func) > 2) { + $prefixArgs = array_slice($func, 2); + $func = array_slice($func, 0, 2); + } + [$c, $f] = $func; + switch ($type) { + case self::TYPE_FUNCTION: + $reflection = new ReflectionFunction($f); + break; + case self::TYPE_CLASS: + $reflection = new ReflectionClass($c); + break; + case self::TYPE_METHOD: + if ($c === null) { + $reflection = null; + } else { + $reflection = new ReflectionMethod($c, $f); + if (is_object($c)) $object = $c; + } + break; + default: + throw StateException::unexpected_state(); + } + } + A::merge($prefixArgs, $args); + + $this->type = $type; + $this->flags = $flags; + $this->func = $func; + $this->bound = $bound; + $this->reason = $reason; + $this->object = $object; + $this->prefixArgs = $prefixArgs; + $this->updateReflection($reflection); + } + + protected int $type; + + protected int $flags; + + protected ?array $func; + + protected bool $bound; + + protected ?string $reason; + + protected ?object $object; + + protected array $prefixArgs; + + /** @var Closure|ReflectionFunction|ReflectionMethod|ReflectionClass */ + protected $reflection; + + protected bool $variadic; + + protected int $minArgs; + + protected int $maxArgs; + + protected function updateReflection($reflection): void { + $variadic = false; + $minArgs = $maxArgs = 0; + if ($reflection instanceof Closure) { + $r = new ReflectionFunction($reflection); + $variadic = $r->isVariadic(); + $minArgs = $r->getNumberOfRequiredParameters(); + $maxArgs = $r->getNumberOfParameters(); + } elseif ($reflection instanceof ReflectionClass) { + $r = $reflection->getConstructor(); + if ($r === null) { + $variadic = false; + $minArgs = $maxArgs = 0; + } else { + $variadic = $r->isVariadic(); + $minArgs = $r->getNumberOfRequiredParameters(); + $maxArgs = $r->getNumberOfParameters(); + } + } elseif ($reflection !== null) { + $variadic = $reflection->isVariadic(); + $minArgs = $reflection->getNumberOfRequiredParameters(); + $maxArgs = $reflection->getNumberOfParameters(); + } + $this->reflection = $reflection; + $this->variadic = $variadic; + $this->minArgs = $minArgs; + $this->maxArgs = $maxArgs; + } + + function isBound(): bool { + if ($this->type !== self::TYPE_METHOD) return true; + if ($this->flags & self::FLAG_STATIC) return $this->bound; + else return $this->bound && $this->object !== null; + } + + function bind($object): self { + if ($this->type !== self::TYPE_METHOD) return $this; + + [$c, $f] = $this->func; + if ($this->reflection === null) { + $this->func[0] = $c = $object; + $this->updateReflection(new ReflectionMethod($c, $f)); + } + if (is_object($object) && !($this->flags & self::FLAG_STATIC)) { + if (is_object($c)) $c = get_class($c); + if (is_string($c) && !($object instanceof $c)) { + throw ValueException::invalid_type($object, $c); + } + $this->object = $object; + $this->bound = true; + } + return $this; + } + + function invoke(?array $args=null) { + $args = array_merge($this->prefixArgs, $args ?? []); + if (!$this->variadic) $args = array_slice($args, 0, $this->maxArgs); + $minArgs = $this->minArgs; + while (count($args) < $minArgs) $args[] = null; + + switch ($this->type) { + case self::TYPE_CLOSURE: + /** @var Closure $closure */ + $closure = $this->reflection; + return $closure(...$args); + case self::TYPE_FUNCTION: + /** @var ReflectionFunction $function */ + $function = $this->reflection; + return $function->invoke(...$args); + case self::TYPE_METHOD: + /** @var ReflectionMethod $method */ + $method = $this->reflection; + if ($method === null) throw self::not_a_callable($this->func, $this->reason); + return $method->invoke($this->object, ...$args); + case self::TYPE_CLASS: + /** @var ReflectionClass $class */ + $class = $this->reflection; + return $class->newInstance(...$args); + default: + throw StateException::unexpected_state(); + } + } +} diff --git a/php/src/php/iter/AbstractIterator.php b/php/src/php/iter/AbstractIterator.php new file mode 100644 index 0000000..1e9f991 --- /dev/null +++ b/php/src/php/iter/AbstractIterator.php @@ -0,0 +1,154 @@ +rewind(); + } + + ############################################################################# + # Implémentation par défaut + + private $setup = false; + + protected function _hasIteratorBeenSetup(): bool { + return $this->setup; + } + + private $valid = false; + private $toredown = true; + + private $index = 0; + protected $key; + protected $item = null; + + function key() { + return $this->key; + } + + function current() { + return $this->item; + } + + function next(): void { + if ($this->toredown) return; + $this->valid = false; + try { + $item = $this->iter_next($key); + } catch (NoMoreDataException $e) { + $this->iter_beforeClose(); + try { + $this->iter_teardown(); + } catch (Exception $e) { + } + $this->toredown = true; + return; + } + $this->iter_cook($item); + $this->item = $item; + if ($key !== null) { + $this->key = $key; + } else { + $this->index++; + $this->key = $this->index; + } + $this->valid = true; + } + + function rewind(): void { + if ($this->setup) { + if (!$this->toredown) { + $this->iter_beforeClose(); + try { + $this->iter_teardown(); + } catch (Exception $e) { + } + } + $this->setup = false; + $this->valid = false; + $this->toredown = true; + $this->index = 0; + $this->key = null; + $this->item = null; + } + } + + function valid(): bool { + if (!$this->setup) { + try { + $this->iter_setup(); + } catch (Exception $e) { + } + $this->setup = true; + $this->toredown = false; + $this->iter_beforeStart(); + $this->next(); + } + return $this->valid; + } +} diff --git a/php/src/php/mprop.php b/php/src/php/mprop.php new file mode 100644 index 0000000..a0bc28d --- /dev/null +++ b/php/src/php/mprop.php @@ -0,0 +1,122 @@ +getMethod($method); + } catch (ReflectionException $e) { + return oprop::get($object, $property, $default); + } + return nur_func::call([$object, $m], $default); + } + + /** spécifier la valeur d'une propriété */ + static final function set(object $object, string $property, $value, ?string $method=null) { + $c = new ReflectionClass($object); + return self::_set($c, $object, $property, $value, $method); + } + + private static function _set(ReflectionClass $c, object $object, string $property, $value, ?string $method) { + if ($method === null) $method = self::get_setter_name($property); + try { + $m = $c->getMethod($method); + } catch (ReflectionException $e) { + return oprop::_set($c, $object, $property, $value); + } + nur_func::call([$object, $m], $value); + return $value; + } + + /** + * initialiser $dest avec les valeurs de $values + * + * les noms des clés de $values sont transformées en camelCase pour avoir les + * noms des propriétés correspondantes + */ + static final function set_values(object $object, ?array $values, ?array $keys=null): void { + if ($values === null) return; + if ($keys === null) $keys = array_keys($values); + $c = new ReflectionClass($object); + foreach ($keys as $key) { + if (array_key_exists($key, $values)) { + $property = str::us2camel($key); + self::_set($c, $object, $property, $values[$key], null); + } + } + } + + /** incrémenter la valeur d'une propriété */ + static final function inc(object $object, string $property): int { + $value = intval(self::get($object, $property, 0)); + $value++; + self::set($object, $property, $value); + return $value; + } + + /** décrémenter la valeur d'une propriété */ + static final function dec(object $object, string $property, bool $allow_negative=false): int { + $value = intval(self::get($object, $property, 0)); + if ($allow_negative || $value > 0) { + $value--; + self::set($object, $property, $value); + } + return $value; + } + + /** + * Fusionner la valeur à la propriété qui est transformée en tableau si + * nécessaire + */ + static final function merge(object $object, string $property, $array): void { + $values = cl::with(self::get($object, $property)); + $values = cl::merge($values, cl::with($array)); + self::set($object, $property, $values); + } + + /** + * Ajouter la valeur à la propriété qui est transformée en tableau si + * nécessaire + */ + static final function append(object $object, string $property, $value): void { + $values = cl::with(self::get($object, $property)); + $values[] = $value; + self::set($object, $property, $values); + } +} diff --git a/php/src/php/nur_func.php b/php/src/php/nur_func.php new file mode 100644 index 0000000..ba4cc06 --- /dev/null +++ b/php/src/php/nur_func.php @@ -0,0 +1,453 @@ + 1) { + if (!array_key_exists(1, $func)) return false; + if (!is_string($func[1]) || strlen($func[1]) == 0) return false; + if (strpos($func[1], "\\") !== false) return false; + return true; + } + } + return false; + } + + /** + * si $func est une chaine de la forme "::method" alors la remplacer par la + * chaine "$class::method" + * + * si $func est un tableau de la forme ["method"] ou [null, "method"], alors + * le remplacer par [$class, "method"] + * + * on assume que {@link is_static()}($func) retourne true + * + * @return bool true si la correction a été faite + */ + static final function fix_static(&$func, $class): bool { + if (is_object($class)) $class = get_class($class); + + if (is_string($func) && substr($func, 0, 2) == "::") { + $func = "$class$func"; + return true; + } elseif (is_array($func) && array_key_exists(0, $func)) { + $count = count($func); + if ($count == 1) { + $func = [$class, $func[0]]; + return true; + } elseif ($count > 1 && $func[0] === null) { + $func[0] = $class; + return true; + } + } + return false; + } + + /** tester si $method est une chaine de la forme "->method" */ + private static function isam($method): bool { + return is_string($method) + && strlen($method) > 2 + && substr($method, 0, 2) == "->"; + } + + /** + * tester si $func est une chaine de la forme "->method" ou un tableau de la + * forme ["->method", ...] ou [anything, "->method", ...] + */ + static final function is_method($func): bool { + if (is_string($func)) { + return self::isam($func); + } elseif (is_array($func) && array_key_exists(0, $func)) { + if (self::isam($func[0])) { + # ["->method", ...] + return true; + } + if (array_key_exists(1, $func) && self::isam($func[1])) { + # [anything, "->method", ...] + return true; + } + } + return false; + } + + /** + * si $func est une chaine de la forme "->method" alors la remplacer par le + * tableau [$object, "method"] + * + * si $func est un tableau de la forme ["->method"] ou [anything, "->method"], + * alors le remplacer par [$object, "method"] + * + * @return bool true si la correction a été faite + */ + static final function fix_method(&$func, $object): bool { + if (!is_object($object)) return false; + + if (is_string($func)) { + if (self::isam($func)) { + $func = [$object, substr($func, 2)]; + return true; + } + } elseif (is_array($func) && array_key_exists(0, $func)) { + if (self::isam($func[0])) $func = array_merge([null], $func); + if (count($func) > 1 && array_key_exists(1, $func) && self::isam($func[1])) { + $func[0] = $object; + $func[1] = substr($func[1], 2); + return true; + } + } + return false; + } + + /** + * si $func est un tableau de plus de 2 éléments, alors déplacer les éléments + * supplémentaires au début de $args. par exemple: + * ~~~ + * $func = ["class", "method", "arg1", "arg2"]; + * $args = ["arg3"]; + * func::fix_args($func, $args) + * # $func === ["class", "method"] + * # $args === ["arg1", "arg2", "arg3"] + * ~~~ + * + * @return bool true si la correction a été faite + */ + static final function fix_args(&$func, ?array &$args): bool { + if ($args === null) $args = []; + if (is_array($func) && count($func) > 2) { + $prefix_args = array_slice($func, 2); + $func = array_slice($func, 0, 2); + $args = array_merge($prefix_args, $args); + return true; + } + return false; + } + + /** + * s'assurer que $func est un appel de méthode ou d'une méthode statique; + * et renseigner le cas échéant les arguments. si $func ne fait pas mention + * de la classe ou de l'objet, le renseigner avec $class_or_object. + * + * @return bool true si c'est une fonction valide. il ne reste plus qu'à + * l'appeler avec {@link call()} + */ + static final function check_func(&$func, $class_or_object, &$args=null): bool { + if ($func instanceof Closure) return true; + if (self::is_method($func)) { + # méthode + self::fix_method($func, $class_or_object); + self::fix_args($func, $args); + return true; + } elseif (self::is_static($func)) { + # méthode statique + self::fix_static($func, $class_or_object); + self::fix_args($func, $args); + return true; + } + return false; + } + + /** + * Comme {@link check_func()} mais lance une exception si la fonction est + * invalide + * + * @throws ValueException si $func n'est pas une fonction ou une méthode valide + */ + static final function ensure_func(&$func, $class_or_object, &$args=null): void { + if (!self::check_func($func, $class_or_object, $args)) { + throw ValueException::invalid_type($func, "callable"); + } + } + + static final function _prepare($func): array { + $object = null; + if (is_callable($func)) { + if (is_array($func)) { + $rf = new ReflectionMethod(...$func); + $object = $func[0]; + if (is_string($object)) $object = null; + } elseif ($func instanceof Closure) { + $rf = new ReflectionFunction($func); + } elseif (is_string($func) && strpos($func, "::") === false) { + $rf = new ReflectionFunction($func); + } else { + $rf = new ReflectionMethod($func); + } + } elseif ($func instanceof ReflectionMethod) { + $rf = $func; + } elseif ($func instanceof ReflectionFunction) { + $rf = $func; + } elseif (is_array($func) && count($func) == 2 && isset($func[0]) && isset($func[1]) + && ($func[1] instanceof ReflectionMethod || $func[1] instanceof ReflectionFunction)) { + $object = $func[0]; + if (is_string($object)) $object = null; + $rf = $func[1]; + } elseif (is_string($func) && strpos($func, "::") === false) { + $rf = new ReflectionFunction($func); + } else { + throw ValueException::invalid_type($func, "callable"); + } + $minArgs = $rf->getNumberOfRequiredParameters(); + $maxArgs = $rf->getNumberOfParameters(); + $variadic = $rf->isVariadic(); + return [$rf instanceof ReflectionMethod, $object, $rf, $minArgs, $maxArgs, $variadic]; + } + + static final function _fill(array $context, array &$args): void { + $minArgs = $context[3]; + $maxArgs = $context[4]; + $variadic = $context[5]; + if (!$variadic) $args = array_slice($args, 0, $maxArgs); + while (count($args) < $minArgs) $args[] = null; + } + + static final function _call($context, array $args) { + self::_fill($context, $args); + $use_object = $context[0]; + $object = $context[1]; + $method = $context[2]; + if ($use_object) { + if (count($args) === 0) return $method->invoke($object); + else return $method->invokeArgs($object, $args); + } else { + if (count($args) === 0) return $method->invoke(); + else return $method->invokeArgs($args); + } + } + + /** + * Appeler la fonction spécifiée avec les arguments spécifiés. + * Adapter $args en fonction du nombre réel d'arguments de $func + * + * @param callable|ReflectionFunction|ReflectionMethod $func + */ + static final function call($func, ...$args) { + return self::_call(self::_prepare($func), $args); + } + + /** remplacer $value par $func($value, ...$args) */ + static final function apply(&$value, $func, ...$args): void { + if ($func !== null) { + if ($args) $args = array_merge([$value], $args); + else $args = [$value]; + $value = self::call($func, ...$args); + } + } + + const MASK_PS = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC; + const MASK_P = ReflectionMethod::IS_PUBLIC; + const METHOD_PS = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC; + const METHOD_P = ReflectionMethod::IS_PUBLIC; + + private static function matches(string $name, array $includes, array $excludes): bool { + if ($includes) { + $matches = false; + foreach ($includes as $include) { + if (substr($include, 0, 1) == "/") { + # expression régulière + if (preg_match($include, $name)) { + $matches = true; + break; + } + } else { + # tester la présence de la sous-chaine + if (strpos($name, $include) !== false) { + $matches = true; + break; + } + } + } + if (!$matches) return false; + } + foreach ($excludes as $exclude) { + if (substr($exclude, 0, 1) == "/") { + # expression régulière + if (preg_match($exclude, $name)) return false; + } else { + # tester la présence de la sous-chaine + if (strpos($name, $exclude) !== false) return false; + } + } + return true; + } + + /** @var Schema */ + private static $call_all_params_schema; + + /** + * retourner la liste des méthodes de $class_or_object qui correspondent au + * filtre $options. le filtre doit respecter le schéme {@link CALL_ALL_PARAMS_SCHEMA} + */ + static function get_all($class_or_object, $params=null): array { + Schema::nv($paramsv, $params, null + , self::$call_all_params_schema, ref_func::CALL_ALL_PARAMS_SCHEMA); + if (is_callable($class_or_object, true) && is_array($class_or_object)) { + # callable sous forme de tableau + $class_or_object = $class_or_object[0]; + } + if (is_string($class_or_object)) { + # lister les méthodes publiques statiques de la classe + $mask = self::MASK_PS; + $expected = self::METHOD_PS; + $c = new ReflectionClass($class_or_object); + } elseif (is_object($class_or_object)) { + # lister les méthodes publiques de la classe + $c = new ReflectionClass($class_or_object); + $mask = $params["static_only"]? self::MASK_PS: self::MASK_P; + $expected = $params["static_only"]? self::METHOD_PS: self::METHOD_P; + } else { + throw new ValueException("$class_or_object: vous devez spécifier une classe ou un objet"); + } + $prefix = $params["prefix"]; $prefixlen = strlen($prefix); + $args = $params["args"]; + $includes = $params["include"]; + $excludes = $params["exclude"]; + $methods = []; + foreach ($c->getMethods() as $m) { + if (($m->getModifiers() & $mask) != $expected) continue; + $name = $m->getName(); + if (substr($name, 0, $prefixlen) != $prefix) continue; + if (!self::matches($name, $includes, $excludes)) continue; + $methods[] = cl::merge([$class_or_object, $name], $args); + } + return $methods; + } + + /** + * Appeler toutes les méthodes publiques de $object_or_class et retourner un + * tableau [$method_name => $return_value] des valeurs de retour. + */ + static final function call_all($class_or_object, $params=null): array { + $methods = self::get_all($class_or_object, $params); + $values = []; + foreach ($methods as $method) { + self::fix_args($method, $args); + $values[$method[1]] = self::call($method, ...$args); + } + return $values; + } + + /** + * tester si $func est une chaine de la forme "XXX" où XXX est une classe + * valide, ou un tableau de la forme ["XXX", ...] + * + * NB: il est possible d'avoir {@link is_static()} et {@link is_class()} + * vraies pour la même valeur. s'il faut supporter les deux cas, appeler + * {@link is_static()} d'abord, mais dans ce cas, on ne supporte que les + * classes qui sont dans un package + */ + static final function is_class($class): bool { + if (is_string($class)) { + return class_exists($class); + } elseif (is_array($class) && array_key_exists(0, $class)) { + return class_exists($class[0]); + } + return false; + } + + /** + * en assumant que {@link is_class()} est vrai, si $class est un tableau de + * plus de 1 éléments, alors déplacer les éléments supplémentaires au début de + * $args. par exemple: + * ~~~ + * $class = ["class", "arg1", "arg2"]; + * $args = ["arg3"]; + * func::fix_class_args($class, $args) + * # $class === "class" + * # $args === ["arg1", "arg2", "arg3"] + * ~~~ + * + * @return bool true si la correction a été faite + */ + static final function fix_class_args(&$class, ?array &$args): bool { + if ($args === null) $args = []; + if (is_array($class)) { + if (count($class) > 1) { + $prefix_args = array_slice($class, 1); + $class = array_slice($class, 0, 1)[0]; + $args = array_merge($prefix_args, $args); + } else { + $class = $class[0]; + } + return true; + } + return false; + } + + /** + * s'assurer que $class est une classe et renseigner le cas échéant les + * arguments. + * + * @return bool true si c'est une classe valide. il ne reste plus qu'à + * l'instancier avec {@link cons()} + */ + static final function check_class(&$class, &$args=null): bool { + if (self::is_class($class)) { + self::fix_class_args($class, $args); + return true; + } + return false; + } + + /** + * Comme {@link check_class()} mais lance une exception si la classe est + * invalide + * + * @throws ValueException si $class n'est pas une classe valide + */ + static final function ensure_class(&$class, &$args=null): void { + if (!self::check_class($class, $args)) { + throw ValueException::invalid_type($class, "class"); + } + } + + /** + * Instancier la classe avec les arguments spécifiés. + * Adapter $args en fonction du nombre réel d'arguments du constructeur + */ + static final function cons(string $class, ...$args) { + $c = new ReflectionClass($class); + $rf = $c->getConstructor(); + if ($rf === null) { + return $c->newInstance(); + } else { + if (!$rf->isVariadic()) { + $minArgs = $rf->getNumberOfRequiredParameters(); + $maxArgs = $rf->getNumberOfParameters(); + $args = array_slice($args, 0, $maxArgs); + while (count($args) < $minArgs) { + $args[] = null; + } + } + return $c->newInstanceArgs($args); + } + } +} diff --git a/php/src/php/oprop.php b/php/src/php/oprop.php new file mode 100644 index 0000000..599645e --- /dev/null +++ b/php/src/php/oprop.php @@ -0,0 +1,152 @@ +getProperty($property); + $p->setAccessible(true); + return $p->getValue($object); + } catch (ReflectionException $e) { + if (property_exists($object, $property)) return $object->$property; + else return $default; + } + } + + static final function _set(ReflectionClass $c, object $object, string $property, $value) { + try { + $p = $c->getProperty($property); + $p->setAccessible(true); + $p->setValue($object, $value); + } catch (ReflectionException $e) { + $object->$property = $value; + } + return $value; + } + + /** spécifier la valeur d'une propriété */ + static final function set(object $object, string $property, $value) { + $c = new ReflectionClass($object); + return self::_set($c, $object, $property, $value); + } + + /** + * initialiser $dest avec les valeurs de $values + * + * les noms des clés de $values sont transformées en camelCase pour avoir les + * noms des propriétés correspondantes + */ + static final function set_values(object $object, ?array $values, ?array $keys=null): void { + if ($values === null) return; + if ($keys === null) $keys = array_keys($values); + $c = new ReflectionClass($object); + foreach ($keys as $key) { + if (array_key_exists($key, $values)) { + $property = str::us2camel($key); + self::_set($c, $object, $property, $values[$key]); + } + } + } + + /** incrémenter la valeur d'une propriété */ + static final function inc(object $object, string $property): int { + $c = new ReflectionClass($object); + try { + $p = $c->getProperty($property); + $p->setAccessible(true); + $value = (int)$p->getValue($object); + $value++; + $p->setValue($object, $value); + } catch (ReflectionException $e) { + if (property_exists($object, $property)) { + $value = (int)$object->$property; + $value++; + } else { + $value = 1; + } + $object->$property = $value; + } + return $value; + } + + /** décrémenter la valeur d'une propriété */ + static final function dec(object $object, string $property, bool $allow_negative=false): int { + $c = new ReflectionClass($object); + try { + $p = $c->getProperty($property); + $p->setAccessible(true); + $value = (int)$p->getValue($object); + if ($allow_negative || $value > 0) { + $value --; + $p->setValue($object, $value); + } + } catch (ReflectionException $e) { + if (property_exists($object, $property)) { + $value = (int)$object->$property; + } else { + $value = 0; + } + if ($allow_negative || $value > 0) $value--; + $object->$property = $value; + } + return $value; + } + + /** + * Fusionner la valeur à la propriété qui est transformée en tableau si + * nécessaire + */ + static final function merge(object $object, string $property, $array): void { + $c = new ReflectionClass($object); + try { + $p = $c->getProperty($property); + $p->setAccessible(true); + $values = cl::with($p->getValue($object)); + $values = cl::merge($values, cl::with($array)); + $p->setValue($object, $values); + } catch (ReflectionException $e) { + if (property_exists($object, $property)) { + $values = cl::with($object->$property); + } else { + $values = []; + } + $values = cl::merge($values, cl::with($array)); + $object->$property = $values; + } + } + + /** + * Ajouter la valeur à la propriété qui est transformée en tableau si + * nécessaire + */ + static final function append(object $object, string $property, $value): void { + $c = new ReflectionClass($object); + try { + $p = $c->getProperty($property); + $p->setAccessible(true); + $values = cl::with($p->getValue($object)); + $values[] = $value; + $p->setValue($object, $values); + } catch (ReflectionException $e) { + if (property_exists($object, $property)) { + $values = cl::with($object->$property); + } else { + $values = []; + } + $values[] = $value; + $object->$property = $values; + } + } +} diff --git a/php/src/php/time/Date.php b/php/src/php/time/Date.php new file mode 100644 index 0000000..3556ed4 --- /dev/null +++ b/php/src/php/time/Date.php @@ -0,0 +1,20 @@ +setTime(0, 0); + } + + function format($format=self::DEFAULT_FORMAT): string { + return \DateTime::format($format); + } +} diff --git a/php/src/php/time/DateInterval.php b/php/src/php/time/DateInterval.php new file mode 100644 index 0000000..c9ca935 --- /dev/null +++ b/php/src/php/time/DateInterval.php @@ -0,0 +1,59 @@ +y; + $m = $interval->m; + $d = $interval->d; + if ($y > 0) $string .= "${y}Y"; + if ($m > 0) $string .= "${m}M"; + if ($d > 0) $string .= "${d}D"; + $string .= "T"; + $h = $interval->h; + $i = $interval->i; + $s = $interval->s; + if ($h > 0) $string .= "${h}H"; + if ($i > 0) $string .= "${i}M"; + if ($s > 0 || $string == "PT") $string .= "${s}S"; + if ($interval->invert == 1) $string = "-$string"; + return $string; + } + + function __construct($duration) { + if (is_int($duration)) $duration = "PT${duration}S"; + if ($duration instanceof \DateInterval) { + $this->y = $duration->y; + $this->m = $duration->m; + $this->d = $duration->d; + $this->h = $duration->h; + $this->i = $duration->i; + $this->s = $duration->s; + $this->invert = $duration->invert; + $this->days = $duration->days; + } elseif (!is_string($duration)) { + throw new InvalidArgumentException("duration must be a string"); + } else { + if (substr($duration, 0, 1) == "-") { + $duration = substr($duration, 1); + $invert = true; + } else { + $invert = false; + } + parent::__construct($duration); + if ($invert) $this->invert = 1; + } + } + + function __toString(): string { + return self::to_string($this); + } +} diff --git a/php/src/php/time/DateTime.php b/php/src/php/time/DateTime.php new file mode 100644 index 0000000..9238b97 --- /dev/null +++ b/php/src/php/time/DateTime.php @@ -0,0 +1,265 @@ +format("Ymd\\THis"); + $Z = $datetime->format("P"); + if ($Z === "+00:00") $Z = "Z"; + return "$YmdHMS$Z"; + } + + const DEFAULT_FORMAT = "d/m/Y H:i:s"; + const INT_FORMATS = [ + "year" => "Y", + "month" => "m", + "day" => "d", + "hour" => "H", + "minute" => "i", + "second" => "s", + "wday" => "N", + "wnum" => "W", + ]; + const STRING_FORMATS = [ + "timezone" => "P", + "datetime" => "d/m/Y H:i:s", + "date" => "d/m/Y", + "Ymd" => "Ymd", + "YmdHMS" => "Ymd\\THis", + "YmdHMSZ" => [self::class, "_YmdHMSZ_format"], + ]; + + static function clone(DateTimeInterface $dateTime): self { + if ($dateTime instanceof static) return clone $dateTime; + $clone = new static(); + $clone->setTimestamp($dateTime->getTimestamp()); + $clone->setTimezone($dateTime->getTimezone()); + return $clone; + } + + /** + * corriger une année à deux chiffres qui est située dans le passé et + * retourner l'année à 4 chiffres. + * + * par exemple, si l'année courante est 2019, alors: + * - fix_past_year('18') === '2018' + * - fix_past_year('19') === '1919' + * - fix_past_year('20') === '1920' + */ + static function fix_past_year(int $year): int { + if ($year < 100) { + $y = getdate(); $y = $y["year"]; + $r = $y % 100; + $c = $y - $r; + if ($year >= $r) $year += $c - 100; + else $year += $c; + } + return $year; + } + + /** + * corriger une année à deux chiffres et retourner l'année à 4 chiffres. + * l'année charnière entre année passée et année future est 70 + * + * par exemple, si l'année courante est 2019, alors: + * - fix_past_year('18') === '2018' + * - fix_past_year('19') === '2019' + * - fix_past_year('20') === '2020' + * - fix_past_year('69') === '2069' + * - fix_past_year('70') === '1970' + * - fix_past_year('71') === '1971' + */ + static function fix_any_year(int $year): int { + if ($year < 100) { + $y = intval(date("Y")); + $r = $y % 100; + $c = $y - $r; + if ($year >= 70) $year += $c - 100; + else $year += $c; + } + return $year; + } + + function __construct($datetime="now", DateTimeZone $timezone=null) { + $datetime ??= "now"; + if ($datetime instanceof \DateTimeInterface) { + if ($timezone === null) $timezone = $datetime->getTimezone(); + parent::__construct(); + $this->setTimestamp($datetime->getTimestamp()); + $this->setTimezone($timezone); + } elseif (is_int($datetime)) { + parent::__construct("now", $timezone); + $this->setTimestamp($datetime); + } elseif (!is_string($datetime)) { + throw new InvalidArgumentException("datetime must be a string or an instance of DateTimeInterface"); + } else { + $Y = $H = $Z = null; + if (preg_match(self::DMY_PATTERN, $datetime, $ms)) { + $Y = $ms[3] ?? null; + if ($Y !== null) $Y = self::fix_any_year(intval($Y)); + else $Y = intval(date("Y")); + $m = intval($ms[2]); + $d = intval($ms[1]); + } elseif (preg_match(self::YMD_PATTERN, $datetime, $ms)) { + $Y = $ms[1]; + if (strlen($Y) == 2) $Y = self::fix_any_year(intval($ms[1])); + else $Y = intval($Y); + $m = intval($ms[2]); + $d = intval($ms[3]); + } elseif (preg_match(self::DMYHIS_PATTERN, $datetime, $ms)) { + $Y = $ms[3]; + if ($Y !== null) $Y = self::fix_any_year(intval($Y)); + else $Y = intval(date("Y")); + $m = intval($ms[2]); + $d = intval($ms[1]); + $H = intval($ms[4]); + $M = intval($ms[5]); + $S = intval($ms[6] ?? 0); + } elseif (preg_match(self::YMDHISZ_PATTERN, $datetime, $ms)) { + $Y = $ms[1]; + if (strlen($Y) == 2) $Y = self::fix_any_year(intval($ms[1])); + else $Y = intval($Y); + $m = intval($ms[2]); + $d = intval($ms[3]); + $H = intval($ms[4]); + $M = intval($ms[5]); + $S = intval($ms[6] ?? 0); + $Z = $ms[7] ?? null; + } + if ($Y !== null) { + if ($H === null) $datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d); + else $datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H, $M, $S); + if ($Z !== null) $timezone = new DateTimeZone("UTC"); + } + parent::__construct($datetime, $timezone); + } + } + + function diff($target, $absolute=false): DateInterval { + return new DateInterval(parent::diff($target, $absolute)); + } + + function format($format=self::DEFAULT_FORMAT): string { + return \DateTime::format($format); + } + + /** + * modifier cet objet pour que l'heure soit à 00:00:00 ce qui le rend propice + * à l'utilisation comme borne inférieure d'une période + */ + function wrapStartOfDay(): self { + $this->setTime(0, 0); + return $this; + } + + /** + * modifier cet objet pour que l'heure soit à 23:59:59.999999 ce qui le rend + * propice à l'utilisation comme borne supérieure d'une période + */ + function wrapEndOfDay(): self { + $this->setTime(23, 59, 59, 999999); + return $this; + } + + function getPrevDay(int $nbDays=1, bool $skipWeekend=false): self { + if ($nbDays == 1 && $skipWeekend && $this->wday == 1) { + $nbdays = 3; + } + return static::with($this->sub(new \DateInterval("P${nbDays}D"))); + } + + function getNextDay(int $nbDays=1, bool $skipWeekend=false): self { + if ($nbDays == 1 && $skipWeekend) { + $wday = $this->wday; + if ($wday > 5) $nbDays = 8 - $this->wday; + } + return static::with($this->add(new \DateInterval("P${nbDays}D"))); + } + + function getElapsedAt(?DateTime $now=null, ?int $resolution=null): string { + return Elapsed::format_at($this, $now, $resolution); + } + + function getElapsedSince(?DateTime $now=null, ?int $resolution=null): string { + return Elapsed::format_since($this, $now, $resolution); + } + + function getElapsedDelay(?DateTime $now=null, ?int $resolution=null): string { + return Elapsed::format_delay($this, $now, $resolution); + } + + function __toString(): string { + return $this->format(); + } + + function __get($name) { + if (array_key_exists($name, self::INT_FORMATS)) { + $format = self::INT_FORMATS[$name]; + if (is_callable($format)) return $format($this); + else return intval($this->format($format)); + } elseif (array_key_exists($name, self::STRING_FORMATS)) { + $format = self::STRING_FORMATS[$name]; + if (is_callable($format)) return $format($this); + else return $this->format($format); + } + throw new InvalidArgumentException("Unknown property $name"); + } +} diff --git a/php/src/php/time/Delay.php b/php/src/php/time/Delay.php new file mode 100644 index 0000000..14a5307 --- /dev/null +++ b/php/src/php/time/Delay.php @@ -0,0 +1,174 @@ + [0, 5], + "d" => [1, 5], + "h" => [1, 0], + "m" => [1, 0], + "s" => [1, 0], + ]; + + static function compute_dest(int $x, string $u, ?int $y, DateTime $from): array { + $dest = DateTime::clone($from); + $yu = null; + switch ($u) { + case "w": + if ($x > 0) { + $x *= 7; + $dest->add(new \DateInterval("P${x}D")); + } + $w = 7 - intval($dest->format("w")); + $dest->add(new \DateInterval("P${w}D")); + $yu = "h"; + break; + case "d": + $dest->add(new \DateInterval("P${x}D")); + $yu = "h"; + break; + case "h": + $dest->add(new \DateInterval("PT${x}H")); + $yu = "m"; + break; + case "m": + $dest->add(new \DateInterval("PT${x}M")); + $yu = "s"; + break; + case "s": + $dest->add(new \DateInterval("PT${x}S")); + break; + } + if ($y !== null && $yu !== null) { + $h = intval($dest->format("H")); + $m = intval($dest->format("i")); + switch ($yu) { + case "h": + $dest->setTime($y, 0, 0, 0); + break; + case "m": + $dest->setTime($h, $y, 0, 0); + break; + case "s": + $dest->setTime($h, $m, $y, 0); + break; + } + } + $u = strtoupper($u); + $repr = $y !== null? "$x$u$y": "$x"; + return [$dest, $repr]; + } + + function __construct($delay, ?DateTimeInterface $from=null) { + if ($from === null) $from = new DateTime(); + if ($delay === "INF") { + $dest = DateTime::clone($from); + $dest->add(new DateInterval("P9999Y")); + $repr = "INF"; + } elseif (is_int($delay)) { + [$dest, $repr] = self::compute_dest($delay, "s", null, $from); + } elseif (is_string($delay) && preg_match('/^\d+$/', $delay)) { + $x = intval($delay); + [$dest, $repr] = self::compute_dest($x, "s", null, $from); + } elseif (is_string($delay) && preg_match('/^(\d*)([wdhms])(\d*)$/i', $delay, $ms)) { + [$x, $u, $y] = [$ms[1], $ms[2], $ms[3]]; + $u = strtolower($u); + $default = self::DEFAULTS[$u]; + if ($x === "") $x = $default[0]; + else $x = intval($x); + if ($y === "") $y = $default[1]; + else $y = intval($y); + [$dest, $repr] = self::compute_dest($x, $u, $y, $from); + } else { + throw new InvalidArgumentException("invalid delay"); + } + $this->dest = $dest; + $this->repr = $repr; + } + + function __serialize(): array { + return [$this->dest, $this->repr]; + } + function __unserialize(array $data): void { + [$this->dest, $this->repr] = $data; + } + + /** @var DateTime */ + protected $dest; + + function getDest(): DateTime { + return $this->dest; + } + + function addDuration($duration) { + if (is_int($duration) && $duration < 0) { + $this->dest->sub(DateInterval::with(-$duration)); + } else { + $this->dest->add(DateInterval::with($duration)); + } + } + + function subDuration($duration) { + if (is_int($duration) && $duration < 0) { + $this->dest->add(DateInterval::with(-$duration)); + } else { + $this->dest->sub(DateInterval::with($duration)); + } + } + + /** @var string */ + protected $repr; + + function __toString(): string { + return $this->repr; + } + + protected function _getDiff(?DateTimeInterface $now=null): \DateInterval { + if ($now === null) $now = new DateTime(); + return $this->dest->diff($now); + } + + /** retourner true si le délai imparti est écoulé */ + function isElapsed(?DateTimeInterface $now=null): bool { + if ($this->repr === "INF") return false; + else return $this->_getDiff($now)->invert == 0; + } + + /** + * retourner l'intervalle entre le moment courant et la destination. + * + * l'intervalle est négatif si le délai n'est pas écoulé, positif sinon + */ + function getDiff(?DateTimeInterface $now=null): DateInterval { + return new DateInterval($this->_getDiff($now)); + } +} diff --git a/php/src/php/time/Elapsed.php b/php/src/php/time/Elapsed.php new file mode 100644 index 0000000..37f22c6 --- /dev/null +++ b/php/src/php/time/Elapsed.php @@ -0,0 +1,174 @@ + 0) { + if ($text !== "") $text .= " "; + $text .= sprintf("%d jour", $d); + if ($d > 1) $text .= "s"; + } + if ($h > 0) { + if ($text !== "") $text .= " "; + $text .= sprintf("%d heure", $h); + if ($h > 1) $text .= "s"; + } + if ($m > 0) { + if ($text !== "") $text .= " "; + $text .= sprintf("%d minute", $m); + if ($m > 1) $text .= "s"; + } + return $text; + } + + private static function format_seconds(int $seconds, string $prefix, ?string $zero): string { + $seconds = abs($seconds); + + if ($zero === null) $zero = "maintenant"; + if ($seconds == 0) return $zero; + + if ($seconds <= 3) { + if ($prefix !== "") $prefix .= " "; + return "${prefix}quelques secondes"; + } + + if ($seconds < 60) { + if ($prefix !== "") $prefix .= " "; + return sprintf("${prefix}%d secondes", $seconds); + } + + $oneDay = 60 * 60 * 24; + $oneHour = 60 * 60; + $oneMinute = 60; + $rs = $seconds; + $d = intdiv($rs, $oneDay); $rs = $rs % $oneDay; + $h = intdiv($rs, $oneHour); $rs = $rs % $oneHour; + $m = intdiv($rs, $oneMinute); + return self::format_generic($prefix, $d, $h, $m); + } + + private static function format_minutes(int $seconds, string $prefix, ?string $zero): string { + $minutes = intdiv(abs($seconds), 60); + + if ($zero === null) $zero = "maintenant"; + if ($minutes == 0) return $zero; + + if ($minutes <= 3) { + if ($prefix !== "") $prefix .= " "; + return "${prefix}quelques minutes"; + } + + $oneDay = 60 * 24; + $oneHour = 60; + $rs = $minutes; + $d = intdiv($rs, $oneDay); $rs = $rs % $oneDay; + $h = intdiv($rs, $oneHour); $rs = $rs % $oneHour; + $m = $rs; + return self::format_generic($prefix, $d, $h, $m); + } + + private static function format_hours(int $seconds, string $prefix, ?string $zero): string { + $minutes = intdiv(abs($seconds), 60); + + if ($zero === null) $zero = "maintenant"; + if ($minutes == 0) return $zero; + + if ($minutes < 60) { + if ($prefix !== "") $prefix .= " "; + return "${prefix}moins d'une heure"; + } elseif ($minutes < 120) { + if ($prefix !== "") $prefix .= " "; + return "${prefix}moins de deux heures"; + } + + $oneDay = 60 * 24; + $oneHour = 60; + $rs = $minutes; + $d = intdiv($rs, $oneDay); $rs = $rs % $oneDay; + $h = intdiv($rs, $oneHour); + return self::format_generic($prefix, $d, $h, 0); + } + + private static function format_days(int $seconds, string $prefix, ?string $zero): string { + $hours = intdiv(abs($seconds), 60 * 60); + + if ($zero === null) $zero = "aujourd'hui"; + if ($hours < 24) return $zero; + + $oneDay = 24; + $rs = $hours; + $d = intdiv($rs, $oneDay); + return self::format_generic($prefix, $d, 0, 0); + } + + static function format_at(DateTime $start, ?DateTime $now=null, ?int $resolution=null): string { + $now ??= new DateTime(); + $seconds = $now->getTimestamp() - $start->getTimestamp(); + return (new self($seconds, $resolution))->formatAt(); + } + + static function format_since(DateTime $start, ?DateTime $now=null, ?int $resolution=null): string { + $now ??= new DateTime(); + $seconds = $now->getTimestamp() - $start->getTimestamp(); + return (new self($seconds, $resolution))->formatSince(); + } + + static function format_delay(DateTime $start, ?DateTime $now=null, ?int $resolution=null): string { + $now ??= new DateTime(); + $seconds = $now->getTimestamp() - $start->getTimestamp(); + return (new self($seconds, $resolution))->formatDelay(); + } + + function __construct(int $seconds, ?int $resolution=null) { + $resolution ??= static::DEFAULT_RESOLUTION; + if ($resolution < self::RESOLUTION_SECONDS) $resolution = self::RESOLUTION_SECONDS; + elseif ($resolution > self::RESOLUTION_DAYS) $resolution = self::RESOLUTION_DAYS; + $this->seconds = $seconds; + $this->resolution = $resolution; + } + + /** @var int */ + private $seconds; + + /** @var int */ + private $resolution; + + function formatAt(): string { + $seconds = $this->seconds; + if ($seconds < 0) return self::format($seconds, $this->resolution, "dans"); + else return self::format($seconds, $this->resolution, "il y a"); + } + + function formatSince(): string { + $seconds = $this->seconds; + if ($seconds < 0) return self::format(-$seconds, $this->resolution, "dans"); + else return self::format($seconds, $this->resolution, "depuis"); + } + + function formatDelay(): string { + return self::format($this->seconds, $this->resolution, "", "immédiat"); + } +} diff --git a/php/src/php/valm.php b/php/src/php/valm.php new file mode 100644 index 0000000..99d5961 --- /dev/null +++ b/php/src/php/valm.php @@ -0,0 +1,84 @@ + [null, null, "tableau contenant des paramètres et des options par défaut"], + "merge_arrays" => [null, null, "liste de tableaux à merger à celui-ci avant de calculer la liste effective des options"], + "merge" => [null, null, "tableau à merger à celui-ci avant de calculer la liste effective des options", + # si merge_arrays et merge sont spécifiés tous les deux, "merge" est mergé après "merge_arrays" + ], + "prefix" => [null, null, "texte à afficher avant l'aide générée automatiquement"], + "name" => [null, null, "nom du programme, utilisé pour l'affichage de l'aide"], + "purpose" => [null, null, "courte description de l'objet de ce programme"], + "usage" => [null, null, "exposé textuel des arguments valides du programme", + # ce peut être une chaine e.g '[options] SRC DESC' + # ou un tableau auquel cas autant de lignes que nécessaire sont affichées + ], + "description" => [null, null, "description longue de l'objet du programme, affiché après usage"], + "suffix" => [null, null, "texte à afficher après l'aide générée automatiquement"], + "dynamic_command" => [null, null, "fonction indiquant si une commande est valide", + # la signature de la fonction est function(string $command):?array + # elle doit retourner un tableau au format DEFS_SCHEMA qui définit la + # commande spécifiée, ou null si ce n'est pas une commande valide + ], + "sections" => [null, null, "liste de sections permettant de grouper les arguments"], + "commandname" => [null, null, "propriété ou clé qui obtient la commande courante", + # la valeur par défaut est "command" si ni commandproperty ni commandkey ne sont définis + ], + "commandproperty" => [null, null, "comme commandname mais force l'utilisation d'une propriété"], + "commandkey" => [null, null, "comme commandname mais force l'utilisation d'une clé"], + "argsname" => [null, null, "propriété ou clé qui obtient les arguments restants", + # la valeur par défaut est "args" si ni argsproperty ni argskey ne sont définis + ], + "argsproperty" => [null, null, "comme argsname mais force l'utilisation d'une propriété"], + "argskey" => [null, null, "comme argsname mais force l'utilisation d'une clé"], + "autohelp" => ["?bool", null, "faut-il ajouter automatiquement le support de l'option --help"], + "autoremains" => ["?bool", null, "faut-il ajouter automatiquement la prise en compte des arguments restants"], + ]; + + const SECTION_SCHEMA = [ + "show" => ["bool", true, "faut-il afficher cette section?"], + "title" => [null, null, "titre de la section"], + "prefix" => [null, null, "texte à afficher avant l'aide générée automatiquement"], + "suffix" => [null, null, "texte à afficher après l'aide générée automatiquement"], + + # ces valeurs sont calculées + "defs" => [null, null, "(interne) liste des définitions de cette section"], + ]; + + const DEF_SCHEMA = [ + "set_defaults" => [null, null, "tableau contenant des paramètres par défaut"], + "merge_arrays" => [null, null, "liste de tableaux à merger à celui-ci"], + "merge" => [null, null, "tableau à merger à celui-ci", + # si merge_arrays et merge sont spécifiés tous les deux, "merge" est mergé après "merge_arrays" + ], + "kind" => [null, null, "type de définition: 'option' ou 'command'"], + "arg" => [null, null, "type de l'argument attendu par l'option"], + "args" => [null, null, "type des arguments attendus par l'option", + # si args est spécifié, arg est ignoré + ], + "argsdesc" => [null, null, "description textuelle des arguments, utilisé pour l'affichage de l'aide"], + "type" => [null, null, "types dans lesquels convertir les arguments avant de les fournir à l'utilisateur"], + "action" => [null, null, "fonction à appeler quand cette option est utilisée", + # la signature de la fonction est ($value, $name, $arg, $dest, $def) + ], + "name" => [null, null, "propriété ou clé à initialiser en réponse à l'utilisation de cette option", + # le nom à spécifier est au format under_score, qui est transformée en camelCase si la destination est un objet + ], + "property" => [null, null, "comme name mais force l'utilisation d'une propriété"], + "key" => [null, null, "comme name mais force l'utilisation d'une clé"], + "inverse" => ["bool", false, "décrémenter la destination au lieu de l'incrémenter pour une option sans argument"], + "value" => ["mixed", null, "valeur à forcer au lieu d'incrémenter la destination"], + "ensure_array" => [null, null, "forcer la destination à être un tableau"], + "help" => [null, null, "description de cette option, utilisé pour l'affichage de l'aide"], + "cmd_args" => [null, null, "définition des sous-options pour une commande"], + + # ces valeurs sont calculées + "cmd_defs" => [null, null, "(interne) liste des définitions correspondant au paramètre options"], + ]; + + const ARGS_ALLOWED_VALUES = ["value", "path", "dir", "file", "host"]; +} diff --git a/php/src/ref/file/csv/ref_csv.php b/php/src/ref/file/csv/ref_csv.php new file mode 100644 index 0000000..22fcb9c --- /dev/null +++ b/php/src/ref/file/csv/ref_csv.php @@ -0,0 +1,32 @@ + ["string", null, "Ne sélectionner que les méthode dont le nom commence par ce préfixe"], + "args" => ["?array", null, "Arguments avec lesquels appeler les méthodes"], + "static_only" => ["bool", false, "N'appeler que les méthodes statiques si un objet est spécifié"], + "include" => ["?array", null, "N'inclure que les méthodes dont le nom correspond à ce motif, qui peut être une sous-chaine ou une expression régulière"], + "exclude" => ["?array", null, "Exclure les méthodes dont le nom correspond à ce motif, qui peut être une sous-chaine ou une expression régulière"], + ]; +} diff --git a/php/src/ref/schema/ref_analyze.php b/php/src/ref/schema/ref_analyze.php new file mode 100644 index 0000000..060d68b --- /dev/null +++ b/php/src/ref/schema/ref_analyze.php @@ -0,0 +1,25 @@ + ["string", null, "nature du schéma", + "pkey" => 0, + "allowed_values" => ["assoc", "list", "scalar"], + ], + "title" => ["?string", null, "libellé de la valeur"], + "required" => ["bool", false, "la valeur est-elle requise?"], + "nullable" => ["?bool", null, "la valeur peut-elle être nulle?"], + "desc" => ["?content", null, "description de la valeur"], + "name" => ["?key", null, "identifiant de la valeur"], + "schema" => ["?array", null, "définition du schéma"], + ]; + + /** @var array meta-schema d'un schéma de nature scalaire */ + const SCALAR_METASCHEMA = [ + "type" => ["array", null, "types possibles de la valeur", "required" => true], + "default" => [null, null, "valeur par défaut si la valeur n'existe pas"], + "title" => ["?string", null, "libellé de la valeur"], + "required" => ["bool", false, "la valeur est-elle requise?"], + "nullable" => ["?bool", null, "la valeur peut-elle être nulle?"], + "desc" => ["?content", null, "description de la valeur"], + "analyzer_func" => ["?callable", null, "fonction qui analyse une valeur entrante et indique comment la traiter"], + "extractor_func" => ["?callable", null, "fonction qui extrait la valeur à analyser dans une chaine de caractère"], + "parser_func" => ["?callable", null, "fonction qui analyse une chaine de caractères pour produire la valeur"], + "normalizer_func" => ["?callable", null, "fonction qui normalise la valeur"], + "messages" => ["?array", null, "messages à afficher en cas d'erreur d'analyse"], + "formatter_func" => ["?callable", null, "fonction qui formatte la valeur pour affichage"], + "format" => [null, null, "format à utiliser pour l'affichage"], + "" => ["array", "scalar", "nature du schéma", + "" => ["assoc", "schema" => self::NATURE_METASCHEMA], + ], + "name" => ["?string", null, "identifiant de la valeur"], + "pkey" => ["?pkey", null, "chemin de clé de la valeur dans un tableau associatif"], + "header" => ["?string", null, "nom de l'en-tête s'il faut présenter cette donnée dans un tableau"], + "composite" => ["?bool", null, "ce champ fait-il partie d'une valeur composite?"], + ]; + + const MESSAGES = [ + "missing" => "{key}: Vous devez spécifier cette valeur", + "unavailable" => "{key}: Vous devez spécifier cette valeur", + "null" => "{key}: cette valeur ne doit pas être nulle", + "empty" => "{key}: cette valeur ne doit pas être vide", + "invalid" => "{key}: {orig}: cette valeur est invalide", + ]; + + /** @var array meta-schema d'un schéma de nature associative */ + const ASSOC_METASCHEMA = [ + ]; + + /** @var array meta-schema d'un schéma de nature liste */ + const LIST_METASCHEMA = [ + ]; +} diff --git a/php/src/ref/schema/ref_types.php b/php/src/ref/schema/ref_types.php new file mode 100644 index 0000000..24973d5 --- /dev/null +++ b/php/src/ref/schema/ref_types.php @@ -0,0 +1,10 @@ + "bool", + "integer" => "int", + "flt" => "float", "double" => "float", "dbl" => "float", + ]; +} diff --git a/php/src/ref/web/ref_mimetypes.php b/php/src/ref/web/ref_mimetypes.php new file mode 100644 index 0000000..d896c13 --- /dev/null +++ b/php/src/ref/web/ref_mimetypes.php @@ -0,0 +1,12 @@ + 0) $part = ucfirst($part); $parts[$i] = $part; } - return implode("", $parts); + return $prefix.implode("", $parts); } } diff --git a/php/src/text/Word.php b/php/src/text/Word.php new file mode 100644 index 0000000..7369b5f --- /dev/null +++ b/php/src/text/Word.php @@ -0,0 +1,212 @@ +fem = $fem; + $this->le = $le; + $this->du = $du; + $this->au = $au; + $this->w = $spec; + } + + /** + * retourner le mot sans article + * + * @param bool|int $amount nombre du nom, avec l'équivalence false===0 et + * true===2. à partir de 2, le mot est ecrit au pluriel + * @param bool|string $fem genre du nom avec lequel accorder les adjectifs, + * avec l'équivalence false==="M" et true==="F" + */ + function w($amount=1, bool $upper1=false, $fem=false): string { + if ($amount === true) $amount = 2; + elseif ($amount === false) $amount = 0; + $amount = abs($amount); + $w = $this->w; + # marque du nombre + if ($amount <= 1) { + $w = preg_replace('/#[sx]/', "", $w); + } else { + $w = preg_replace('/#([sx])/', "$1", $w); + } + # marque du genre + if ($fem === "f" || $fem === "F") $fem = true; + elseif ($fem === "m" || $fem === "M") $fem = false; + $repl = $fem? "$1": ""; + $w = preg_replace('/#([e])/', $repl, $w); + # mise en majuscule + if ($upper1) { + if (strpos($w, "^") === false) { + # uniquement la première lettre + $w = txt::upper1($w); + } else { + # toutes les lettres qui suivent les occurences de ^ + $w = preg_replace_callback('/\^([[:alpha:]])/u', function ($ms) { + return mb_strtoupper($ms[1]); + }, $w); + } + } + return $w; + } + + /** + * retourner le mot sans article avec la première lettre en majuscule. + * alias pour $this->w($amount, true, $fem) + * + * @param bool|int $amount + */ + function u($amount=1, $fem=false): string { + return $this->w($amount, true, $fem); + } + + /** + * retourner l'adjectif accordé avec le genre spécifié. + * alias pour $this->w($amount, false, $fem) + * + * @param bool|int $amount + */ + function a($fem=false, $amount=1): string { + return $this->w($amount, false, $fem); + } + + /** retourner le mot sans article et avec la quantité */ + function q(int $amount=1, $fem=false): string { + return $amount." ".$this->w($amount, $fem); + } + + /** retourner le mot sans article et avec la quantité $amount/$max */ + function r(int $amount, int $max, $fem=false): string { + return "$amount/$max ".$this->w($amount, $fem); + } + + /** retourner le mot avec l'article indéfini et la quantité */ + function un(int $amount=1, $fem=false): string { + $abs_amount = abs($amount); + if ($abs_amount == 0) { + $aucun = $this->fem? "aucune ": "aucun "; + return $aucun.$this->w($amount, $fem); + } elseif ($abs_amount == 1) { + $un = $this->fem? "une ": "un "; + return $un.$this->w($amount, $fem); + } else { + return "les $amount ".$this->w($amount, $fem); + } + } + + function le(int $amount=1, $fem=false): string { + $abs_amount = abs($amount); + if ($abs_amount == 0) { + $le = $this->fem? "la 0 ": "le 0 "; + return $le.$this->w($amount, $fem); + } elseif ($abs_amount == 1) { + return $this->le.$this->w($amount, $fem); + } else { + return "les $amount ".$this->w($amount, $fem); + } + } + + function du(int $amount=1, $fem=false): string { + $abs_amount = abs($amount); + if ($abs_amount == 0) { + $du = $this->fem? "de la 0 ": "du 0 "; + return $du.$this->w($amount, $fem); + } elseif ($abs_amount == 1) { + return $this->du.$this->w($amount, $fem); + } else { + return "des $amount ".$this->w($amount, $fem); + } + } + + function au(int $amount=1, $fem=false): string { + $abs_amount = abs($amount); + if ($abs_amount == 0) { + $au = $this->fem? "à la 0 ": "au 0 "; + return $au.$this->w($amount, $fem); + } elseif ($abs_amount == 1) { + return $this->au.$this->w($amount, $fem); + } else { + return "aux $amount ".$this->w($amount, $fem); + } + } +} diff --git a/php/src/text/words.php b/php/src/text/words.php new file mode 100644 index 0000000..f11c392 --- /dev/null +++ b/php/src/text/words.php @@ -0,0 +1,14 @@ +q($count); + } + + static function r(int $count, int $max, string $spec, bool $adjective=true): string { + $word = new Word($spec, $adjective); + return $word->r($count, $max); + } +} diff --git a/php/src/tools/BgLauncherApp.php b/php/src/tools/BgLauncherApp.php new file mode 100644 index 0000000..c3ed581 --- /dev/null +++ b/php/src/tools/BgLauncherApp.php @@ -0,0 +1,124 @@ + "lancer un script en tâche de fond", + "usage" => "ApplicationClass args...", + + "sections" => [ + parent::VERBOSITY_SECTION, + ], + + ["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS, + "help" => "Afficher des informations sur la tâche", + ], + ["-s", "--start", "name" => "action", "value" => self::ACTION_START, + "help" => "Démarrer la tâche", + ], + ["-k", "--stop", "name" => "action", "value" => self::ACTION_STOP, + "help" => "Arrêter la tâche", + ], + ]; + + protected int $action = self::ACTION_START; + + protected ?array $args = null; + + static function show_infos(RunFile $runfile, ?int $level=null): void { + msg::print($runfile->getDesc(), $level); + msg::print(yaml::with(["data" => $runfile->read()]), ($level ?? 0) - 1); + } + + function main() { + $args = $this->args; + + $appClass = $args[0] ?? null; + if ($appClass === null) { + self::die("Vous devez spécifier la classe de l'application"); + } + $appClass = $args[0] = str_replace("/", "\\", $appClass); + if (!class_exists($appClass)) { + self::die("$appClass: classe non trouvée"); + } + + $useRunfile = constant("$appClass::USE_RUNFILE"); + if (!$useRunfile) { + self::die("Cette application ne supporte le lancement en tâche de fond"); + } + + $runfile = app::with($appClass)->getRunfile(); + switch ($this->action) { + case self::ACTION_START: + $argc = count($args); + $appClass::_manage_runfile($argc, $args, $runfile); + if ($runfile->warnIfLocked()) self::exit(app::EC_LOCKED); + array_splice($args, 0, 0, [ + PHP_BINARY, + path::abspath(NULIB_APP_app_launcher), + ]); + app::params_putenv(); + self::_start($args, $runfile); + break; + case self::ACTION_STOP: + self::_stop($runfile); + self::show_infos($runfile, -1); + break; + case self::ACTION_INFOS: + self::show_infos($runfile); + break; + } + } + + public static function _start(array $args, Runfile $runfile): void { + $pid = pcntl_fork(); + if ($pid == -1) { + # parent, impossible de forker + throw new ExitError(app::EC_FORK_PARENT, "Unable to fork"); + } elseif (!$pid) { + # child, fork ok + $runfile->wfPrepare($pid); + $outfile = $runfile->getOutfile() ?? "/tmp/NULIB_APP_app_console.out"; + $exitcode = app::EC_FORK_CHILD; + try { + # rediriger STDIN, STDOUT et STDERR + fclose(fopen($outfile, "wb")); // vider le fichier + fclose(STDIN); $in = fopen("/dev/null", "rb"); + fclose(STDOUT); $out = fopen($outfile, "ab"); + fclose(STDERR); $err = fopen($outfile, "ab"); + # puis lancer la commande + $cmd = new Cmd($args); + $cmd->addSource("/g/init.env"); + $cmd->addRedir("both", $outfile, true); + $cmd->fork_exec($exitcode, false); + sh::_waitpid(-$pid, $exitcode); + } finally { + $runfile->wfReaped($exitcode); + } + } + } + + public static function _stop(Runfile $runfile): bool { + $data = $runfile->read(); + $pid = $runfile->_getCid($data); + msg::action("stop $pid"); + if ($runfile->wfKill($reason)) { + msg::asuccess(); + return true; + } else { + msg::afailure($reason); + return false; + } + } +} diff --git a/php/src/tools/SteamTrainApp.php b/php/src/tools/SteamTrainApp.php new file mode 100644 index 0000000..3827fe4 --- /dev/null +++ b/php/src/tools/SteamTrainApp.php @@ -0,0 +1,53 @@ + self::TITLE, + "description" => << 1, + "help" => "spécifier le nombre d'étapes", + ], + ["-f", "--force-enabled", "value" => true, + "help" => "lancer la commande même si les tâches planifiées sont désactivées", + ], + ["-n", "--no-install-signal-handler", "value" => false, + "help" => "ne pas installer le gestionnaire de signaux", + ], + ]; + + protected $count = 100; + + protected bool $forceEnabled = false; + + protected bool $installSignalHandler = true; + + function main() { + app::check_bgapplication_enabled($this->forceEnabled); + if ($this->installSignalHandler) app::install_signal_handler(); + $count = intval($this->count); + msg::info("Starting train for ".words::q($count, "step#s")); + app::action("Running train...", $count); + for ($i = 1; $i <= $count; $i++) { + msg::print("Tchou-tchou! x $i"); + app::step(); + sleep(1); + } + msg::info("Stopping train at ".new DateTime()); + } +} diff --git a/php/src/txt.php b/php/src/txt.php new file mode 100644 index 0000000..9857473 --- /dev/null +++ b/php/src/txt.php @@ -0,0 +1,294 @@ + $length) { + if ($ellips && $length > 3) $s = mb_substr($s, 0, $length - 3)."..."; + else $s = mb_substr($s, 0, $length); + } + if ($suffix !== null) $s .= $suffix; + return $s; + } + + /** trimmer $s */ + static final function trim(?string $s): ?string { + if ($s === null) return null; + return trim($s); + } + + /** trimmer $s à gauche */ + static final function ltrim(?string $s): ?string { + if ($s === null) return null; + return ltrim($s); + } + + /** trimmer $s à droite */ + static final function rtrim(?string $s): ?string { + if ($s === null) return null; + return rtrim($s); + } + + static final function left(?string $s, int $size): ?string { + if ($s === null) return null; + return mb_str_pad($s, $size); + } + + static final function right(?string $s, int $size): ?string { + if ($s === null) return null; + return mb_str_pad($s, $size, " ", STR_PAD_LEFT); + } + + static final function center(?string $s, int $size): ?string { + if ($s === null) return null; + return mb_str_pad($s, $size, " ", STR_PAD_BOTH); + } + + static final function pad0(?string $s, int $size): ?string { + if ($s === null) return null; + return mb_str_pad($s, $size, "0", STR_PAD_LEFT); + } + + static final function lower(?string $s): ?string { + if ($s === null) return null; + return mb_strtolower($s); + } + + static final function lower1(?string $s): ?string { + if ($s === null) return null; + return mb_strtolower(mb_substr($s, 0, 1)).mb_substr($s, 1); + } + + static final function upper(?string $s): ?string { + if ($s === null) return null; + return mb_strtoupper($s); + } + + static final function upper1(?string $s): ?string { + if ($s === null) return null; + return mb_strtoupper(mb_substr($s, 0, 1)).mb_substr($s, 1); + } + + static final function upperw(?string $s, ?string $delimiters=null): ?string { + if ($s === null) return null; + if ($delimiters === null) $delimiters = " _-\t\r\n\f\v"; + $pattern = "/([".preg_quote($delimiters)."])/u"; + $words = preg_split($pattern, $s, -1, PREG_SPLIT_DELIM_CAPTURE); + $max = count($words) - 1; + $ucwords = []; + for ($i = 0; $i < $max; $i += 2) { + $s = $words[$i]; + $ucwords[] = mb_strtoupper(mb_substr($s, 0, 1)).mb_substr($s, 1); + $ucwords[] = $words[$i + 1]; + } + $s = $words[$max]; + $ucwords[] = mb_strtoupper(mb_substr($s, 0, 1)).mb_substr($s, 1); + return implode("", $ucwords); + } + + protected static final function _starts_with(string $prefix, string $s, ?int $min_len=null): bool { + if ($prefix === $s) return true; + $len = mb_strlen($prefix); + if ($min_len !== null && ($len < $min_len || $len > mb_strlen($s))) return false; + return $len == 0 || $prefix === mb_substr($s, 0, $len); + } + + /** + * tester si $s commence par $prefix + * par exemple: + * - starts_with("", "whatever") est true + * - starts_with("fi", "first") est true + * - starts_with("no", "yes") est false + * + * si $min_len n'est pas null, c'est la longueur minimum requise de $prefix + * pour qu'on teste la correspondance. dans le cas contraire, la valeur de + * retour est toujours false, sauf s'il y a égalité. e.g + * - starts_with("a", "abc", 2) est false + * - starts_with("a", "a", 2) est true + */ + static final function starts_with(?string $prefix, ?string $s, ?int $min_len=null): bool { + if ($s === null || $prefix === null) return false; + else return self::_starts_with($prefix, $s, $min_len); + } + + /** Retourner $s sans le préfixe $prefix s'il existe */ + static final function without_prefix(?string $prefix, ?string $s): ?string { + if ($s === null || $prefix === null) return $s; + if (self::_starts_with($prefix, $s)) $s = mb_substr($s, mb_strlen($prefix)); + return $s; + } + + /** + * modifier $s en place pour supprimer le préfixe $prefix s'il existe + * + * retourner true si le préfixe a été enlevé. + */ + static final function del_prefix(?string &$s, ?string $prefix): bool { + if ($s === null || !self::_starts_with($prefix, $s)) return false; + $s = self::without_prefix($prefix, $s); + return true; + } + + /** + * Retourner $s avec le préfixe $prefix + * + * Si $unless_exists, ne pas ajouter le préfixe s'il existe déjà + */ + static final function with_prefix(?string $prefix, ?string $s, ?string $sep=null, bool $unless_exists=false): ?string { + if ($s === null || $prefix === null) return $s; + if (!self::_starts_with($prefix, $s) || !$unless_exists) $s = $prefix.$sep.$s; + return $s; + } + + /** + * modifier $s en place pour ajouter le préfixe $prefix + * + * retourner true si le préfixe a été ajouté. + */ + static final function add_prefix(?string &$s, ?string $prefix, bool $unless_exists=true): bool { + if (($s === null || self::_starts_with($prefix, $s)) && $unless_exists) return false; + $s = self::with_prefix($prefix, $s, null, $unless_exists); + return true; + } + + protected static final function _ends_with(string $suffix, string $s, ?int $min_len=null): bool { + if ($suffix === $s) return true; + $len = mb_strlen($suffix); + if ($min_len !== null && ($len < $min_len || $len > mb_strlen($s))) return false; + return $len == 0 || $suffix === mb_substr($s, -$len); + } + + /** + * tester si $string se termine par $suffix + * par exemple: + * - ends_with("", "whatever") est true + * - ends_with("st", "first") est true + * - ends_with("no", "yes") est false + * + * si $min_len n'est pas null, c'est la longueur minimum requise de $prefix + * pour qu'on teste la correspondance. dans le cas contraire, la valeur de + * retour est toujours false, sauf s'il y a égalité. e.g + * - ends_with("c", "abc", 2) est false + * - ends_with("c", "c", 2) est true + */ + static final function ends_with(?string $suffix, ?string $s, ?int $min_len=null): bool { + if ($s === null || $suffix === null) return false; + else return self::_ends_with($suffix, $s, $min_len); + } + + /** Retourner $s sans le suffixe $suffix s'il existe */ + static final function without_suffix(?string $suffix, ?string $s): ?string { + if ($s === null || $suffix === null) return $s; + if (self::_ends_with($suffix, $s)) $s = mb_substr($s, 0, -mb_strlen($suffix)); + return $s; + } + + /** + * modifier $s en place pour supprimer le suffixe $suffix s'il existe + * + * retourner true si le suffixe a été enlevé. + */ + static final function del_suffix(?string &$s, ?string $suffix): bool { + if ($s === null || !self::_ends_with($suffix, $s)) return false; + $s = self::without_suffix($suffix, $s); + return true; + } + + /** + * Retourner $s avec le suffixe $suffix + * + * Si $unless_exists, ne pas ajouter le suffixe s'il existe déjà + */ + static final function with_suffix(?string $suffix, ?string $s, ?string $sep=null, bool $unless_exists=false): ?string { + if ($s === null || $suffix === null) return $s; + if (!self::_ends_with($suffix, $s) || !$unless_exists) $s = $s.$sep.$suffix; + return $s; + } + + /** + * modifier $s en place pour ajouter le suffixe $suffix + * + * retourner true si le suffixe a été ajouté. + */ + static final function add_suffix(?string &$s, ?string $suffix, bool $unless_exists=true): bool { + if (($s === null || self::_ends_with($suffix, $s)) && $unless_exists) return false; + $s = self::with_suffix($suffix, $s, null, $unless_exists); + return true; + } + + /** + * ajouter $sep$prefix$text$suffix à $s si $text est non vide + * + * NB: ne rajouter $sep que si $s est non vide + */ + static final function addsep(?string &$s, ?string $sep, ?string $text, ?string $prefix=null, ?string $suffix=null): void { + if (!$text) return; + if ($s) $s .= $sep; + $s .= $prefix.$text.$suffix; + } + + /** si $text est non vide, ajouter $prefix$text$suffix à $s en séparant la valeur avec un espace */ + static final function add(?string &$s, ?string $text, ?string $prefix=null, ?string $suffix=null): void { + self::addsep($s, " ", $text, $prefix, $suffix); + } + + ############################################################################# + # divers + + /** + * supprimer les diacritiques de la chaine $text + * + * la translitération se fait avec les règles de la locale spécifiée. + * NB: la translitération ne fonctionne pas si LC_CTYPE == C ou POISX + */ + static final function remove_diacritics(?string $text, string $locale="fr_FR.UTF-8"): ?string { + if ($text === null) return null; + #XXX est-ce thread-safe? + $olocale = setlocale(LC_CTYPE, 0); + try { + setlocale(LC_CTYPE, $locale); + $clean = @iconv("UTF-8", "US-ASCII//TRANSLIT", $text); + if ($clean === false) $clean = ""; + return $clean; + } finally { + setlocale(LC_CTYPE, $olocale); + } + } +} diff --git a/php/src/web/curl/CurlException.php b/php/src/web/curl/CurlException.php new file mode 100644 index 0000000..53fda92 --- /dev/null +++ b/php/src/web/curl/CurlException.php @@ -0,0 +1,22 @@ + $value) { + if (is_array($value)) { + self::parse_files($files, "$pkey.$key", $value, $lastkey); + } else { + cl::pset($files, "$pkey.$key.$lastkey", $value); + } + } + } + + /** @var array */ + private static $_files; + + static function _files(?array $_files=null): ?array { + if (self::$_files === null) { + $files = []; + if ($_files === null) $_files = $_FILES; + foreach ($_files as $pkey => $values) { + $name = $values["name"] ?? null; + $type = $values["type"] ?? null; + $error = $values["error"] ?? null; + if (is_scalar($name) && is_scalar($type) && is_scalar($error)) { + $files[$pkey] = $values; + } else { + self::parse_files($files, $pkey, $values["name"], "name"); + self::parse_files($files, $pkey, $values["type"], "type"); + self::parse_files($files, $pkey, $values["tmp_name"], "tmp_name"); + self::parse_files($files, $pkey, $values["error"], "error"); + self::parse_files($files, $pkey, $values["size"], "size"); + $full_path = $values["full_path"] ?? null; + if ($full_path !== null) { + self::parse_files($files, $pkey, $full_path, "full_path"); + } + } + } + self::$_files = $files; + } + return self::$_files; + } + + static function get(string $pkey, bool $required=true, bool $check=true): Upload { + $_files = self::_files(); + return new Upload(cl::pget($_files, $pkey), $required, $check); + } + + static function all(string $pkey, bool $required=true, bool $check=true) { + $_files = self::_files(); + $uploads = []; + foreach (cl::pget($_files, $pkey) as $file) { + $uploads[] = new Upload($file, $required, $check); + } + return $uploads; + } +} diff --git a/php/src_base/ValueException.php b/php/src_base/ValueException.php deleted file mode 100644 index 665549c..0000000 --- a/php/src_base/ValueException.php +++ /dev/null @@ -1,48 +0,0 @@ -"; - } elseif (is_array($value)) { - $values = $value; - $parts = []; - foreach ($values as $value) { - $parts[] = self::value($value); - } - return "[".implode(", ", $parts)."]"; - } else { - return var_export($value, true); - } - } - - private static function message($value, ?string $message, ?string $kind, ?string $prefix, ?string $suffix): string { - if ($kind === null) $kind = "value"; - if ($message === null) $message = "$kind$suffix"; - if ($value !== null) { - $value = self::value($value); - if ($prefix) $prefix = "$prefix: $value"; - else $prefix = $value; - } - if ($prefix) $prefix = "$prefix: "; - return $prefix.$message; - } - - static final function null(?string $kind=null, ?string $prefix=null, ?string $message=null): self { - return new static(self::message(null, $message, $kind, $prefix, " is null")); - } - - static final function invalid($value=null, ?string $kind=null, ?string $prefix=null, ?string $message=null): self { - return new static(self::message($value, $message, $kind, $prefix, " is invalid")); - } - - static final function forbidden($value=null, ?string $kind=null, ?string $prefix=null, ?string $message=null): self { - return new static(self::message($value, $message, $kind, $prefix, " is forbidden")); - } -} diff --git a/php/tests/app/argsTest.php b/php/tests/app/argsTest.php new file mode 100644 index 0000000..90252d9 --- /dev/null +++ b/php/tests/app/argsTest.php @@ -0,0 +1,26 @@ + false])); + self::assertSame(["--opt"], args::from_array(["opt" => true])); + self::assertSame(["--opt", "value"], args::from_array(["opt" => "value"])); + self::assertSame(["--opt", "42"], args::from_array(["opt" => 42])); + self::assertSame(["--opt", "1", "2", "3", "--"], args::from_array(["opt" => [1, 2, 3]])); + + self::assertSame(["x", "1", "2", "3", "y"], args::from_array(["x", [1, 2, 3], "y"])); + } +} diff --git a/php/tests/appTest.php b/php/tests/appTest.php new file mode 100644 index 0000000..8d86b6f --- /dev/null +++ b/php/tests/appTest.php @@ -0,0 +1,132 @@ + $projdir, + "vendor" => [ + "bindir" => "$projdir/vendor/bin", + "autoload" => "$projdir/vendor/autoload.php", + ], + "appcode" => "nur-sery", + "cwd" => $cwd, + "datadir" => "$projdir/devel", + "etcdir" => "$projdir/devel/etc", + "vardir" => "$projdir/devel/var", + "logdir" => "$projdir/devel/log", + "profile" => "devel", + "appgroup" => null, + "name" => "my-application1", + "title" => null, + ], $app1->getParams()); + + $app2 = myapp::with(MyApplication2::class, $app1); + self::assertSame([ + "projdir" => $projdir, + "vendor" => [ + "bindir" => "$projdir/vendor/bin", + "autoload" => "$projdir/vendor/autoload.php", + ], + "appcode" => "nur-sery", + "cwd" => $cwd, + "datadir" => "$projdir/devel", + "etcdir" => "$projdir/devel/etc", + "vardir" => "$projdir/devel/var", + "logdir" => "$projdir/devel/log", + "profile" => "devel", + "appgroup" => null, + "name" => "my-application2", + "title" => null, + ], $app2->getParams()); + } + + function testInit() { + $projdir = config::get_projdir(); + $cwd = getcwd(); + + myapp::reset(); + myapp::init(MyApplication1::class); + self::assertSame([ + "projdir" => $projdir, + "vendor" => [ + "bindir" => "$projdir/vendor/bin", + "autoload" => "$projdir/vendor/autoload.php", + ], + "appcode" => "nur-sery", + "cwd" => $cwd, + "datadir" => "$projdir/devel", + "etcdir" => "$projdir/devel/etc", + "vardir" => "$projdir/devel/var", + "logdir" => "$projdir/devel/log", + "profile" => "devel", + "appgroup" => null, + "name" => "my-application1", + "title" => null, + ], myapp::get()->getParams()); + + myapp::init(MyApplication2::class); + self::assertSame([ + "projdir" => $projdir, + "vendor" => [ + "bindir" => "$projdir/vendor/bin", + "autoload" => "$projdir/vendor/autoload.php", + ], + "appcode" => "nur-sery", + "cwd" => $cwd, + "datadir" => "$projdir/devel", + "etcdir" => "$projdir/devel/etc", + "vardir" => "$projdir/devel/var", + "logdir" => "$projdir/devel/log", + "profile" => "devel", + "appgroup" => null, + "name" => "my-application2", + "title" => null, + ], myapp::get()->getParams()); + } + } +} + +namespace nulib\impl { + + use nulib\app\cli\Application; + use nulib\os\path; + use nulib\app; + + class config { + const PROJDIR = __DIR__.'/..'; + + static function get_projdir(): string { + return path::abspath(self::PROJDIR); + } + } + + class myapp extends app { + static function reset(): void { + self::$app = null; + } + } + + class MyApplication1 extends Application { + const PROJDIR = config::PROJDIR; + + function main() { + } + } + class MyApplication2 extends Application { + const PROJDIR = null; + + function main() { + } + } +} diff --git a/php/tests/cstrTest.php b/php/tests/cstrTest.php deleted file mode 100644 index 132a538..0000000 --- a/php/tests/cstrTest.php +++ /dev/null @@ -1,56 +0,0 @@ -reset($channel); + $storage->charge($channel, "first"); + $storage->charge($channel, "second"); + $storage->charge($channel, "third"); + $items = cl::all($storage->discharge($channel, false)); + self::assertSame(["first", "second", "third"], $items); + } + + function _testChargeArrays(SqliteStorage $storage, ?string $channel) { + $storage->reset($channel); + $storage->charge($channel, ["id" => 10, "name" => "first"]); + $storage->charge($channel, ["name" => "second", "id" => 20]); + $storage->charge($channel, ["name" => "third", "id" => "30"]); + } + + function testChargeStrings() { + $storage = new SqliteStorage(__DIR__.'/capacitor.db'); + $this->_testChargeStrings($storage, null); + $storage->close(); + } + + function testChargeArrays() { + $storage = new SqliteStorage(__DIR__.'/capacitor.db'); + $storage->addChannel(new class extends CapacitorChannel { + const NAME = "arrays"; + const COLUMN_DEFINITIONS = ["id" => "integer"]; + + function getItemValues($item): ?array { + return ["id" => $item["id"] ?? null]; + } + }); + + $this->_testChargeStrings($storage, "strings"); + $this->_testChargeArrays($storage, "arrays"); + $storage->close(); + } + + function testEach() { + $storage = new SqliteStorage(__DIR__.'/capacitor.db'); + $capacitor = new Capacitor($storage, new class extends CapacitorChannel { + const NAME = "each"; + const COLUMN_DEFINITIONS = [ + "age" => "integer", + "done" => "integer default 0", + ]; + + function getItemValues($item): ?array { + return [ + "age" => $item["age"], + ]; + } + }); + + $capacitor->reset(); + $capacitor->charge(["name" => "first", "age" => 5]); + $capacitor->charge(["name" => "second", "age" => 10]); + $capacitor->charge(["name" => "third", "age" => 15]); + $capacitor->charge(["name" => "fourth", "age" => 20]); + + $setDone = function ($item, $row, $suffix=null) { + $updates = ["done" => 1]; + if ($suffix !== null) { + $item["name"] .= $suffix; + $updates["item"] = $item; + } + return $updates; + }; + $capacitor->each(["age" => [">", 10]], $setDone, ["++"]); + $capacitor->each(["done" => 0], $setDone, null); + + Txx(cl::all($capacitor->discharge(false))); + $capacitor->close(); + self::assertTrue(true); + } + + function testPrimayKey() { + $storage = new SqliteStorage(__DIR__.'/capacitor.db'); + $capacitor = new Capacitor($storage, new class extends CapacitorChannel { + const NAME = "pk"; + const COLUMN_DEFINITIONS = [ + "id_" => "varchar primary key", + "done" => "integer default 0", + ]; + + function getItemValues($item): ?array { + return [ + "id_" => $item["numero"], + ]; + } + }); + + $capacitor->charge(["numero" => "a", "name" => "first", "age" => 5]); + $capacitor->charge(["numero" => "b", "name" => "second", "age" => 10]); + $capacitor->charge(["numero" => "c", "name" => "third", "age" => 15]); + $capacitor->charge(["numero" => "d", "name" => "fourth", "age" => 20]); + sleep(2); + $capacitor->charge(["numero" => "b", "name" => "second", "age" => 100]); + $capacitor->charge(["numero" => "d", "name" => "fourth", "age" => 200]); + + $capacitor->close(); + self::assertTrue(true); + } + + function testSum() { + $storage = new SqliteStorage(__DIR__.'/capacitor.db'); + $capacitor = new Capacitor($storage, new class extends CapacitorChannel { + const NAME = "sum"; + const COLUMN_DEFINITIONS = [ + "a__" => "varchar", + "b__" => "varchar", + "b__sum_" => self::SUM_DEFINITION, + ]; + + function getItemValues($item): ?array { + return [ + "a" => $item["a"], + "b" => $item["b"], + ]; + } + }); + + $capacitor->reset(); + $capacitor->charge(["a" => null, "b" => null]); + $capacitor->charge(["a" => "first", "b" => "second"]); + + Txx("=== all"); + /** @var Sqlite $sqlite */ + $sqlite = $capacitor->getStorage()->db(); + Txx(cl::all($sqlite->all([ + "select", + "from" => $capacitor->getChannel()->getTableName(), + ]))); + Txx("=== each"); + $capacitor->each(null, function ($item, $values) { + Txx($values); + }); + + $capacitor->close(); + self::assertTrue(true); + } + + function testEachValues() { + # tester que values contient bien toutes les valeurs de la ligne + $storage = new SqliteStorage(__DIR__.'/capacitor.db'); + $capacitor = new Capacitor($storage, new class extends CapacitorChannel { + const NAME = "each_values"; + const COLUMN_DEFINITIONS = [ + "name" => "varchar primary key", + "age" => "integer", + "done" => "integer default 0", + "notes" => "text", + ]; + + function getItemValues($item): ?array { + return [ + "name" => $item["name"], + "age" => $item["age"], + ]; + } + }); + + $capacitor->reset(); + $capacitor->charge(["name" => "first", "age" => 5], function($item, ?array $values, ?array $pvalues) { + self::assertSame("first", $item["name"]); + self::assertSame(5, $item["age"]); + self::assertnotnull($values); + self::assertSame(["name", "age", "item", "item__sum_", "created_", "modified_"], array_keys($values)); + self::assertSame([ + "name" => "first", + "age" => 5, + "item" => $item, + ], cl::select($values, ["name", "age", "item"])); + self::assertNull($pvalues); + }); + $capacitor->charge(["name" => "first", "age" => 10], function($item, ?array $values, ?array $pvalues) { + self::assertSame("first", $item["name"]); + self::assertSame(10, $item["age"]); + self::assertnotnull($values); + self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($values)); + self::assertSame([ + "name" => "first", + "age" => 10, + "done" => 0, + "notes" => null, + "item" => $item, + ], cl::select($values, ["name", "age", "done", "notes", "item"])); + self::assertNotNull($pvalues); + self::assertSame([ + "name" => "first", + "age" => 5, + "done" => 0, + "notes" => null, + "item" => ["name" => "first", "age" => 5], + ], cl::select($pvalues, ["name", "age", "done", "notes", "item"])); + }); + + $capacitor->each(null, function($item, ?array $values) { + self::assertSame("first", $item["name"]); + self::assertSame(10, $item["age"]); + self::assertnotnull($values); + self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($values)); + self::assertSame([ + "name" => "first", + "age" => 10, + "done" => 0, + "notes" => null, + "item" => $item, + ], cl::select($values, ["name", "age", "done", "notes", "item"])); + return [ + "done" => 1, + "notes" => "modified", + ]; + }); + $capacitor->charge(["name" => "first", "age" => 10], function($item, ?array $values, ?array $pvalues) { + self::assertSame("first", $item["name"]); + self::assertSame(10, $item["age"]); + self::assertnotnull($values); + self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($values)); + self::assertSame([ + "name" => "first", + "age" => 10, + "done" => 1, + "notes" => "modified", + "item" => $item, + ], cl::select($values, ["name", "age", "done", "notes", "item"])); + self::assertNotNull($pvalues); + self::assertSame([ + "name" => "first", + "age" => 10, + "done" => 1, + "notes" => "modified", + "item" => $item, + ], cl::select($pvalues, ["name", "age", "done", "notes", "item"])); + }); + + $capacitor->charge(["name" => "first", "age" => 20], function($item, ?array $values, ?array $pvalues) { + self::assertSame("first", $item["name"]); + self::assertSame(20, $item["age"]); + self::assertnotnull($values); + self::assertSame(["name", "age", "done", "notes", "item", "item__sum_", "created_", "modified_"], array_keys($values)); + self::assertSame([ + "name" => "first", + "age" => 20, + "done" => 1, + "notes" => "modified", + "item" => $item, + ], cl::select($values, ["name", "age", "done", "notes", "item"])); + self::assertNotNull($pvalues); + self::assertSame([ + "name" => "first", + "age" => 10, + "done" => 1, + "notes" => "modified", + "item" => ["name" => "first", "age" => 10], + ], cl::select($pvalues, ["name", "age", "done", "notes", "item"])); + }); + } + + function testSetItemNull() { + # tester le forçage de $îtem à null pour économiser la place + $storage = new SqliteStorage(__DIR__.'/capacitor.db'); + $capacitor = new Capacitor($storage, new class extends CapacitorChannel { + const NAME = "set_item_null"; + const COLUMN_DEFINITIONS = [ + "name" => "varchar primary key", + "age" => "integer", + "done" => "integer default 0", + "notes" => "text", + ]; + + function getItemValues($item): ?array { + return [ + "name" => $item["name"], + "age" => $item["age"], + ]; + } + }); + + $capacitor->reset(); + $nbModified = $capacitor->charge(["name" => "first", "age" => 5], function ($item, ?array $values, ?array $pvalues) { + self::assertSame([ + "name" => "first", "age" => 5, + "item" => $item, + ], cl::select($values, ["name", "age", "item"])); + return ["item" => null]; + }); + self::assertSame(1, $nbModified); + sleep(1); + # nb: on met des sleep() pour que la date de modification soit systématiquement différente + + $nbModified = $capacitor->charge(["name" => "first", "age" => 10], function ($item, ?array $values, ?array $pvalues) { + self::assertSame([ + "name" => "first", "age" => 10, + "item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc", + ], cl::select($values, ["name", "age", "item", "item__sum_"])); + self::assertSame([ + "name" => "first", "age" => 5, + "item" => null, "item__sum_" => null, + ], cl::select($pvalues, ["name", "age", "item", "item__sum_"])); + return ["item" => null]; + }); + self::assertSame(1, $nbModified); + sleep(1); + + # pas de modification ici + $nbModified = $capacitor->charge(["name" => "first", "age" => 10], function ($item, ?array $values, ?array $pvalues) { + self::assertSame([ + "name" => "first", "age" => 10, + "item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc", + ], cl::select($values, ["name", "age", "item", "item__sum_"])); + self::assertSame([ + "name" => "first", "age" => 10, + "item" => null, "item__sum_" => null, + ], cl::select($pvalues, ["name", "age", "item", "item__sum_"])); + return ["item" => null]; + }); + self::assertSame(0, $nbModified); + sleep(1); + + $nbModified = $capacitor->charge(["name" => "first", "age" => 20], function ($item, ?array $values, ?array $pvalues) { + self::assertSame([ + "name" => "first", "age" => 20, + "item" => $item, "item__sum_" => "001b91982b4e0883b75428c0eb28573a5dc5f7a5", + ], cl::select($values, ["name", "age", "item", "item__sum_"])); + self::assertSame([ + "name" => "first", "age" => 10, + "item" => null, "item__sum_" => null, + ], cl::select($pvalues, ["name", "age", "item", "item__sum_"])); + return ["item" => null]; + }); + self::assertSame(1, $nbModified); + sleep(1); + } +} diff --git a/php/tests/db/sqlite/SqliteTest.php b/php/tests/db/sqlite/SqliteTest.php new file mode 100644 index 0000000..b56855c --- /dev/null +++ b/php/tests/db/sqlite/SqliteTest.php @@ -0,0 +1,146 @@ + [ + self::CREATE_PERSON, + self::INSERT_JEPHTE, + ], + ]); + self::assertSame("clain", $sqlite->get("select nom, age from person")); + self::assertSame([ + "nom" => "clain", + "age" => 50, + ], $sqlite->get("select nom, age from person", null, true)); + + $sqlite->exec(self::INSERT_JEAN); + self::assertSame("payet", $sqlite->get("select nom, age from person where nom = 'payet'")); + self::assertSame([ + "nom" => "payet", + "age" => 32, + ], $sqlite->get("select nom, age from person where nom = 'payet'", null, true)); + + self::assertSame([ + ["key" => "0", "value" => self::CREATE_PERSON, "done" => 1], + ["key" => "1", "value" => self::INSERT_JEPHTE, "done" => 1], + ], iterator_to_array($sqlite->all("select key, value, done from _migration"))); + } + + function testException() { + $sqlite = new Sqlite(":memory:"); + self::assertException(Exception::class, [$sqlite, "exec"], "prout"); + self::assertException(SqliteException::class, [$sqlite, "exec"], ["prout"]); + } + + protected function assertInserted(Sqlite $sqlite, array $row, array $query): void { + $sqlite->exec($query); + self::assertSame($row, $sqlite->one("select * from mapping where i = :i", [ + "i" => $query["values"]["i"], + ])); + } + function testInsert() { + $sqlite = new Sqlite(":memory:", [ + "migrate" => "create table mapping (i integer, s varchar)", + ]); + $sqlite->exec(["insert into mapping", "values" => ["i" => 1, "s" => "un"]]); + $sqlite->exec(["insert mapping", "values" => ["i" => 2, "s" => "deux"]]); + $sqlite->exec(["insert into", "into" => "mapping", "values" => ["i" => 3, "s" => "trois"]]); + $sqlite->exec(["insert", "into" => "mapping", "values" => ["i" => 4, "s" => "quatre"]]); + $sqlite->exec(["insert into mapping(i)", "values" => ["i" => 5, "s" => "cinq"]]); + $sqlite->exec(["insert into (i)", "into" => "mapping", "values" => ["i" => 6, "s" => "six"]]); + $sqlite->exec(["insert into mapping(i) values ()", "values" => ["i" => 7, "s" => "sept"]]); + $sqlite->exec(["insert into mapping(i) values (8)", "values" => ["i" => 42, "s" => "whatever"]]); + $sqlite->exec(["insert into mapping(i, s) values (9, 'neuf')", "values" => ["i" => 43, "s" => "garbage"]]); + $sqlite->exec(["insert into mapping", "cols" => ["i"], "values" => ["i" => 10, "s" => "dix"]]); + + self::assertSame([ + ["i" => 1, "s" => "un"], + ["i" => 2, "s" => "deux"], + ["i" => 3, "s" => "trois"], + ["i" => 4, "s" => "quatre"], + ["i" => 5, "s" => null/*"cinq"*/], + ["i" => 6, "s" => null/*"six"*/], + ["i" => 7, "s" => null/*"sept"*/], + ["i" => 8, "s" => null/*"huit"*/], + ["i" => 9, "s" => "neuf"], + ["i" => 10, "s" => null/*"dix"*/], + ], iterator_to_array($sqlite->all("select * from mapping"))); + } + + function testSelect() { + $sqlite = new Sqlite(":memory:", [ + "migrate" => "create table user (name varchar, amount integer)", + ]); + $sqlite->exec(["insert into user", "values" => ["name" => "jclain1", "amount" => 1]]); + $sqlite->exec(["insert into user", "values" => ["name" => "jclain2", "amount" => 2]]); + $sqlite->exec(["insert into user", "values" => ["name" => "jclain5", "amount" => 5]]); + $sqlite->exec(["insert into user", "values" => ["name" => "fclain7", "amount" => 7]]); + $sqlite->exec(["insert into user", "values" => ["name" => "fclain9", "amount" => 9]]); + $sqlite->exec(["insert into user", "values" => ["name" => "fclain10", "amount" => 10]]); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one("select * from user where name = 'jclain1'")); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select * from user where name = 'jclain1'"])); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select from user where name = 'jclain1'"])); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select from user where", "where" => ["name = 'jclain1'"]])); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select from user", "where" => ["name = 'jclain1'"]])); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select", "from" => "user", "where" => ["name = 'jclain1'"]])); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select", "from" => "user", "where" => ["name" => "jclain1"]])); + self::assertSame([ + "name" => "jclain1", + ], $sqlite->one(["select name", "from" => "user", "where" => ["name" => "jclain1"]])); + self::assertSame([ + "name" => "jclain1", + ], $sqlite->one(["select", "cols" => "name", "from" => "user", "where" => ["name" => "jclain1"]])); + self::assertSame([ + "name" => "jclain1", + ], $sqlite->one(["select", "cols" => ["name"], "from" => "user", "where" => ["name" => "jclain1"]])); + self::assertSame([ + "plouf" => "jclain1", + ], $sqlite->one(["select", "cols" => ["plouf" => "name"], "from" => "user", "where" => ["name" => "jclain1"]])); + } + + function testSelectGroupBy() { + $sqlite = new Sqlite(":memory:", [ + "migrate" => "create table user (name varchar, amount integer)", + ]); + $sqlite->exec(["insert into user", "values" => ["name" => "jclain1", "amount" => 1]]); + $sqlite->exec(["insert into user", "values" => ["name" => "jclain2", "amount" => 1]]); + $sqlite->exec(["insert into user", "values" => ["name" => "jclain5", "amount" => 2]]); + $sqlite->exec(["insert into user", "values" => ["name" => "fclain7", "amount" => 2]]); + $sqlite->exec(["insert into user", "values" => ["name" => "fclain9", "amount" => 2]]); + $sqlite->exec(["insert into user", "values" => ["name" => "fclain10", "amount" => 3]]); + + self::assertSame([ + ["count" => 2], + ], iterator_to_array($sqlite->all(["select count(name) as count from user", "group by" => ["amount"], "having" => ["count(name) = 2"]]))); + } +} diff --git a/php/tests/db/sqlite/_queryTest.php b/php/tests/db/sqlite/_queryTest.php new file mode 100644 index 0000000..6d92412 --- /dev/null +++ b/php/tests/db/sqlite/_queryTest.php @@ -0,0 +1,125 @@ + null], $sql, $params); + self::assertSame(["col is null"], $sql); + self::assertNull($params); + + $sql = $params = null; + _query_base::parse_conds(["col = 'value'"], $sql, $params); + self::assertSame(["col = 'value'"], $sql); + self::assertNull($params); + + $sql = $params = null; + _query_base::parse_conds([["col = 'value'"]], $sql, $params); + self::assertSame(["col = 'value'"], $sql); + self::assertNull($params); + + $sql = $params = null; + _query_base::parse_conds(["int" => 42, "string" => "value"], $sql, $params); + self::assertSame(["(int = :int and string = :string)"], $sql); + self::assertSame(["int" => 42, "string" => "value"], $params); + + $sql = $params = null; + _query_base::parse_conds(["or", "int" => 42, "string" => "value"], $sql, $params); + self::assertSame(["(int = :int or string = :string)"], $sql); + self::assertSame(["int" => 42, "string" => "value"], $params); + + $sql = $params = null; + _query_base::parse_conds([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); + self::assertSame(["((int = :int and string = :string) and (int = :int2 and string = :string2))"], $sql); + self::assertSame(["int" => 42, "string" => "value", "int2" => 24, "string2" => "eulav"], $params); + + $sql = $params = null; + _query_base::parse_conds(["int" => ["is null"], "string" => ["<>", "value"]], $sql, $params); + self::assertSame(["(int is null and string <> :string)"], $sql); + self::assertSame(["string" => "value"], $params); + + $sql = $params = null; + _query_base::parse_conds(["col" => ["between", "lower", "upper"]], $sql, $params); + self::assertSame(["col between :col and :col2"], $sql); + self::assertSame(["col" => "lower", "col2" => "upper"], $params); + + $sql = $params = null; + _query_base::parse_conds(["col" => ["in", "one"]], $sql, $params); + self::assertSame(["col in (:col)"], $sql); + self::assertSame(["col" => "one"], $params); + + $sql = $params = null; + _query_base::parse_conds(["col" => ["in", ["one", "two"]]], $sql, $params); + self::assertSame(["col in (:col, :col2)"], $sql); + self::assertSame(["col" => "one", "col2" => "two"], $params); + + $sql = $params = null; + _query_base::parse_conds(["col" => ["=", ["one", "two"]]], $sql, $params); + self::assertSame(["col = :col and col = :col2"], $sql); + self::assertSame(["col" => "one", "col2" => "two"], $params); + + $sql = $params = null; + _query_base::parse_conds(["or", "col" => ["=", ["one", "two"]]], $sql, $params); + self::assertSame(["col = :col or col = :col2"], $sql); + self::assertSame(["col" => "one", "col2" => "two"], $params); + + $sql = $params = null; + _query_base::parse_conds(["col" => ["<>", ["one", "two"]]], $sql, $params); + self::assertSame(["col <> :col and col <> :col2"], $sql); + self::assertSame(["col" => "one", "col2" => "two"], $params); + + $sql = $params = null; + _query_base::parse_conds(["or", "col" => ["<>", ["one", "two"]]], $sql, $params); + self::assertSame(["col <> :col or col <> :col2"], $sql); + self::assertSame(["col" => "one", "col2" => "two"], $params); + } + + function testParseValues(): void { + $sql = $params = null; + _query_base::parse_set_values(null, $sql, $params); + self::assertNull($sql); + self::assertNull($params); + + $sql = $params = null; + _query_base::parse_set_values([], $sql, $params); + self::assertNull($sql); + self::assertNull($params); + + $sql = $params = null; + _query_base::parse_set_values(["col = 'value'"], $sql, $params); + self::assertSame(["col = 'value'"], $sql); + self::assertNull($params); + + $sql = $params = null; + _query_base::parse_set_values([["col = 'value'"]], $sql, $params); + self::assertSame(["col = 'value'"], $sql); + self::assertNull($params); + + $sql = $params = null; + _query_base::parse_set_values(["int" => 42, "string" => "value"], $sql, $params); + self::assertSame(["int = :int", "string = :string"], $sql); + self::assertSame(["int" => 42, "string" => "value"], $params); + + $sql = $params = null; + _query_base::parse_set_values(["int" => 42, "string" => "value"], $sql, $params); + self::assertSame(["int = :int", "string = :string"], $sql); + self::assertSame(["int" => 42, "string" => "value"], $params); + + $sql = $params = null; + _query_base::parse_set_values([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); + self::assertSame(["int = :int", "string = :string", "int = :int2", "string = :string2"], $sql); + self::assertSame(["int" => 42, "string" => "value", "int2" => 24, "string2" => "eulav"], $params); + } +} diff --git a/php/tests/file/base/FileReaderTest.php b/php/tests/file/base/FileReaderTest.php new file mode 100644 index 0000000..de36b56 --- /dev/null +++ b/php/tests/file/base/FileReaderTest.php @@ -0,0 +1,63 @@ +fread(10)); + self::assertSame(10, $reader->ftell()); + $reader->seek(30); + self::assertSame("abcdefghij", $reader->fread(10)); + self::assertSame(40, $reader->ftell()); + $reader->seek(10); + self::assertSame("ABCDEFGHIJ", $reader->fread(10)); + self::assertSame(20, $reader->ftell()); + $reader->seek(40); + self::assertSame("0123456789\n", $reader->getContents()); + $reader->close(); + ## avec BOM + $reader = new FileReader(__DIR__ . '/impl/avec_bom.txt'); + self::assertSame("0123456789", $reader->fread(10)); + self::assertSame(10, $reader->ftell()); + $reader->seek(30); + self::assertSame("abcdefghij", $reader->fread(10)); + self::assertSame(40, $reader->ftell()); + $reader->seek(10); + self::assertSame("ABCDEFGHIJ", $reader->fread(10)); + self::assertSame(20, $reader->ftell()); + $reader->seek(40); + self::assertSame("0123456789\n", $reader->getContents()); + $reader->close(); + } + + function testCsvAutoParams() { + $reader = new FileReader(__DIR__ . '/impl/msexcel.csv'); + self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv()); + self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv()); + self::assertNull($reader->fgetcsv()); + $reader->close(); + + $reader = new FileReader(__DIR__ . '/impl/ooffice.csv'); + self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv()); + self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv()); + self::assertNull($reader->fgetcsv()); + $reader->close(); + + $reader = new FileReader(__DIR__ . '/impl/weird.tsv'); + self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv()); + self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv()); + self::assertNull($reader->fgetcsv()); + $reader->close(); + + $reader = new FileReader(__DIR__ . '/impl/avec_bom.csv'); + self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv()); + self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv()); + self::assertNull($reader->fgetcsv()); + $reader->close(); + } +} diff --git a/php/tests/file/base/impl/avec_bom.csv b/php/tests/file/base/impl/avec_bom.csv new file mode 100644 index 0000000..d1512a2 --- /dev/null +++ b/php/tests/file/base/impl/avec_bom.csv @@ -0,0 +1,2 @@ +nom,prenom,age +clain,jephte,50 diff --git a/php/tests/file/base/impl/avec_bom.txt b/php/tests/file/base/impl/avec_bom.txt new file mode 100644 index 0000000..9e55899 --- /dev/null +++ b/php/tests/file/base/impl/avec_bom.txt @@ -0,0 +1 @@ +0123456789ABCDEFGHIJ0123456789abcdefghij0123456789 diff --git a/php/tests/file/base/impl/msexcel.csv b/php/tests/file/base/impl/msexcel.csv new file mode 100644 index 0000000..b2d95c4 --- /dev/null +++ b/php/tests/file/base/impl/msexcel.csv @@ -0,0 +1,2 @@ +nom;prenom;age +clain;jephte;50 diff --git a/php/tests/file/base/impl/ooffice.csv b/php/tests/file/base/impl/ooffice.csv new file mode 100644 index 0000000..f00d4ff --- /dev/null +++ b/php/tests/file/base/impl/ooffice.csv @@ -0,0 +1,2 @@ +nom,prenom,age +clain,jephte,50 diff --git a/php/tests/file/base/impl/sans_bom.txt b/php/tests/file/base/impl/sans_bom.txt new file mode 100644 index 0000000..f16e49f --- /dev/null +++ b/php/tests/file/base/impl/sans_bom.txt @@ -0,0 +1 @@ +0123456789ABCDEFGHIJ0123456789abcdefghij0123456789 diff --git a/php/tests/file/base/impl/weird.tsv b/php/tests/file/base/impl/weird.tsv new file mode 100644 index 0000000..cd8bf3a --- /dev/null +++ b/php/tests/file/base/impl/weird.tsv @@ -0,0 +1,2 @@ +nom prenom age +clain jephte 50 diff --git a/php/tests/php/access/KeyAccessTest.php b/php/tests/php/access/KeyAccessTest.php new file mode 100644 index 0000000..dc5bd4c --- /dev/null +++ b/php/tests/php/access/KeyAccessTest.php @@ -0,0 +1,67 @@ + null, "false" => false, "empty" => ""]; + + # + $a = new KeyAccess($array, "inexistant"); + self::assertFalse($a->exists()); + self::assertFalse($a->available()); + self::assertSame($default, $a->get($default)); + + $a = new KeyAccess($array, "null"); + self::assertTrue($a->exists()); + self::assertTrue($a->available()); + self::assertSame(null, $a->get($default)); + + $a = new KeyAccess($array, "false"); + self::assertTrue($a->exists()); + self::assertFalse($a->available()); + self::assertSame($default, $a->get($default)); + + $a = new KeyAccess($array, "empty"); + self::assertTrue($a->exists()); + self::assertTrue($a->available()); + self::assertSame("", $a->get($default)); + + # + $a = new KeyAccess($array, "null", ["allow_null" => false]); + self::assertTrue($a->exists()); + self::assertFalse($a->available()); + self::assertSame($default, $a->get($default)); + + $a = new KeyAccess($array, "null", ["allow_null" => true]); + self::assertTrue($a->exists()); + self::assertTrue($a->available()); + self::assertSame(null, $a->get($default)); + + # + $a = new KeyAccess($array, "false", ["allow_false" => false]); + self::assertTrue($a->exists()); + self::assertFalse($a->available()); + self::assertSame($default, $a->get($default)); + + $a = new KeyAccess($array, "false", ["allow_false" => true]); + self::assertTrue($a->exists()); + self::assertTrue($a->available()); + self::assertSame(false, $a->get($default)); + + # + $a = new KeyAccess($array, "empty", ["allow_empty" => false]); + self::assertTrue($a->exists()); + self::assertFalse($a->available()); + self::assertSame($default, $a->get($default)); + + $a = new KeyAccess($array, "empty", ["allow_empty" => true]); + self::assertTrue($a->exists()); + self::assertTrue($a->available()); + self::assertSame("", $a->get($default)); + } +} diff --git a/php/tests/php/access/ValueAccessTest.php b/php/tests/php/access/ValueAccessTest.php new file mode 100644 index 0000000..a7d08c9 --- /dev/null +++ b/php/tests/php/access/ValueAccessTest.php @@ -0,0 +1,70 @@ +exists()); + self::assertFalse($a->available()); + self::assertSame($default, $a->get($default)); + + $i = false; + $a = new ValueAccess($i); + self::assertTrue($a->exists()); + self::assertTrue($a->available()); + self::assertSame(false, $a->get($default)); + + $i = ""; + $a = new ValueAccess($i); + self::assertTrue($a->exists()); + self::assertTrue($a->available()); + self::assertSame("", $a->get($default)); + + # + $i = null; + $a = new ValueAccess($i, ["allow_null" => false]); + self::assertFalse($a->exists()); + self::assertFalse($a->available()); + self::assertSame($default, $a->get($default)); + + $i = null; + $a = new ValueAccess($i, ["allow_null" => true]); + self::assertTrue($a->exists()); + self::assertTrue($a->available()); + self::assertSame(null, $a->get($default)); + + # + $i = false; + $a = new ValueAccess($i, ["allow_false" => false]); + self::assertTrue($a->exists()); + self::assertFalse($a->available()); + self::assertSame($default, $a->get($default)); + + $i = false; + $a = new ValueAccess($i, ["allow_false" => true]); + self::assertTrue($a->exists()); + self::assertTrue($a->available()); + self::assertSame(false, $a->get($default)); + + # + $i = ""; + $a = new ValueAccess($i, ["allow_empty" => false]); + self::assertTrue($a->exists()); + self::assertFalse($a->available()); + self::assertSame($default, $a->get($default)); + + $i = ""; + $a = new ValueAccess($i, ["allow_empty" => true]); + self::assertTrue($a->exists()); + self::assertTrue($a->available()); + self::assertSame("", $a->get($default)); + } +} diff --git a/php/tests/php/content/cTest.php b/php/tests/php/content/cTest.php new file mode 100644 index 0000000..2f31b4a --- /dev/null +++ b/php/tests/php/content/cTest.php @@ -0,0 +1,40 @@ +")); + self::assertSame("pouet", c::to_string(["pouet"])); + self::assertSame("hello world", c::to_string(["hello", "world"])); + self::assertSame("hello1 world", c::to_string(["hello1", "world"])); + self::assertSame("hello", c::to_string(["hello", ""])); + self::assertSame("world", c::to_string(["", "world"])); + self::assertSame("hello,world", c::to_string(["hello,", "world"])); + self::assertSame("hello world", c::to_string(["hello ", "world"])); + self::assertSame("hello. world", c::to_string(["hello.", "world"])); + self::assertSame("hello.", c::to_string(["hello.", ""])); + + self::assertSame( + "

title<q/>

hellobrave<q/>world

", + c::to_string([ + [html::H1, "title"], + [html::P, [ + "hello", + [html::SPAN, "brave"], + [html::SPAN, ["world"]], + ]], + ])); + } + + function testXxx() { + $content = [[v::h1, "hello"]]; + self::assertSame("

hello

", c::to_string($content)); + } +} + diff --git a/php/tests/php/content/impl/AContent.php b/php/tests/php/content/impl/AContent.php new file mode 100644 index 0000000..c53c376 --- /dev/null +++ b/php/tests/php/content/impl/AContent.php @@ -0,0 +1,10 @@ +content"]; + } +} diff --git a/php/tests/php/content/impl/APrintable.php b/php/tests/php/content/impl/APrintable.php new file mode 100644 index 0000000..7a75c2f --- /dev/null +++ b/php/tests/php/content/impl/APrintable.php @@ -0,0 +1,10 @@ +printable

"; + } +} diff --git a/php/tests/php/content/impl/ATag.php b/php/tests/php/content/impl/ATag.php new file mode 100644 index 0000000..b4cc2ed --- /dev/null +++ b/php/tests/php/content/impl/ATag.php @@ -0,0 +1,23 @@ +tag = $tag; + $this->content = $content; + } + + protected $tag; + protected $content; + + function getContent(): iterable { + return [ + "<$this->tag>", + ...c::q($this->content), + "tag>", + ]; + } +} diff --git a/php/tests/php/content/impl/html.php b/php/tests/php/content/impl/html.php new file mode 100644 index 0000000..3567b2e --- /dev/null +++ b/php/tests/php/content/impl/html.php @@ -0,0 +1,14 @@ +", + false, null, + false, null, + ], + ["tsimple", + true, [false, "tsimple"], + true, [false, "tsimple"], + ], + ['nulib\php\impl\ntsimple', + true, [false, 'nulib\php\impl\ntsimple'], + true, [false, 'nulib\php\impl\ntsimple'], + ], + ['tmissing', + false, null, + true, [false, 'tmissing'], + ], + ["::tstatic", + false, null, + false, null, + ], + ["->tmethod", + false, null, + false, null, + ], + ["::tmissing", + false, null, + false, null, + ], + ["->tmissing", + false, null, + false, null, + ], + ["xxx::tmissing", + false, null, + false, null, + ], + ["xxx->tmissing", + false, null, + false, null, + ], + [SC::class."::tstatic", + false, null, + false, null, + ], + [SC::class."->tmethod", + false, null, + false, null, + ], + [SC::class."::tmissing", + false, null, + false, null, + ], + [SC::class."->tmissing", + false, null, + false, null, + ], + # tableaux avec un seul scalaire + [[], + false, null, + false, null, + ], + [[null], + false, null, + false, null, + ], + [[false], + false, null, + false, null, + ], + [[""], + false, null, + false, null, + ], + [["::"], + false, null, + false, null, + ], + [["->"], + false, null, + false, null, + ], + [["tsimple"], + false, null, + false, null, + ], + [['nulib\php\impl\ntsimple'], + false, null, + false, null, + ], + [["::tstatic"], + false, null, + false, null, + ], + [["->tmethod"], + false, null, + false, null, + ], + [["::tmissing"], + false, null, + false, null, + ], + [["->tmissing"], + false, null, + false, null, + ], + [["xxx::tmissing"], + false, null, + false, null, + ], + [["xxx->tmissing"], + false, null, + false, null, + ], + [[SC::class."::tstatic"], + false, null, + false, null, + ], + [[SC::class."->tmethod"], + false, null, + false, null, + ], + [[SC::class."::tmissing"], + false, null, + false, null, + ], + [[SC::class."->tmissing"], + false, null, + false, null, + ], + # tableaux avec deux scalaires + [[null, "tsimple"], + false, null, + false, null, + ], + [[null, 'nulib\php\impl\ntsimple'], + false, null, + false, null, + ], + [[null, "tmissing"], + false, null, + false, null, + ], + [[null, "::tstatic"], + false, null, + false, null, + ], + [[null, "->tmethod"], + false, null, + false, null, + ], + [[null, "::tmissing"], + false, null, + false, null, + ], + [[null, "->tmissing"], + false, null, + false, null, + ], + [[false, "tsimple"], + true, [false, "tsimple"], + true, [false, "tsimple"], + ], + [[false, 'nulib\php\impl\ntsimple'], + true, [false, 'nulib\php\impl\ntsimple'], + true, [false, 'nulib\php\impl\ntsimple'], + ], + [[false, "tmissing"], + false, null, + true, [false, "tmissing"], + ], + [[false, "::tstatic"], + false, null, + false, null, + ], + [[false, "->tmethod"], + false, null, + false, null, + ], + [[false, "::tmissing"], + false, null, + false, null, + ], + [[false, "->tmissing"], + false, null, + false, null, + ], + [["", "tsimple"], + false, null, + false, null, + ], + [["", 'nulib\php\impl\ntsimple'], + false, null, + false, null, + ], + [["", "tmissing"], + false, null, + false, null, + ], + [["", "::tstatic"], + false, null, + false, null, + ], + [["", "->tmethod"], + false, null, + false, null, + ], + [["", "::tmissing"], + false, null, + false, null, + ], + [["", "->tmissing"], + false, null, + false, null, + ], + [["xxx", "tmissing"], + false, null, + false, null, + ], + [["xxx", "::tmissing"], + false, null, + false, null, + ], + [["xxx", "->tmissing"], + false, null, + false, null, + ], + [[SC::class, "tstatic"], + false, null, + false, null, + ], + [[SC::class, "::tstatic"], + false, null, + false, null, + ], + [[SC::class, "tmethod"], + false, null, + false, null, + ], + [[SC::class, "->tmethod"], + false, null, + false, null, + ], + [[SC::class, "tmissing"], + false, null, + false, null, + ], + [[SC::class, "::tmissing"], + false, null, + false, null, + ], + [[SC::class, "->tmissing"], + false, null, + false, null, + ], + ]; + + function testFunction() { + foreach (self::FUNCTION_TESTS as $args) { + [$func, + $verifix1, $func1, + $verifix2, $func2, + ] = $args; + if ($func === ["", "tsimple"]) { + //echo "breakpoint"; + } + + $workf = $func; + $msg = var_export($func, true)." (strict)"; + self::assertSame($verifix1, func::verifix_function($workf, true), "$msg --> verifix"); + if ($verifix1) { + self::assertSame($func1, $workf, "$msg --> func"); + } + + $workf = $func; + $msg = var_export($func, true)." (lenient)"; + self::assertSame($verifix2, func::verifix_function($workf, false), "$msg --> verifix"); + if ($verifix2) { + self::assertSame($func2, $workf, "$msg --> func"); + } + } + } + + const STATIC_TESTS = [ + # scalaires + [null, + false, null, null, + false, null, null, + ], + [false, + false, null, null, + false, null, null, + ], + ["", + false, null, null, + false, null, null, + ], + ["::", + false, null, null, + false, null, null, + ], + ["->", + false, null, null, + false, null, null, + ], + ["tsimple", + false, null, null, + false, null, null, + ], + ['nulib\php\impl\ntsimple', + false, null, null, + false, null, null, + ], + ['tmissing', + false, null, null, + false, null, null, + ], + ["::tstatic", + true, false, [null, "tstatic"], + true, false, [null, "tstatic"], + ], + ["->tmethod", + false, null, null, + false, null, null, + ], + ["::tmissing", + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + ["->tmissing", + false, null, null, + false, null, null, + ], + ["xxx::tmissing", + false, null, null, + true, true, ["xxx", "tmissing"], + ], + ["xxx->tmissing", + false, null, null, + false, null, null, + ], + [SC::class."::tstatic", + true, true, [SC::class, "tstatic"], + true, true, [SC::class, "tstatic"], + ], + [SC::class."->tmethod", + false, null, null, + false, null, null, + ], + [SC::class."::tmissing", + false, null, null, + true, true, [SC::class, "tmissing"], + ], + [SC::class."->tmissing", + false, null, null, + false, null, null, + ], + # tableaux avec un seul scalaire + [[], + false, null, null, + false, null, null, + ], + [[null], + false, null, null, + false, null, null, + ], + [[false], + false, null, null, + false, null, null, + ], + [[""], + false, null, null, + false, null, null, + ], + [["::"], + false, null, null, + false, null, null, + ], + [["->"], + false, null, null, + false, null, null, + ], + [["tsimple"], + false, null, null, + false, null, null, + ], + [['nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [["::tstatic"], + true, false, [null, "tstatic"], + true, false, [null, "tstatic"], + ], + [["->tmethod"], + false, null, null, + false, null, null, + ], + [["::tmissing"], + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + [["->tmissing"], + false, null, null, + false, null, null, + ], + [["xxx::tmissing"], + false, null, null, + true, true, ["xxx", "tmissing"], + ], + [["xxx->tmissing"], + false, null, null, + false, null, null, + ], + [[SC::class."::tstatic"], + true, true, [SC::class, "tstatic"], + true, true, [SC::class, "tstatic"], + ], + [[SC::class."->tmethod"], + false, null, null, + false, null, null, + ], + [[SC::class."::tmissing"], + false, null, null, + true, true, [SC::class, "tmissing"], + ], + [[SC::class."->tmissing"], + false, null, null, + false, null, null, + ], + # tableaux avec deux scalaires + [[null, "tsimple"], + true, false, [null, "tsimple"], + true, false, [null, "tsimple"], + ], + [[null, 'nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [[null, "tmissing"], + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + [[null, "::tstatic"], + true, false, [null, "tstatic"], + true, false, [null, "tstatic"], + ], + [[null, "->tmethod"], + false, null, null, + false, null, null, + ], + [[null, "::tmissing"], + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + [[null, "->tmissing"], + false, null, null, + false, null, null, + ], + [[false, "tsimple"], + false, null, null, + false, null, null, + ], + [[false, 'nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [[false, "tmissing"], + false, null, null, + false, null, null, + ], + [[false, "::tstatic"], + false, null, null, + false, null, null, + ], + [[false, "->tmethod"], + false, null, null, + false, null, null, + ], + [[false, "::tmissing"], + false, null, null, + false, null, null, + ], + [[false, "->tmissing"], + false, null, null, + false, null, null, + ], + [["", "tsimple"], + false, null, null, + false, null, null, + ], + [["", 'nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [["", "tmissing"], + false, null, null, + false, null, null, + ], + [["", "::tstatic"], + false, null, null, + false, null, null, + ], + [["", "->tmethod"], + false, null, null, + false, null, null, + ], + [["", "::tmissing"], + false, null, null, + false, null, null, + ], + [["", "->tmissing"], + false, null, null, + false, null, null, + ], + [["xxx", "tmissing"], + false, null, null, + true, true, ["xxx", "tmissing"], + ], + [["xxx", "::tmissing"], + false, null, null, + true, true, ["xxx", "tmissing"], + ], + [["xxx", "->tmissing"], + false, null, null, + false, null, null, + ], + [[SC::class, "tstatic"], + true, true, [SC::class, "tstatic"], + true, true, [SC::class, "tstatic"], + ], + [[SC::class, "::tstatic"], + true, true, [SC::class, "tstatic"], + true, true, [SC::class, "tstatic"], + ], + [[SC::class, "tmethod"], + true, true, [SC::class, "tmethod"], + true, true, [SC::class, "tmethod"], + ], + [[SC::class, "->tmethod"], + false, null, null, + false, null, null, + ], + [[SC::class, "tmissing"], + false, null, null, + true, true, [SC::class, "tmissing"], + ], + [[SC::class, "::tmissing"], + false, null, null, + true, true, [SC::class, "tmissing"], + ], + [[SC::class, "->tmissing"], + false, null, null, + false, null, null, + ], + ]; + + function testStatic() { + foreach (self::STATIC_TESTS as $args) { + [$func, + $verifix1, $bound1, $func1, + $verifix2, $bound2, $func2, + ] = $args; + if ($func === ["", "tsimple"]) { + //echo "breakpoint"; + } + + $workf = $func; + $msg = var_export($func, true)." (strict)"; + self::assertSame($verifix1, func::verifix_static($workf, true, $bound), "$msg --> verifix"); + if ($verifix1) { + self::assertSame($bound1, $bound, "$msg --> bound"); + self::assertSame($func1, $workf, "$msg --> func"); + } + + $workf = $func; + $msg = var_export($func, true)." (lenient)"; + self::assertSame($verifix2, func::verifix_static($workf, false, $bound), "$msg --> verifix"); + if ($verifix2) { + self::assertSame($bound2, $bound, "$msg --> bound"); + self::assertSame($func2, $workf, "$msg --> func"); + } + } + } + + const METHOD_TESTS = [ + # scalaires + [null, + false, null, null, + false, null, null, + ], + [false, + false, null, null, + false, null, null, + ], + ["", + false, null, null, + false, null, null, + ], + ["::", + false, null, null, + false, null, null, + ], + ["->", + false, null, null, + false, null, null, + ], + ["tsimple", + false, null, null, + false, null, null, + ], + ['nulib\php\impl\ntsimple', + false, null, null, + false, null, null, + ], + ['tmissing', + false, null, null, + false, null, null, + ], + ["::tstatic", + false, null, null, + false, null, null, + ], + ["->tmethod", + true, false, [null, "tmethod"], + true, false, [null, "tmethod"], + ], + ["::tmissing", + false, null, null, + false, null, null, + ], + ["->tmissing", + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + ["xxx::tmissing", + false, null, null, + false, null, null, + ], + ["xxx->tmissing", + false, null, null, + true, true, ["xxx", "tmissing"], + ], + [SC::class."::tstatic", + false, null, null, + false, null, null, + ], + [SC::class."->tmethod", + true, true, [SC::class, "tmethod"], + true, true, [SC::class, "tmethod"], + ], + [SC::class."::tmissing", + false, null, null, + false, null, null, + ], + [SC::class."->tmissing", + false, null, null, + true, true, [SC::class, "tmissing"], + ], + # tableaux avec un seul scalaire + [[], + false, null, null, + false, null, null, + ], + [[null], + false, null, null, + false, null, null, + ], + [[false], + false, null, null, + false, null, null, + ], + [[""], + false, null, null, + false, null, null, + ], + [["::"], + false, null, null, + false, null, null, + ], + [["->"], + false, null, null, + false, null, null, + ], + [["tsimple"], + false, null, null, + false, null, null, + ], + [['nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [["::tstatic"], + false, null, null, + false, null, null, + ], + [["->tmethod"], + true, false, [null, "tmethod"], + true, false, [null, "tmethod"], + ], + [["::tmissing"], + false, null, null, + false, null, null, + ], + [["->tmissing"], + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + [["xxx::tmissing"], + false, null, null, + false, null, null, + ], + [["xxx->tmissing"], + false, null, null, + true, true, ["xxx", "tmissing"], + ], + [[SC::class."::tstatic"], + false, null, null, + false, null, null, + ], + [[SC::class."->tmethod"], + true, true, [SC::class, "tmethod"], + true, true, [SC::class, "tmethod"], + ], + [[SC::class."::tmissing"], + false, null, null, + false, null, null, + ], + [[SC::class."->tmissing"], + false, null, null, + true, true, [SC::class, "tmissing"], + ], + # tableaux avec deux scalaires + [[null, "tsimple"], + true, false, [null, "tsimple"], + true, false, [null, "tsimple"], + ], + [[null, 'nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [[null, "tmissing"], + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + [[null, "::tstatic"], + false, null, null, + false, null, null, + ], + [[null, "->tmethod"], + true, false, [null, "tmethod"], + true, false, [null, "tmethod"], + ], + [[null, "::tmissing"], + false, null, null, + false, null, null, + ], + [[null, "->tmissing"], + true, false, [null, "tmissing"], + true, false, [null, "tmissing"], + ], + [[false, "tsimple"], + false, null, null, + false, null, null, + ], + [[false, 'nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [[false, "tmissing"], + false, null, null, + false, null, null, + ], + [[false, "::tstatic"], + false, null, null, + false, null, null, + ], + [[false, "->tmethod"], + false, null, null, + false, null, null, + ], + [[false, "::tmissing"], + false, null, null, + false, null, null, + ], + [[false, "->tmissing"], + false, null, null, + false, null, null, + ], + [["", "tsimple"], + false, null, null, + false, null, null, + ], + [["", 'nulib\php\impl\ntsimple'], + false, null, null, + false, null, null, + ], + [["", "tmissing"], + false, null, null, + false, null, null, + ], + [["", "::tstatic"], + false, null, null, + false, null, null, + ], + [["", "->tmethod"], + false, null, null, + false, null, null, + ], + [["", "::tmissing"], + false, null, null, + false, null, null, + ], + [["", "->tmissing"], + false, null, null, + false, null, null, + ], + [["xxx", "tmissing"], + false, null, null, + true, true, ["xxx", "tmissing"], + ], + [["xxx", "::tmissing"], + false, null, null, + false, null, null, + ], + [["xxx", "->tmissing"], + false, null, null, + true, true, ["xxx", "tmissing"], + ], + [[SC::class, "tstatic"], + true, true, [SC::class, "tstatic"], + true, true, [SC::class, "tstatic"], + ], + [[SC::class, "::tstatic"], + false, null, null, + false, null, null, + ], + [[SC::class, "tmethod"], + true, true, [SC::class, "tmethod"], + true, true, [SC::class, "tmethod"], + ], + [[SC::class, "->tmethod"], + true, true, [SC::class, "tmethod"], + true, true, [SC::class, "tmethod"], + ], + [[SC::class, "tmissing"], + false, null, null, + true, true, [SC::class, "tmissing"], + ], + [[SC::class, "::tmissing"], + false, null, null, + false, null, null, + ], + [[SC::class, "->tmissing"], + false, null, null, + true, true, [SC::class, "tmissing"], + ], + ]; + + function testMethod() { + foreach (self::METHOD_TESTS as $args) { + [$func, + $verifix1, $bound1, $func1, + $verifix2, $bound2, $func2, + ] = $args; + + $workf = $func; + $msg = var_export($func, true)." (strict)"; + self::assertSame($verifix1, func::verifix_method($workf, true, $bound), "$msg --> verifix"); + if ($verifix1) { + self::assertSame($bound1, $bound, "$msg --> bound"); + self::assertSame($func1, $workf, "$msg --> func"); + } + + $workf = $func; + $msg = var_export($func, true)." (lenient)"; + self::assertSame($verifix2, func::verifix_method($workf, false, $bound), "$msg --> verifix"); + if ($verifix2) { + self::assertSame($bound2, $bound, "$msg --> bound"); + self::assertSame($func2, $workf, "$msg --> func"); + } + } + } + + function testInvokeFunction() { + # m1 + self::assertSame([null], func::call("tm1")); + self::assertSame([null], func::call("tm1", null)); + self::assertSame([null], func::call("tm1", null, null)); + self::assertSame([null], func::call("tm1", null, null, null)); + self::assertSame([null], func::call("tm1", null, null, null, null)); + self::assertSame([1], func::call("tm1", 1)); + self::assertSame([1], func::call("tm1", 1, 2)); + self::assertSame([1], func::call("tm1", 1, 2, 3)); + self::assertSame([1], func::call("tm1", 1, 2, 3, 4)); + + # o1 + self::assertSame([9], func::call("to1")); + self::assertSame([null], func::call("to1", null)); + self::assertSame([null], func::call("to1", null, null)); + self::assertSame([null], func::call("to1", null, null, null)); + self::assertSame([null], func::call("to1", null, null, null, null)); + self::assertSame([1], func::call("to1", 1)); + self::assertSame([1], func::call("to1", 1, 2)); + self::assertSame([1], func::call("to1", 1, 2, 3)); + self::assertSame([1], func::call("to1", 1, 2, 3, 4)); + + # v + self::assertSame([], func::call("tv")); + self::assertSame([null], func::call("tv", null)); + self::assertSame([null, null], func::call("tv", null, null)); + self::assertSame([null, null, null], func::call("tv", null, null, null)); + self::assertSame([null, null, null, null], func::call("tv", null, null, null, null)); + self::assertSame([1], func::call("tv", 1)); + self::assertSame([1, 2], func::call("tv", 1, 2)); + self::assertSame([1, 2, 3], func::call("tv", 1, 2, 3)); + self::assertSame([1, 2, 3, 4], func::call("tv", 1, 2, 3, 4)); + + # m1o1 + self::assertSame([null, 9], func::call("tm1o1")); + self::assertSame([null, 9], func::call("tm1o1", null)); + self::assertSame([null, null], func::call("tm1o1", null, null)); + self::assertSame([null, null], func::call("tm1o1", null, null, null)); + self::assertSame([null, null], func::call("tm1o1", null, null, null, null)); + self::assertSame([1, 9], func::call("tm1o1", 1)); + self::assertSame([1, 2], func::call("tm1o1", 1, 2)); + self::assertSame([1, 2], func::call("tm1o1", 1, 2, 3)); + self::assertSame([1, 2], func::call("tm1o1", 1, 2, 3, 4)); + + # m1v + self::assertSame([null], func::call("tm1v")); + self::assertSame([null], func::call("tm1v", null)); + self::assertSame([null, null], func::call("tm1v", null, null)); + self::assertSame([null, null, null], func::call("tm1v", null, null, null)); + self::assertSame([null, null, null, null], func::call("tm1v", null, null, null, null)); + self::assertSame([1], func::call("tm1v", 1)); + self::assertSame([1, 2], func::call("tm1v", 1, 2)); + self::assertSame([1, 2, 3], func::call("tm1v", 1, 2, 3)); + self::assertSame([1, 2, 3, 4], func::call("tm1v", 1, 2, 3, 4)); + + # m1o1v + self::assertSame([null, 9], func::call("tm1o1v")); + self::assertSame([null, 9], func::call("tm1o1v", null)); + self::assertSame([null, null], func::call("tm1o1v", null, null)); + self::assertSame([null, null, null], func::call("tm1o1v", null, null, null)); + self::assertSame([null, null, null, null], func::call("tm1o1v", null, null, null, null)); + self::assertSame([1, 9], func::call("tm1o1v", 1)); + self::assertSame([1, 2], func::call("tm1o1v", 1, 2)); + self::assertSame([1, 2, 3], func::call("tm1o1v", 1, 2, 3)); + self::assertSame([1, 2, 3, 4], func::call("tm1o1v", 1, 2, 3, 4)); + + # o1v + self::assertSame([9], func::call("to1v")); + self::assertSame([null], func::call("to1v", null)); + self::assertSame([null, null], func::call("to1v", null, null)); + self::assertSame([null, null, null], func::call("to1v", null, null, null)); + self::assertSame([null, null, null, null], func::call("to1v", null, null, null, null)); + self::assertSame([1], func::call("to1v", 1)); + self::assertSame([1, 2], func::call("to1v", 1, 2)); + self::assertSame([1, 2, 3], func::call("to1v", 1, 2, 3)); + self::assertSame([1, 2, 3, 4], func::call("to1v", 1, 2, 3, 4)); + } + + function testInvokeClass() { + $func = func::with(SC::class); + self::assertInstanceOf(SC::class, $func->invoke()); + self::assertInstanceOf(SC::class, $func->invoke([])); + self::assertInstanceOf(SC::class, $func->invoke([1])); + self::assertInstanceOf(SC::class, $func->invoke([1, 2])); + self::assertInstanceOf(SC::class, $func->invoke([1, 2, 3])); + + $func = func::with(C0::class); + self::assertInstanceOf(C0::class, $func->invoke()); + self::assertInstanceOf(C0::class, $func->invoke([])); + self::assertInstanceOf(C0::class, $func->invoke([1])); + self::assertInstanceOf(C0::class, $func->invoke([1, 2])); + self::assertInstanceOf(C0::class, $func->invoke([1, 2, 3])); + + $func = func::with(C1::class); + /** @var C1 $i1 */ + $i1 = $func->invoke(); + self::assertInstanceOf(C1::class, $i1); self::assertSame(0, $i1->base); + $i1 = $func->invoke([]); + self::assertInstanceOf(C1::class, $i1); self::assertSame(0, $i1->base); + $i1 = $func->invoke([1]); + self::assertInstanceOf(C1::class, $i1); self::assertSame(1, $i1->base); + $i1 = $func->invoke([1, 2]); + self::assertInstanceOf(C1::class, $i1); self::assertSame(1, $i1->base); + } + + private static function invoke_asserts(): array { + $inv_ok = function($func) { + return func::with($func)->invoke(); + }; + $inv_ko = function($func) use ($inv_ok) { + return function() use ($func, $inv_ok) { + return $inv_ok($func); + }; + }; + $bind_ok = function($func, $objet) { + return func::with($func)->bind($objet)->invoke(); + }; + $bind_ko = function($func, $object) use ($bind_ok) { + return function() use ($func, $object, $bind_ok) { + return $bind_ok($func, $object); + }; + }; + return [$inv_ok, $inv_ko, $bind_ok, $bind_ko]; + } + + function testInvokeStatic() { + [$inv_ok, $inv_ko, $bind_ok, $bind_ko] = self::invoke_asserts(); + $sc = new SC(); + + self::assertSame(10, $inv_ok([SC::class, "tstatic"])); + self::assertSame(10, $inv_ok([SC::class, "::tstatic"])); + self::assertSame(10, $inv_ok([SC::class, "->tstatic"])); + + self::assertSame(10, $inv_ok([$sc, "tstatic"])); + self::assertSame(10, $inv_ok([$sc, "::tstatic"])); + self::assertSame(10, $inv_ok([$sc, "->tstatic"])); + + self::assertException(ValueException::class, $inv_ko([null, "tstatic"])); + self::assertException(ValueException::class, $inv_ko([null, "::tstatic"])); + self::assertException(ValueException::class, $inv_ko([null, "->tstatic"])); + + self::assertSame(10, $bind_ok([null, "tstatic"], SC::class)); + self::assertSame(10, $bind_ok([null, "::tstatic"], SC::class)); + self::assertSame(10, $bind_ok([null, "->tstatic"], SC::class)); + + self::assertSame(10, $bind_ok([null, "tstatic"], $sc)); + self::assertSame(10, $bind_ok([null, "::tstatic"], $sc)); + self::assertSame(10, $bind_ok([null, "->tstatic"], $sc)); + } + + function testInvokeMethod() { + [$inv_ok, $inv_ko, $bind_ok, $bind_ko] = self::invoke_asserts(); + $sc = new SC(); + + self::assertException(ReflectionException::class, $inv_ko([SC::class, "tmethod"])); + self::assertException(ReflectionException::class, $inv_ko([SC::class, "::tmethod"])); + self::assertException(ReflectionException::class, $inv_ko([SC::class, "->tmethod"])); + + self::assertSame(11, $inv_ok([$sc, "tmethod"])); + self::assertException(ReflectionException::class, $inv_ko([$sc, "::tmethod"])); + self::assertSame(11, $inv_ok([$sc, "->tmethod"])); + + self::assertException(ValueException::class, $inv_ko([null, "tmethod"])); + self::assertException(ValueException::class, $inv_ko([null, "::tmethod"])); + self::assertException(ValueException::class, $inv_ko([null, "->tmethod"])); + + self::assertException(ReflectionException::class, $bind_ko([null, "tmethod"], SC::class)); + self::assertException(ReflectionException::class, $bind_ko([null, "::tmethod"], SC::class)); + self::assertException(ReflectionException::class, $bind_ko([null, "->tmethod"], SC::class)); + + self::assertSame(11, $bind_ok([null, "tmethod"], $sc)); + self::assertException(ReflectionException::class, $bind_ko([null, "::tmethod"], $sc)); + self::assertSame(11, $bind_ok([null, "->tmethod"], $sc)); + } + + function testArgs() { + $func = function(int $a, int $b, int $c): int { + return $a + $b + $c; + }; + + self::assertSame(6, func::call($func, 1, 2, 3)); + self::assertSame(6, func::call($func, 1, 2, 3, 4)); + + self::assertSame(6, func::with($func)->invoke([1, 2, 3])); + self::assertSame(6, func::with($func, [1])->invoke([2, 3])); + self::assertSame(6, func::with($func, [1, 2])->invoke([3])); + self::assertSame(6, func::with($func, [1, 2, 3])->invoke()); + self::assertSame(6, func::with($func, [1, 2, 3, 4])->invoke()); + } + + function testRebind() { + $func = func::with([C1::class, "tmethod"]); + self::assertSame(11, $func->bind(new C1(0))->invoke()); + self::assertSame(12, $func->bind(new C1(1))->invoke()); + self::assertException(ValueException::class, function() use ($func) { + $func->bind(new C0())->invoke(); + }); + } + } +} + +namespace { + function tsimple(): int { return 0; } + function tm1($a): array { return [$a]; } + function to1($b=9): array { return [$b]; } + function tv(...$c): array { return [...$c]; } + function tm1o1($a, $b=9): array { return [$a, $b]; } + function tm1v($a, ...$c): array { return [$a, ...$c]; } + function tm1o1v($a, $b=9, ...$c): array { return [$a, $b, ...$c]; } + function to1v($b=9, ...$c): array { return [$b, ...$c]; } +} + +namespace nulib\php\impl { + function ntsimple(): int { return 0; } + + class SC { + static function tstatic(): int { + return 10; + } + + function tmethod(): int { + return 11; + } + } + + class C0 { + function __construct() { + } + + static function tstatic(): int { + return 10; + } + + function tmethod(): int { + return 11; + } + } + + class C1 { + function __construct(int $base=0) { + $this->base = $base; + } + + public int $base; + + static function tstatic(): int { + return 10; + } + + function tmethod(): int { + return 11 + $this->base; + } + } +} diff --git a/php/tests/php/nur_funcTest.php b/php/tests/php/nur_funcTest.php new file mode 100644 index 0000000..44fa744 --- /dev/null +++ b/php/tests/php/nur_funcTest.php @@ -0,0 +1,292 @@ +")); + self::assertFalse(nur_func::is_method([])); + self::assertFalse(nur_func::is_method([""])); + self::assertFalse(nur_func::is_method([null, "->"])); + self::assertFalse(nur_func::is_method(["xxx", "->"])); + + self::assertTrue(nur_func::is_method("->xxx")); + self::assertTrue(nur_func::is_method(["->xxx"])); + self::assertTrue(nur_func::is_method([null, "->yyy"])); + self::assertTrue(nur_func::is_method(["xxx", "->yyy"])); + self::assertTrue(nur_func::is_method([null, "->yyy", "aaa"])); + self::assertTrue(nur_func::is_method(["xxx", "->yyy", "aaa"])); + } + + function testFix_method() { + $object = new \stdClass(); + $func= "->xxx"; + nur_func::fix_method($func, $object); + self::assertSame([$object, "xxx"], $func); + $func= ["->xxx"]; + nur_func::fix_method($func, $object); + self::assertSame([$object, "xxx"], $func); + $func= [null, "->yyy"]; + nur_func::fix_method($func, $object); + self::assertSame([$object, "yyy"], $func); + $func= ["xxx", "->yyy"]; + nur_func::fix_method($func, $object); + self::assertSame([$object, "yyy"], $func); + $func= [null, "->yyy", "aaa"]; + nur_func::fix_method($func, $object); + self::assertSame([$object, "yyy", "aaa"], $func); + $func= ["xxx", "->yyy", "aaa"]; + nur_func::fix_method($func, $object); + self::assertSame([$object, "yyy", "aaa"], $func); + } + + function testCall() { + self::assertSame(36, nur_func::call("func36")); + self::assertSame(12, nur_func::call(TC::class."::method")); + self::assertSame(12, nur_func::call([TC::class, "method"])); + $closure = function() { + return 21; + }; + self::assertSame(21, nur_func::call($closure)); + } + + function test_prepare_fill() { + # vérifier que les arguments sont bien remplis, en fonction du fait qu'ils + # soient obligatoires, facultatifs ou variadiques + + # m1 + self::assertSame([null], nur_func::call("func_m1")); + self::assertSame([null], nur_func::call("func_m1", null)); + self::assertSame([null], nur_func::call("func_m1", null, null)); + self::assertSame([null], nur_func::call("func_m1", null, null, null)); + self::assertSame([null], nur_func::call("func_m1", null, null, null, null)); + self::assertSame([1], nur_func::call("func_m1", 1)); + self::assertSame([1], nur_func::call("func_m1", 1, 2)); + self::assertSame([1], nur_func::call("func_m1", 1, 2, 3)); + self::assertSame([1], nur_func::call("func_m1", 1, 2, 3, 4)); + + # o1 + self::assertSame([9], nur_func::call("func_o1")); + self::assertSame([null], nur_func::call("func_o1", null)); + self::assertSame([null], nur_func::call("func_o1", null, null)); + self::assertSame([null], nur_func::call("func_o1", null, null, null)); + self::assertSame([null], nur_func::call("func_o1", null, null, null, null)); + self::assertSame([1], nur_func::call("func_o1", 1)); + self::assertSame([1], nur_func::call("func_o1", 1, 2)); + self::assertSame([1], nur_func::call("func_o1", 1, 2, 3)); + self::assertSame([1], nur_func::call("func_o1", 1, 2, 3, 4)); + + # v + self::assertSame([], nur_func::call("func_v")); + self::assertSame([null], nur_func::call("func_v", null)); + self::assertSame([null, null], nur_func::call("func_v", null, null)); + self::assertSame([null, null, null], nur_func::call("func_v", null, null, null)); + self::assertSame([null, null, null, null], nur_func::call("func_v", null, null, null, null)); + self::assertSame([1], nur_func::call("func_v", 1)); + self::assertSame([1, 2], nur_func::call("func_v", 1, 2)); + self::assertSame([1, 2, 3], nur_func::call("func_v", 1, 2, 3)); + self::assertSame([1, 2, 3, 4], nur_func::call("func_v", 1, 2, 3, 4)); + + # m1o1 + self::assertSame([null, 9], nur_func::call("func_m1o1")); + self::assertSame([null, 9], nur_func::call("func_m1o1", null)); + self::assertSame([null, null], nur_func::call("func_m1o1", null, null)); + self::assertSame([null, null], nur_func::call("func_m1o1", null, null, null)); + self::assertSame([null, null], nur_func::call("func_m1o1", null, null, null, null)); + self::assertSame([1, 9], nur_func::call("func_m1o1", 1)); + self::assertSame([1, 2], nur_func::call("func_m1o1", 1, 2)); + self::assertSame([1, 2], nur_func::call("func_m1o1", 1, 2, 3)); + self::assertSame([1, 2], nur_func::call("func_m1o1", 1, 2, 3, 4)); + + # m1v + self::assertSame([null], nur_func::call("func_m1v")); + self::assertSame([null], nur_func::call("func_m1v", null)); + self::assertSame([null, null], nur_func::call("func_m1v", null, null)); + self::assertSame([null, null, null], nur_func::call("func_m1v", null, null, null)); + self::assertSame([null, null, null, null], nur_func::call("func_m1v", null, null, null, null)); + self::assertSame([1], nur_func::call("func_m1v", 1)); + self::assertSame([1, 2], nur_func::call("func_m1v", 1, 2)); + self::assertSame([1, 2, 3], nur_func::call("func_m1v", 1, 2, 3)); + self::assertSame([1, 2, 3, 4], nur_func::call("func_m1v", 1, 2, 3, 4)); + + # m1o1v + self::assertSame([null, 9], nur_func::call("func_m1o1v")); + self::assertSame([null, 9], nur_func::call("func_m1o1v", null)); + self::assertSame([null, null], nur_func::call("func_m1o1v", null, null)); + self::assertSame([null, null, null], nur_func::call("func_m1o1v", null, null, null)); + self::assertSame([null, null, null, null], nur_func::call("func_m1o1v", null, null, null, null)); + self::assertSame([1, 9], nur_func::call("func_m1o1v", 1)); + self::assertSame([1, 2], nur_func::call("func_m1o1v", 1, 2)); + self::assertSame([1, 2, 3], nur_func::call("func_m1o1v", 1, 2, 3)); + self::assertSame([1, 2, 3, 4], nur_func::call("func_m1o1v", 1, 2, 3, 4)); + + # o1v + self::assertSame([9], nur_func::call("func_o1v")); + self::assertSame([null], nur_func::call("func_o1v", null)); + self::assertSame([null, null], nur_func::call("func_o1v", null, null)); + self::assertSame([null, null, null], nur_func::call("func_o1v", null, null, null)); + self::assertSame([null, null, null, null], nur_func::call("func_o1v", null, null, null, null)); + self::assertSame([1], nur_func::call("func_o1v", 1)); + self::assertSame([1, 2], nur_func::call("func_o1v", 1, 2)); + self::assertSame([1, 2, 3], nur_func::call("func_o1v", 1, 2, 3)); + self::assertSame([1, 2, 3, 4], nur_func::call("func_o1v", 1, 2, 3, 4)); + } + + function testCall_all() { + $c1 = new C1(); + $c2 = new C2(); + $c3 = new C3(); + + self::assertSameValues([11, 12], nur_func::call_all(C1::class)); + self::assertSameValues([11, 12, 21, 22], nur_func::call_all($c1)); + self::assertSameValues([13, 11, 12], nur_func::call_all(C2::class)); + self::assertSameValues([13, 23, 11, 12, 21, 22], nur_func::call_all($c2)); + self::assertSameValues([111, 13, 12], nur_func::call_all(C3::class)); + self::assertSameValues([111, 121, 13, 23, 12, 22], nur_func::call_all($c3)); + + $options = "conf"; + self::assertSameValues([11], nur_func::call_all(C1::class, $options)); + self::assertSameValues([11, 21], nur_func::call_all($c1, $options)); + self::assertSameValues([11], nur_func::call_all(C2::class, $options)); + self::assertSameValues([11, 21], nur_func::call_all($c2, $options)); + self::assertSameValues([111], nur_func::call_all(C3::class, $options)); + self::assertSameValues([111, 121], nur_func::call_all($c3, $options)); + + $options = ["prefix" => "conf"]; + self::assertSameValues([11], nur_func::call_all(C1::class, $options)); + self::assertSameValues([11, 21], nur_func::call_all($c1, $options)); + self::assertSameValues([11], nur_func::call_all(C2::class, $options)); + self::assertSameValues([11, 21], nur_func::call_all($c2, $options)); + self::assertSameValues([111], nur_func::call_all(C3::class, $options)); + self::assertSameValues([111, 121], nur_func::call_all($c3, $options)); + + self::assertSameValues([11, 12], nur_func::call_all($c1, ["include" => "x"])); + self::assertSameValues([11, 21], nur_func::call_all($c1, ["include" => "y"])); + self::assertSameValues([11, 12, 21], nur_func::call_all($c1, ["include" => ["x", "y"]])); + + self::assertSameValues([21, 22], nur_func::call_all($c1, ["exclude" => "x"])); + self::assertSameValues([12, 22], nur_func::call_all($c1, ["exclude" => "y"])); + self::assertSameValues([22], nur_func::call_all($c1, ["exclude" => ["x", "y"]])); + + self::assertSameValues([12], nur_func::call_all($c1, ["include" => "x", "exclude" => "y"])); + } + + function testCons() { + $obj1 = nur_func::cons(WoCons::class, 1, 2, 3); + self::assertInstanceOf(WoCons::class, $obj1); + + $obj2 = nur_func::cons(WithEmptyCons::class, 1, 2, 3); + self::assertInstanceOf(WithEmptyCons::class, $obj2); + + $obj3 = nur_func::cons(WithCons::class, 1, 2, 3); + self::assertInstanceOf(WithCons::class, $obj3); + self::assertSame(1, $obj3->first); + } + } + + class WoCons { + } + class WithEmptyCons { + function __construct() { + } + } + class WithCons { + public $first; + function __construct($first) { + $this->first = $first; + } + } + + class TC { + static function method() { + return 12; + } + } + + class C1 { + static function confps1_xy() { + return 11; + } + static function ps2_x() { + return 12; + } + function confp1_y() { + return 21; + } + function p2() { + return 22; + } + } + class C2 extends C1 { + static function ps3() { + return 13; + } + function p3() { + return 23; + } + } + class C3 extends C2 { + static function confps1_xy() { + return 111; + } + function confp1_y() { + return 121; + } + } +} diff --git a/php/tests/php/time/DateTest.php b/php/tests/php/time/DateTest.php new file mode 100644 index 0000000..857ca33 --- /dev/null +++ b/php/tests/php/time/DateTest.php @@ -0,0 +1,85 @@ +format()); + self::assertSame("05/04/2024", strval($date)); + self::assertSame(2024, $date->year); + self::assertSame(4, $date->month); + self::assertSame(5, $date->day); + self::assertSame(0, $date->hour); + self::assertSame(0, $date->minute); + self::assertSame(0, $date->second); + self::assertSame(5, $date->wday); + self::assertSame(14, $date->wnum); + self::assertSame("+04:00", $date->timezone); + self::assertSame("05/04/2024 00:00:00", $date->datetime); + self::assertSame("05/04/2024", $date->date); + } + + function testClone() { + $date = self::dt("now"); + $clone = Date::clone($date); + self::assertInstanceOf(DateTime::class, $clone); + } + + function testConstruct() { + $y = date("Y"); + self::assertSame("05/04/$y", strval(new Date("5/4"))); + self::assertSame("05/04/2024", strval(new Date("5/4/24"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024"))); + self::assertSame("05/04/2024", strval(new Date("05/04/2024"))); + self::assertSame("05/04/2024", strval(new Date("20240405"))); + self::assertSame("05/04/2024", strval(new Date("240405"))); + self::assertSame("05/04/2024", strval(new Date("20240405T091523"))); + self::assertSame("05/04/2024", strval(new Date("20240405T091523Z"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 9:15:23"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 9.15.23"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 9:15"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 9.15"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 9h15"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 09:15:23"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 09:15"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 09h15"))); + } + + function testCompare() { + $a = new Date("10/02/2024"); + $b = new Date("15/02/2024"); + $c = new Date("20/02/2024"); + $a2 = new Date("10/02/2024"); + $b2 = new Date("15/02/2024"); + $c2 = new Date("20/02/2024"); + + self::assertTrue($a == $a2); + self::assertFalse($a === $a2); + self::assertTrue($b == $b2); + self::assertTrue($c == $c2); + + self::assertFalse($a < $a); + self::assertTrue($a < $b); + self::assertTrue($a < $c); + + self::assertTrue($a <= $a); + self::assertTrue($a <= $b); + self::assertTrue($a <= $c); + + self::assertFalse($c > $c); + self::assertTrue($c > $b); + self::assertTrue($c > $a); + + self::assertTrue($c >= $c); + self::assertTrue($c >= $b); + self::assertTrue($c >= $a); + } +} diff --git a/php/tests/php/time/DateTimeTest.php b/php/tests/php/time/DateTimeTest.php new file mode 100644 index 0000000..088bc79 --- /dev/null +++ b/php/tests/php/time/DateTimeTest.php @@ -0,0 +1,109 @@ +format()); + self::assertEquals("05/04/2024 09:15:23", strval($date)); + self::assertSame(2024, $date->year); + self::assertSame(4, $date->month); + self::assertSame(5, $date->day); + self::assertSame(9, $date->hour); + self::assertSame(15, $date->minute); + self::assertSame(23, $date->second); + self::assertSame(5, $date->wday); + self::assertSame(14, $date->wnum); + self::assertEquals("+04:00", $date->timezone); + self::assertSame("05/04/2024 09:15:23", $date->datetime); + self::assertSame("05/04/2024", $date->date); + self::assertSame("20240405", $date->Ymd); + self::assertSame("20240405T091523", $date->YmdHMS); + self::assertSame("20240405T091523+04:00", $date->YmdHMSZ); + } + + function testDateTimeZ() { + $date = new DateTime("20240405T091523Z"); + self::assertSame("20240405T091523", $date->YmdHMS); + self::assertSame("20240405T091523Z", $date->YmdHMSZ); + } + + function testClone() { + $date = self::dt("now"); + $clone = DateTime::clone($date); + self::assertInstanceOf(DateTime::class, $clone); + } + + function testConstruct() { + $y = date("Y"); + self::assertSame("05/04/$y 00:00:00", strval(new DateTime("5/4"))); + self::assertSame("05/04/2024 00:00:00", strval(new DateTime("5/4/24"))); + self::assertSame("05/04/2024 00:00:00", strval(new DateTime("5/4/2024"))); + self::assertSame("05/04/2024 00:00:00", strval(new DateTime("05/04/2024"))); + self::assertSame("05/04/2024 00:00:00", strval(new DateTime("20240405"))); + self::assertSame("05/04/2024 00:00:00", strval(new DateTime("240405"))); + self::assertSame("05/04/2024 09:15:23", strval(new DateTime("20240405T091523"))); + self::assertSame("05/04/2024 09:15:23", strval(new DateTime("20240405T091523Z"))); + self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 9:15:23"))); + self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 9.15.23"))); + self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 9:15"))); + self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 9.15"))); + self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 9h15"))); + self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 09:15:23"))); + self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 09:15"))); + self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 09h15"))); + } + + function testCompare() { + $a = new DateTime("10/02/2024"); + $a2 = new DateTime("10/02/2024 8:30"); + $a3 = new DateTime("10/02/2024 15:45"); + $b = new DateTime("15/02/2024"); + $b2 = new DateTime("15/02/2024 8:30"); + $b3 = new DateTime("15/02/2024 15:45"); + $x = new DateTime("10/02/2024"); + $x2 = new DateTime("10/02/2024 8:30"); + $x3 = new DateTime("10/02/2024 15:45"); + + self::assertTrue($a == $x); + self::assertFalse($a === $x); + self::assertTrue($a2 == $x2); + self::assertTrue($a3 == $x3); + + self::assertFalse($a < $a); + self::assertTrue($a < $a2); + self::assertTrue($a < $a3); + self::assertTrue($a < $b); + self::assertTrue($a < $b2); + self::assertTrue($a < $b3); + + self::assertTrue($a <= $a); + self::assertTrue($a <= $a2); + self::assertTrue($a <= $a3); + self::assertTrue($a <= $b); + self::assertTrue($a <= $b2); + self::assertTrue($a <= $b3); + + self::assertTrue($b > $a); + self::assertTrue($b > $a2); + self::assertTrue($b > $a3); + self::assertFalse($b > $b); + self::assertFalse($b > $b2); + self::assertFalse($b > $b3); + + self::assertTrue($b >= $a); + self::assertTrue($b >= $a2); + self::assertTrue($b >= $a3); + self::assertTrue($b >= $b); + self::assertFalse($b >= $b2); + self::assertFalse($b >= $b3); + } +} diff --git a/php/tests/php/time/DelayTest.php b/php/tests/php/time/DelayTest.php new file mode 100644 index 0000000..132bc4d --- /dev/null +++ b/php/tests/php/time/DelayTest.php @@ -0,0 +1,83 @@ +getDest()); + + $delay = new Delay("10", $from); + self::assertEquals(self::dt("2024-04-05 09:15:33"), $delay->getDest()); + + $delay = new Delay("10s", $from); + self::assertEquals(self::dt("2024-04-05 09:15:33"), $delay->getDest()); + + $delay = new Delay("s", $from); + self::assertEquals(self::dt("2024-04-05 09:15:24"), $delay->getDest()); + + $delay = new Delay("5m", $from); + self::assertEquals(self::dt("2024-04-05 09:20:00"), $delay->getDest()); + + $delay = new Delay("5m0", $from); + self::assertEquals(self::dt("2024-04-05 09:20:00"), $delay->getDest()); + + $delay = new Delay("5m2", $from); + self::assertEquals(self::dt("2024-04-05 09:20:02"), $delay->getDest()); + + $delay = new Delay("m", $from); + self::assertEquals(self::dt("2024-04-05 09:16:00"), $delay->getDest()); + + $delay = new Delay("5h", $from); + self::assertEquals(self::dt("2024-04-05 14:00:00"), $delay->getDest()); + + $delay = new Delay("5h0", $from); + self::assertEquals(self::dt("2024-04-05 14:00:00"), $delay->getDest()); + + $delay = new Delay("5h2", $from); + self::assertEquals(self::dt("2024-04-05 14:02:00"), $delay->getDest()); + + $delay = new Delay("h", $from); + self::assertEquals(self::dt("2024-04-05 10:00:00"), $delay->getDest()); + + $delay = new Delay("5d", $from); + self::assertEquals(self::dt("2024-04-10 05:00:00"), $delay->getDest()); + + $delay = new Delay("5d2", $from); + self::assertEquals(self::dt("2024-04-10 02:00:00"), $delay->getDest()); + + $delay = new Delay("5d0", $from); + self::assertEquals(self::dt("2024-04-10 00:00:00"), $delay->getDest()); + + $delay = new Delay("d", $from); + self::assertEquals(self::dt("2024-04-06 05:00:00"), $delay->getDest()); + + $delay = new Delay("2w", $from); + self::assertEquals(self::dt("2024-04-21 05:00:00"), $delay->getDest()); + + $delay = new Delay("2w2", $from); + self::assertEquals(self::dt("2024-04-21 02:00:00"), $delay->getDest()); + + $delay = new Delay("2w0", $from); + self::assertEquals(self::dt("2024-04-21 00:00:00"), $delay->getDest()); + + $delay = new Delay("w", $from); + self::assertEquals(self::dt("2024-04-07 05:00:00"), $delay->getDest()); + } + + function testElapsed() { + $delay = new Delay(5); + sleep(2); + self::assertFalse($delay->isElapsed()); + sleep(5); + self::assertTrue($delay->isElapsed()); + } +} diff --git a/php/tests/schema/_scalar/ScalarSchemaTest.php b/php/tests/schema/_scalar/ScalarSchemaTest.php new file mode 100644 index 0000000..e004168 --- /dev/null +++ b/php/tests/schema/_scalar/ScalarSchemaTest.php @@ -0,0 +1,64 @@ + [null], + "default" => null, + "title" => null, + "required" => false, + "nullable" => true, + "desc" => null, + "analyzer_func" => null, + "extractor_func" => null, + "parser_func" => null, + "normalizer_func" => null, + "messages" => null, + "formatter_func" => null, + "format" => null, + "" => ["scalar"], + "name" => null, + "pkey" => null, + "header" => null, + "composite" => null, + ]; + + static function schema(array $schema): array { + return array_merge(self::NULL_SCHEMA, $schema); + } + + function testNormalize() { + self::assertSame(self::NULL_SCHEMA, ScalarSchema::normalize(null)); + self::assertSame(self::NULL_SCHEMA, ScalarSchema::normalize([])); + self::assertSame(self::NULL_SCHEMA, ScalarSchema::normalize([null])); + self::assertException(SchemaException::class, function () { + ScalarSchema::normalize([[]]); + }); + self::assertException(SchemaException::class, function () { + ScalarSchema::normalize([[null]]); + }); + + $string = self::schema(["type" => ["string"], "nullable" => false]); + self::assertSame($string, ScalarSchema::normalize("string")); + self::assertSame($string, ScalarSchema::normalize(["string"])); + + $nstring = self::schema(["type" => ["string"]]); + self::assertSame($nstring, ScalarSchema::normalize(["?string"])); + self::assertSame($nstring, ScalarSchema::normalize(["?string|null"])); + self::assertSame($nstring, ScalarSchema::normalize(["string|null"])); + self::assertSame($nstring, ScalarSchema::normalize([["?string", "null"]])); + self::assertSame($nstring, ScalarSchema::normalize([["string", "null"]])); + self::assertSame($nstring, ScalarSchema::normalize([["string", null]])); + + $key = self::schema(["type" => ["string", "int"], "nullable" => false]); + self::assertSame($key, ScalarSchema::normalize("string|int")); + + $nkey = self::schema(["type" => ["string", "int"], "nullable" => true]); + self::assertSame($nkey, ScalarSchema::normalize("?string|int")); + self::assertSame($nkey, ScalarSchema::normalize("string|?int")); + } +} diff --git a/php/tests/schema/types/boolTest.php b/php/tests/schema/types/boolTest.php new file mode 100644 index 0000000..8f990e3 --- /dev/null +++ b/php/tests/schema/types/boolTest.php @@ -0,0 +1,111 @@ +set(true); + self::assertSame(true, $destv->get()); + self::assertSame(true, $dest); + self::assertSame("Oui", $destv->format()); + self::assertSame("Oui", $destv->format("OuiNonNull")); + self::assertSame("O", $destv->format("ON")); + self::assertSame("O", $destv->format("ONN")); + + $destv->set(false); + self::assertSame(false, $destv->get()); + self::assertSame(false, $dest); + self::assertSame("Non", $destv->format()); + self::assertSame("Non", $destv->format("OuiNonNull")); + self::assertSame("N", $destv->format("ON")); + self::assertSame("N", $destv->format("ONN")); + + $destv->set("yes"); + self::assertSame(true, $destv->get()); + + $destv->set(" yes "); + self::assertSame(true, $destv->get()); + + $destv->set("12"); + self::assertSame(true, $destv->get()); + + $destv->set(12); + self::assertSame(true, $destv->get()); + + $destv->set("no"); + self::assertSame(false, $destv->get()); + + $destv->set(" no "); + self::assertSame(false, $destv->get()); + + $destv->set("0"); + self::assertSame(false, $destv->get()); + + $destv->set(0); + self::assertSame(false, $destv->get()); + + $destv->set(12.34); + self::assertSame(true, $destv->get()); + + self::assertException(Exception::class, $destvSetter("a")); + self::assertException(Exception::class, $destvSetter([])); + self::assertException(Exception::class, $destvSetter(["a"])); + + } + + function testBool() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "bool"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + self::assertException(Exception::class, $destvSetter(null)); + self::assertException(Exception::class, $destvSetter("")); + self::assertException(Exception::class, $destvSetter(" ")); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testNbool() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "?bool"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + $destv->set(null); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("Non", $destv->format()); + self::assertSame("", $destv->format("OuiNonNull")); + self::assertSame("N", $destv->format("ON")); + self::assertSame("", $destv->format("ONN")); + + $destv->set(""); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("Non", $destv->format()); + self::assertSame("", $destv->format("OuiNonNull")); + self::assertSame("N", $destv->format("ON")); + self::assertSame("", $destv->format("ONN")); + + $destv->set(" "); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("Non", $destv->format()); + self::assertSame("", $destv->format("OuiNonNull")); + self::assertSame("N", $destv->format("ON")); + self::assertSame("", $destv->format("ONN")); + + $this->commonTests($destv, $dest, $destvSetter); + } +} diff --git a/php/tests/schema/types/floatTest.php b/php/tests/schema/types/floatTest.php new file mode 100644 index 0000000..193d407 --- /dev/null +++ b/php/tests/schema/types/floatTest.php @@ -0,0 +1,139 @@ +set(12); + self::assertSame(12.0, $destv->get()); + self::assertSame(12.0, $dest); + self::assertSame("12", $destv->format()); + self::assertSame("0012", $destv->format("%04u")); + + $destv->set("12"); + self::assertSame(12.0, $destv->get()); + + $destv->set(" 12 "); + self::assertSame(12.0, $destv->get()); + + $destv->set(12.34); + self::assertSame(12.34, $destv->get()); + + $destv->set(true); + self::assertSame(1.0, $destv->get()); + + self::assertException(Exception::class, $destvSetter("a")); + self::assertException(Exception::class, $destvSetter([])); + self::assertException(Exception::class, $destvSetter(["a"])); + } + + function testFloat() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "float"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + self::assertException(Exception::class, $destvSetter(null)); + self::assertException(Exception::class, $destvSetter("")); + self::assertException(Exception::class, $destvSetter(" ")); + + // valeur non requise donc retourne null + $destv->set(false); + self::assertNull($destv->get()); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testRequiredFloat() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, [ + "float", null, + "required" => true, + ]); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + self::assertException(Exception::class, $destvSetter(null)); + self::assertException(Exception::class, $destvSetter("")); + self::assertException(Exception::class, $destvSetter(" ")); + + // valeur requise donc lance une exception + self::assertException(Exception::class, $destvSetter(false)); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testNfloat() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "?float"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + $destv->set(null); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(""); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(" "); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + // valeur non requise donc retourne null + $destv->set(false); + self::assertNull($destv->get()); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testRequiredNfloat() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, [ + "?float", null, + "required" => true, + ]); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + $destv->set(null); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(""); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(" "); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + // valeur requise donc lance une exception + self::assertException(Exception::class, $destvSetter(false)); + + $this->commonTests($destv, $dest, $destvSetter); + } +} diff --git a/php/tests/schema/types/intTest.php b/php/tests/schema/types/intTest.php new file mode 100644 index 0000000..29de7ce --- /dev/null +++ b/php/tests/schema/types/intTest.php @@ -0,0 +1,139 @@ +set(12); + self::assertSame(12, $destv->get()); + self::assertSame(12, $dest); + self::assertSame("12", $destv->format()); + self::assertSame("0012", $destv->format("%04u")); + + $destv->set("12"); + self::assertSame(12, $destv->get()); + + $destv->set(" 12 "); + self::assertSame(12, $destv->get()); + + $destv->set(12.34); + self::assertSame(12, $destv->get()); + + $destv->set(true); + self::assertSame(1, $destv->get()); + + self::assertException(Exception::class, $destvSetter("a")); + self::assertException(Exception::class, $destvSetter([])); + self::assertException(Exception::class, $destvSetter(["a"])); + } + + function testInt() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "int"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + self::assertException(Exception::class, $destvSetter(null)); + self::assertException(Exception::class, $destvSetter("")); + self::assertException(Exception::class, $destvSetter(" ")); + + // valeur non requise donc retourne null + $destv->set(false); + self::assertNull($destv->get()); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testRequiredInt() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, [ + "int", null, + "required" => true, + ]); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + self::assertException(Exception::class, $destvSetter(null)); + self::assertException(Exception::class, $destvSetter("")); + self::assertException(Exception::class, $destvSetter(" ")); + + // valeur requise donc lance une exception + self::assertException(Exception::class, $destvSetter(false)); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testNint() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "?int"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + $destv->set(null); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(""); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(" "); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + // valeur non requise donc retourne null + $destv->set(false); + self::assertNull($destv->get()); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testRequiredNint() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, [ + "?int", null, + "required" => true, + ]); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + $destv->set(null); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(""); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(" "); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + // valeur requise donc lance une exception + self::assertException(Exception::class, $destvSetter(false)); + + $this->commonTests($destv, $dest, $destvSetter); + } +} diff --git a/php/tests/schema/types/strTest.php b/php/tests/schema/types/strTest.php new file mode 100644 index 0000000..45eda17 --- /dev/null +++ b/php/tests/schema/types/strTest.php @@ -0,0 +1,123 @@ +set(""); + self::assertSame("", $destv->get()); + self::assertSame("", $dest); + + $destv->set(" "); + self::assertSame(" ", $destv->get()); + self::assertSame(" ", $dest); + + $destv->set("a"); + self::assertSame("a", $destv->get()); + self::assertSame("a", $dest); + + $destv->set("12"); + self::assertSame("12", $destv->get()); + + $destv->set(" 12 "); + self::assertSame(" 12 ", $destv->get()); + + $destv->set(12); + self::assertSame("12", $destv->get()); + + $destv->set(12.34); + self::assertSame("12.34", $destv->get()); + + $destv->set(true); + self::assertSame("1", $destv->get()); + + self::assertException(Exception::class, $destvSetter([])); + self::assertException(Exception::class, $destvSetter(["a"])); + } + + function testStr() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "string"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + self::assertException(Exception::class, $destvSetter(null)); + + // valeur non requise donc retourne null + $destv->set(false); + self::assertNull($destv->get()); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testRequiredStr() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, [ + "string", null, + "required" => true, + ]); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + self::assertException(Exception::class, $destvSetter(null)); + + // valeur requise donc lance une exception + self::assertException(Exception::class, $destvSetter(false)); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testNstr() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "?string"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + $destv->set(null); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + // valeur non requise donc retourne null + $destv->set(false); + self::assertNull($destv->get()); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testRequiredNstr() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, [ + "?string", null, + "required" => true, + ]); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + $destv->set(null); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + // valeur requise donc lance une exception + self::assertException(Exception::class, $destvSetter(false)); + + $this->commonTests($destv, $dest, $destvSetter); + } +} diff --git a/php/tests/schema/types/unionTest.php b/php/tests/schema/types/unionTest.php new file mode 100644 index 0000000..c208087 --- /dev/null +++ b/php/tests/schema/types/unionTest.php @@ -0,0 +1,29 @@ +set("12"); + self::assertSame("12", $si); + $siv->set(12); + self::assertSame(12, $si); + + # int puis string + Schema::nv($isv, $is, null, $iss, "int|string"); + + $isv->set("12"); + self::assertSame("12", $is); + $isv->set(12); + self::assertSame(12, $is); + } +} diff --git a/php/tests/strTest.php b/php/tests/strTest.php new file mode 100644 index 0000000..92785fc --- /dev/null +++ b/php/tests/strTest.php @@ -0,0 +1,28 @@ + [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + # name=multiple[], name=multiple[] + 'multiple' => [ + 'name' => [ + 0 => '', + 1 => '', + ], + 'type' => [ + 0 => '', + 1 => '', + ], + 'tmp_name' => [ + 0 => '', + 1 => '', + ], + 'error' => [ + 0 => 4, + 1 => 4, + ], + 'size' => [ + 0 => 0, + 1 => 0, + ], + ], + # name=onelevel[a], name=onelevel[b] + 'onelevel' => [ + 'name' => [ + 'a' => '', + 'b' => '', + ], + 'type' => [ + 'a' => '', + 'b' => '', + ], + 'tmp_name' => [ + 'a' => '', + 'b' => '', + ], + 'error' => [ + 'a' => 4, + 'b' => 4, + ], + 'size' => [ + 'a' => 0, + 'b' => 0, + ], + ], + # name=multiplelevel[a][], name=multiplelevel[a][], name=multiplelevel[b][], name=multiplelevel[b][] + 'multiplelevel' => [ + 'name' => [ + 'a' => [ + 0 => '', + 1 => '', + ], + 'b' => [ + 0 => '', + 1 => '', + ], + ], + 'type' => [ + 'a' => [ + 0 => '', + 1 => '', + ], + 'b' => [ + 0 => '', + 1 => '', + ], + ], + 'tmp_name' => [ + 'a' => [ + 0 => '', + 1 => '', + ], + 'b' => [ + 0 => '', + 1 => '', + ], + ], + 'error' => [ + 'a' => [ + 0 => 4, + 1 => 4, + ], + 'b' => [ + 0 => 4, + 1 => 4, + ], + ], + 'size' => [ + 'a' => [ + 0 => 0, + 1 => 0, + ], + 'b' => [ + 0 => 0, + 1 => 0, + ], + ], + ], + ]; + + const PARSED = [ + # name="simple" + 'simple' => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + # name=multiple[], name=multiple[] + 'multiple' => [ + 0 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + 1 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + ], + # name=onelevel[a], name=onelevel[b] + 'onelevel' => [ + 'a' => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + 'b' => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + ], + # name=multiplelevel[a][], name=multiplelevel[a][], name=multiplelevel[b][], name=multiplelevel[b][] + 'multiplelevel' => [ + 'a' => [ + 0 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + 1 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + ], + 'b' => [ + 0 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + 1 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + ], + ], + ]; + + function test_files() { + self::assertSame(self::PARSED, uploads::_files(self::_FILES)); + } +} diff --git a/runphp/Dockerfile.runphp b/runphp/Dockerfile.runphp new file mode 100644 index 0000000..ef17f83 --- /dev/null +++ b/runphp/Dockerfile.runphp @@ -0,0 +1,30 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=php /g/ /g/ +RUN /g/build @php-cli php-utils + +ENTRYPOINT ["/g/entrypoint"] diff --git a/runphp/Dockerfile.runphp+ic b/runphp/Dockerfile.runphp+ic new file mode 100644 index 0000000..b380090 --- /dev/null +++ b/runphp/Dockerfile.runphp+ic @@ -0,0 +1,43 @@ +# -*- coding: utf-8 mode: dockerfile -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +ARG NDIST=12 +ARG REGISTRY=pubdocker.univ-reunion.fr + +FROM $REGISTRY/src/base AS base +FROM $REGISTRY/src/legacytools AS legacytools +FROM $REGISTRY/src/instantclient AS instantclient +FROM $REGISTRY/src/php AS php + +################################################################################ +FROM debian:${NDIST}-slim AS builder +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=base /src/ /src/ +RUN /g/build core lite _builder +RUN /g/build _su-exec_builder + +COPY --from=instantclient /g/ /g/ +COPY --from=instantclient /src/ /src/ +RUN /g/build _instantclient_builder + +################################################################################ +FROM debian:${NDIST}-slim +ARG APT_MIRROR SEC_MIRROR APT_PROXY TIMEZONE +ENV APT_MIRROR=$APT_MIRROR SEC_MIRROR=$SEC_MIRROR APT_PROXY=$APT_PROXY TIMEZONE=$TIMEZONE + +COPY --from=base /g/ /g/ +COPY --from=builder /src/su-exec/su-exec /g/ +RUN /g/build + +COPY --from=legacytools /g/ /g/ +RUN /g/build nutools + +COPY --from=php /g/ /g/ +RUN /g/build @php-cli php-utils + +COPY --from=instantclient /g/ /g/ +COPY --from=builder /opt/oracle/ /opt/oracle/ +RUN /g/build instantclient + +ENTRYPOINT ["/g/entrypoint"] diff --git a/runphp/build b/runphp/build new file mode 100755 index 0000000..c1f1cbc --- /dev/null +++ b/runphp/build @@ -0,0 +1,193 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +MYDIR="$(cd "$(dirname -- "$0")"; pwd)" +RUNPHP="$MYDIR/runphp" +"$RUNPHP" --bs --ue --ci || exit 1 +RUNPHP_STANDALONE= +PROJDIR=; COMPOSERDIR=; COMPOSERPHAR=; VENDORDIR=; BUILDENV0=; BUILDENV= +BUILD_IMAGES=(php-apache mariadb10); export BUILD_FLAVOUR=; DIST=; IMAGENAME= +source "$RUNPHP" || exit 1 +source "$PROJDIR/$VENDORDIR/nulib/php/load.sh" || exit 1 +require: template + +BUILD_ARGS=( + DIST NDIST + REGISTRY + APT_PROXY + APT_MIRROR + SEC_MIRROR + TIMEZONE +) + +function dklsnet() { + docker network ls --no-trunc --format '{{.Name}}' -f name="$1" 2>/dev/null +} + +function dklsimg() { + local image="$1" version="$2" + docker image ls --no-trunc --format '{{.Repository}}:{{.Tag}}' "$image${version:+:$version}" 2>/dev/null +} + +function dklsct() { + # afficher le container dont l'image correspondante est $1 + docker ps --no-trunc --format '{{.Image}} {{.Names}}' | awk -v image="$1" '$1 == image { print $2 }' +} + +function dkrunning() { + # vérifier si le container d'image $1 tourne + [ -n "$(dklsct "$@")" ] +} + +function dclsct() { + # afficher les containers correspondant à $1(=docker-compose.yml) + docker compose ${1:+-f "$1"} ps -q +} + +function dcrunning() { + # vérifier si les containers correspondant à $1(=docker-compose.yml) tournent + # si $2 est spécifié, c'est le nombre de service qui doit tourner + if [ -n "$2" ]; then + [ "$(dclsct "${@:1:1}" | wc -l)" -eq "$2" ] + else + [ -n "$(dclsct "${@:1:1}")" ] + fi +} + +function build_check_env() { + eval "$(template_locals)" + + template_copy_missing "$PROJDIR/$BUILDENV0" && updated=1 + template_process_userfiles + + if [ -n "$updated" ]; then + if [ $(id -u) -ne 0 ]; then + setx userent=getent passwd "$(id -un)" + setx userent=qval "$userent" + setx groupent=getent group "$(id -gn)" + setx groupent=qval "$groupent" + sed -i " +/^#DEVUSER_.*=/s/^#// +/^DEVUSER_USERENT=/s/=.*/=${userent//\//\\\/}/ +/^DEVUSER_GROUPENT=/s/=.*/=${groupent//\//\\\/}/ +" "$PROJDIR/$BUILDENV" + fi + + enote "IMPORTANT: Veuillez faire le paramétrage en éditant le fichier $BUILDENV + ${EDITOR:-nano} $BUILDENV +ENSUITE, vous pourrez relancer la commande" + return 1 + fi +} + +function _build() { + local dockerfile image="${PRIVAREG:+$PRIVAREG/}${IMAGENAME%/*}/$1" + if [ -n "$ForceBuild" -o -z "$(dklsimg "$image")" ]; then + estep "Construction de $image" + dockerfiles=( + "$MYDIR/Dockerfile.$1.local" + "$MYDIR/Dockerfile.$1$BUILD_FLAVOUR" + "$PROJDIR/$VENDORDIR/nulib/php/dockerfiles/Dockerfile.$1$BUILD_FLAVOUR" + "$MYDIR/Dockerfile.$1" + "$PROJDIR/$VENDORDIR/nulib/php/dockerfiles/Dockerfile.$1" + ) + for dockerfile in "${dockerfiles[@]}"; do + [ -f "$dockerfile" ] && break + done + args=( + -f "$dockerfile" + ${Pull:+--pull} + ${NoCache:+--no-cache} + ${PlainOutput:+--progress plain} + -t "$image" + ) + for arg in "${BUILD_ARGS[@]}"; do + args+=(--build-arg "$arg=${!arg}") + done + for arg in "${!PROXY_VARS[@]}"; do + args+=(--build-arg "$arg=${PROXY_VARS[$arg]}") + done + for host in "${HOST_MAPPINGS[@]}"; do + args+=(--add-host "$host") + done + docker build "${args[@]}" "$PROJDIR" || die + if [ -n "$Push" ]; then + if [ -n "$PRIVAREG" ]; then + estep "Poussement de $image" + docker push "$image" || die + else + ewarn "PRIVAREG non défini: impossible de pousser l'image" + fi + fi + fi +} +function build_images() { + local image sourced + + [ $# -gt 0 ] || set -- runphp "${BUILD_IMAGES[@]}" + for image in "$@"; do + case "$image" in + runphp) + [ ${#Configs[*]} -gt 0 ] && export RUNPHP_FORCE_BUILDENVS="${Configs[*]}" + local -a args=(--bs) + [ "$ForceBuild" != all ] && args+=(--ue) + [ -n "$Pull" ] && args+=(--pull) + [ -n "$NoCache" ] && args+=(--no-cache) + "$RUNPHP" "${args[@]}" || die + ;; + *) + if [ -z "$sourced" ]; then + [ ${#Configs[*]} -gt 0 ] || Configs=("$PROJDIR/$BUILDENV") + for config in "${Configs[@]}"; do + source "$config" + done + after_source_buildenv + read -a HOST_MAPPINGS <<<"${HOST_MAPPINGS// +/ }" + sourced=1 + fi + _build "$image" + ;; + esac + done +} + +action=build +Configs=() +ForceBuild= +Pull= +NoCache= +PlainOutput= +Push= +args=( + "Construire les images pour le projet" + #"usage" + --check-only action=none "++Ne faire que la vérification de l'environnement" + -c:,--config:BUILDENV Configs "Spécifier un fichier d'environnement pour le build" + -r,--rebuild ForceBuild=1 "Forcer la (re)construction des images" + -R,--rebuild-all ForceBuild=all "++Comme --rebuild, mais reconstruire aussi runphp" + -U,--pull Pull=1 "++Forcer le re-téléchargement des images dépendantes" + -j,--no-cache NoCache=1 "++Construire l'image en invalidant le cache" + -D,--plain-output PlainOutput=1 "++Afficher le détail du build" + -p,--push Push=1 "Pousser les images vers le registry après construction" +) +parse_args "$@"; set -- "${args[@]}" + +if [ ${#Configs[*]} -gt 0 ]; then + aconfigs=() + for config in "${Configs[@]}"; do + setx config=abspath "$config" + aconfigs+=("$config") + done + Configs=("${aconfigs[@]}") + # pas de vérification d'environnement si on spécifie Configs + # ne pas oublier d'implémenter un traitement spécifique si build_check_env() + # contient d'autres vérifications +else + build_check_env || die +fi +[ "$action" == none ] && exit 0 + +case "$action" in +build) build_images "$@";; +*) die "$action: action non implémentée";; +esac diff --git a/runphp/dot-build.env.dist b/runphp/dot-build.env.dist new file mode 100644 index 0000000..15f02fd --- /dev/null +++ b/runphp/dot-build.env.dist @@ -0,0 +1,21 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +# Source des paquets et proxy +APT_PROXY= +APT_MIRROR=default +SEC_MIRROR=default + +# Timezone du serveur +TIMEZONE=Europe/Paris + +# registre docker privé d'après lequel sont nommées les images +PRIVAREG= + +################################################################################ +# Ne pas toucher à partir d'ici + +REGISTRY=pubdocker.univ-reunion.fr +DIST=d12 +IMAGENAME=runphp +#DEVUSER_USERENT=user:x:1000:1000:User,,,:/home/user:/bin/bash +#DEVUSER_GROUPENT=user:x:1000: diff --git a/runphp/dot-dkbuild.env.dist b/runphp/dot-dkbuild.env.dist new file mode 100644 index 0000000..06a0348 --- /dev/null +++ b/runphp/dot-dkbuild.env.dist @@ -0,0 +1,28 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +default_profile "${DKBUILD_PROFILE:-prod}" + +# Source des paquets et proxy +setenv APT_PROXY= +setenv APT_MIRROR=default +setenv SEC_MIRROR=default + +# Timezone du serveur +setenv TIMEZONE=Europe/Paris + +if profile prod test; then + setenv REGISTRY=pubdocker.univ-reunion.fr + setenv PRIVAREG=pridocker.univ-reunion.fr + host_mappings=( + pridocker.univ-reunion.fr:10.85.1.56 + pubdocker.univ-reunion.fr:10.85.1.57 + repos.univ-reunion.fr:10.85.1.57 + git.univ-reunion.fr:10.85.1.55 + ) + default docker host-mappings="${host_mappings[*]}" +elif profile devel; then + setenv REGISTRY=docker.devel.self + setenv PRIVAREG=docker.devel.self +else + setenv REGISTRY=pubdocker.univ-reunion.fr + setenv PRIVAREG= +fi diff --git a/runphp/dot-runphp.conf b/runphp/dot-runphp.conf new file mode 100644 index 0000000..b409a45 --- /dev/null +++ b/runphp/dot-runphp.conf @@ -0,0 +1,8 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +# Chemin vers runphp, e.g sbin/runphp +RUNPHP= + +# Si RUNPHP n'est pas défini, les variables suivantes peuvent être définies +#DIST=d12 +#REGISTRY=pubdocker.univ-reunion.fr diff --git a/runphp/runphp b/runphp/runphp new file mode 100755 index 0000000..87691d7 --- /dev/null +++ b/runphp/runphp @@ -0,0 +1,583 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +# Script permettant de lancer une commande dans docker et/ou de bootstrapper +# l'utilisation de nulib dans un projet PHP +# Les fichiers suivants doivent être copiés à un endroit quelconque du projet: +# - runphp (ce script, à générer avec update-runphp.sh) +# - Dockerfile.runphp +# Les fichiers suivants peuvent être intégrés dans le projet comme exemples: +# - dot-build.env.dist (à renommer en .build.env.dist) +# - dot-dkbuild.env.dist (indiquer qu'il faut le copier en ~/.dkbuild.env) +# Par défaut, ce script assume que runphp est copié dans le répertoire sbin/ +# du projet, et que le fichier composer.json et le répertoire vendor/ sont à la +# racine du projet. Le cas échéant, modifier les valeurs ci-dessous +(return 0 2>/dev/null) && _sourced=1 || _sourced= + +############################################################################### +# Modifier les valeurs suivantes si nécessaire +#SOF:runphp.userconf:ne pas modifier cette ligne + +# répertoire du projet. ce chemin doit être absolu. s'il est relatif, il est +# exprimé par rapport au répertoire de ce script +PROJDIR= + +# composer: répertoire du projet composer (celui qui contient le fichier +# composer.json), chemin de composer.phar et répertoire vendor. ces chemins +# doivent être relatifs à $PROJDIR +COMPOSERDIR= +COMPOSERPHAR= +VENDORDIR= + +# fichier de configuration pour le build +BUILDENV0= +BUILDENV= + +# Listes des images que le script build construit automatiquement +BUILD_IMAGES=() +BUILD_FLAVOUR= + +## En ce qui concerne DIST et IMAGENAME, les valeurs dans BUILDENV prennent le +## dessus. si BUILDENV *n'est pas* utilisé, ces valeurs peuvent être spécifiées +## ici + +# version de debian à utiliser pour l'image +# d12=php8.2, d11=php7.4, d10=php7.3 +DIST= + +# Nom de base de l'image (sans le registry), e.g prefix/ +IMAGENAME= + +#EOF:runphp.userconf:ne pas modifier cette ligne +################################################################################ + +# Ne pas modifier à partir d'ici + +if [ -n "$_sourced" ]; then + if [ "${0#-}" != "$0" ]; then + # sourcé depuis la ligne de commande + MYSELF="${BASH_SOURCE[1]}" + else + # sourcé depuis un script + MYSELF="${BASH_SOURCE[0]}" + fi + MYDIR="$(cd "$(dirname -- "$MYSELF")"; pwd)" + MYNAME="$(basename -- "$MYSELF")" +else + MYDIR="$(cd "$(dirname -- "$0")"; pwd)" + MYNAME="$(basename -- "$0")" +fi +if [ -f "$MYDIR/runphp.userconf.local" ]; then + source "$MYDIR/runphp.userconf.local" +fi + +DEFAULT_DIST=d12 +if [ -n "$RUNPHP_STANDALONE" ]; then + PROJDIR="$RUNPHP_PROJDIR" + + COMPOSERDIR=. + COMPOSERPHAR= + VENDORDIR=vendor + BUILDENV0= + BUILDENV= + DIST="${RUNPHP_DIST:-$DEFAULT_DIST}" + IMAGENAME=nulib/ + + PRIVAREG=docker.io + REGISTRY="$RUNPHP_REGISTRY" + + [ -n "$RUNPHP_BUILD_FLAVOUR" ] && BUILD_FLAVOUR="$RUNPHP_BUILD_FLAVOUR" + +else + [ -n "$PROJDIR" ] || PROJDIR="$(dirname -- "$MYDIR")" + [ "${PROJDIR#/}" != "$PROJDIR" ] || PROJDIR="$(cd "$MYDIR/$PROJDIR"; pwd)" + + [ -n "$COMPOSERDIR" ] || COMPOSERDIR=. + [ -n "$COMPOSERPHAR" ] || COMPOSERPHAR=sbin/composer.phar + [ -n "$VENDORDIR" ] || VENDORDIR=vendor + [ -n "$BUILDENV0" ] || BUILDENV0=.build.env.dist + [ -n "$BUILDENV" ] || BUILDENV=build.env + [ -n "$DIST" ] || DIST="$DEFAULT_DIST" + [ -n "$IMAGENAME" ] || IMAGENAME=nulib/ + + [ "$COMPOSERPHAR" == none ] && COMPOSERPHAR= + [ "$BUILDENV0" == none ] && BUILDENV0= + [ "$BUILDENV" == none ] && BUILDENV= +fi +[ "$BUILD_FLAVOUR" == none ] && BUILD_FLAVOUR= + +function after_source_buildenv() { + NDIST="${DIST#d}" +} +after_source_buildenv + +[ -n "$_sourced" ] && return 0 + +function eecho() { echo "$*" 1>&2; } +function eerror() { eecho "ERROR: $*"; } +function die() { [ $# -gt 0 ] && eerror "$*"; exit 1; } +function is_defined() { [ -n "$(declare -p "$1" 2>/dev/null)" ]; } +function in_path() { [ -n "$1" -a -x "$(which "$1" 2>/dev/null)" ]; } +function composer() { + cd "$PROJDIR/$COMPOSERDIR" || exit 1 + if [ -n "$COMPOSERPHAR" -a -x "$PROJDIR/$COMPOSERPHAR" ]; then + "$PROJDIR/$COMPOSERPHAR" "$@" + elif in_path composer; then + command composer "$@" + elif [ -x /usr/bin/composer ]; then + /usr/bin/composer "$@" + elif [ -x /usr/local/bin/composer ]; then + /usr/local/bin/composer "$@" + else + die "impossible de trouver composer" + fi + if [ -z "$RUNPHP_STANDALONE" -a -f composer.lock ]; then + cp composer.lock "$PROJDIR/.composer.lock.runphp" + fi +} +function ensure_image() { + local dfdir suffix dockerfiles dockerfile + local privareg imagename + if [ -z "$Image" ]; then + [ -n "$RUNPHP_STANDALONE" ] && dfdir="$RUNPHP_STANDALONE/runphp" || dfdir="$MYDIR" + dockerfiles=( + "_local:$dfdir/Dockerfile.runphp.local" + "${BUILD_FLAVOUR//+/_}:$dfdir/Dockerfile.runphp$BUILD_FLAVOUR" + ":$dfdir/Dockerfile.runphp" + ) + for dockerfile in "${dockerfiles[@]}"; do + suffix="${dockerfile%:*}" + dockerfile="${dockerfile##*:}" + [ -f "$dockerfile" ] && break + done + Dockerfile="$dockerfile" + + [[ "$IMAGENAME" == */ ]] && imagename=runphp || imagename="${IMAGENAME%/*}/runphp" + privareg="$PRIVAREG" + if [ "$imagename" == runphp ]; then + [ -z "$privareg" -o "$privareg" == docker.io ] && privareg=docker.io/library + else + [ -z "$privareg" ] && privareg=docker.io + fi + Image="$privareg/$imagename$suffix:$DIST" + fi +} +function check_image() { + local image="$Image" + for prefix in docker.io/library/ docker.io; do + if [ "${image#$prefix}" != "$image" ]; then + image="${image#$prefix}" + break + fi + done + [ -n "$(docker image ls --no-trunc --format '{{.Repository}}:{{.Tag}}' "$image" 2>/dev/null)" ] +} + +## Arguments initiaux + +Bootstrap= +ComposerInstall= +if [ "$1" == --runphp-bootstrap -o "$1" == --bs ]; then + Bootstrap=1 + shift +elif [ "$1" == --runphp-exec ]; then + Bootstrap= + shift +elif [ "$1" == --runphp-install -o "$1" == --ci ]; then + ComposerInstall=1 + shift +fi + +ForcedBootstrap= +if [ -z "$Bootstrap" -a -z "$RUNPHP_STANDALONE" ]; then + # si vendor/ n'existe pas, alors on doit faire bootstrap + if [ ! -f "$PROJDIR/$VENDORDIR/nulib/php/load.sh" ]; then + ForcedBootstrap=1 + elif [ ! -f "$PROJDIR/.composer.lock.runphp" ]; then + ForcedBootstrap=1 + elif ! diff -q "$PROJDIR/$COMPOSERDIR/composer.lock" "$PROJDIR/.composer.lock.runphp" >&/dev/null; then + ForcedBootstrap=1 + fi + if [ -n "$ForcedBootstrap" ]; then + [ "$RUNPHP_MODE" != docker ] && eecho "== bootstrapping runphp" + Bootstrap=1 + ComposerInstall=1 + fi +fi + +if [ "$RUNPHP_MODE" != docker ]; then + ############################################################################ + # Lancement depuis l'extérieur du container + ############################################################################ + + ## Charger ~/.dkbuild.env + + APT_PROXY= + APT_MIRROR= + SEC_MIRROR= + TIMEZONE= + PRIVAREG= + REGISTRY= + PROFILE= + HOST_MAPPINGS=() + function default_profile() { + PROFILE="$1" + } + function profile() { + local profile + for profile in "$@"; do + [ "$profile" == "$PROFILE" ] && return 0 + done + return 1 + } + function setenv() { + eval "export $1" + } + function default() { + local command="$1"; shift + local nv n v + case "$command" in + docker) + for nv in "$@"; do + [[ "$nv" == *=* ]] || continue + n="${nv%%=*}" + v="${nv#*=}" + case "$n" in + host-mappings) + read -a ns <<<"$v" + for v in "${ns[@]}"; do + HOST_MAPPINGS+=("$v") + done + ;; + esac + done + ;; + esac + } + [ -f ~/.dkbuild.env ] && source ~/.dkbuild.env + [ -n "$APT_PROXY" ] || APT_PROXY= + [ -n "$APT_MIRROR" ] || APT_MIRROR=default + [ -n "$SEC_MIRROR" ] || SEC_MIRROR=default + [ -n "$TIMEZONE" ] || TIMEZONE=Europe/Paris + [ -n "$PRIVAREG" ] || PRIVAREG= + [ -n "$REGISTRY" ] || REGISTRY=pubdocker.univ-reunion.fr + + ## Charger la configuration + + # Recenser les valeur de proxy + declare -A PROXY_VARS + for var in {HTTPS,ALL,NO}_PROXY {http,https,all,no}_proxy; do + is_defined "$var" && PROXY_VARS[${var,,}]="${!var}" + done + + # Paramètres de montage + if [ -n "$RUNPHP_NO_USE_RSLAVE" ]; then + UseRslave= + elif [ -n "$RUNPHP_USE_RSLAVE" ]; then + UseRslave=1 + elif [ -e /proc/sys/fs/binfmt_misc/WSLInterop ]; then + # pas de mount propagation sous WSL + UseRslave= + else + UseRslave=1 + fi + + # Toujours vérifier l'existence de l'image + Image= + if [ -z "$Bootstrap" ]; then + if [ -n "$RUNPHP_FORCE_BUILDENVS" ]; then + eval "Configs=($RUNPHP_FORCE_BUILDENVS)" + for config in "${Configs[@]}"; do + source "$config" || exit 1 + done + after_source_buildenv + elif [ -n "$BUILDENV" -a -f "$PROJDIR/$BUILDENV" ]; then + source "$PROJDIR/$BUILDENV" || exit 1 + after_source_buildenv + elif [ -n "$BUILDENV0" -a -f "$PROJDIR/$BUILDENV0" ]; then + source "$PROJDIR/$BUILDENV0" || exit 1 + after_source_buildenv + fi + ensure_image + check_image || Bootstrap=1 + fi + + Chdir= + Verbose="$RUNPHP_VERBOSE" + if [ -n "$Bootstrap" ]; then + ## Mode bootstrap de l'image ########################################### + # Ici, on a déterminé que l'image doit être construite + + BUILD_ARGS=( + DIST NDIST + REGISTRY + APT_PROXY + APT_MIRROR + SEC_MIRROR + TIMEZONE + ) + + SOPTS=+d:9876543210:c:UjDx:z:r:p + LOPTS=help,dist:,d19,d18,d17,d16,d15,d14,d13,d12,d11,d10,config:,ue,unless-exists,pull,nc,no-cache,po,plain-output,apt-proxy:,timezone:,privareg:,push,ci,no-use-rslave + args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args" + + Dist= + if [ -n "$RUNPHP_FORCE_BUILDENVS" ]; then + eval "Configs=($RUNPHP_FORCE_BUILDENVS)" + elif [ -n "$BUILDENV" -a -f "$PROJDIR/$BUILDENV" ]; then + Configs=("$PROJDIR/$BUILDENV") + elif [ -n "$BUILDENV0" -a -f "$PROJDIR/$BUILDENV0" ]; then + Configs=("$PROJDIR/$BUILDENV0") + else + Configs=() + fi + UnlessExists= + Pull= + NoCache= + PlainOutput= + while [ $# -gt 0 ]; do + case "$1" in + --) shift; break;; + --help) + eecho "\ +runphp: construire l'image docker + +USAGE + $MYNAME --bootstrap [options...] + +OPTIONS + -c, --config build.env + --unless-exists + -U, --pull + -j, --no-cache + -D, --plain-output + -x, --apt-proxy APT_PROXY + -z, --timezone TIMEZONE + -r, --privareg PRIVAREG + -p, --push + paramètres pour la consruction de l'image" + exit 0 + ;; + -d|--dist) shift; Dist="$1";; + -[0-9]) Dist="d1${1#-}";; + --d*) Dist="${1#--}";; + -c|--config) shift; Configs+="$1";; + --ue|--unless-exists) UnlessExists=1;; + -U|--pull) Pull=1;; + -j|--nc|--no-cache) NoCache=1;; + -D|--po|--plain-output) PlainOutput=1;; + -x|--apt-proxy) shift; APT_PROXY="$1";; + -z|--timezone) shift; TIMEZONE="$1";; + -r|--privareg) shift; PRIVAREG="$1";; + -p|--push) Push=1;; + --ci) ComposerInstall=1;; + --no-use-rslave) UseRslave=;; + *) die "$1: option non configurée";; + esac + shift + done + + for config in "${Configs[@]}"; do + if [ "$config" == none ]; then + Configs=() + break + fi + done + if [ ${#Configs[*]} -gt 0 ]; then + for config in "${Configs[@]}"; do + source "$config" || exit 1 + done + after_source_buildenv + fi + [ -n "$Dist" ] && DIST="$Dist" + + ensure_image + check_image && exists=1 || exists= + if [ -z "$UnlessExists" -o -z "$exists" ]; then + eecho "== Building $Image" + args=( + -f "$Dockerfile" + ${Pull:+--pull} + ${NoCache:+--no-cache} + ${BuildPlain:+--progress plain} + -t "$Image" + ) + for arg in "${BUILD_ARGS[@]}"; do + args+=(--build-arg "$arg=${!arg}") + done + for arg in "${!PROXY_VARS[@]}"; do + args+=(--build-arg "$arg=${PROXY_VARS[$arg]}") + done + for host in "${HOST_MAPPINGS[@]}"; do + args+=(--add-host "$host") + done + mkdir -p /tmp/runphp-build + docker build "${args[@]}" /tmp/runphp-build || exit 1 + + if [ -n "$Push" -a -n "$PRIVAREG" ]; then + eecho "== Pushing $Image" + docker push "$Image" || exit 1 + fi + fi + if [ -z "$RUNPHP_STANDALONE" -a ! -f "$PROJDIR/$VENDORDIR/nulib/php/load.sh" ]; then + # Forcer l'installation des dépendances si nécessaire + ComposerInstall=1 + fi + [ -z "$ComposerInstall" -o -n "$UnlessExists" ] && exit 0 + + else + ## Mode exécution de commande ########################################## + # Ici, on a déterminé qu'il faut lancer une commande + + SOPTS=+w: + LOPTS=help,chdir:,no-use-rslave + args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args" + + while [ $# -gt 0 ]; do + case "$1" in + --) shift; break;; + --help) + eecho "\ +runphp: lancer une commande dans un environnement PHP déterminé + +USAGE + $MYNAME ci|cu|composer + $MYNAME [options] command [args...] + +COMMANDES COMPOSER + ci + cu + installer/mettre à jour les dépendances du projet avec composer + composer [args...] + lancer composer avec les arguments spécifiés. + +pour les commandes ci-dessus, l'option --chdir est ignorée: le répertoire +courant est forcé au répertoire du projet composer + +OPTIONS + -w, --chdir CHDIR + aller dans le répertoire spécifié avant de lancer la commande" + exit 0 + ;; + -w|--chdir) shift; Chdir="$1";; + --no-use-rslave) UseRslave=;; + *) die "$1: option non configurée";; + esac + shift + done + + if [ -z "$RUNPHP_STANDALONE" -a ! -f "$PROJDIR/$VENDORDIR/nulib/php/load.sh" ]; then + # Forcer l'installation des dépendances si nécessaire + ComposerInstall=1 + fi + fi + + ## Lancer la commande + + args=( + run -it --rm + --name "runphp-$(basename -- "$1")-$$" + -e RUNPHP_MODE=docker + ) + for arg in "${!PROXY_VARS[@]}"; do + args+=(-e "$arg=${PROXY_VARS[$arg]}") + done + if [ -n "$RUNPHP_STANDALONE" ]; then + args+=( + -e "RUNPHP_STANDALONE=$RUNPHP_STANDALONE" + -e "RUNPHP_PROJDIR=$PROJDIR" + ) + fi + for host in "${HOST_MAPPINGS[@]}"; do + args+=(--add-host "$host") + done + + # monter le répertoire qui contient $PROJDIR + mount_composer= + mount_runphp=1 + if [ -z "$PROJDIR" -o "${PROJDIR#$HOME/}" != "$PROJDIR" -o "$PROJDIR" == "$HOME" ]; then + # bind mount $HOME + args+=(-v "$HOME:$HOME${UseRslave:+:rslave}") + [ -n "$RUNPHP_STANDALONE" ] && + [ "${RUNPHP_STANDALONE#$HOME/}" != "$RUNPHP_STANDALONE" ] && + mount_runphp= + else + # bind mount uniquement le répertoire du projet + args+=(-v "$PROJDIR:$PROJDIR${UseRslave:+:rslave}") + mount_composer=1 + [ "$RUNPHP_STANDALONE" == "$PROJDIR" ] && mount_runphp= + fi + if [ -n "$mount_composer" -a -d "$HOME/.composer" ]; then + # monter la configuration de composer + args+=(-v "$HOME/.composer:$HOME/.composer") + fi + if [ -n "$RUNPHP_STANDALONE" -a -n "$mount_runphp" ]; then + args+=(-v "$RUNPHP_STANDALONE:$RUNPHP_STANDALONE") + fi + args+=(-w "$(pwd)") + + # lancer avec l'utilisateur courant + if [ $(id -u) -ne 0 ]; then + # si c'est un utilisateur lambda, il faut monter les informations + # nécessaires. composer est déjà monté via $HOME + args+=( + -e DEVUSER_USERENT="$(getent passwd "$(id -un)")" + -e DEVUSER_GROUPENT="$(getent group "$(id -gn)")" + ) + fi + + args+=( + "$Image" + exec "$0" ${Chdir:+-w "$Chdir"} + ) + [ -n "$ComposerInstall" ] && set -- ci + [ -n "$Verbose" ] && eecho "\$ docker ${args[*]} $*" + exec docker "${args[@]}" "$@" + +else + ############################################################################ + # Lancement depuis l'intérieur du container + ############################################################################ + + if [ -n "$DEVUSER_USERENT" ]; then + user="${DEVUSER_USERENT%%:*}" + export DEVUSER_USERENT= + export DEVUSER_GROUPENT= + if in_path su-exec; then + exec su-exec "$user" "$0" "$@" + else + exec runuser -u "$user" -- "$0" "$@" + fi + fi + + SOPTS=+w: + LOPTS=chdir: + args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args" + + chdir= + action= + while [ $# -gt 0 ]; do + case "$1" in + --) shift; break;; + -w|--chdir) shift; chdir="$1";; + *) die "$1: option non configurée";; + esac + shift + done + + if [ -z "$1" ]; then + die "no command specified" + elif [ "$1" == ci ]; then + eecho "== installing composer dependencies" + composer i + elif [ "$1" == cu ]; then + eecho "== upgrading composer dependencies" + composer u + elif [ "$1" == composer ]; then + "$@" + else + if [ -n "$chdir" ]; then + cd "$chdir" || exit 1 + fi + exec "$@" + fi +fi diff --git a/runphp/runphp.1preamble b/runphp/runphp.1preamble new file mode 100644 index 0000000..f290705 --- /dev/null +++ b/runphp/runphp.1preamble @@ -0,0 +1,18 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +# Script permettant de lancer une commande dans docker et/ou de bootstrapper +# l'utilisation de nulib dans un projet PHP +# Les fichiers suivants doivent être copiés à un endroit quelconque du projet: +# - runphp (ce script, à générer avec update-runphp.sh) +# - Dockerfile.runphp +# Les fichiers suivants peuvent être intégrés dans le projet comme exemples: +# - dot-build.env.dist (à renommer en .build.env.dist) +# - dot-dkbuild.env.dist (indiquer qu'il faut le copier en ~/.dkbuild.env) +# Par défaut, ce script assume que runphp est copié dans le répertoire sbin/ +# du projet, et que le fichier composer.json et le répertoire vendor/ sont à la +# racine du projet. Le cas échéant, modifier les valeurs ci-dessous +(return 0 2>/dev/null) && _sourced=1 || _sourced= + +############################################################################### +# Modifier les valeurs suivantes si nécessaire +#SOF:runphp.userconf:ne pas modifier cette ligne diff --git a/runphp/runphp.2postamble b/runphp/runphp.2postamble new file mode 100644 index 0000000..83f987b --- /dev/null +++ b/runphp/runphp.2postamble @@ -0,0 +1,534 @@ +#EOF:runphp.userconf:ne pas modifier cette ligne +################################################################################ + +# Ne pas modifier à partir d'ici + +if [ -n "$_sourced" ]; then + if [ "${0#-}" != "$0" ]; then + # sourcé depuis la ligne de commande + MYSELF="${BASH_SOURCE[1]}" + else + # sourcé depuis un script + MYSELF="${BASH_SOURCE[0]}" + fi + MYDIR="$(cd "$(dirname -- "$MYSELF")"; pwd)" + MYNAME="$(basename -- "$MYSELF")" +else + MYDIR="$(cd "$(dirname -- "$0")"; pwd)" + MYNAME="$(basename -- "$0")" +fi +if [ -f "$MYDIR/runphp.userconf.local" ]; then + source "$MYDIR/runphp.userconf.local" +fi + +DEFAULT_DIST=d12 +if [ -n "$RUNPHP_STANDALONE" ]; then + PROJDIR="$RUNPHP_PROJDIR" + + COMPOSERDIR=. + COMPOSERPHAR= + VENDORDIR=vendor + BUILDENV0= + BUILDENV= + DIST="${RUNPHP_DIST:-$DEFAULT_DIST}" + IMAGENAME=nulib/ + + PRIVAREG=docker.io + REGISTRY="$RUNPHP_REGISTRY" + + [ -n "$RUNPHP_BUILD_FLAVOUR" ] && BUILD_FLAVOUR="$RUNPHP_BUILD_FLAVOUR" + +else + [ -n "$PROJDIR" ] || PROJDIR="$(dirname -- "$MYDIR")" + [ "${PROJDIR#/}" != "$PROJDIR" ] || PROJDIR="$(cd "$MYDIR/$PROJDIR"; pwd)" + + [ -n "$COMPOSERDIR" ] || COMPOSERDIR=. + [ -n "$COMPOSERPHAR" ] || COMPOSERPHAR=sbin/composer.phar + [ -n "$VENDORDIR" ] || VENDORDIR=vendor + [ -n "$BUILDENV0" ] || BUILDENV0=.build.env.dist + [ -n "$BUILDENV" ] || BUILDENV=build.env + [ -n "$DIST" ] || DIST="$DEFAULT_DIST" + [ -n "$IMAGENAME" ] || IMAGENAME=nulib/ + + [ "$COMPOSERPHAR" == none ] && COMPOSERPHAR= + [ "$BUILDENV0" == none ] && BUILDENV0= + [ "$BUILDENV" == none ] && BUILDENV= +fi +[ "$BUILD_FLAVOUR" == none ] && BUILD_FLAVOUR= + +function after_source_buildenv() { + NDIST="${DIST#d}" +} +after_source_buildenv + +[ -n "$_sourced" ] && return 0 + +function eecho() { echo "$*" 1>&2; } +function eerror() { eecho "ERROR: $*"; } +function die() { [ $# -gt 0 ] && eerror "$*"; exit 1; } +function is_defined() { [ -n "$(declare -p "$1" 2>/dev/null)" ]; } +function in_path() { [ -n "$1" -a -x "$(which "$1" 2>/dev/null)" ]; } +function composer() { + cd "$PROJDIR/$COMPOSERDIR" || exit 1 + if [ -n "$COMPOSERPHAR" -a -x "$PROJDIR/$COMPOSERPHAR" ]; then + "$PROJDIR/$COMPOSERPHAR" "$@" + elif in_path composer; then + command composer "$@" + elif [ -x /usr/bin/composer ]; then + /usr/bin/composer "$@" + elif [ -x /usr/local/bin/composer ]; then + /usr/local/bin/composer "$@" + else + die "impossible de trouver composer" + fi + if [ -z "$RUNPHP_STANDALONE" -a -f composer.lock ]; then + cp composer.lock "$PROJDIR/.composer.lock.runphp" + fi +} +function ensure_image() { + local dfdir suffix dockerfiles dockerfile + local privareg imagename + if [ -z "$Image" ]; then + [ -n "$RUNPHP_STANDALONE" ] && dfdir="$RUNPHP_STANDALONE/runphp" || dfdir="$MYDIR" + dockerfiles=( + "_local:$dfdir/Dockerfile.runphp.local" + "${BUILD_FLAVOUR//+/_}:$dfdir/Dockerfile.runphp$BUILD_FLAVOUR" + ":$dfdir/Dockerfile.runphp" + ) + for dockerfile in "${dockerfiles[@]}"; do + suffix="${dockerfile%:*}" + dockerfile="${dockerfile##*:}" + [ -f "$dockerfile" ] && break + done + Dockerfile="$dockerfile" + + [[ "$IMAGENAME" == */ ]] && imagename=runphp || imagename="${IMAGENAME%/*}/runphp" + privareg="$PRIVAREG" + if [ "$imagename" == runphp ]; then + [ -z "$privareg" -o "$privareg" == docker.io ] && privareg=docker.io/library + else + [ -z "$privareg" ] && privareg=docker.io + fi + Image="$privareg/$imagename$suffix:$DIST" + fi +} +function check_image() { + local image="$Image" + for prefix in docker.io/library/ docker.io; do + if [ "${image#$prefix}" != "$image" ]; then + image="${image#$prefix}" + break + fi + done + [ -n "$(docker image ls --no-trunc --format '{{.Repository}}:{{.Tag}}' "$image" 2>/dev/null)" ] +} + +## Arguments initiaux + +Bootstrap= +ComposerInstall= +if [ "$1" == --runphp-bootstrap -o "$1" == --bs ]; then + Bootstrap=1 + shift +elif [ "$1" == --runphp-exec ]; then + Bootstrap= + shift +elif [ "$1" == --runphp-install -o "$1" == --ci ]; then + ComposerInstall=1 + shift +fi + +ForcedBootstrap= +if [ -z "$Bootstrap" -a -z "$RUNPHP_STANDALONE" ]; then + # si vendor/ n'existe pas, alors on doit faire bootstrap + if [ ! -f "$PROJDIR/$VENDORDIR/nulib/php/load.sh" ]; then + ForcedBootstrap=1 + elif [ ! -f "$PROJDIR/.composer.lock.runphp" ]; then + ForcedBootstrap=1 + elif ! diff -q "$PROJDIR/$COMPOSERDIR/composer.lock" "$PROJDIR/.composer.lock.runphp" >&/dev/null; then + ForcedBootstrap=1 + fi + if [ -n "$ForcedBootstrap" ]; then + [ "$RUNPHP_MODE" != docker ] && eecho "== bootstrapping runphp" + Bootstrap=1 + ComposerInstall=1 + fi +fi + +if [ "$RUNPHP_MODE" != docker ]; then + ############################################################################ + # Lancement depuis l'extérieur du container + ############################################################################ + + ## Charger ~/.dkbuild.env + + APT_PROXY= + APT_MIRROR= + SEC_MIRROR= + TIMEZONE= + PRIVAREG= + REGISTRY= + PROFILE= + HOST_MAPPINGS=() + function default_profile() { + PROFILE="$1" + } + function profile() { + local profile + for profile in "$@"; do + [ "$profile" == "$PROFILE" ] && return 0 + done + return 1 + } + function setenv() { + eval "export $1" + } + function default() { + local command="$1"; shift + local nv n v + case "$command" in + docker) + for nv in "$@"; do + [[ "$nv" == *=* ]] || continue + n="${nv%%=*}" + v="${nv#*=}" + case "$n" in + host-mappings) + read -a ns <<<"$v" + for v in "${ns[@]}"; do + HOST_MAPPINGS+=("$v") + done + ;; + esac + done + ;; + esac + } + [ -f ~/.dkbuild.env ] && source ~/.dkbuild.env + [ -n "$APT_PROXY" ] || APT_PROXY= + [ -n "$APT_MIRROR" ] || APT_MIRROR=default + [ -n "$SEC_MIRROR" ] || SEC_MIRROR=default + [ -n "$TIMEZONE" ] || TIMEZONE=Europe/Paris + [ -n "$PRIVAREG" ] || PRIVAREG= + [ -n "$REGISTRY" ] || REGISTRY=pubdocker.univ-reunion.fr + + ## Charger la configuration + + # Recenser les valeur de proxy + declare -A PROXY_VARS + for var in {HTTPS,ALL,NO}_PROXY {http,https,all,no}_proxy; do + is_defined "$var" && PROXY_VARS[${var,,}]="${!var}" + done + + # Paramètres de montage + if [ -n "$RUNPHP_NO_USE_RSLAVE" ]; then + UseRslave= + elif [ -n "$RUNPHP_USE_RSLAVE" ]; then + UseRslave=1 + elif [ -e /proc/sys/fs/binfmt_misc/WSLInterop ]; then + # pas de mount propagation sous WSL + UseRslave= + else + UseRslave=1 + fi + + # Toujours vérifier l'existence de l'image + Image= + if [ -z "$Bootstrap" ]; then + if [ -n "$RUNPHP_FORCE_BUILDENVS" ]; then + eval "Configs=($RUNPHP_FORCE_BUILDENVS)" + for config in "${Configs[@]}"; do + source "$config" || exit 1 + done + after_source_buildenv + elif [ -n "$BUILDENV" -a -f "$PROJDIR/$BUILDENV" ]; then + source "$PROJDIR/$BUILDENV" || exit 1 + after_source_buildenv + elif [ -n "$BUILDENV0" -a -f "$PROJDIR/$BUILDENV0" ]; then + source "$PROJDIR/$BUILDENV0" || exit 1 + after_source_buildenv + fi + ensure_image + check_image || Bootstrap=1 + fi + + Chdir= + Verbose="$RUNPHP_VERBOSE" + if [ -n "$Bootstrap" ]; then + ## Mode bootstrap de l'image ########################################### + # Ici, on a déterminé que l'image doit être construite + + BUILD_ARGS=( + DIST NDIST + REGISTRY + APT_PROXY + APT_MIRROR + SEC_MIRROR + TIMEZONE + ) + + SOPTS=+d:9876543210:c:UjDx:z:r:p + LOPTS=help,dist:,d19,d18,d17,d16,d15,d14,d13,d12,d11,d10,config:,ue,unless-exists,pull,nc,no-cache,po,plain-output,apt-proxy:,timezone:,privareg:,push,ci,no-use-rslave + args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args" + + Dist= + if [ -n "$RUNPHP_FORCE_BUILDENVS" ]; then + eval "Configs=($RUNPHP_FORCE_BUILDENVS)" + elif [ -n "$BUILDENV" -a -f "$PROJDIR/$BUILDENV" ]; then + Configs=("$PROJDIR/$BUILDENV") + elif [ -n "$BUILDENV0" -a -f "$PROJDIR/$BUILDENV0" ]; then + Configs=("$PROJDIR/$BUILDENV0") + else + Configs=() + fi + UnlessExists= + Pull= + NoCache= + PlainOutput= + while [ $# -gt 0 ]; do + case "$1" in + --) shift; break;; + --help) + eecho "\ +runphp: construire l'image docker + +USAGE + $MYNAME --bootstrap [options...] + +OPTIONS + -c, --config build.env + --unless-exists + -U, --pull + -j, --no-cache + -D, --plain-output + -x, --apt-proxy APT_PROXY + -z, --timezone TIMEZONE + -r, --privareg PRIVAREG + -p, --push + paramètres pour la consruction de l'image" + exit 0 + ;; + -d|--dist) shift; Dist="$1";; + -[0-9]) Dist="d1${1#-}";; + --d*) Dist="${1#--}";; + -c|--config) shift; Configs+="$1";; + --ue|--unless-exists) UnlessExists=1;; + -U|--pull) Pull=1;; + -j|--nc|--no-cache) NoCache=1;; + -D|--po|--plain-output) PlainOutput=1;; + -x|--apt-proxy) shift; APT_PROXY="$1";; + -z|--timezone) shift; TIMEZONE="$1";; + -r|--privareg) shift; PRIVAREG="$1";; + -p|--push) Push=1;; + --ci) ComposerInstall=1;; + --no-use-rslave) UseRslave=;; + *) die "$1: option non configurée";; + esac + shift + done + + for config in "${Configs[@]}"; do + if [ "$config" == none ]; then + Configs=() + break + fi + done + if [ ${#Configs[*]} -gt 0 ]; then + for config in "${Configs[@]}"; do + source "$config" || exit 1 + done + after_source_buildenv + fi + [ -n "$Dist" ] && DIST="$Dist" + + ensure_image + check_image && exists=1 || exists= + if [ -z "$UnlessExists" -o -z "$exists" ]; then + eecho "== Building $Image" + args=( + -f "$Dockerfile" + ${Pull:+--pull} + ${NoCache:+--no-cache} + ${BuildPlain:+--progress plain} + -t "$Image" + ) + for arg in "${BUILD_ARGS[@]}"; do + args+=(--build-arg "$arg=${!arg}") + done + for arg in "${!PROXY_VARS[@]}"; do + args+=(--build-arg "$arg=${PROXY_VARS[$arg]}") + done + for host in "${HOST_MAPPINGS[@]}"; do + args+=(--add-host "$host") + done + mkdir -p /tmp/runphp-build + docker build "${args[@]}" /tmp/runphp-build || exit 1 + + if [ -n "$Push" -a -n "$PRIVAREG" ]; then + eecho "== Pushing $Image" + docker push "$Image" || exit 1 + fi + fi + if [ -z "$RUNPHP_STANDALONE" -a ! -f "$PROJDIR/$VENDORDIR/nulib/php/load.sh" ]; then + # Forcer l'installation des dépendances si nécessaire + ComposerInstall=1 + fi + [ -z "$ComposerInstall" -o -n "$UnlessExists" ] && exit 0 + + else + ## Mode exécution de commande ########################################## + # Ici, on a déterminé qu'il faut lancer une commande + + SOPTS=+w: + LOPTS=help,chdir:,no-use-rslave + args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args" + + while [ $# -gt 0 ]; do + case "$1" in + --) shift; break;; + --help) + eecho "\ +runphp: lancer une commande dans un environnement PHP déterminé + +USAGE + $MYNAME ci|cu|composer + $MYNAME [options] command [args...] + +COMMANDES COMPOSER + ci + cu + installer/mettre à jour les dépendances du projet avec composer + composer [args...] + lancer composer avec les arguments spécifiés. + +pour les commandes ci-dessus, l'option --chdir est ignorée: le répertoire +courant est forcé au répertoire du projet composer + +OPTIONS + -w, --chdir CHDIR + aller dans le répertoire spécifié avant de lancer la commande" + exit 0 + ;; + -w|--chdir) shift; Chdir="$1";; + --no-use-rslave) UseRslave=;; + *) die "$1: option non configurée";; + esac + shift + done + + if [ -z "$RUNPHP_STANDALONE" -a ! -f "$PROJDIR/$VENDORDIR/nulib/php/load.sh" ]; then + # Forcer l'installation des dépendances si nécessaire + ComposerInstall=1 + fi + fi + + ## Lancer la commande + + args=( + run -it --rm + --name "runphp-$(basename -- "$1")-$$" + -e RUNPHP_MODE=docker + ) + for arg in "${!PROXY_VARS[@]}"; do + args+=(-e "$arg=${PROXY_VARS[$arg]}") + done + if [ -n "$RUNPHP_STANDALONE" ]; then + args+=( + -e "RUNPHP_STANDALONE=$RUNPHP_STANDALONE" + -e "RUNPHP_PROJDIR=$PROJDIR" + ) + fi + for host in "${HOST_MAPPINGS[@]}"; do + args+=(--add-host "$host") + done + + # monter le répertoire qui contient $PROJDIR + mount_composer= + mount_runphp=1 + if [ -z "$PROJDIR" -o "${PROJDIR#$HOME/}" != "$PROJDIR" -o "$PROJDIR" == "$HOME" ]; then + # bind mount $HOME + args+=(-v "$HOME:$HOME${UseRslave:+:rslave}") + [ -n "$RUNPHP_STANDALONE" ] && + [ "${RUNPHP_STANDALONE#$HOME/}" != "$RUNPHP_STANDALONE" ] && + mount_runphp= + else + # bind mount uniquement le répertoire du projet + args+=(-v "$PROJDIR:$PROJDIR${UseRslave:+:rslave}") + mount_composer=1 + [ "$RUNPHP_STANDALONE" == "$PROJDIR" ] && mount_runphp= + fi + if [ -n "$mount_composer" -a -d "$HOME/.composer" ]; then + # monter la configuration de composer + args+=(-v "$HOME/.composer:$HOME/.composer") + fi + if [ -n "$RUNPHP_STANDALONE" -a -n "$mount_runphp" ]; then + args+=(-v "$RUNPHP_STANDALONE:$RUNPHP_STANDALONE") + fi + args+=(-w "$(pwd)") + + # lancer avec l'utilisateur courant + if [ $(id -u) -ne 0 ]; then + # si c'est un utilisateur lambda, il faut monter les informations + # nécessaires. composer est déjà monté via $HOME + args+=( + -e DEVUSER_USERENT="$(getent passwd "$(id -un)")" + -e DEVUSER_GROUPENT="$(getent group "$(id -gn)")" + ) + fi + + args+=( + "$Image" + exec "$0" ${Chdir:+-w "$Chdir"} + ) + [ -n "$ComposerInstall" ] && set -- ci + [ -n "$Verbose" ] && eecho "\$ docker ${args[*]} $*" + exec docker "${args[@]}" "$@" + +else + ############################################################################ + # Lancement depuis l'intérieur du container + ############################################################################ + + if [ -n "$DEVUSER_USERENT" ]; then + user="${DEVUSER_USERENT%%:*}" + export DEVUSER_USERENT= + export DEVUSER_GROUPENT= + if in_path su-exec; then + exec su-exec "$user" "$0" "$@" + else + exec runuser -u "$user" -- "$0" "$@" + fi + fi + + SOPTS=+w: + LOPTS=chdir: + args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args" + + chdir= + action= + while [ $# -gt 0 ]; do + case "$1" in + --) shift; break;; + -w|--chdir) shift; chdir="$1";; + *) die "$1: option non configurée";; + esac + shift + done + + if [ -z "$1" ]; then + die "no command specified" + elif [ "$1" == ci ]; then + eecho "== installing composer dependencies" + composer i + elif [ "$1" == cu ]; then + eecho "== upgrading composer dependencies" + composer u + elif [ "$1" == composer ]; then + "$@" + else + if [ -n "$chdir" ]; then + cd "$chdir" || exit 1 + fi + exec "$@" + fi +fi diff --git a/runphp/runphp.userconf b/runphp/runphp.userconf new file mode 100644 index 0000000..b849b4b --- /dev/null +++ b/runphp/runphp.userconf @@ -0,0 +1,29 @@ +# répertoire du projet. ce chemin doit être absolu. s'il est relatif, il est +# exprimé par rapport au répertoire de ce script +PROJDIR= + +# composer: répertoire du projet composer (celui qui contient le fichier +# composer.json), chemin de composer.phar et répertoire vendor. ces chemins +# doivent être relatifs à $PROJDIR +COMPOSERDIR= +COMPOSERPHAR= +VENDORDIR= + +# fichier de configuration pour le build +BUILDENV0= +BUILDENV= + +# Listes des images que le script build construit automatiquement +BUILD_IMAGES=(php-apache mariadb10) +BUILD_FLAVOUR= + +## En ce qui concerne DIST et IMAGENAME, les valeurs dans BUILDENV prennent le +## dessus. si BUILDENV *n'est pas* utilisé, ces valeurs peuvent être spécifiées +## ici + +# version de debian à utiliser pour l'image +# d12=php8.2, d11=php7.4, d10=php7.3 +DIST= + +# Nom de base de l'image (sans le registry), e.g prefix/ +IMAGENAME= diff --git a/runphp/runphp.userconf.local b/runphp/runphp.userconf.local new file mode 100644 index 0000000..a8aa3ed --- /dev/null +++ b/runphp/runphp.userconf.local @@ -0,0 +1,3 @@ +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 + +BUILD_FLAVOUR=+ic diff --git a/runphp/template.sh b/runphp/template.sh new file mode 100755 index 0000000..233c527 --- /dev/null +++ b/runphp/template.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +# Modèle de script utilisant runphp pour lancer un traitement dans un container +# RUNPHP est le chemin relatif vers runphp à partir du chemin du script +RUNPHP=sbin/runphp + +MYDIR="$(dirname -- "$0")"; MYNAME="$(basename -- "$0")" +if [ -z "$_RUNPHP_IN_DOCKER" ]; then + "$MYDIR/$RUNPHP" --bs --ue --ci || exit 1 + exec "$MYDIR/$RUNPHP" "$0" "$@" +fi +source "$MYDIR/$RUNPHP" || exit 1 +source "$PROJDIR/$VENDORDIR/nulib/php/load.sh" || exit 1 + +args=( + "description" + #"usage" +) +parse_args "$@"; set -- "${args[@]}" + +echo "je tourne dans un container..." +sleep 1000 diff --git a/runphp/update-runphp.sh b/runphp/update-runphp.sh new file mode 100755 index 0000000..c17a544 --- /dev/null +++ b/runphp/update-runphp.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +source "$(dirname "$0")/../load.sh" || exit 1 + +projdir= +args=( + "Mettre à jour le script runphp" + "[path/to/runphp]" + -d:,--projdir:PROJDIR . "Copier les fichiers pour un projet de l'université de la Réunion" +) +parse_args "$@"; set -- "${args[@]}" + +if [ -n "$projdir" ]; then + setx projdir=abspath "$projdir" + set -- "$projdir/sbin/runphp" +fi + +runphp="${1:-.}" +[ -d "$runphp" ] && runphp="$runphp/runphp" + +setx rundir=dirname -- "$runphp" +[ -d "$rundir" ] || mkdir -p "$rundir" + +if [ -f "$runphp" ]; then + ac_set_tmpfile userconf + <"$runphp" awk ' +# extraire la configuration depuis le fichier +BEGIN { p = 0 } +/SOF:runphp.userconf:/ { p = 1; next } +/EOF:runphp.userconf:/ { p = 0; next } +p == 1 { print } +' | awk ' +# mettre en forme le fichier: pas de lignes vides avant et après +BEGIN { p = 0; have_pending = 0; pending = "" } +$0 != "" { p = 1 } +p == 1 { + if ($0 != "") { + if (have_pending) print pending + print + have_pending = 0 + pending = "" + } else { + if (!have_pending) have_pending = 1 + else pending = pending "\n" + } +} +' >"$userconf" + initial_config= + +elif [ -n "$projdir" ]; then + # forcer BUILDENV0=..env.dist et BUILDENV=.env pour les projets de + # l'université de la Réunion + initial_config=1 + ac_set_tmpfile userconf + sed <"$MYDIR/runphp.userconf" >"$userconf" ' +/^BUILDENV0=/s/=.*/=..env.dist/ +/^BUILDENV=/s/=.*/=.env/ +' + +else + initial_config=1 + userconf="$MYDIR/runphp.userconf" +fi + +( + cat "$MYDIR/runphp.1preamble" + echo + cat "$userconf" + echo + cat "$MYDIR/runphp.2postamble" +) >"$runphp" +[ -x "$runphp" ] || chmod +x "$runphp" + +rsync -lpt "$MYDIR/Dockerfile.runphp" "$rundir/" + +if [ -n "$projdir" ]; then + if testdiff "$rundir/build" "$MYDIR/build"; then + cp "$MYDIR/build" "$rundir/build" + chmod +x "$rundir/build" + fi + if [ ! -f "$projdir/..env.dist" ]; then + sed <"$MYDIR/dot-build.env.dist" >"$projdir/..env.dist" ' +/^IMAGENAME=/s/=.*\//='"$(basename -- "$projdir")"'\// +' + initial_config=1 + fi + if [ ! -f "$projdir/.runphp.conf" ]; then + sed <"$MYDIR/dot-runphp.conf" >"$projdir/.runphp.conf" ' +/^RUNPHP=/s/=.*/=sbin\/runphp/ +' + fi +fi + +[ -n "$initial_config" ] diff --git a/sbin/composer.phar b/sbin/composer.phar index 03c724b..7a44c36 100755 Binary files a/sbin/composer.phar and b/sbin/composer.phar differ