11
0
Fork 0
mirror of https://github.com/n3w/helpers-cli-input.git synced 2025-12-21 21:53:23 +00:00

Compare commits

...

39 commits

Author SHA1 Message Date
Norbert Wagner
c6fe64321e Passing the separator after the array is no longer supported 2023-03-23 08:47:59 +01:00
Alannah Kearney
9622c98258 Removed dev packages from composer.json to avoid circular package dependency version issues and removed example for manpage() method. 2019-11-28 03:33:52 +00:00
Alannah Kearney
2d87f04dff Removed version information from composer.json 2019-08-06 09:23:59 +10:00
Alannah Kearney
1e903ea6e1 Updated README and CHANGELOG for 1.2.2 release 2019-08-06 09:03:28 +10:00
Alannah Kearney
326a979302 Minor improvement to logic in AbstractInputHandler::validateInput(). Ensures that an input with a validator, but with a default value and no user suplied input, will have the default value used. 2019-08-06 09:02:03 +10:00
Alannah Kearney
d501a680ff Updated README, CHANGELOG, and composer.json for 1.2.1 release 2019-06-04 23:23:35 +10:00
Alannah Kearney
e542f6fd94 Updated InputHandlerFactory and InputTypeFactory to work with changes in pointybeard/helpers-foundation-factory 1.0.2 2019-06-04 23:22:15 +10:00
Alannah Kearney
46b87d3d62 Updated README, CHANGELOG, and composer.json for 1.2.0 release 2019-06-01 22:57:42 +10:00
Alannah Kearney
27c04d1d43 Updated example 2019-06-01 22:49:10 +10:00
Alannah Kearney
f7a9b66590 Added Argv::findOptionInCollection() and Argv::findArgumentInCollection() wrapper method, which are used in Argv::parse() 2019-06-01 22:44:09 +10:00
Alannah Kearney
05d283b6d2 InputCollection now implements Iterator and Countable (implementing required methods). Removed use of $type. Added getItemsExcludeByType(). getItemsByType() and getItems() now returns an Iterator. Renamed append() to add() and added $position flag. Added POSITION_APPEND and POSITION_PREPEND flags. 2019-06-01 22:43:19 +10:00
Alannah Kearney
90db5c2047 Added InputTypeFilterIterator class 2019-06-01 22:34:32 +10:00
Alannah Kearney
9f5c5c7d4d Made getCollection(), getInput(), find(), and validate() in AbstractInputHandler final. Removed categorising input by type. Abstracted most of validate() into it's own protected method called validateInput(). Removed $skipValidation argument from bind() and relaced with $flags. Added FLAG_BIND_SKIP_VALIDATION, FLAG_VALIDATION_SKIP_REQUIRED, FLAG_VALIDATION_SKIP_CUSTOM, and FLAG_VALIDATION_SKIP_UNRECOGNISED flags. Added check in validate() to look for unrecognised options and arguments. 2019-06-01 22:31:59 +10:00
Alannah Kearney
1100d5d959 Removed InputHandlerFactory::FLAG_SKIP_VALIDATION. Passing flags in call to bind() 2019-06-01 22:28:20 +10:00
Alannah Kearney
0cbbbdff66 Updated InputHandlerInterface bind() and validate() methods to support flags 2019-06-01 22:27:18 +10:00
Alannah Kearney
7069019301 Added UnrecognisedInputException exception 2019-06-01 15:09:02 +10:00
Alannah Kearney
302e4378fb Updated README, CHANGELOG, and composer.json for 1.1.4 release 2019-05-27 00:58:51 +10:00
Alannah Kearney
900f9f2885 Fixed misnamed variable in InputCollection::merge() 2019-05-27 00:57:09 +10:00
Alannah Kearney
ad21e667c3 Updated README, CHANGELOG, and composer.json for 1.1.3 release 2019-05-27 00:24:35 +10:00
Alannah Kearney
419a4a9f79 Code tidy 2019-05-27 00:22:25 +10:00
Alannah Kearney
866a2e1c9e Fixed logic bug that prevented $index and $type from getting set when InputCollection::append() is called. 2019-05-27 00:22:01 +10:00
Alannah Kearney
32ed646220 Updated README, CHANGELOG, and compose.json for 1.1.2 release 2019-05-25 12:02:35 +10:00
Alannah Kearney
695b4ac75a Updated validation logic for inputs that have a validator, no default, and are not set. Using InputValidationFailedException exception when validation fails 2019-05-25 11:59:29 +10:00
Alannah Kearney
c5aa26b19d Updated RequiredInputMissingException and RequiredInputMissingValueException exceptions to use getDisplayName() when producing their message 2019-05-25 11:58:33 +10:00
Alannah Kearney
50e56a8649 Added InputValidationFailedException exception 2019-05-25 11:57:32 +10:00
Alannah Kearney
e71ea40616 Removed unused RequiredArgumentMissingException exception 2019-05-25 11:57:16 +10:00
Alannah Kearney
4117ed266c Added getDisplayName() method to standardise how the name of an InputTypeInterface class wants to display it's name 2019-05-25 11:56:47 +10:00
Alannah Kearney
b89f2a6008 Updated README, CHANGELOG, example/example.php, and composer.json for 1.1.1 release 2019-05-24 18:06:57 +10:00
Alannah Kearney
96bf279a70 AbstractInputHandler::find() returns NULL if it cannot find any input with the supplied name. It is easier to test for NULL than it is to catch an exception. 2019-05-24 18:04:00 +10:00
Alannah Kearney
a4a8d5e351 Updated README, CHANGELOG, example code, and composer.json for 1.1.0 2019-05-24 17:20:50 +10:00
Alannah Kearney
9e27b52991 Updated to work with more than just Argument and Option input types. Makes use of InputTypeFactory to allow addition of new types as needed. Added InputTypeFactory to help with loading input type classes 2019-05-24 17:17:24 +10:00
Alannah Kearney
b49ee5559d Expanded input types to include Flag, IncrementingFlag, and LongOption. Refactored Option and Argument types. 2019-05-24 17:13:24 +10:00
Alannah Kearney
b36bfeac3d Updated README and CHANGELOG for 1.0.3 release 2019-05-24 13:29:08 +10:00
Alannah Kearney
9ac0e21d04 Updated example to reflect changes to manpage() function in pointybeard/helpers-functions-cli package 2019-05-24 13:28:34 +10:00
Alannah Kearney
95e93ec1bc Refactoring and improvemnts to Argument::__toString() and Option::__toString() 2019-05-24 13:27:38 +10:00
Alannah Kearney
6e987bb4eb Updated README, composer.json, and CHANGELOG for 1.0.2 release 2019-05-23 23:44:52 +10:00
Alannah Kearney
25f3fb8014 Fixed InputCollection::getArgumentsByIndex() so it returns NULL if the index does not exist instead of throwing an E_NOTICE message 2019-05-23 23:44:24 +10:00
Alannah Kearney
8d3141c79f Updated README and CHANGELOG for 1.0.1 2019-05-20 22:39:48 +10:00
Alannah Kearney
7c06febefe Updated example to use manpage() provided by the pointybeard/helpers-functions-cli package 2019-05-20 22:39:08 +10:00
24 changed files with 728 additions and 353 deletions

