Browse Source

Add required option support

Allow CLI flags to be marked as required flags/options.

Fixes #12498
Mark Story 6 years ago
parent
commit
4d2ae46f6c

+ 32 - 3
src/Console/ConsoleInputOption.php

@@ -77,6 +77,13 @@ class ConsoleInputOption
     protected $_choices;
 
     /**
+     * Is the option required.
+     *
+     * @var bool
+     */
+    protected $required;
+
+    /**
      * Make a new Input Option
      *
      * @param string $name The long name of the option, or an array with all the properties.
@@ -86,6 +93,7 @@ class ConsoleInputOption
      * @param string|bool|null $default The default value for this option.
      * @param string[] $choices Valid choices for this option.
      * @param bool $multiple Whether this option can accept multiple value definition.
+     * @param bool $required Whether this option is required or not.
      * @throws \Cake\Console\Exception\ConsoleException
      */
     public function __construct(
@@ -95,7 +103,8 @@ class ConsoleInputOption
         bool $isBoolean = false,
         $default = null,
         array $choices = [],
-        bool $multiple = false
+        bool $multiple = false,
+        bool $required = false
     ) {
         $this->_name = $name;
         $this->_short = $short;
@@ -103,6 +112,7 @@ class ConsoleInputOption
         $this->_boolean = $isBoolean;
         $this->_choices = $choices;
         $this->_multiple = $multiple;
+        $this->required = $required;
 
         if ($isBoolean) {
             $this->_default = (bool)$default;
@@ -159,8 +169,12 @@ class ConsoleInputOption
         if (strlen($name) < $width) {
             $name = str_pad($name, $width, ' ');
         }
+        $required = '';
+        if ($this->isRequired()) {
+            $required = ' <comment>(required)</comment>';
+        }
 
-        return sprintf('%s%s%s', $name, $this->_help, $default);
+        return sprintf('%s%s%s%s', $name, $this->_help, $default, $required);
     }
 
     /**
@@ -178,8 +192,12 @@ class ConsoleInputOption
         if ($this->_choices) {
             $default = ' ' . implode('|', $this->_choices);
         }
+        $template = '[%s%s]';
+        if ($this->isRequired()) {
+            $template = '%s%s';
+        }
 
-        return sprintf('[%s%s]', $name, $default);
+        return sprintf($template, $name, $default);
     }
 
     /**
@@ -193,6 +211,16 @@ class ConsoleInputOption
     }
 
     /**
+     * Check if this option is required
+     *
+     * @return bool
+     */
+    public function isRequired(): bool
+    {
+        return (bool)$this->required;
+    }
+
+    /**
      * Check if this option is a boolean option
      *
      * @return bool
@@ -261,6 +289,7 @@ class ConsoleInputOption
         $option->addAttribute('short', $short);
         $option->addAttribute('help', $this->_help);
         $option->addAttribute('boolean', (string)(int)$this->_boolean);
+        $option->addAttribute('required', (string)(int)$this->required);
         $option->addChild('default', (string)$default);
         $choices = $option->addChild('choices');
         foreach ($this->_choices as $valid) {

+ 9 - 2
src/Console/ConsoleOptionParser.php

@@ -425,6 +425,7 @@ class ConsoleOptionParser
                 'boolean' => false,
                 'multiple' => false,
                 'choices' => [],
+                'required' => false,
             ];
             $options += $defaults;
             $option = new ConsoleInputOption(
@@ -434,7 +435,8 @@ class ConsoleOptionParser
                 $options['boolean'],
                 $options['default'],
                 $options['choices'],
-                $options['multiple']
+                $options['multiple'],
+                $options['required'],
             );
         }
         $this->_options[$name] = $option;
@@ -705,7 +707,7 @@ class ConsoleOptionParser
         foreach ($this->_args as $i => $arg) {
             if ($arg->isRequired() && !isset($args[$i]) && empty($params['help'])) {
                 throw new ConsoleException(
-                    sprintf('Missing required arguments. The `%s` argument is required.', $arg->name())
+                    sprintf('Missing required argument. The `%s` argument is required.', $arg->name())
                 );
             }
         }
@@ -720,6 +722,11 @@ class ConsoleOptionParser
             if ($isBoolean && !isset($params[$name])) {
                 $params[$name] = false;
             }
+            if ($option->isRequired() && !isset($params[$name])) {
+                throw new ConsoleException(
+                    sprintf('Missing required option. The `%s` option is required and has no default value.', $name)
+                );
+            }
         }
 
         return [$params, $args];

+ 46 - 10
tests/TestCase/Console/ConsoleOptionParserTest.php

@@ -20,8 +20,10 @@ use Cake\Console\ConsoleInputArgument;
 use Cake\Console\ConsoleInputOption;
 use Cake\Console\ConsoleInputSubcommand;
 use Cake\Console\ConsoleOptionParser;
+use Cake\Console\Exception\ConsoleException;
 use Cake\Console\Exception\MissingOptionException;
 use Cake\TestSuite\TestCase;
+use LogicException;
 
 /**
  * ConsoleOptionParserTest
@@ -220,7 +222,7 @@ class ConsoleOptionParserTest extends TestCase
      */
     public function testAddOptionShortOneLetter()
     {
-        $this->expectException(\Cake\Console\Exception\ConsoleException::class);
+        $this->expectException(ConsoleException::class);
         $parser = new ConsoleOptionParser('test', false);
         $parser->addOption('test', ['short' => 'te']);
     }
@@ -271,7 +273,7 @@ class ConsoleOptionParserTest extends TestCase
      *
      * @return void
      */
-    public function testMultipleOptions()
+    public function testAddOptionMultipleOptions()
     {
         $parser = new ConsoleOptionParser('test', false);
         $parser->addOption('test')
@@ -309,7 +311,7 @@ class ConsoleOptionParserTest extends TestCase
      *
      * @return void
      */
-    public function testAddMultipleOptionsWithMultiple()
+    public function testAddOptionMultipleOptionsWithMultiple()
     {
         $parser = new ConsoleOptionParser('test', false);
         $parser
@@ -331,6 +333,38 @@ class ConsoleOptionParserTest extends TestCase
     }
 
     /**
+     * test adding a required option.
+     *
+     * @return void
+     */
+    public function testAddOptionRequired()
+    {
+        $parser = new ConsoleOptionParser('test', false);
+        $parser
+            ->addOption('test', [
+                'default' => 'default value',
+                'required' => true,
+            ])
+            ->addOption('no-default', [
+                'required' => true,
+            ]);
+        $result = $parser->parse(['--test', '--no-default', 'value']);
+        $this->assertSame(
+            ['test' => 'default value', 'no-default' => 'value', 'help' => false],
+            $result[0],
+        );
+
+        $result = $parser->parse(['--no-default', 'value']);
+        $this->assertSame(
+            ['no-default' => 'value', 'help' => false, 'test' => 'default value'],
+            $result[0],
+        );
+
+        $this->expectException(ConsoleException::class);
+        $parser->parse(['--test']);
+    }
+
+    /**
      * Test adding multiple options.
      *
      * @return void
@@ -415,7 +449,7 @@ class ConsoleOptionParserTest extends TestCase
      */
     public function testOptionWithChoices()
     {
-        $this->expectException(\Cake\Console\Exception\ConsoleException::class);
+        $this->expectException(ConsoleException::class);
         $parser = new ConsoleOptionParser('test', false);
         $parser->addOption('name', ['choices' => ['mark', 'jose']]);
 
@@ -514,7 +548,7 @@ class ConsoleOptionParserTest extends TestCase
      */
     public function testParseArgumentTooMany()
     {
-        $this->expectException(\Cake\Console\Exception\ConsoleException::class);
+        $this->expectException(ConsoleException::class);
         $parser = new ConsoleOptionParser('test', false);
         $parser->addArgument('name', ['help' => 'An argument'])
             ->addArgument('other');
@@ -547,7 +581,7 @@ class ConsoleOptionParserTest extends TestCase
      */
     public function testPositionalArgNotEnough()
     {
-        $this->expectException(\Cake\Console\Exception\ConsoleException::class);
+        $this->expectException(ConsoleException::class);
         $parser = new ConsoleOptionParser('test', false);
         $parser->addArgument('name', ['required' => true])
             ->addArgument('other', ['required' => true]);
@@ -562,7 +596,7 @@ class ConsoleOptionParserTest extends TestCase
      */
     public function testPositionalArgRequiredAfterOptional()
     {
-        $this->expectException(\LogicException::class);
+        $this->expectException(LogicException::class);
         $parser = new ConsoleOptionParser('test');
         $parser->addArgument('name', ['required' => false])
             ->addArgument('other', ['required' => true]);
@@ -575,7 +609,7 @@ class ConsoleOptionParserTest extends TestCase
      */
     public function testPositionalArgWithChoices()
     {
-        $this->expectException(\Cake\Console\Exception\ConsoleException::class);
+        $this->expectException(ConsoleException::class);
         $parser = new ConsoleOptionParser('test', false);
         $parser->addArgument('name', ['choices' => ['mark', 'jose']])
             ->addArgument('alias', ['choices' => ['cowboy', 'samurai']])
@@ -902,18 +936,20 @@ TEXT;
         $parser = new ConsoleOptionParser('sample', false);
         $parser->setDescription('A command!')
             ->setRootName('tool')
-            ->addOption('test', ['help' => 'A test option.']);
+            ->addOption('test', ['help' => 'A test option.'])
+            ->addOption('reqd', ['help' => 'A required option.', 'required' => true]);
 
         $result = $parser->help();
         $expected = <<<TEXT
 A command!
 
 <info>Usage:</info>
-tool sample [-h] [--test]
+tool sample [-h] --reqd [--test]
 
 <info>Options:</info>
 
 --help, -h  Display this help.
+--reqd      A required option. <comment>(required)</comment>
 --test      A test option.
 
 TEXT;