diff --git a/php/WP_CLI/Bootstrap/DefineProtectedCommands.php b/php/WP_CLI/Bootstrap/DefineProtectedCommands.php index f56da5a65e..5a77a5d62d 100644 --- a/php/WP_CLI/Bootstrap/DefineProtectedCommands.php +++ b/php/WP_CLI/Bootstrap/DefineProtectedCommands.php @@ -40,7 +40,7 @@ public function process( BootstrapState $state ) { private function get_protected_commands() { return array( 'cli info', - 'package', + 'package', ); } diff --git a/php/class-wp-cli.php b/php/class-wp-cli.php index 649a071027..872e88b6ed 100644 --- a/php/class-wp-cli.php +++ b/php/class-wp-cli.php @@ -381,9 +381,9 @@ private static function wp_hook_build_unique_id( $tag, $function, $priority ) { * @access public * @category Registration * - * @param string $name Name for the command (e.g. "post list" or "site empty"). - * @param string $callable Command implementation as a class, function or closure. - * @param array $args { + * @param string $name Name for the command (e.g. "post list" or "site empty"). + * @param callable $callable Command implementation as a class, function or closure. + * @param array $args { * Optional. An associative array with additional registration parameters. * * @type callable $before_invoke Callback to execute before invoking the command. diff --git a/php/commands/help.php b/php/commands/help.php index 9a1c1a5259..23a477d839 100644 --- a/php/commands/help.php +++ b/php/commands/help.php @@ -60,20 +60,28 @@ private static function show_help( $command ) { $wordwrap_width = \cli\Shell::columns(); // Wordwrap with indent. - $out = preg_replace_callback( '/^( *)([^\n]+)\n/m', function ( $matches ) use ( $wordwrap_width ) { - return $matches[1] . str_replace( "\n", "\n{$matches[1]}", wordwrap( $matches[2], $wordwrap_width - strlen( $matches[1] ) ) ) . "\n"; - }, $out ); + $out = preg_replace_callback( + '/^( *)([^\n]+)\n/m', + function ( $matches ) use ( $wordwrap_width ) { + return $matches[1] . str_replace( "\n", "\n{$matches[1]}", wordwrap( $matches[2], $wordwrap_width - strlen( $matches[1] ) ) ) . "\n"; + }, + $out + ); if ( $subcommands ) { // Wordwrap with column indent. - $subcommands = preg_replace_callback( '/^(' . $column_subpattern . ')([^\n]+)\n/m', function ( $matches ) use ( $wordwrap_width, $tab ) { - // Need to de-tab for wordwrapping to work properly. - $matches[1] = str_replace( "\t", $tab, $matches[1] ); - $matches[2] = str_replace( "\t", $tab, $matches[2] ); - $padding_len = strlen( $matches[1] ); - $padding = str_repeat( ' ', $padding_len ); - return $matches[1] . str_replace( "\n", "\n$padding", wordwrap( $matches[2], $wordwrap_width - $padding_len ) ) . "\n"; - }, $subcommands ); + $subcommands = preg_replace_callback( + '/^(' . $column_subpattern . ')([^\n]+)\n/m', + function ( $matches ) use ( $wordwrap_width, $tab ) { + // Need to de-tab for wordwrapping to work properly. + $matches[1] = str_replace( "\t", $tab, $matches[1] ); + $matches[2] = str_replace( "\t", $tab, $matches[2] ); + $padding_len = strlen( $matches[1] ); + $padding = str_repeat( ' ', $padding_len ); + return $matches[1] . str_replace( "\n", "\n$padding", wordwrap( $matches[2], $wordwrap_width - $padding_len ) ) . "\n"; + }, + $subcommands + ); // Put subcommands back. $out = str_replace( $subcommands_header, $subcommands, $out ); @@ -111,7 +119,7 @@ private static function pass_through_pager( $out ) { } // convert string to file handle - $fd = fopen( "php://temp", "r+" ); + $fd = fopen( 'php://temp', 'r+' ); fputs( $fd, $out ); rewind( $fd ); @@ -171,8 +179,9 @@ private static function get_max_len( $strings ) { $max_len = 0; foreach ( $strings as $str ) { $len = strlen( $str ); - if ( $len > $max_len ) + if ( $len > $max_len ) { $max_len = $len; + } } return $max_len; diff --git a/php/commands/src/CLI_Command.php b/php/commands/src/CLI_Command.php index 3e75509f01..67f8c83f37 100644 --- a/php/commands/src/CLI_Command.php +++ b/php/commands/src/CLI_Command.php @@ -192,7 +192,7 @@ public function check_update( $_, $assoc_args ) { array( 'version', 'update_type', 'package_url' ) ); $formatter->display_items( $updates ); - } else if ( empty( $assoc_args['format'] ) || 'table' == $assoc_args['format'] ) { + } elseif ( empty( $assoc_args['format'] ) || 'table' == $assoc_args['format'] ) { $update_type = $this->get_update_type_str( $assoc_args ); WP_CLI::success( "WP-CLI is at the latest{$update_type}version." ); } @@ -244,22 +244,22 @@ public function check_update( $_, $assoc_args ) { */ public function update( $_, $assoc_args ) { if ( ! Utils\inside_phar() ) { - WP_CLI::error( "You can only self-update Phar files." ); + WP_CLI::error( 'You can only self-update Phar files.' ); } $old_phar = realpath( $_SERVER['argv'][0] ); if ( ! is_writable( $old_phar ) ) { - WP_CLI::error( sprintf( "%s is not writable by current user.", $old_phar ) ); - } else if ( ! is_writeable( dirname( $old_phar ) ) ) { - WP_CLI::error( sprintf( "%s is not writable by current user.", dirname( $old_phar ) ) ); + WP_CLI::error( sprintf( '%s is not writable by current user.', $old_phar ) ); + } elseif ( ! is_writeable( dirname( $old_phar ) ) ) { + WP_CLI::error( sprintf( '%s is not writable by current user.', dirname( $old_phar ) ) ); } if ( Utils\get_flag_value( $assoc_args, 'nightly' ) ) { WP_CLI::confirm( sprintf( 'You have version %s. Would you like to update to the latest nightly?', WP_CLI_VERSION ), $assoc_args ); $download_url = 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli-nightly.phar'; $md5_url = 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli-nightly.phar.md5'; - } else if ( Utils\get_flag_value( $assoc_args, 'stable' ) ) { + } elseif ( Utils\get_flag_value( $assoc_args, 'stable' ) ) { WP_CLI::confirm( sprintf( 'You have version %s. Would you like to update to the latest stable release?', WP_CLI_VERSION ), $assoc_args ); $download_url = 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar'; $md5_url = 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar.md5'; @@ -283,7 +283,7 @@ public function update( $_, $assoc_args ) { WP_CLI::log( sprintf( 'Downloading from %s...', $download_url ) ); - $temp = \WP_CLI\Utils\get_temp_dir() . uniqid('wp_') . '.phar'; + $temp = \WP_CLI\Utils\get_temp_dir() . uniqid( 'wp_' ) . '.phar'; $headers = array(); $options = array( @@ -320,18 +320,18 @@ public function update( $_, $assoc_args ) { $mode = fileperms( $old_phar ) & 511; if ( false === @chmod( $temp, $mode ) ) { - WP_CLI::error( sprintf( "Cannot chmod %s.", $temp ) ); + WP_CLI::error( sprintf( 'Cannot chmod %s.', $temp ) ); } class_exists( '\cli\Colors' ); // This autoloads \cli\Colors - after we move the file we no longer have access to this class. if ( false === @rename( $temp, $old_phar ) ) { - WP_CLI::error( sprintf( "Cannot move %s to %s", $temp, $old_phar ) ); + WP_CLI::error( sprintf( 'Cannot move %s to %s', $temp, $old_phar ) ); } if ( Utils\get_flag_value( $assoc_args, 'nightly' ) ) { $updated_version = 'the latest nightly release'; - } else if ( Utils\get_flag_value( $assoc_args, 'stable' ) ) { + } elseif ( Utils\get_flag_value( $assoc_args, 'stable' ) ) { $updated_version = 'the latest stable release'; } else { $updated_version = $newest['version']; @@ -359,7 +359,7 @@ private function get_updates( $assoc_args ) { $response = Utils\http_request( 'GET', $url, null, $headers, $options ); if ( ! $response->success || 200 !== $response->status_code ) { - WP_CLI::error( sprintf( "Failed to get latest version (HTTP code %d).", $response->status_code ) ); + WP_CLI::error( sprintf( 'Failed to get latest version (HTTP code %d).', $response->status_code ) ); } $release_data = json_decode( $response->body ); @@ -393,13 +393,13 @@ private function get_updates( $assoc_args ) { ); } - foreach( $updates as $type => $value ) { + foreach ( $updates as $type => $value ) { if ( empty( $value ) ) { unset( $updates[ $type ] ); } } - foreach( array( 'major', 'minor', 'patch' ) as $type ) { + foreach ( array( 'major', 'minor', 'patch' ) as $type ) { if ( true === \WP_CLI\Utils\get_flag_value( $assoc_args, $type ) ) { return ! empty( $updates[ $type ] ) ? array( $updates[ $type ] ) : false; } @@ -409,7 +409,7 @@ private function get_updates( $assoc_args ) { $version_url = 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/NIGHTLY_VERSION'; $response = Utils\http_request( 'GET', $version_url ); if ( ! $response->success || 200 !== $response->status_code ) { - WP_CLI::error( sprintf( "Failed to get current nightly version (HTTP code %d)", $response->status_code ) ); + WP_CLI::error( sprintf( 'Failed to get current nightly version (HTTP code %d)', $response->status_code ) ); } $nightly_version = trim( $response->body ); if ( WP_CLI_VERSION != $nightly_version ) { @@ -467,12 +467,12 @@ function param_dump( $_, $assoc_args ) { $config = \WP_CLI::get_configurator()->to_array(); // Copy current config values to $spec foreach ( $spec as $key => $value ) { - if ( isset( $config[0][$key] ) ) { - $current = $config[0][$key]; + if ( isset( $config[0][ $key ] ) ) { + $current = $config[0][ $key ]; } else { - $current = NULL; + $current = null; } - $spec[$key]['current'] = $current; + $spec[ $key ]['current'] = $current; } } @@ -566,7 +566,7 @@ public function alias( $_, $assoc_args ) { */ private function get_update_type_str( $assoc_args ) { $update_type = ' '; - foreach( array( 'major', 'minor', 'patch' ) as $type ) { + foreach ( array( 'major', 'minor', 'patch' ) as $type ) { if ( true === \WP_CLI\Utils\get_flag_value( $assoc_args, $type ) ) { $update_type = ' ' . $type . ' '; break; diff --git a/php/utils.php b/php/utils.php index fc8d0df84b..ed7a2282c1 100644 --- a/php/utils.php +++ b/php/utils.php @@ -908,24 +908,125 @@ function isPiped() { * Has no effect on paths which do not use glob patterns. * * @param string|array $paths Single path as a string, or an array of paths. - * @param int $flags Flags to pass to glob. + * @param int $flags Optional. Flags to pass to glob. Defaults to GLOB_BRACE. * * @return array Expanded paths. */ -function expand_globs( $paths, $flags = GLOB_BRACE ) { +function expand_globs( $paths, $flags = 'default' ) { + // Compatibility for systems without GLOB_BRACE. + $glob_func = 'glob'; + if ( 'default' === $flags ) { + if ( ! defined( 'GLOB_BRACE' ) || getenv( 'WP_CLI_TEST_EXPAND_GLOBS_NO_GLOB_BRACE' ) ) { + $glob_func = 'WP_CLI\Utils\glob_brace'; + } else { + $flags = GLOB_BRACE; + } + } + $expanded = array(); foreach ( (array) $paths as $path ) { $matching = array( $path ); if ( preg_match( '/[' . preg_quote( '*?[]{}!', '/' ) . ']/', $path ) ) { - $matching = glob( $path, $flags ) ?: array(); + $matching = $glob_func( $path, $flags ) ?: array(); } - $expanded = array_merge( $expanded, $matching ); } - return array_unique( $expanded ); + return array_values( array_unique( $expanded ) ); +} + +/** + * Simulate a `glob()` with the `GLOB_BRACE` flag set. For systems (eg Alpine Linux) built against a libc library (eg https://www.musl-libc.org/) that lacks it. + * Copied and adapted from Zend Framework's `Glob::fallbackGlob()` and Glob::nextBraceSub()`. + * + * Zend Framework (http://framework.zend.com/) + * + * @link http://github.com/zendframework/zf2 for the canonical source repository + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * + * @param string $pattern Filename pattern. + * @param void $dummy_flags Not used. + * + * @return array Array of paths. + */ +function glob_brace( $pattern, $dummy_flags = null ) { + + static $next_brace_sub; + if ( ! $next_brace_sub ) { + // Find the end of the subpattern in a brace expression. + $next_brace_sub = function ( $pattern, $current ) { + $length = strlen( $pattern ); + $depth = 0; + + while ( $current < $length ) { + if ( '\\' === $pattern[ $current ] ) { + if ( ++$current === $length ) { + break; + } + $current++; + } else { + if ( ( '}' === $pattern[ $current ] && $depth-- === 0 ) || ( ',' === $pattern[ $current ] && 0 === $depth ) ) { + break; + } elseif ( '{' === $pattern[ $current++ ] ) { + $depth++; + } + } + } + + return $current < $length ? $current : null; + }; + } + + $length = strlen( $pattern ); + + // Find first opening brace. + for ( $begin = 0; $begin < $length; $begin++ ) { + if ( '\\' === $pattern[ $begin ] ) { + $begin++; + } elseif ( '{' === $pattern[ $begin ] ) { + break; + } + } + + // Find comma or matching closing brace. + if ( null === ( $next = $next_brace_sub( $pattern, $begin + 1 ) ) ) { + return glob( $pattern ); + } + + $rest = $next; + + // Point `$rest` to matching closing brace. + while ( '}' !== $pattern[ $rest ] ) { + if ( null === ( $rest = $next_brace_sub( $pattern, $rest + 1 ) ) ) { + return glob( $pattern ); + } + } + + $paths = array(); + $p = $begin + 1; + + // For each comma-separated subpattern. + do { + $subpattern = substr( $pattern, 0, $begin ) + . substr( $pattern, $p, $next - $p ) + . substr( $pattern, $rest + 1 ); + + if ( ( $result = glob_brace( $subpattern ) ) ) { + $paths = array_merge( $paths, $result ); + } + + if ( '}' === $pattern[ $next ] ) { + break; + } + + $p = $next + 1; + $next = $next_brace_sub( $pattern, $p ); + } while ( null !== $next ); + + return array_values( array_unique( $paths ) ); } /** diff --git a/tests/data/expand_globs/bar.ab1 b/tests/data/expand_globs/bar.ab1 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/data/expand_globs/bar.ab2 b/tests/data/expand_globs/bar.ab2 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/data/expand_globs/baz.ab1 b/tests/data/expand_globs/baz.ab1 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/data/expand_globs/baz.ac1 b/tests/data/expand_globs/baz.ac1 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/data/expand_globs/baz.efg2 b/tests/data/expand_globs/baz.efg2 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/data/expand_globs/foo.ab1 b/tests/data/expand_globs/foo.ab1 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/data/expand_globs/foo.ab2 b/tests/data/expand_globs/foo.ab2 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/data/expand_globs/foo.efg1 b/tests/data/expand_globs/foo.efg1 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/data/expand_globs/foo.efg2 b/tests/data/expand_globs/foo.efg2 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test-utils.php b/tests/test-utils.php index d68469d74a..756b8757af 100644 --- a/tests/test-utils.php +++ b/tests/test-utils.php @@ -494,4 +494,41 @@ public function dataPastTenseVerb() { ); } + /** + * @dataProvider dataExpandGlobs + */ + public function testExpandGlobs( $path, $expected ) { + $expand_globs_no_glob_brace = getenv( 'WP_CLI_TEST_EXPAND_GLOBS_NO_GLOB_BRACE' ); + + $dir = __DIR__ . '/data/expand_globs/'; + $expected = array_map( function ( $v ) use ( $dir ) { return $dir . $v; }, $expected ); + + putenv( 'WP_CLI_TEST_EXPAND_GLOBS_NO_GLOB_BRACE=0' ); + $out = Utils\expand_globs( $dir . $path ); + $this->assertSame( $expected, $out ); + + putenv( 'WP_CLI_TEST_EXPAND_GLOBS_NO_GLOB_BRACE=1' ); + $out = Utils\expand_globs( $dir . $path ); + $this->assertSame( $expected, $out ); + + putenv( false === $expand_globs_no_glob_brace ? 'WP_CLI_TEST_EXPAND_GLOBS_NO_GLOB_BRACE' : "WP_CLI_TEST_EXPAND_GLOBS_NO_GLOB_BRACE=$expand_globs_no_glob_brace" ); + } + + public function dataExpandGlobs() { + // Files in "data/expand_globs": foo.ab1, foo.ab2, foo.efg1, foo.efg2, bar.ab1, bar.ab2, baz.ab1, baz.ac1, baz.efg2. + return array( + array( 'foo.ab1', array( 'foo.ab1' ) ), + array( '{foo,bar}.ab1', array( 'foo.ab1', 'bar.ab1' ) ), + array( '{foo,baz}.a{b,c}1', array( 'foo.ab1', 'baz.ab1' , 'baz.ac1' ) ), + array( '{foo,baz}.{ab,ac}1', array( 'foo.ab1', 'baz.ab1' , 'baz.ac1' ) ), + array( '{foo,bar}.{ab1,efg1}', array( 'foo.ab1', 'foo.efg1', 'bar.ab1' ) ), + array( '{foo,bar,baz}.{ab,ac,efg}1', array( 'foo.ab1', 'foo.efg1', 'bar.ab1', 'baz.ab1', 'baz.ac1' ) ), + array( '{foo,ba{r,z}}.ab1', array( 'foo.ab1', 'bar.ab1', 'baz.ab1' ) ), + array( '{foo,ba{r,z}}.{ab1,efg1}', array( 'foo.ab1', 'foo.efg1', 'bar.ab1', 'baz.ab1') ), + array( '{foo,bar}.{ab{1,2},efg1}', array( 'foo.ab1', 'foo.ab2', 'foo.efg1', 'bar.ab1', 'bar.ab2' ) ), + array( '{foo,ba{r,z}}.{a{b,c}{1,2},efg{1,2}}', array( 'foo.ab1', 'foo.ab2', 'foo.efg1', 'foo.efg2', 'bar.ab1', 'bar.ab2', 'baz.ab1', 'baz.ac1', 'baz.efg2' ) ), + + array( 'no_such_file', array( 'no_such_file' ) ), // Documenting this behaviour here, which is odd (though advertized) - more natural to return an empty array. + ); + } }