View file

@ -3,10 +3,92 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
**View all [Unreleased][] changes here** ## [1.2.2][]
#### Changed
- Minor improvement to logic in `AbstractInputHandler::validateInput()`. Ensures that an input with a validator, but with a default value and no user suplied input, will have the default value used.
## [1.2.1][]
#### Changed
- Updated `InputHandlerFactory` and `InputTypeFactory` to work with changes in `pointybeard/helpers-foundation-factory` 1.0.2
## [1.2.0][]
#### Added
- Added `InputTypeFilterIterator` class
- Added `UnrecognisedInputException` exception
#### Changed
- `InputCollection` now implements `Iterator` and `Countable` (implementing required methods)
- Removed use of `$type` in `InputCollection`
- Added `InputCollection::getItemsExcludeByType()`
- `InputCollection::getItemsByType()` and `InputCollection::getItems()` now returns an `Iterator`
- Renamed `InputCollection::append()` to `add()` and added `$position` flag
- Added `POSITION_APPEND` and `POSITION_PREPEND` flags to `InputCollection`
- Made `getCollection()`, `getInput()`, `find()`, and `validate()` in `AbstractInputHandler` final
- Removed all categorisation of items by type in `AbstractInputHandler::$input`
- Abstracted most of `AbstractInputHandler::validate()` into it's own protected method called `validateInput()`
- Removed `$skipValidation` argument from `AbstractInputHandler::bind()` and relaced with `$flags`
- Added `FLAG_BIND_SKIP_VALIDATION`, `FLAG_VALIDATION_SKIP_REQUIRED`, `FLAG_VALIDATION_SKIP_CUSTOM`, and `FLAG_VALIDATION_SKIP_UNRECOGNISED` flags to `AbstractInputHandler`
- Added check in `AbstractInputHandler::validate()` to look for unrecognised options and arguments
- Removed `InputHandlerFactory::FLAG_SKIP_VALIDATION` from `InputHandlerFactory`
- Passing flags in call from `InputHandlerFactory::build()` to `AbstractInputHandler::bind()`
- Updated `InputHandlerInterface::bind()` and `validate()` methods to support flags
## [1.1.4][]
#### Fixed
- Fixed misnamed variable in `InputCollection::merge()`
## [1.1.3][]
#### Fixed
- Fixed logic bug that prevented `$index` and `$type` from being set in `InputCollection::append()`. This means replaceing items in an `InputCollection` now works as expected
## [1.1.2][]
#### Added
- Added `InputValidationFailedException` exception
- Added `InputTypeInterface::getDisplayName()` method to standardise how the name of an `InputTypeInterface` class wants to display it's name
#### Changed
- Updated validation logic for inputs that have a validator, no default, and are not set
- Throwing `InputValidationFailedException` exception when validation fails
- Updated `RequiredInputMissingException` and `RequiredInputMissingValueException` exceptions to use `InputTypeInterface::getDisplayName()` when producing their message
- Removed unused `RequiredArgumentMissingException` exception
## [1.1.1][]
#### Changed
- `AbstractInputHandler::find()` returns NULL if it cannot find any input with the supplied name. It is easier to test for NULL than it is to catch an exception
## [1.1.0][]
#### Added
- Expanded input types to include `Flag`, `IncrementingFlag`, and `LongOption`
- Added `InputTypeFactory` to help with loading input type classes
#### Changed
- Updated to work with more than just `Argument` and `Option` input types. Makes use of `InputTypeFactory` to allow addition of new types as needed
## [1.0.2][]
#### Changed
- Updated example to reflect changes to `manpage()` function in `pointybeard/helpers-functions-cli` package
- Refactoring and improvemnts to `Argument::__toString()` and `Option::__toString()`
## [1.0.2][]
#### Fixed
- Fixed `InputCollection::getArgumentsByIndex()` so it returns NULL if the index does not exist instead of throwing an E_NOTICE message
## [1.0.1][]
#### Changed
- Updated example to use `Cli\manpage()` provided by the `pointybeard/helpers-functions-cli` package
## 1.0.0 ## 1.0.0
#### Added #### Added
- Initial release - Initial release
[Unreleased]: https://github.com/pointybeard/helpers-cli-input/compare/1.0.0...integration [1.2.2]: https://github.com/pointybeard/helpers-functions-cli/compare/1.2.1...1.2.2
[1.2.1]: https://github.com/pointybeard/helpers-functions-cli/compare/1.2.0...1.2.1
[1.2.0]: https://github.com/pointybeard/helpers-functions-cli/compare/1.1.4...1.2.0
[1.1.4]: https://github.com/pointybeard/helpers-functions-cli/compare/1.1.3...1.1.4
[1.1.3]: https://github.com/pointybeard/helpers-functions-cli/compare/1.1.2...1.1.3
[1.1.2]: https://github.com/pointybeard/helpers-functions-cli/compare/1.1.1...1.1.2
[1.1.1]: https://github.com/pointybeard/helpers-functions-cli/compare/1.1.0...1.1.1
[1.1.0]: https://github.com/pointybeard/helpers-functions-cli/compare/1.0.3...1.1.0
[1.0.3]: https://github.com/pointybeard/helpers-functions-cli/compare/1.0.2...1.0.3
[1.0.2]: https://github.com/pointybeard/helpers-functions-cli/compare/1.0.1...1.0.2
[1.0.1]: https://github.com/pointybeard/helpers-functions-cli/compare/1.0.0...1.0.1

