OSDN Git Service

Rewrite SynopsisParser to support optional values for assoc parameters.
authorscribu <mail@scribu.net>
Tue, 30 Jul 2013 01:10:08 +0000 (04:10 +0300)
committerscribu <mail@scribu.net>
Tue, 30 Jul 2013 01:46:19 +0000 (04:46 +0300)
Props @jmslbam for initial implementation and tests.

see #570

php/WP_CLI/SynopsisParser.php
php/WP_CLI/SynopsisValidator.php
tests/test-synopsis.php

index 3daf70d..d7e4c1b 100644 (file)
@@ -4,68 +4,90 @@ namespace WP_CLI;
 
 class SynopsisParser {
 
-       private static $patterns = array();
-
        /**
-        * @param string
+        * @param string A synopsis
         * @return array List of parameters
         */
        static function parse( $synopsis ) {
-               if ( empty( self::$patterns ) )
-                       self::init_patterns();
-
                $tokens = array_filter( preg_split( '/[\s\t]+/', $synopsis ) );
 
                $params = array();
-
                foreach ( $tokens as $token ) {
-                       $type = false;
-
-                       foreach ( self::$patterns as $regex => $desc ) {
-                               if ( preg_match( $regex, $token, $matches ) ) {
-                                       $type = $desc['type'];
-                                       $params[] = array_merge( $matches, $desc );
-                                       break;
+                       $param = self::classify_token( $token );
+
+                       // Some types of parameters shouldn't be mandatory
+                       if ( isset( $param['optional'] ) && !$param['optional'] ) {
+                               if ( 'flag' === $param['type'] ||
+                                  ( 'assoc' === $param['type'] && $param['value']['optional'] )
+                               ) {
+                                       $param['type'] = 'unknown';
                                }
                        }
 
-                       if ( !$type ) {
-                               $params[] = array(
-                                       'type' => 'unknown',
-                                       'token' => $token
-                               );
-                       }
+                       $param['token'] = $token;
+                       $params[] = $param;
                }
 
                return $params;
        }
 
-       private static function init_patterns() {
-               $p_name = '(?P<name>[a-z-_]+)';
-               $p_value = '(?P<value>[a-zA-Z-|]+)';
+       private static function classify_token( $token ) {
+               $param = array();
 
-               self::gen_patterns( 'positional', "<$p_value>",           array( 'mandatory', 'optional', 'repeating' ) );
-               self::gen_patterns( 'generic',    "--<field>=<value>",    array( 'mandatory', 'optional', 'repeating' ) );
-               self::gen_patterns( 'assoc',      "--$p_name=<$p_value>", array( 'mandatory', 'optional' ) );
-               self::gen_patterns( 'flag',       "--$p_name",            array( 'optional' ) );
-       }
+               list( $param['optional'], $token ) = self::is_optional( $token );
+               list( $param['repeating'], $token ) = self::is_repeating( $token );
 
-       private static function gen_patterns( $type, $pattern, $flavour_types ) {
-               static $flavours = array(
-                       'mandatory' => ':pattern:',
-                       'optional' => '\[:pattern:\]',
-                       'repeating' => array( ':pattern:...', '\[:pattern:...\]' )
-               );
-
-               foreach ( $flavour_types as $flavour_type ) {
-                       foreach ( (array) $flavours[ $flavour_type ] as $flavour ) {
-                               $final_pattern = str_replace( ':pattern:', $pattern, $flavour );
-
-                               self::$patterns[ '/^' . $final_pattern . '$/' ] = array(
-                                       'type' => $type,
-                                       'flavour' => $flavour_type
-                               );
+               $p_name = '([a-z-_]+)';
+               $p_value = '([a-zA-Z-|]+)';
+
+               if ( '--<field>=<value>' === $token ) {
+                       $param['type'] = 'generic';
+               } elseif ( preg_match( "/^<$p_name>$/", $token, $matches ) ) {
+                       $param['type'] = 'positional';
+               } elseif ( preg_match( "/^--$p_name/", $token, $matches ) ) {
+                       $param['name'] = $matches[1];
+
+                       $value = substr( $token, strlen( $matches[0] ) );
+
+                       if ( false === $value ) {
+                               $param['type'] = 'flag';
+                       } else {
+                               $param['type'] = 'assoc';
+
+                               list( $param['value']['optional'], $value ) = self::is_optional( $value );
+
+                               if ( preg_match( "/^=<$p_value>$/", $value, $matches ) ) {
+                                       $param['value']['name'] = $matches[1];
+                               } else {
+                                       $param = array( 'type' => 'unknown' );
+                               }
                        }
+               } else {
+                       $param['type'] = 'unknown';
+               }
+
+               return $param;
+       }
+
+       /**
+        * An optional parameter is surrounded by square brackets.
+        */
+       private static function is_optional( $token ) {
+               if ( '[' == substr( $token, 0, 1 ) && ']' == substr( $token, -1 ) ) {
+                       return array( true, substr( $token, 1, -1 ) );
+               } else {
+                       return array( false, $token );
+               }
+       }
+
+       /**
+        * A repeating parameter is followed by an ellipsis.
+        */
+       private static function is_repeating( $token ) {
+               if ( '...' === substr( $token, -3 ) ) {
+                       return array( true, substr( $token, 0, -3 ) );
+               } else {
+                       return array( false, $token );
                }
        }
 }
index 597d89b..902b537 100644 (file)
@@ -16,7 +16,7 @@ class SynopsisValidator {
        public function enough_positionals( $args ) {
                $positional = $this->query_spec( array(
                        'type' => 'positional',
-                       'flavour' => 'mandatory'
+                       'optional' => false
                ) );
 
                return count( $args ) >= count( $positional );
@@ -39,12 +39,12 @@ class SynopsisValidator {
                                continue;
 
                        if ( !isset( $assoc_args[ $key ] ) ) {
-                               if ( 'mandatory' == $param['flavour'] ) {
+                               if ( !$param['optional'] ) {
                                        $errors['fatal'][] = "missing --$key parameter";
                                }
                        } else {
                                if ( true === $assoc_args[ $key ] ) {
-                                       $error_type = ( 'mandatory' == $param['flavour'] ) ? 'fatal' : 'warning';
+                                       $error_type = ( !$param['optional'] ) ? 'fatal' : 'warning';
                                        $errors[ $error_type ][] = "--$key parameter needs a value";
 
                                        unset( $assoc_args[ $key ] );
index 9262b59..22e7cb9 100644 (file)
@@ -15,54 +15,80 @@ class SynopsisParserTest extends PHPUnit_Framework_TestCase {
 
                $this->assertCount( 2, $r );
 
-               $this->assertEquals( 'positional', $r[0]['type'] );
-               $this->assertEquals( 'mandatory', $r[0]['flavour'] );
+               $param = $r[0];
+               $this->assertEquals( 'positional', $param['type'] );
+               $this->assertFalse( $param['optional'] );
 
-               $this->assertEquals( 'positional', $r[1]['type'] );
-               $this->assertEquals( 'optional', $r[1]['flavour'] );
+               $param = $r[1];
+               $this->assertEquals( 'positional', $param['type'] );
+               $this->assertTrue( $param['optional'] );
        }
 
        function testFlag() {
                $r = SynopsisParser::parse( '[--foo]' );
 
                $this->assertCount( 1, $r );
-               $this->assertEquals( 'flag', $r[0]['type'] );
-               $this->assertEquals( 'optional', $r[0]['flavour'] );
+
+               $param = $r[0];
+               $this->assertEquals( 'flag', $param['type'] );
+               $this->assertTrue( $param['optional'] );
 
                // flags can't be mandatory
                $r = SynopsisParser::parse( '--foo' );
 
                $this->assertCount( 1, $r );
-               $this->assertEquals( 'unknown', $r[0]['type'] );
+
+               $param = $r[0];
+               $this->assertEquals( 'unknown', $param['type'] );
        }
 
        function testGeneric() {
-               $r = SynopsisParser::parse( '--<field>=<value> [--<field>=<value>]' );
+               $r = SynopsisParser::parse( '--<field>=<value> [--<field>=<value>] --<field>[=<value>] [--<field>[=<value>]]' );
 
-               $this->assertCount( 2, $r );
+               $this->assertCount( 4, $r );
 
-               $this->assertEquals( 'generic', $r[0]['type'] );
-               $this->assertEquals( 'mandatory', $r[0]['flavour'] );
+               $param = $r[0];
+               $this->assertEquals( 'generic', $param['type'] );
+               $this->assertFalse( $param['optional'] );
 
-               $this->assertEquals( 'generic', $r[1]['type'] );
-               $this->assertEquals( 'optional', $r[1]['flavour'] );
+               $param = $r[1];
+               $this->assertEquals( 'generic', $param['type'] );
+               $this->assertTrue( $param['optional'] );
+
+               $param = $r[2];
+               $this->assertEquals( 'unknown', $param['type'] );
+
+               $param = $r[3];
+               $this->assertEquals( 'unknown', $param['type'] );
        }
 
        function testAssoc() {
-               $r = SynopsisParser::parse( '--foo=<value> [--bar=<value>]' );
+               $r = SynopsisParser::parse( '--foo=<value> [--bar=<value>] [--bar[=<value>]]' );
 
-               $this->assertCount( 2, $r );
+               $this->assertCount( 3, $r );
 
-               $this->assertEquals( 'assoc', $r[0]['type'] );
-               $this->assertEquals( 'mandatory', $r[0]['flavour'] );
+               $param = $r[0];
+               $this->assertEquals( 'assoc', $param['type'] );
+               $this->assertFalse( $param['optional'] );
 
-               $this->assertEquals( 'assoc', $r[1]['type'] );
-               $this->assertEquals( 'optional', $r[1]['flavour'] );
+               $param = $r[1];
+               $this->assertEquals( 'assoc', $param['type'] );
+               $this->assertTrue( $param['optional'] );
+
+               $param = $r[2];
+               $this->assertEquals( 'assoc', $param['type'] );
+               $this->assertTrue( $param['optional'] );
+               $this->assertTrue( $param['value']['optional'] );
+       }
+
+       function testInvalidAssoc() {
+               $r = SynopsisParser::parse( '--bar[=<value>] --bar=[<value>] --count=100' );
+
+               $this->assertCount( 3, $r );
 
-               // shouldn't pass defaults to assoc parameters
-               $r = SynopsisParser::parse( '--count=100' );
-               $this->assertCount( 1, $r );
                $this->assertEquals( 'unknown', $r[0]['type'] );
+               $this->assertEquals( 'unknown', $r[1]['type'] );
+               $this->assertEquals( 'unknown', $r[2]['type'] );
        }
 
        function testRepeating() {
@@ -70,38 +96,44 @@ class SynopsisParserTest extends PHPUnit_Framework_TestCase {
 
                $this->assertCount( 2, $r );
 
-               $this->assertEquals( 'positional', $r[0]['type'] );
-               $this->assertEquals( 'repeating', $r[0]['flavour'] );
+               $param = $r[0];
+               $this->assertEquals( 'positional', $param['type'] );
+               $this->assertTrue( $param['repeating'] );
 
-               $this->assertEquals( 'generic', $r[1]['type'] );
-               $this->assertEquals( 'repeating', $r[1]['flavour'] );
+               $param = $r[1];
+               $this->assertEquals( 'generic', $param['type'] );
+               $this->assertTrue( $param['repeating'] );
        }
 
        function testCombined() {
                $r = SynopsisParser::parse( '<positional> --assoc=<someval> --<field>=<value> [--flag]' );
 
+               $this->assertCount( 4, $r );
+
                $this->assertEquals( 'positional', $r[0]['type'] );
                $this->assertEquals( 'assoc', $r[1]['type'] );
                $this->assertEquals( 'generic', $r[2]['type'] );
                $this->assertEquals( 'flag', $r[3]['type'] );
        }
 
-    function testAllowedValueCharacters() {
-        $r = SynopsisParser::parse( '--capitals=<VALUE> --hyphen=<val-ue> --combined=<VAL-ue> --disallowed=<wrong:char>' );
+       function testAllowedValueCharacters() {
+               $r = SynopsisParser::parse( '--capitals=<VALUE> --hyphen=<val-ue> --combined=<VAL-ue> --disallowed=<wrong:char>' );
 
-        $this->assertCount( 4, $r );
+               $this->assertCount( 4, $r );
 
-        $this->assertEquals( 'assoc', $r[0]['type'] );
-        $this->assertEquals( 'mandatory', $r[0]['flavour'] );
+               $param = $r[0];
+               $this->assertEquals( 'assoc', $param['type'] );
+               $this->assertFalse( $param['optional'] );
 
-        $this->assertEquals( 'assoc', $r[1]['type'] );
-        $this->assertEquals( 'mandatory', $r[1]['flavour'] );
+               $param = $r[1];
+               $this->assertEquals( 'assoc', $param['type'] );
+               $this->assertFalse( $param['optional'] );
 
-        $this->assertEquals( 'assoc', $r[2]['type'] );
-        $this->assertEquals( 'mandatory', $r[2]['flavour'] );
-
-        $this->assertEquals( 'unknown', $r[3]['type'] );
-    }
+               $param = $r[2];
+               $this->assertEquals( 'assoc', $param['type'] );
+               $this->assertFalse( $param['optional'] );
 
+               $this->assertEquals( 'unknown', $r[3]['type'] );
+       }
 }