From 0620d00f089ed176fb9a8221fa4981b01c3bf83a Mon Sep 17 00:00:00 2001 From: Alannah Kearney Date: Mon, 20 May 2019 15:08:41 +1000 Subject: [PATCH] Initial commit --- .gitignore | 4 + CHANGELOG.md | 12 ++ CONTRIBUTING.md | 19 ++ LICENCE | 26 +++ README.md | 46 +++++ composer.json | 35 ++++ example/example.json | 1 + example/example.php | 155 ++++++++++++++++ src/Input/AbstractInputHandler.php | 117 ++++++++++++ src/Input/AbstractInputType.php | 35 ++++ .../Exceptions/InputNotFoundException.php | 13 ++ .../RequiredArgumentMissingException.php | 22 +++ .../RequiredInputMissingException.php | 29 +++ .../RequiredInputMissingValueException.php | 29 +++ .../UnableToLoadInputHandlerException.php | 13 ++ src/Input/Handlers/Argv.php | 174 ++++++++++++++++++ src/Input/InputCollection.php | 119 ++++++++++++ src/Input/InputHandlerFactory.php | 43 +++++ .../Interfaces/InputHandlerInterface.php | 26 +++ src/Input/Interfaces/InputTypeInterface.php | 19 ++ .../Interfaces/InputValidatorInterface.php | 12 ++ src/Input/Types/Argument.php | 25 +++ src/Input/Types/Option.php | 42 +++++ src/Input/Validator.php | 38 ++++ 24 files changed, 1054 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENCE create mode 100644 README.md create mode 100644 composer.json create mode 100644 example/example.json create mode 100644 example/example.php create mode 100644 src/Input/AbstractInputHandler.php create mode 100644 src/Input/AbstractInputType.php create mode 100644 src/Input/Exceptions/InputNotFoundException.php create mode 100644 src/Input/Exceptions/RequiredArgumentMissingException.php create mode 100644 src/Input/Exceptions/RequiredInputMissingException.php create mode 100644 src/Input/Exceptions/RequiredInputMissingValueException.php create mode 100644 src/Input/Exceptions/UnableToLoadInputHandlerException.php create mode 100644 src/Input/Handlers/Argv.php create mode 100644 src/Input/InputCollection.php create mode 100644 src/Input/InputHandlerFactory.php create mode 100644 src/Input/Interfaces/InputHandlerInterface.php create mode 100644 src/Input/Interfaces/InputTypeInterface.php create mode 100644 src/Input/Interfaces/InputValidatorInterface.php create mode 100644 src/Input/Types/Argument.php create mode 100644 src/Input/Types/Option.php create mode 100644 src/Input/Validator.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5fc641 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +vendor/ +composer.lock +.DS_Store +.php_cs.cache diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..75f524d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Change Log + +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). + +**View all [Unreleased][] changes here** + +## 1.0.0 +#### Added +- Initial release + +[Unreleased]: https://github.com/pointybeard/helpers-cli-input/compare/1.0.0...integration diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..281f57a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Contributing to this project + +We encourage contribution to this project and only ask you follow some simple rules to make everyone's job a little easier. + +## Found a bug? + +Please lodge an issue at the GitHub issue tracker for this project -- [https://github.com/pointybeard/helpers-cli-input/issues](https://github.com/pointybeard/helpers-cli-input/issues) + +Include details on the behaviour you are seeing, and steps needed to reproduce the problem. + +## Want to contribute code? + +* Fork the project +* Make your feature addition or bug fix +* Ensure your code is nicely formatted +* Commit just the modifications, do not alter CHANGELOG.md. If relevant, link to GitHub issue (see [https://help.github.com/articles/closing-issues-via-commit-messages/](https://help.github.com/articles/closing-issues-via-commit-messages/)) +* Send the pull request + +We will review the code and either merge it in, or leave some feedback. diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..7c0c9dc --- /dev/null +++ b/LICENCE @@ -0,0 +1,26 @@ +All source code included in the "PHP Helpers: Command-line Input and Input Type Handlers" archive is, +unless otherwise specified, released under the MIT licence as follows: + +----- begin license block ----- + +Copyright 2019 Alannah Kearney + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----- end license block ----- diff --git a/README.md b/README.md new file mode 100644 index 0000000..993641a --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# PHP Helpers: Command-line Input and Input Type Handlers + +- Version: v1.0.0 +- Date: May 20 2019 +- [Release notes](https://github.com/pointybeard/helpers-cli-input/blob/master/CHANGELOG.md) +- [GitHub repository](https://github.com/pointybeard/helpers-cli-input) + +Collection of classes for handling argv (and other) input when calling command-line scripts. Helps with parsing, collecting and validating arguments, options, and flags. + +## Installation + +This library is installed via [Composer](http://getcomposer.org/). To install, use `composer require pointybeard/helpers-cli-input` or add `"pointybeard/helpers-cli-input": "~1.0"` to your `composer.json` file. + +And run composer to update your dependencies: + + $ curl -s http://getcomposer.org/installer | php + $ php composer.phar update + +### Requirements + +This library makes use of the [PHP Helpers: Flag Functions](https://github.com/pointybeard/helpers-functions-flags) (`pointybeard/helpers-functions-flags`) and [PHP Helpers: Factory Foundation Classes](https://github.com/pointybeard/helpers-foundation-factory) packages. They are installed automatically via composer. + +To include all the [PHP Helpers](https://github.com/pointybeard/helpers) packages on your project, use `composer require pointybeard/helpers` or add `"pointybeard/helpers": "~1.1"` to your composer file. + +## Usage + +Include this library in your PHP files with `use pointybeard\Helpers\Cli`. See example code in `example/example.php`. The example code can be run with the following command: + + php -f example/example.php -- -vvv -d example/example.json import + +## Support + +If you believe you have found a bug, please report it using the [GitHub issue tracker](https://github.com/pointybeard/helpers-cli-input/issues), +or better yet, fork the library and submit a pull request. + +## Contributing + +We encourage you to contribute to this project. Please check out the [Contributing documentation](https://github.com/pointybeard/helpers-cli-input/blob/master/CONTRIBUTING.md) for guidelines about how to get involved. + +## License + +"PHP Helpers: Command-line Input and Input Type Handlers" is released under the [MIT License](http://www.opensource.org/licenses/MIT). + +## Credits + +* Some inspiration taken from the [Symfony Console Component](https://github.com/symfony/console) (although no code has been used). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..081d489 --- /dev/null +++ b/composer.json @@ -0,0 +1,35 @@ +{ + "name": "pointybeard/helpers-cli-input", + "version": "1.0.0", + "description": "Collection of classes for handling argv (and other) input when calling command-line scripts. Helps with parsing, collecting and validating arguments, options, and flags.", + "homepage": "https://github.com/pointybeard/helpers-cli-input", + "license": "MIT", + "authors": [ + { + "name": "Alannah Kearney", + "email": "hi@alannahkearney.com", + "homepage": "http://alannahkearney.com", + "role": "Developer" + } + ], + "require": { + "php": ">=7.2", + "pointybeard/helpers-foundation-factory": "~1.0", + "pointybeard/helpers-functions-flags": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "^8", + "pointybeard/helpers-functions-strings": "~1.0", + "pointybeard/helpers-cli-colour": "~1.0" + }, + "support": { + "issues": "https://github.com/pointybeard/helpers-cli-input/issues", + "wiki": "https://github.com/pointybeard/helpers-cli-input/wiki" + }, + "minimum-stability": "stable", + "autoload": { + "psr-4": { + "pointybeard\\Helpers\\Cli\\": "src/" + } + } +} diff --git a/example/example.json b/example/example.json new file mode 100644 index 0000000..139d010 --- /dev/null +++ b/example/example.json @@ -0,0 +1 @@ +{"fruit": ["apple", "banana"]} diff --git a/example/example.php b/example/example.php new file mode 100644 index 0000000..075ff81 --- /dev/null +++ b/example/example.php @@ -0,0 +1,155 @@ +append(new Input\Types\Argument( + 'action', + Input\AbstractInputType::FLAG_REQUIRED, + 'The name of the action to perform' + )) + ->append(new Input\Types\Option( + 'v', + null, + Input\AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_TYPE_INCREMENTING, + 'verbosity level. -v (errors only), -vv (warnings and errors), -vvv (everything).', + null, + 0 + )) + ->append(new Input\Types\Option( + 'd', + 'data', + Input\AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED, + 'Path to the input JSON data', + function (Input\AbstractInputType $input, Input\AbstractInputHandler $context) { + // Make sure -d (--data) is a valid file that can be read + $file = $context->getOption('d'); + + if (!is_readable($file)) { + throw new \Exception('The file specified via option -d (--data) does not exist or is not readable.'); + } + + // Now make sure it is valid JSON + try { + $json = json_decode(file_get_contents($file), false, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $ex) { + throw new \Exception(sprintf('The file specified via option -d (--data) does not appear to be a valid JSON ddocument. Returned: %s: %s', $ex->getCode(), $ex->getMessage())); + } + + return $json; + } + )) +; + +// Get the supplied input. Passing the collection will make the handler bind values +// and validate the input according to our collection +$argv = Input\InputHandlerFactory::build('Argv', $collection); + +// Example of using an input collection to generate a usage string +function usage(Input\InputCollection $collection): string { + $arguments = []; + foreach ($collection->getArguments() as $a) { + $arguments[] = strtoupper( + // Wrap with square brackets if it's not required + Flags\is_flag_set(Input\AbstractInputType::FLAG_OPTIONAL, $a->flags()) || + !Flags\is_flag_set(Input\AbstractInputType::FLAG_REQUIRED, $a->flags()) + ? "[{$a->name()}]" + : $a->name() + ); + } + $arguments = trim(implode($arguments, ' ')); + return sprintf( + "Usage: php -f example.php -- [OPTIONS]... %s%s", + $arguments, + strlen($arguments) > 0 ? '...' : '' + ); +} + +// Example of using an input collection to generate a manual page +function manpage(Input\InputCollection $collection) : string { + + $arguments = $options = []; + + foreach ($collection->getArguments() as $a) { + $arguments[] = (string) $a; + } + + foreach ($collection->getOptions() as $o) { + $options[] = (string) $o; + } + + $arguments = implode($arguments, PHP_EOL.' '); + $options = implode($options, PHP_EOL.' '); + + return sprintf('%s 1.0.0, %s +%s + +Mandatory values for long options are mandatory for short options too. + +Arguments: + %s + +Options: + %s + +Examples: + php -f example/example.php -- -vvv -d example/example.json import +', + basename(__FILE__), + Strings\utf8_wordwrap( + "An example script for the PHP Helpers: Command-line Input and Input Type Handlers composer library (pointybeard/helpers-cli-input)." + ), + usage($collection), + $arguments, + $options + ); +} + +// Display the manual in green text +echo Colour::colourise(manpage($collection), Colour::FG_GREEN) . PHP_EOL . PHP_EOL; + +/* +example.php 1.0.0, An example script for the PHP Helpers: Command-line Input and Input Type Handlers +composer library (pointybeard/helpers-cli-input). +Usage: php -f example.php -- [OPTIONS]... ACTION... + +Mandatory values for long options are mandatory for short options too. + +Arguments: + ACTION The name of the action to perform + +Options: + -v verbosity level. -v (errors only), -vv (warnings + and errors), -vvv (everything). + -d, --data=VALUE Path to the input JSON data + +Examples: + php -f example/example.php -- -vvv -d example/example.json import +*/ + +var_dump($argv->getArgument('action')); +// string(6) "import" + +var_dump($argv->getOption('v')); +//int(3) + +var_dump($argv->getOption('s')); +//bool(true) + +var_dump($argv->getOption('d')); +// class stdClass#11 (1) { +// public $fruit => +// array(2) { +// [0] => +// string(5) "apple" +// [1] => +// string(6) "banana" +// } +// } diff --git a/src/Input/AbstractInputHandler.php b/src/Input/AbstractInputHandler.php new file mode 100644 index 0000000..d35eb47 --- /dev/null +++ b/src/Input/AbstractInputHandler.php @@ -0,0 +1,117 @@ +options = []; + $this->arguments = []; + $this->collection = $inputCollection; + + $this->parse(); + + if (true !== $skipValidation) { + $this->validate(); + } + + return true; + } + + private static function checkRequiredAndRequiredValue(AbstractInputType $input, array $context): void + { + if (!isset($context[$input->name()])) { + if (Flags\is_flag_set($input->flags(), AbstractInputType::FLAG_REQUIRED)) { + throw new Exceptions\RequiredInputMissingException($input); + } + } elseif (Flags\is_flag_set($input->flags(), AbstractInputType::FLAG_VALUE_REQUIRED) && (null == $context[$input->name()] || true === $context[$input->name()])) { + throw new Exceptions\RequiredInputMissingValueException($input); + } + } + + public function validate(): void + { + // Do basic missing option and value checking here + foreach ($this->collection->getOptions() as $input) { + self::checkRequiredAndRequiredValue($input, $this->options); + } + + // Option validation. + foreach ($this->collection->getoptions() as $o) { + $result = false; + + if (!array_key_exists($o->name(), $this->options)) { + $result = $o->default(); + } else { + if (null === $o->validator()) { + $result = $o->default(); + continue; + } elseif ($o->validator() instanceof \Closure) { + $validator = new Validator($o->validator()); + } elseif ($o->validator() instanceof Validator) { + $validator = $o->validator(); + } else { + throw new \Exception("Validator for option {$o->name()} must be NULL or an instance of either Closure or Input\Validator."); + } + + $result = $validator->validate($o, $this); + } + + $this->options[$o->name()] = $result; + } + + // Argument validation. + foreach ($this->collection->getArguments() as $a) { + self::checkRequiredAndRequiredValue($a, $this->arguments); + + if (isset($this->arguments[$a->name()]) && null !== $a->validator()) { + if ($a->validator() instanceof \Closure) { + $validator = new Validator($a->validator()); + } elseif ($a->validator() instanceof Validator) { + $validator = $a->validator(); + } else { + throw new \Exception("Validator for argument {$a->name()} must be NULL or an instance of either Closure or Input\Validator."); + } + + $validator->validate($a, $this); + } + } + } + + public function getArgument(string $name): ?string + { + return $this->arguments[$name] ?? null; + } + + public function getOption(string $name) + { + return $this->options[$name] ?? null; + } + + public function getArguments(): array + { + return $this->arguments; + } + + public function getOptions(): array + { + return $this->options; + } + + public function getCollection(): ?InputCollection + { + return $this->collection; + } +} diff --git a/src/Input/AbstractInputType.php b/src/Input/AbstractInputType.php new file mode 100644 index 0000000..9fadc5e --- /dev/null +++ b/src/Input/AbstractInputType.php @@ -0,0 +1,35 @@ +name = $name; + $this->flags = $flags; + $this->description = $description; + $this->validator = $validator; + } + + public function __call($name, array $args = []) + { + return $this->$name; + } + + public function getType(): string + { + return strtolower((new \ReflectionClass(static::class))->getShortName()); + } +} diff --git a/src/Input/Exceptions/InputNotFoundException.php b/src/Input/Exceptions/InputNotFoundException.php new file mode 100644 index 0000000..475cc66 --- /dev/null +++ b/src/Input/Exceptions/InputNotFoundException.php @@ -0,0 +1,13 @@ +argument = strtoupper($argument); + + return parent::__construct("missing argument {$this->argument}.", $code, $previous); + } + + public function getArgumentName(): string + { + return $this->argument; + } +} diff --git a/src/Input/Exceptions/RequiredInputMissingException.php b/src/Input/Exceptions/RequiredInputMissingException.php new file mode 100644 index 0000000..c13bfd4 --- /dev/null +++ b/src/Input/Exceptions/RequiredInputMissingException.php @@ -0,0 +1,29 @@ +input = $input; + + return parent::__construct(sprintf( + 'missing %s %s%s', + $input->getType(), + 'option' == $input->getType() ? '-' : '', + 'option' == $input->getType() ? $input->name() : strtoupper($input->name()) + ), $code, $previous); + } + + public function getInput(): Input\AbstractInputType + { + return $this->input; + } +} diff --git a/src/Input/Exceptions/RequiredInputMissingValueException.php b/src/Input/Exceptions/RequiredInputMissingValueException.php new file mode 100644 index 0000000..b04c292 --- /dev/null +++ b/src/Input/Exceptions/RequiredInputMissingValueException.php @@ -0,0 +1,29 @@ +input = $input; + + return parent::__construct(sprintf( + '%s %s%s is missing a value', + $input->getType(), + 'option' == $input->getType() ? '-' : '', + 'option' == $input->getType() ? $input->name() : strtoupper($input->name()) + ), $code, $previous); + } + + public function getInput(): Input\AbstractInputType + { + return $this->input; + } +} diff --git a/src/Input/Exceptions/UnableToLoadInputHandlerException.php b/src/Input/Exceptions/UnableToLoadInputHandlerException.php new file mode 100644 index 0000000..577276d --- /dev/null +++ b/src/Input/Exceptions/UnableToLoadInputHandlerException.php @@ -0,0 +1,13 @@ +getMessage()), $code, $previous); + } +} diff --git a/src/Input/Handlers/Argv.php b/src/Input/Handlers/Argv.php new file mode 100644 index 0000000..75f30dd --- /dev/null +++ b/src/Input/Handlers/Argv.php @@ -0,0 +1,174 @@ +argv = self::expandOptions($argv); + } + + /** + * This will look for combined options, e.g -vlt and expand them to -v -l -t. + */ + protected static function expandOptions(array $args): array + { + $result = []; + foreach ($args as $a) { + switch (self::findType($a)) { + case self::OPTION_SHORT: + + // If the name is longer than a 2 characters + // it will mean it's a combination of flags. e.g. + // -vlt 12345 is the same as -v -l -t 12345 + if (strlen($a) > 2) { + // Strip the leading hyphen (-) + $a = substr($a, 1); + + for ($ii = 0; $ii < strlen($a); ++$ii) { + $result[] = "-{$a[$ii]}"; + } + + break; + } + + // no break + default: + $result[] = $a; + break; + } + } + + return $result; + } + + protected function parse(): bool + { + // So some parsing here. + $it = new \ArrayIterator($this->argv); + + $position = 0; + + while ($it->valid()) { + $token = $it->current(); + + switch (self::findType($token)) { + case self::OPTION_LONG: + $opt = substr($token, 2); + + if (false !== strstr($opt, '=')) { + list($name, $value) = explode('=', $opt, 2); + } else { + $name = $opt; + $value = true; + } + + $o = $this->collection->findOption($name); + + $this->options[ + $o instanceof Input\AbstractInputType + ? $o->name() + : $name + ] = $value; + + break; + + case self::OPTION_SHORT: + $name = substr($token, 1); + + // Determine if we're expecting a value. + // It also might have a long option equivalent, so we need + // to look for that too. + $o = $this->collection->findOption($name); + + // This could also be an incrementing value + // and needs to be added up. E.g. e.g. -vvv or -v -v -v + // would be -v => 3 + if ($o instanceof Input\AbstractInputType && Flags\is_flag_set($o->flags(), Input\AbstractInputType::FLAG_TYPE_INCREMENTING)) { + $value = isset($this->options[$name]) + ? $this->options[$name] + 1 + : 1 + ; + + // Not incrementing, so resume default behaviour + } else { + // We'll need to look ahead and see what the next value is. + // Ignore it if the next item is another option + // Advance the pointer to grab the next value + $it->next(); + $value = $it->current(); + + // See if the next item is another option and of it is, + // rewind the iterator and set value to 'true'. Also, + // if this option doesn't expect a value (no FLAG_VALUE_REQUIRED or FLAG_VALUE_OPTIONAL flag set), don't capture the next value. + if (null === $value || self::isOption($value) || !($o instanceof Input\AbstractInputType) || ( + !Flags\is_flag_set($o->flags(), Input\AbstractInputType::FLAG_VALUE_REQUIRED) && !Flags\is_flag_set($o->flags(), Input\AbstractInputType::FLAG_VALUE_OPTIONAL) + )) { + $value = true; + $it->seek($position); + } + } + + $this->options[ + $o instanceof Input\AbstractInputType + ? $o->name() + : $name + ] = $value; + + break; + + case self::ARGUMENT: + default: + // Arguments are positional, so we need to keep a track + // of the index and look at the collection for an argument + // with the same index + $a = $this->collection->getArgumentsByIndex(count($this->arguments)); + $this->arguments[ + $a instanceof Input\AbstractInputType + ? $a->name() + : count($this->arguments) + ] = $token; + break; + } + $it->next(); + ++$position; + } + + return true; + } + + private static function isOption(string $value): bool + { + return '-' == $value[0]; + } + + private static function findType(string $value): string + { + if (0 === strpos($value, '--')) { + return self::OPTION_LONG; + } elseif (self::isOption($value)) { + return self::OPTION_SHORT; + } else { + return self::ARGUMENT; + } + } +} diff --git a/src/Input/InputCollection.php b/src/Input/InputCollection.php new file mode 100644 index 0000000..525ab44 --- /dev/null +++ b/src/Input/InputCollection.php @@ -0,0 +1,119 @@ +{'append'.$class->getShortName()}($input, $replace); + + return $this; + } + + public function findArgument(string $name, ?int &$index = null): ?AbstractInputType + { + foreach ($this->arguments as $index => $a) { + if ($a->name() == $name) { + return $a; + } + } + + $index = null; + + return null; + } + + public function findOption(string $name, ?int &$index = null): ?AbstractInputType + { + $type = 1 == strlen($name) ? 'name' : 'long'; + + foreach ($this->options as $index => $o) { + if ($o->$type() == $name) { + return $o; + } + } + + $index = null; + + return null; + } + + private function appendArgument(Interfaces\InputTypeInterface $argument, bool $replace = false): void + { + if (null !== $this->findArgument($argument->name(), $index) && !$replace) { + throw new \Exception("Argument {$argument->name()} already exists in collection"); + } + + if (true == $replace && null !== $index) { + $this->arguments[$index] = $argument; + } else { + $this->arguments[] = $argument; + } + } + + private function appendOption(Interfaces\InputTypeInterface $option, bool $replace = false): void + { + if (null !== $this->findOption($option->name(), $index) && !$replace) { + throw new \Exception("Option -{$option->name()} already exists in collection"); + } + if (true == $replace && null !== $index) { + $this->options[$index] = $option; + } else { + $this->options[] = $option; + } + } + + public function getArgumentsByIndex(int $index): ?AbstractInputType + { + return $this->arguments[$index]; + } + + public function getArguments(): array + { + return $this->arguments; + } + + public function getOptions(): array + { + return $this->options; + } + + public static function merge(self ...$collections): self + { + $arguments = []; + $options = []; + + foreach ($collections as $c) { + $arguments = array_merge($arguments, $c->getArguments()); + $options = array_merge($options, $c->getOptions()); + } + + $mergedCollection = new self(); + + $it = new \AppendIterator(); + $it->append(new \ArrayIterator($arguments)); + $it->append(new \ArrayIterator($options)); + + foreach ($it as $input) { + try { + $mergedCollection->append($input, true); + } catch (\Exception $ex) { + // Already exists, so skip it. + } + } + + return $mergedCollection; + } +} diff --git a/src/Input/InputHandlerFactory.php b/src/Input/InputHandlerFactory.php new file mode 100644 index 0000000..a899812 --- /dev/null +++ b/src/Input/InputHandlerFactory.php @@ -0,0 +1,43 @@ +bind( + $collection, + Flags\is_flag_set($flags, self::FLAG_SKIP_VALIDATION) + ); + } + + return $handler; + } +} diff --git a/src/Input/Interfaces/InputHandlerInterface.php b/src/Input/Interfaces/InputHandlerInterface.php new file mode 100644 index 0000000..c75ce6b --- /dev/null +++ b/src/Input/Interfaces/InputHandlerInterface.php @@ -0,0 +1,26 @@ +name()); + + $first = str_pad(sprintf('%s ', $name), 20, ' '); + + $second = Strings\utf8_wordwrap_array($this->description(), 40); + for ($ii = 1; $ii < count($second); ++$ii) { + $second[$ii] = str_pad('', 22, ' ', \STR_PAD_LEFT).$second[$ii]; + } + + return $first.implode($second, PHP_EOL); + } +} diff --git a/src/Input/Types/Option.php b/src/Input/Types/Option.php new file mode 100644 index 0000000..3771de1 --- /dev/null +++ b/src/Input/Types/Option.php @@ -0,0 +1,42 @@ +default = $default; + $this->long = $long; + parent::__construct($name, $flags, $description, $validator); + } + + public function __toString() + { + $long = null !== $this->long() ? ', --'.$this->long() : null; + if (null != $long) { + if (Flags\is_flag_set($this->flags(), self::FLAG_VALUE_REQUIRED)) { + $long .= '=VALUE'; + } elseif (Flags\is_flag_set($this->flags(), self::FLAG_VALUE_OPTIONAL)) { + $long .= '[=VALUE]'; + } + } + $first = str_pad(sprintf('-%s%s ', $this->name(), $long), 36, ' '); + + $second = Strings\utf8_wordwrap_array($this->description(), 40); + for ($ii = 1; $ii < count($second); ++$ii) { + $second[$ii] = str_pad('', 38, ' ', \STR_PAD_LEFT).$second[$ii]; + } + + return $first.implode($second, PHP_EOL); + } +} diff --git a/src/Input/Validator.php b/src/Input/Validator.php new file mode 100644 index 0000000..9351c4c --- /dev/null +++ b/src/Input/Validator.php @@ -0,0 +1,38 @@ +getParameters(); + + // Must have exactly 2 params + if (2 != count($params)) { + throw new \Exception('Closure passed to Validator::__construct() is invalid: Must have exactly 2 parameters.'); + } + + // First must be 'input' and be of type pointybeard\Helpers\Cli\Input\AbstractInputType + if ('input' != $params[0]->getName() || __NAMESPACE__.'\AbstractInputType' != (string) $params[0]->getType()) { + throw new \Exception('Closure passed to Validator::__construct() is invalid: First parameter must match '.__NAMESPACE__."\AbstractInputType \$input. Provided with ".(string) $params[0]->getType()." \${$params[0]->getName()}"); + } + + // Second must be 'context' and be of type pointybeard\Helpers\Cli\Input\AbstractInputHandler + if ('context' != $params[1]->getName() || __NAMESPACE__.'\AbstractInputHandler' != (string) $params[1]->getType()) { + throw new \Exception('Closure passed to Validator::__construct() is invalid: Second parameter must match '.__NAMESPACE__."\AbstractInputHandler \$context. Provided with ".(string) $params[1]->getType()." \${$params[1]->getName()}"); + } + + $this->func = $func; + } + + public function validate(AbstractInputType $input, AbstractInputHandler $context) + { + return ($this->func)($input, $context); + } +}