View file

@ -1,7 +1,7 @@
# PHP Helpers: Command-line Input and Input Type Handlers # PHP Helpers: Command-line Input and Input Type Handlers
- Version: v1.0.0 - Version: v1.2.2
- Date: May 20 2019 - Date: Aug 06 2019
- [Release notes](https://github.com/pointybeard/helpers-cli-input/blob/master/CHANGELOG.md) - [Release notes](https://github.com/pointybeard/helpers-cli-input/blob/master/CHANGELOG.md)
- [GitHub repository](https://github.com/pointybeard/helpers-cli-input) - [GitHub repository](https://github.com/pointybeard/helpers-cli-input)
@ -9,7 +9,7 @@ Collection of classes for handling argv (and other) input when calling command-l
## Installation ## 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. 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.2.0"` to your `composer.json` file.
And run composer to update your dependencies: And run composer to update your dependencies:

View file

@ -1,6 +1,5 @@
{ {
"name": "pointybeard/helpers-cli-input", "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.", "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", "homepage": "https://github.com/pointybeard/helpers-cli-input",
"license": "MIT", "license": "MIT",
@ -17,11 +16,6 @@
"pointybeard/helpers-foundation-factory": "~1.0", "pointybeard/helpers-foundation-factory": "~1.0",
"pointybeard/helpers-functions-flags": "~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": { "support": {
"issues": "https://github.com/pointybeard/helpers-cli-input/issues", "issues": "https://github.com/pointybeard/helpers-cli-input/issues",
"wiki": "https://github.com/pointybeard/helpers-cli-input/wiki" "wiki": "https://github.com/pointybeard/helpers-cli-input/wiki"

View file

@ -4,146 +4,72 @@ declare(strict_types=1);
include __DIR__.'/../vendor/autoload.php'; include __DIR__.'/../vendor/autoload.php';
use pointybeard\Helpers\Cli\Input; use pointybeard\Helpers\Cli\Input;
use pointybeard\Helpers\Functions\Flags; use pointybeard\Helpers\Functions\Cli;
use pointybeard\Helpers\Functions\Strings;
use pointybeard\Helpers\Cli\Colour\Colour;
// Define what we are expecting to get from the command line // Define what we are expecting to get from the command line
$collection = (new Input\InputCollection()) $collection = (new Input\InputCollection())
->append(new Input\Types\Argument( ->add(
'action', Input\InputTypeFactory::build('Argument')
Input\AbstractInputType::FLAG_REQUIRED, ->name('action')
'The name of the action to perform' ->flags(Input\AbstractInputType::FLAG_REQUIRED)
)) ->description('The name of the action to perform')
->append(new Input\Types\Option( )
'v', ->add(
null, Input\InputTypeFactory::build('IncrementingFlag')
Input\AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_TYPE_INCREMENTING, ->name('v')
'verbosity level. -v (errors only), -vv (warnings and errors), -vvv (everything).', ->flags(Input\AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_TYPE_INCREMENTING)
null, ->description('verbosity level. -v (errors only), -vv (warnings and errors), -vvv (everything).')
0 ->validator(new Input\Validator(
)) function (Input\AbstractInputType $input, Input\AbstractInputHandler $context) {
->append(new Input\Types\Option( // Make sure verbosity level never goes above 3
'd', return min(3, (int) $context->find('v'));
'data', }
Input\AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED, ))
'Path to the input JSON data', )
function (Input\AbstractInputType $input, Input\AbstractInputHandler $context) { ->add(
// Make sure -d (--data) is a valid file that can be read Input\InputTypeFactory::build('LongOption')
$file = $context->getOption('d'); ->name('data')
->short('d')
->flags(Input\AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED)
->description('Path to the input JSON data')
->validator(new Input\Validator(
function (Input\AbstractInputType $input, Input\AbstractInputHandler $context) {
// Make sure -d (--data) is a valid file that can be read
$file = $context->find('data');
if (!is_readable($file)) { if (!is_readable($file)) {
throw new \Exception('The file specified via option -d (--data) does not exist or is not readable.'); throw new \Exception('The file specified via option --data does not exist or is not readable.');
} }
// Now make sure it is valid JSON // Now make sure it is valid JSON
try { try {
$json = json_decode(file_get_contents($file), false, 512, JSON_THROW_ON_ERROR); $json = json_decode(file_get_contents($file), false, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $ex) { } 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())); throw new \Exception(sprintf('The file specified via option --data does not appear to be a valid JSON ddocument. Returned: %s: %s', $ex->getCode(), $ex->getMessage()));
} }
return $json; return $json;
} }
)) ))
)
; ;
// Get the supplied input. Passing the collection will make the handler bind values // Get the supplied input. Passing the collection will make the handler bind values
// and validate the input according to our collection // and validate the input according to our collection
$argv = Input\InputHandlerFactory::build('Argv', $collection); try {
$argv = Input\InputHandlerFactory::build('Argv', $collection);
// Example of using an input collection to generate a usage string } catch (\Exception $ex) {
function usage(Input\InputCollection $collection): string { echo 'Error when attempting to bind values to collection. Returned: '.$ex->getMessage().PHP_EOL;
$arguments = []; exit;
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 var_dump($argv->find('action'));
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" // string(6) "import"
var_dump($argv->getOption('v')); var_dump($argv->find('v'));
//int(3) //int(3)
var_dump($argv->getOption('s')); var_dump($argv->find('data'));
//bool(true)
var_dump($argv->getOption('d'));
// class stdClass#11 (1) { // class stdClass#11 (1) {
// public $fruit => // public $fruit =>
// array(2) { // array(2) {
@ -153,3 +79,6 @@ var_dump($argv->getOption('d'));
// string(6) "banana" // string(6) "banana"
// } // }
// } // }
var_dump($argv->find('nope-doesnt-exist'));
// NULL

View file

@ -5,26 +5,49 @@ declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input; namespace pointybeard\Helpers\Cli\Input;
use pointybeard\Helpers\Functions\Flags; use pointybeard\Helpers\Functions\Flags;
use pointybeard\Helpers\Functions\Debug;
abstract class AbstractInputHandler implements Interfaces\InputHandlerInterface abstract class AbstractInputHandler implements Interfaces\InputHandlerInterface
{ {
protected $options = []; /**
protected $arguments = []; * Will skip all validation when bind() is executed. Ignores all other flags
* @var integer
*/
const FLAG_BIND_SKIP_VALIDATION = 0x0001;
/**
* Will skip the required input and required values check
* @var integer
*/
const FLAG_VALIDATION_SKIP_REQUIRED = 0x0002;
/**
* Will skip running custom validators
* @var integer
*/
const FLAG_VALIDATION_SKIP_CUSTOM = 0x0004;
/**
* Will skip checking if an input is in the collection
* @var integer
*/
const FLAG_VALIDATION_SKIP_UNRECOGNISED = 0x0008;
protected $input = [];
protected $collection = null; protected $collection = null;
abstract protected function parse(): bool; abstract protected function parse(): bool;
public function bind(InputCollection $inputCollection, bool $skipValidation = false): bool final public function bind(InputCollection $inputCollection, ?int $flags = null): bool
{ {
// Do the binding stuff here // Do the binding stuff here
$this->options = []; $this->input = [];
$this->arguments = [];
$this->collection = $inputCollection; $this->collection = $inputCollection;
$this->parse(); $this->parse();
if (true !== $skipValidation) { if (!Flags\is_flag_set($flags, self::FLAG_BIND_SKIP_VALIDATION)) {
$this->validate(); $this->validate($flags);
} }
return true; return true;
@ -41,76 +64,86 @@ abstract class AbstractInputHandler implements Interfaces\InputHandlerInterface
} }
} }
public function validate(): void protected function validateInput(AbstractInputType $input, ?int $flags) {
{ if(!Flags\is_flag_set($flags, self::FLAG_VALIDATION_SKIP_REQUIRED)) {
// Do basic missing option and value checking here self::checkRequiredAndRequiredValue($input, $this->input);
foreach ($this->collection->getOptions() as $input) {
self::checkRequiredAndRequiredValue($input, $this->options);
} }
// There is a default value and input has not been set. Assign the
// default value to the result.
if (
null !== $input->default() &&
null === $this->find($input->name())
) {
$result = $input->default();
// Option validation. // Input has been set AND it has a validator. Run the validator over the
foreach ($this->collection->getoptions() as $o) { // input. Note, this will be skipped if FLAG_VALIDATION_SKIP_CUSTOM is
$result = false; // set
} elseif (null !== $this->find($input->name()) && null !== $input->validator() && !Flags\is_flag_set($flags, self::FLAG_VALIDATION_SKIP_CUSTOM)) {
$validator = $input->validator();
if (!array_key_exists($o->name(), $this->options)) { if ($validator instanceof \Closure) {
$result = $o->default(); $validator = new Validator($validator);
} else { } elseif (!($validator instanceof Validator)) {
if (null === $o->validator()) { throw new \Exception("Validator for '{$input->name()}' must be NULL or an instance of either Closure or Input\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; try {
$result = $validator->validate($input, $this);
} catch (\Exception $ex) {
throw new Exceptions\InputValidationFailedException($input, 0, $ex);
}
// No default, but may or may not have been set so assign whatever value
// it might have to the result
} else {
$result = $this->find($input->name());
} }
// Argument validation. return $result;
foreach ($this->collection->getArguments() as $a) { }
self::checkRequiredAndRequiredValue($a, $this->arguments);
if (isset($this->arguments[$a->name()]) && null !== $a->validator()) { protected function isInputRecognised(string $name): bool {
if ($a->validator() instanceof \Closure) { return null === $this->collection->find($name) ? false : true;
$validator = new Validator($a->validator()); }
} elseif ($a->validator() instanceof Validator) {
$validator = $a->validator(); final public function validate(?int $flags = null): void
} else { {
throw new \Exception("Validator for argument {$a->name()} must be NULL or an instance of either Closure or Input\Validator."); if(!Flags\is_flag_set($flags, self::FLAG_VALIDATION_SKIP_UNRECOGNISED)) {
foreach($this->input as $name => $value) {
if(false == static::isInputRecognised((string)$name)) {
throw new Exceptions\UnrecognisedInputException("'{$name}' is not recognised");
} }
$validator->validate($a, $this);
} }
} }
foreach ($this->collection->getItems() as $input) {
$this->input[$input->name()] = static::validateInput($input, $flags);
}
} }
public function getArgument(string $name): ?string final public function find(string $name)
{ {
return $this->arguments[$name] ?? null; if (isset($this->input[$name])) {
return $this->input[$name];
}
// Check the collection to see if anything responds to $name
foreach ($this->collection->getItems() as $item) {
if ($item->respondsTo($name) && isset($this->input[$item->name()])) {
return $this->input[$item->name()];
}
}
return null;
} }
public function getOption(string $name) final public function getInput(): array
{ {
return $this->options[$name] ?? null; return $this->input;
} }
public function getArguments(): array final public function getCollection(): ?InputCollection
{
return $this->arguments;
}
public function getOptions(): array
{
return $this->options;
}
public function getCollection(): ?InputCollection
{ {
return $this->collection; return $this->collection;
} }

View file

@ -12,22 +12,40 @@ abstract class AbstractInputType implements Interfaces\InputTypeInterface
protected $flags; protected $flags;
protected $description; protected $description;
protected $validator; protected $validator;
protected $default;
protected $value; protected $value;
public function __construct($name, int $flags = null, string $description = null, object $validator = null) public function __construct(string $name = null, int $flags = null, string $description = null, object $validator = null, $default = null)
{ {
$this->name = $name; $this->name = $name;
$this->flags = $flags; $this->flags = $flags;
$this->description = $description; $this->description = $description;
$this->validator = $validator; $this->validator = $validator;
$this->default = $default;
} }
public function __call($name, array $args = []) public function __call($name, array $args = [])
{
if (empty($args)) {
return $this->$name;
}
$this->$name = $args[0];
return $this;
}
public function __get($name)
{ {
return $this->$name; return $this->$name;
} }
public function respondsTo(string $name): bool
{
return $name == $this->name;
}
public function getType(): string public function getType(): string
{ {
return strtolower((new \ReflectionClass(static::class))->getShortName()); return strtolower((new \ReflectionClass(static::class))->getShortName());

View file

@ -4,10 +4,10 @@ declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Exceptions; namespace pointybeard\Helpers\Cli\Input\Exceptions;
class InputHandlerNotFoundException extends \Exception class InputNotFoundException extends \Exception
{ {
public function __construct(string $handler, string $command, $code = 0, \Exception $previous = null) public function __construct(string $name, $code = 0, \Exception $previous = null)
{ {
return parent::__construct(sprintf('The input handler %s could not be located.', $handler), $code, $previous); return parent::__construct(sprintf('Input %s could not be found.', $name), $code, $previous);
} }
} }

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Exceptions;
use pointybeard\Helpers\Cli\Input\AbstractInputType;
class InputValidationFailedException extends \Exception
{
public function __construct(AbstractInputType $input, $code = 0, \Exception $previous = null)
{
return parent::__construct(sprintf('Validation failed for %s. Returned: %s', $input->getDisplayName(), $previous->getMessage()), $code, $previous);
}
}

View file

@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Exceptions;
class RequiredArgumentMissingException extends \Exception
{
private $argument;
public function __construct(string $argument, $code = 0, \Exception $previous = null)
{
$this->argument = strtoupper($argument);
return parent::__construct("missing argument {$this->argument}.", $code, $previous);
}
public function getArgumentName(): string
{
return $this->argument;
}
}

View file

@ -15,10 +15,8 @@ class RequiredInputMissingException extends \Exception
$this->input = $input; $this->input = $input;
return parent::__construct(sprintf( return parent::__construct(sprintf(
'missing %s %s%s', 'missing %s',
$input->getType(), $input->getDisplayName()
'option' == $input->getType() ? '-' : '',
'option' == $input->getType() ? $input->name() : strtoupper($input->name())
), $code, $previous); ), $code, $previous);
} }

View file

@ -15,10 +15,8 @@ class RequiredInputMissingValueException extends \Exception
$this->input = $input; $this->input = $input;
return parent::__construct(sprintf( return parent::__construct(sprintf(
'%s %s%s is missing a value', 'a value is required for %s',
$input->getType(), $input->getDisplayName()
'option' == $input->getType() ? '-' : '',
'option' == $input->getType() ? $input->name() : strtoupper($input->name())
), $code, $previous); ), $code, $previous);
} }

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Exceptions;
class UnrecognisedInputException extends \Exception
{
}

View file

@ -3,9 +3,10 @@
declare(strict_types=1); declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Handlers; namespace pointybeard\Helpers\Cli\Input\Handlers;
use pointybeard\Helpers\Cli\Input\Exceptions;
use pointybeard\Helpers\Cli\Input; use pointybeard\Helpers\Cli\Input;
use pointybeard\Helpers\Functions\Flags; use pointybeard\Helpers\Functions\Flags;
use pointybeard\Helpers\Functions\Debug;
class Argv extends Input\AbstractInputHandler class Argv extends Input\AbstractInputHandler
{ {
@ -61,12 +62,35 @@ class Argv extends Input\AbstractInputHandler
return $result; return $result;
} }
protected function findOptionInCollection(string $name): ?Input\AbstractInputType
{
$option = $this->collection->find($name);
if(!($option instanceof Input\AbstractInputType)) {
return null;
}
return $option;
}
protected function findArgumentInCollection(int $index, string $token): ?Input\AbstractInputType
{
$arguments = $this->collection->getItemsByType('Argument');
$position = 0;
foreach($arguments as $a) {
if($position == $index) {
return $a;
}
$position++;
}
return null;
}
protected function parse(): bool protected function parse(): bool
{ {
// So some parsing here.
$it = new \ArrayIterator($this->argv); $it = new \ArrayIterator($this->argv);
$position = 0; $position = 0;
$argumentCount = 0;
while ($it->valid()) { while ($it->valid()) {
$token = $it->current(); $token = $it->current();
@ -82,9 +106,9 @@ class Argv extends Input\AbstractInputHandler
$value = true; $value = true;
} }
$o = $this->collection->findOption($name); $o = $this->findOptionInCollection($name);
$this->options[ $this->input[
$o instanceof Input\AbstractInputType $o instanceof Input\AbstractInputType
? $o->name() ? $o->name()
: $name : $name
@ -98,14 +122,14 @@ class Argv extends Input\AbstractInputHandler
// Determine if we're expecting a value. // Determine if we're expecting a value.
// It also might have a long option equivalent, so we need // It also might have a long option equivalent, so we need
// to look for that too. // to look for that too.
$o = $this->collection->findOption($name); $o = $this->findOptionInCollection($name);
// This could also be an incrementing value // This could also be an incrementing value
// and needs to be added up. E.g. e.g. -vvv or -v -v -v // and needs to be added up. E.g. e.g. -vvv or -v -v -v
// would be -v => 3 // would be -v => 3
if ($o instanceof Input\AbstractInputType && Flags\is_flag_set($o->flags(), Input\AbstractInputType::FLAG_TYPE_INCREMENTING)) { if ($o instanceof Input\AbstractInputType && Flags\is_flag_set($o->flags(), Input\AbstractInputType::FLAG_TYPE_INCREMENTING)) {
$value = isset($this->options[$name]) $value = isset($this->input[$name])
? $this->options[$name] + 1 ? $this->input[$name] + 1
: 1 : 1
; ;
@ -128,7 +152,7 @@ class Argv extends Input\AbstractInputHandler
} }
} }
$this->options[ $this->input[
$o instanceof Input\AbstractInputType $o instanceof Input\AbstractInputType
? $o->name() ? $o->name()
: $name : $name
@ -141,12 +165,14 @@ class Argv extends Input\AbstractInputHandler
// Arguments are positional, so we need to keep a track // Arguments are positional, so we need to keep a track
// of the index and look at the collection for an argument // of the index and look at the collection for an argument
// with the same index // with the same index
$a = $this->collection->getArgumentsByIndex(count($this->arguments)); $a = $this->findArgumentInCollection($argumentCount, $token);
$this->arguments[
$this->input[
$a instanceof Input\AbstractInputType $a instanceof Input\AbstractInputType
? $a->name() ? $a->name()
: count($this->arguments) : $argumentCount
] = $token; ] = $token;
++$argumentCount;
break; break;
} }
$it->next(); $it->next();

View file

@ -4,116 +4,150 @@ declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input; namespace pointybeard\Helpers\Cli\Input;
class InputCollection class InputCollection implements \Iterator, \Countable
{ {
private $arguments = []; private $items = [];
private $options = []; private $position = 0;
public const POSITION_APPEND = 0x0001;
public const POSITION_PREPEND = 0x0002;
// Prevents the class from being instanciated // Prevents the class from being instanciated
public function __construct() public function __construct()
{ {
$this->position = 0;
} }
public function append(Interfaces\InputTypeInterface $input, bool $replace = false): self public function current(): mixed
{ {
$class = new \ReflectionClass($input); return $this->items[$this->position];
$this->{'append'.$class->getShortName()}($input, $replace); }
public function key(): scalar
{
return $this->position;
}
public function next(): void
{
++$this->position;
}
public function rewind(): void
{
$this->position = 0;
}
public function valid(): bool
{
return isset($this->items[$this->position]);
}
public function count() : int {
return count($this->items);
}
public function exists(string $name, &$index=null): bool {
return (null !== $this->find($name, null, null, $index));
}
public function remove(string $name): self {
if(!$this->exists($name, $index)) {
throw new \Exception("Input '{$name}' does not exist in this collection");
}
unset($this->items[$index]);
return $this;
}
public function add(Interfaces\InputTypeInterface $input, bool $replace = false, int $position=self::POSITION_APPEND): self
{
if($this->exists($input->name(), $index) && !$replace) {
throw new \Exception(
(new \ReflectionClass($input))->getShortName()." '{$input->name()}' already exists in this collection"
);
}
if (true == $replace && null !== $index) {
$this->items[$index] = $input;
} else {
if($position == self::POSITION_PREPEND) {
array_unshift($this->items, $input);
} else {
array_push($this->items, $input);
}
}
return $this; return $this;
} }
public function findArgument(string $name, ?int &$index = null): ?AbstractInputType public function find(string $name, array $restrictToType = null, array $excludeType = null, &$index = null): ?AbstractInputType
{ {
foreach ($this->arguments as $index => $a) { foreach ($this->items as $index => $input) {
if ($a->name() == $name) { // Check if we're restricting to or excluding specific types
return $a; if (null !== $restrictToType && !in_array($input->getType(), $restrictToType)) {
continue;
} elseif (null !== $excludeType && in_array($input->getType(), $excludeType)) {
continue;
} }
if ($input->respondsTo($name)) {
return $input;
}
} }
$index = null; $index = null;
return null; return null;
} }
public function findOption(string $name, ?int &$index = null): ?AbstractInputType public function getTypes(): array
{ {
$type = 1 == strlen($name) ? 'name' : 'long'; $types = [];
foreach($this->items as $input) {
foreach ($this->options as $index => $o) { $types[] = $input->getType();
if ($o->$type() == $name) {
return $o;
}
} }
return array_unique($types);
$index = null;
return null;
} }
private function appendArgument(Interfaces\InputTypeInterface $argument, bool $replace = false): void public function getItems(): \Iterator
{ {
if (null !== $this->findArgument($argument->name(), $index) && !$replace) { return (new \ArrayObject($this->items))->getIterator();
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 public function getItemsByType(string $type): \Iterator
{ {
if (null !== $this->findOption($option->name(), $index) && !$replace) { return new InputTypeFilterIterator(
throw new \Exception("Option -{$option->name()} already exists in collection"); $this->getItems(),
} [$type],
if (true == $replace && null !== $index) { InputTypeFilterIterator::FILTER_INCLUDE
$this->options[$index] = $option; );
} else {
$this->options[] = $option;
}
} }
public function getArgumentsByIndex(int $index): ?AbstractInputType public function getItemsExcludeByType(string $type): \Iterator
{ {
return $this->arguments[$index]; return new InputTypeFilterIterator(
$this->getItems(),
[$type],
InputTypeFilterIterator::FILTER_EXCLUDE
);
} }
public function getArguments(): array public function getItemByIndex(int $index): ?AbstractInputType
{ {
return $this->arguments; return $this->items[$index] ?? null;
}
public function getOptions(): array
{
return $this->options;
} }
public static function merge(self ...$collections): self public static function merge(self ...$collections): self
{ {
$arguments = [];
$options = [];
$iterator = new \AppendIterator;
foreach ($collections as $c) { foreach ($collections as $c) {
$arguments = array_merge($arguments, $c->getArguments()); $iterator->append($c->getItems());
$options = array_merge($options, $c->getOptions());
} }
$mergedCollection = new self(); $mergedCollection = new self();
foreach ($iterator as $input) {
$it = new \AppendIterator(); $mergedCollection->add($input, true);
$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; return $mergedCollection;
} }
} }

View file

@ -9,32 +9,37 @@ use pointybeard\Helpers\Foundation\Factory;
final class InputHandlerFactory extends Factory\AbstractFactory final class InputHandlerFactory extends Factory\AbstractFactory
{ {
const FLAG_SKIP_VALIDATION = 0x0001; public function getTemplateNamespace(): string
public static function getTemplateNamespace(): string
{ {
return __NAMESPACE__.'\\Handlers\\%s'; return __NAMESPACE__.'\\Handlers\\%s';
} }
public static function getExpectedClassType(): ?string public function getExpectedClassType(): ?string
{ {
return __NAMESPACE__.'\\Interfaces\\InputHandlerInterface'; return __NAMESPACE__.'\\Interfaces\\InputHandlerInterface';
} }
public static function build(string $name, InputCollection $collection = null, int $flags = null): Interfaces\InputHandlerInterface public static function build(string $name, ...$arguments): object
{ {
// Since passing flags is optional, we can use array_pad
// to ensure there are always at least 2 elements in $arguments
[$collection, $flags] = array_pad($arguments, 2, null);
$factory = new self;
try { try {
$handler = self::instanciate( $handler = $factory->instanciate(
self::generateTargetClassName($name) $factory->generateTargetClassName($name)
); );
} catch (\Exception $ex) { } catch (\Exception $ex) {
throw new Exceptions\UnableToLoadInputHandlerException($name, 0, $ex); throw new Exceptions\UnableToLoadInputHandlerException($name, 0, $ex);
} }
if ($collection instanceof InputCollection) { if (null !== $collection) {
$handler->bind( $handler->bind(
$collection, $collection,
Flags\is_flag_set($flags, self::FLAG_SKIP_VALIDATION) $flags
); );
} }

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input;
use pointybeard\Helpers\Foundation\Factory;
final class InputTypeFactory extends Factory\AbstractFactory
{
public function getTemplateNamespace(): string
{
return __NAMESPACE__.'\\Types\\%s';
}
public function getExpectedClassType(): ?string
{
return __NAMESPACE__.'\\Interfaces\\InputTypeInterface';
}
}

View file

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input;
class InputTypeFilterIterator extends \FilterIterator {
private $types;
private $mode;
public const FILTER_INCLUDE = 0x0001;
public const FILTER_EXCLUDE = 0x0002;
public function __construct(\Iterator $iterator, array $types=[], int $mode=self::FILTER_INCLUDE)
{
parent::__construct($iterator);
$this->types = array_map('strtolower', $types);
$this->mode = $mode;
}
public function accept()
{
$input = $this->getInnerIterator()->current();
switch($this->mode) {
case self::FILTER_EXCLUDE:
return !in_array($input->getType(), $this->types);
break;
case self::FILTER_INCLUDE:
default:
return in_array($input->getType(), $this->types);
break;
}
}
}

View file

@ -8,19 +8,13 @@ use pointybeard\Helpers\Cli\Input;
interface InputHandlerInterface interface InputHandlerInterface
{ {
public function bind(Input\InputCollection $inputCollection, bool $skipValidation = false): bool; public function bind(Input\InputCollection $inputCollection, ?int $flags = null): bool;
public function validate(): void; public function validate(?int $flags = null): void;
public function getArgument(string $name): ?string; public function find(string $name);
// note that the return value of getOption() isn't always going to be public function getInput(): array;
// a string like getArgument()
public function getOption(string $name);
public function getArguments(): array;
public function getOptions(): array;
public function getCollection(): ?Input\InputCollection; public function getCollection(): ?Input\InputCollection;
} }

View file

@ -16,4 +16,10 @@ interface InputTypeInterface
const FLAG_TYPE_INCREMENTING = 0x0400; const FLAG_TYPE_INCREMENTING = 0x0400;
public function getType(): string; public function getType(): string;
public function respondsTo(string $name): bool;
public function __toString(): string;
public function getDisplayName(): string;
} }

View file

@ -6,20 +6,72 @@ namespace pointybeard\Helpers\Cli\Input\Types;
use pointybeard\Helpers\Cli\Input; use pointybeard\Helpers\Cli\Input;
use pointybeard\Helpers\Functions\Strings; use pointybeard\Helpers\Functions\Strings;
use pointybeard\Helpers\Functions\Cli;
class Argument extends Input\AbstractInputType class Argument extends Input\AbstractInputType
{ {
public function __toString() public function __construct(string $name = null, int $flags = null, string $description = null, object $validator = null, $default = null)
{ {
$name = strtoupper($this->name()); if (null === $validator) {
$validator = function (Input\AbstractInputType $input, Input\AbstractInputHandler $context) {
$first = str_pad(sprintf('%s ', $name), 20, ' '); // This dummy validator is necessary otherwise the argument
// value is ALWAYS set to default (most often NULL) regardless
$second = Strings\utf8_wordwrap_array($this->description(), 40); // of if the argument was set or not
for ($ii = 1; $ii < count($second); ++$ii) { return $context->find($input->name());
$second[$ii] = str_pad('', 22, ' ', \STR_PAD_LEFT).$second[$ii]; };
} }
return $first.implode($second, PHP_EOL); parent::__construct($name, $flags, $description, $validator, $default);
}
public function getDisplayName(): string
{
return strtoupper($this->name());
}
public function __toString(): string
{
// MAGIC VALUES!!! OH MY.....
$padCharacter = ' ';
$paddingBufferSize = 0.15; // 15%
$argumentNamePaddedWidth = 20;
$argumentNameMinimumPaddingWidth = 4;
$minimumWindowWidth = 80;
// Get the window dimensions but restrict width to minimum
// of $minimumWindowWidth
$window = Cli\get_window_size();
$window['cols'] = max($minimumWindowWidth, $window['cols']);
// This shrinks the total line length (derived by the window width) by
// $paddingBufferSize
$paddingBuffer = (int) ceil($window['cols'] * $paddingBufferSize);
// Create a string of $padCharacter which is prepended to each secondary
// line
$secondaryLineLeadPadding = str_pad(
'',
$argumentNamePaddedWidth,
$padCharacter,
STR_PAD_LEFT
);
$first = Strings\mb_str_pad(
$this->getDisplayName().str_repeat($padCharacter, $argumentNameMinimumPaddingWidth),
$argumentNamePaddedWidth,
$padCharacter
);
$second = Strings\utf8_wordwrap_array(
$this->description(),
$window['cols'] - $argumentNamePaddedWidth - $paddingBuffer
);
// Skip the first item (notice $ii starts at value of '1')
for ($ii = 1; $ii < count($second); ++$ii) {
$second[$ii] = $secondaryLineLeadPadding.$second[$ii];
}
return $first.implode(PHP_EOL, $second);
} }
} }

18
src/Input/Types/Flag.php Normal file
View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Types;
use pointybeard\Helpers\Functions\Flags;
class Flag extends Option
{
public function __construct(string $name = null, int $flags = null, string $description = null, object $validator = null, $default = false)
{
if (Flags\is_flag_set($flags, self::FLAG_VALUE_REQUIRED) || Flags\is_flag_set($flags, self::FLAG_VALUE_OPTIONAL)) {
throw new \Exception('The flags FLAG_VALUE_REQUIRED and FLAG_VALUE_OPTIONAL cannot be used on an input of type Flag');
}
parent::__construct($name, null, $flags, $description, $validator, $default);
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Types;
use pointybeard\Helpers\Functions\Flags;
class IncrementingFlag extends Flag
{
public function __construct(string $name = null, int $flags = null, string $description = null, object $validator = null, $default = 0)
{
if (Flags\is_flag_set($flags, self::FLAG_TYPE_INCREMENTING)) {
$flags = $flags | self::FLAG_TYPE_INCREMENTING;
}
parent::__construct($name, null, $flags, $description, $validator, $default);
}
}

View file

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Types;
use pointybeard\Helpers\Functions\Flags;
use pointybeard\Helpers\Functions\Strings;
use pointybeard\Helpers\Functions\Cli;
use pointybeard\Helpers\Cli\Input;
class LongOption extends Input\AbstractInputType
{
protected $short;
public function __construct(string $name = null, string $short = null, int $flags = null, string $description = null, object $validator = null, $default = false)
{
$this->short = $short;
parent::__construct($name, $flags, $description, $validator, $default);
}
public function respondsTo(string $name): bool
{
return $name == $this->name || $name == $this->short;
}
public function getDisplayName(): string
{
$short =
null !== $this->short()
? '-'.$this->short().', '
: null
;
return sprintf('%s--%s', $short, $this->name());
}
public function __toString(): string
{
// MAGIC VALUES!!! OH MY.....
$padCharacter = ' ';
$paddingBufferSize = 0.15; // 15%
$optionNamePaddedWidth = 30;
$minimumWindowWidth = 80;
$secondaryLineIndentlength = 2;
// Get the window dimensions but restrict width to minimum
// of $minimumWindowWidth
$window = Cli\get_window_size();
$window['cols'] = max($minimumWindowWidth, $window['cols']);
// This shrinks the total line length (derived by the window width) by
// $paddingBufferSize
$paddingBuffer = (int) ceil($window['cols'] * $paddingBufferSize);
// Create a string of $padCharacter which is prepended to each secondary
// line
$secondaryLineLeadPadding = str_pad(
'',
$optionNamePaddedWidth,
$padCharacter,
STR_PAD_LEFT
);
$long = $this->getDisplayName();
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 = Strings\mb_str_pad(
$long, // -O, --LONG,
$optionNamePaddedWidth,
$padCharacter
);
$second = Strings\utf8_wordwrap_array(
$this->description(),
$window['cols'] - $optionNamePaddedWidth - $paddingBuffer
);
for ($ii = 1; $ii < count($second); ++$ii) {
$second[$ii] = $secondaryLineLeadPadding.$second[$ii];
}
return $first.implode(PHP_EOL, $second);
}
}

View file

@ -4,37 +4,57 @@ declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Types; namespace pointybeard\Helpers\Cli\Input\Types;
use pointybeard\Helpers\Functions\Flags;
use pointybeard\Helpers\Functions\Strings; use pointybeard\Helpers\Functions\Strings;
use pointybeard\Helpers\Functions\Cli;
use pointybeard\Helpers\Cli\Input; use pointybeard\Helpers\Cli\Input;
class Option extends Input\AbstractInputType class Option extends Input\AbstractInputType
{ {
protected $long; public function getDisplayName(): string
protected $default;
public function __construct(string $name, string $long = null, int $flags = null, string $description = null, object $validator = null, $default = false)
{ {
$this->default = $default; return '-'.$this->name();
$this->long = $long;
parent::__construct($name, $flags, $description, $validator);
} }
public function __toString() public function __toString(): string
{ {
$long = null !== $this->long() ? ', --'.$this->long() : null; // MAGIC VALUES!!! OH MY.....
if (null != $long) { $padCharacter = ' ';
if (Flags\is_flag_set($this->flags(), self::FLAG_VALUE_REQUIRED)) { $paddingBufferSize = 0.15; // 15%
$long .= '=VALUE'; $optionNamePaddedWidth = 30;
} elseif (Flags\is_flag_set($this->flags(), self::FLAG_VALUE_OPTIONAL)) { $minimumWindowWidth = 80;
$long .= '[=VALUE]'; $secondaryLineIndentlength = 2;
}
} // Get the window dimensions but restrict width to minimum
$first = str_pad(sprintf('-%s%s ', $this->name(), $long), 36, ' '); // of $minimumWindowWidth
$window = Cli\get_window_size();
$window['cols'] = max($minimumWindowWidth, $window['cols']);
// This shrinks the total line length (derived by the window width) by
// $paddingBufferSize
$paddingBuffer = (int) ceil($window['cols'] * $paddingBufferSize);
// Create a string of $padCharacter which is prepended to each secondary
// line
$secondaryLineLeadPadding = str_pad(
'',
$optionNamePaddedWidth,
$padCharacter,
STR_PAD_LEFT
);
$first = Strings\mb_str_pad(
$this->getDisplayName(),
$optionNamePaddedWidth,
$padCharacter
);
$second = Strings\utf8_wordwrap_array(
$this->description(),
$window['cols'] - $optionNamePaddedWidth - $paddingBuffer
);
$second = Strings\utf8_wordwrap_array($this->description(), 40);
for ($ii = 1; $ii < count($second); ++$ii) { for ($ii = 1; $ii < count($second); ++$ii) {
$second[$ii] = str_pad('', 38, ' ', \STR_PAD_LEFT).$second[$ii]; $second[$ii] = $secondaryLineLeadPadding.$second[$ii];
} }
return $first.implode($second, PHP_EOL); return $first.implode($second, PHP_EOL);