`, the command package is placed in the * WP-CLI `packages/local/` directory. * * ## OPTIONS * * * : Name for the new package. Expects / (e.g. 'wp-cli/scaffold-package'). * * [--description=] * : Human-readable description for the package. * * [--homepage=] * : Homepage for the package. Defaults to 'https://github.com/' * * [--dir=] * : Specify a destination directory for the command. Defaults to WP-CLI's `packages/local/` directory. * * [--license=] * : License for the package. * --- * default: MIT * --- * * [--require_wp_cli=] * : Required WP-CLI version for the package. * --- * default: >=1.1.0 * --- * * [--skip-tests] * : Don't generate files for integration testing. * * [--skip-readme] * : Don't generate a README.md for the package. * * [--force] * : Overwrite files that already exist. * * @when before_wp_load */ public function package( $args, $assoc_args ) { $defaults = array( 'dir' => '', 'description' => '', 'homepage' => '', ); $assoc_args = array_merge( $defaults, $assoc_args ); $assoc_args['name'] = $args[0]; $bits = explode( '/', $assoc_args['name'] ); if ( 2 !== count( $bits ) || empty( $bits[0] ) || empty( $bits[1] ) ) { WP_CLI::error( "'{$assoc_args['name']}' is an invalid package name. Package scaffold expects '/'." ); } if ( ! empty( $assoc_args['dir'] ) ) { $package_dir = $assoc_args['dir']; } else { $package_dir = WP_CLI::get_runner()->get_packages_dir_path() . 'local/' . $assoc_args['name']; } if ( empty( $assoc_args['homepage'] ) ) { $assoc_args['homepage'] = 'https://github.com/' . $assoc_args['name']; } $force = Utils\get_flag_value( $assoc_args, 'force' ); $package_root = dirname( dirname( __FILE__ ) ); $template_path = $package_root . '/templates/'; $files_written = $this->create_files( array( "{$package_dir}/.gitignore" => file_get_contents( "{$package_root}/.gitignore" ), "{$package_dir}/.editorconfig" => file_get_contents( "{$package_root}/.editorconfig" ), "{$package_dir}/.distignore" => file_get_contents( "{$package_root}/.distignore" ), "{$package_dir}/wp-cli.yml" => file_get_contents( "{$package_root}/wp-cli.yml" ), "{$package_dir}/command.php" => Utils\mustache_render( "{$template_path}/command.mustache", $assoc_args ), "{$package_dir}/composer.json" => Utils\mustache_render( "{$template_path}/composer.mustache", $assoc_args ), ), $force ); if ( empty( $files_written ) ) { WP_CLI::log( 'All package files were skipped.' ); } else { WP_CLI::success( "Created package files in {$package_dir}" ); } $force_flag = $force ? '--force' : ''; if ( ! Utils\get_flag_value( $assoc_args, 'skip-tests' ) ) { WP_CLI::runcommand( "scaffold package-tests {$package_dir} {$force_flag}", array( 'launch' => false ) ); } if ( ! Utils\get_flag_value( $assoc_args, 'skip-readme' ) ) { WP_CLI::runcommand( "scaffold package-readme {$package_dir} {$force_flag}", array( 'launch' => false ) ); } WP_CLI::runcommand( "package install {$package_dir}", array( 'launch' => false ) ); } /** * Generate a README.md for your command. * * Creates a README.md with Using, Installing, and Contributing instructions * based on the composer.json file for your WP-CLI package. Run this command * at the beginning of your project, and then every time your usage docs * change. * * These command-specific docs are generated based composer.json -> 'extra' * -> 'commands'. For instance, this package's composer.json includes: * * ``` * { * "name": "wp-cli/scaffold-package-command", * // [...] * "extra": { * "commands": [ * "scaffold package", * "scaffold package-tests", * "scaffold package-readme" * ] * } * } * ``` * * You can also customize the rendering of README.md generally with * composer.json -> 'extra' -> 'readme'. For example, runcommand/hook's * composer.json includes: * * ``` * { * "extra": { * "commands": [ * "hook" * ], * "readme": { * "shields": [ * "[![Build Status](https://travis-ci.org/runcommand/reset-password.svg?branch=master)](https://travis-ci.org/runcommand/reset-password)" * ], * "sections": [ * "Using", * "Installing", * "Support" * ], * "support": { * "body": "https://raw.githubusercontent.com/runcommand/runcommand-theme/master/bin/readme-partials/support-open-source.md" * }, * "show_powered_by": false * } * } * } * ``` * * In this example: * * * "shields" supports arbitrary images as shields to display. * * "sections" permits defining arbitrary sections (instead of default Using, Installing and Contributing). * * "support" -> "body" uses a remote Markdown file as the section contents. This can also be a local file path, or a string. * * "show_powered_by" shows or hides the Powered By mention at the end of the readme. * * ## OPTIONS * * * : Directory of an existing command. * * [--force] * : Overwrite the readme if it already exists. * * @when before_wp_load * @subcommand package-readme */ public function package_readme( $args, $assoc_args ) { list( $package_dir ) = $args; if ( ! is_dir( $package_dir ) || ! file_exists( $package_dir . '/composer.json' ) ) { WP_CLI::error( "Invalid package directory. composer.json file must be present." ); } $composer_obj = json_decode( file_get_contents( $package_dir . '/composer.json' ), true ); if ( ! $composer_obj ) { WP_CLI::error( 'Invalid composer.json in package directory.' ); } $force = Utils\get_flag_value( $assoc_args, 'force' ); $package_root = dirname( dirname( __FILE__ ) ); $template_path = $package_root . '/templates/'; $bits = explode( '/', $composer_obj['name'] ); $readme_args = array( 'package_name' => $composer_obj['name'], 'package_short_name' => $bits[1], 'package_name_border' => str_pad( '', strlen( $composer_obj['name'] ), '=' ), 'package_description' => $composer_obj['description'], 'required_wp_cli_version' => ! empty( $composer_obj['require']['wp-cli/wp-cli'] ) ? str_replace( array( '~', '^', '>=' ), 'v', $composer_obj['require']['wp-cli/wp-cli'] ) : 'v0.23.0', 'shields' => '', 'has_commands' => false, 'wp_cli_update_to_instructions' => 'the latest stable release with `wp cli update`', 'show_powered_by' => isset( $composer_obj['extra']['readme']['show_powered_by'] ) ? (bool) $composer_obj['extra']['readme']['show_powered_by'] : true, ); if ( isset( $composer_obj['extra']['readme']['shields'] ) ) { $readme_args['shields'] = implode( ' ', $composer_obj['extra']['readme']['shields'] ); } else { $shields = array(); if ( file_exists( $package_dir . '/.travis.yml' ) ) { $shields[] = "[![Build Status](https://travis-ci.org/{$readme_args['package_name']}.svg?branch=master)](https://travis-ci.org/{$readme_args['package_name']})"; } if ( file_exists( $package_dir . '/circle.yml' ) ) { $shields[] = "[![CircleCI](https://circleci.com/gh/{$readme_args['package_name']}/tree/master.svg?style=svg)](https://circleci.com/gh/{$readme_args['package_name']}/tree/master)"; } if ( count( $shields ) ) { $readme_args['shields'] = implode( ' ', $shields ); } } if ( false !== stripos( $readme_args['required_wp_cli_version'], 'alpha' ) ) { $readme_args['wp_cli_update_to_instructions'] = 'the latest nightly release with `wp cli update --nightly`'; } if ( ! empty( $composer_obj['extra']['commands'] ) ) { $readme_args['commands'] = array(); $cmd_dump = WP_CLI::runcommand( 'cli cmd-dump', array( 'launch' => false, 'return' => true, 'parse' => 'json' ) ); foreach( $composer_obj['extra']['commands'] as $command ) { $bits = explode( ' ', $command ); $parent_command = $cmd_dump; do { $cmd_bit = array_shift( $bits ); $found = false; foreach( $parent_command['subcommands'] as $subcommand ) { if ( $subcommand['name'] === $cmd_bit ) { $parent_command = $subcommand; $found = true; break; } } if ( ! $found ) { $parent_command = false; } } while( $parent_command && $bits ); if ( empty( $parent_command ) ) { WP_CLI::error( 'Missing one or more commands defined in composer.json -> extras -> commands.' ); } $longdesc = preg_replace( '/## GLOBAL PARAMETERS(.+)/s', '', $parent_command['longdesc'] ); $longdesc = preg_replace( '/##\s(.+)/', '**$1**', $longdesc ); // definition lists $longdesc = preg_replace_callback( '/([^\n]+)\n: (.+?)(\n\n|$)/s', array( __CLASS__, 'rewrap_param_desc' ), $longdesc ); $readme_args['commands'][] = array( 'name' => "wp {$command}", 'shortdesc' => $parent_command['description'], 'synopsis' => "wp {$command}" . ( empty( $parent_command['subcommands'] ) ? " {$parent_command['synopsis']}" : "" ), 'longdesc' => $longdesc, ); } $readme_args['has_commands'] = true; $readme_args['has_multiple_commands'] = count( $readme_args['commands'] ) > 1 ? true : false; } if ( isset( $composer_obj['extra']['readme']['sections'] ) ) { $readme_section_headings = $composer_obj['extra']['readme']['sections']; } else { $readme_section_headings = array( 'Using', 'Installing', 'Contributing', ); } $readme_sections = array(); foreach( $readme_section_headings as $section_heading ) { $key = strtolower( preg_replace( '#[^\da-z-_]#i', '', $section_heading ) ); $readme_sections[ $key ] = array( 'heading' => $section_heading, ); } foreach( array( 'using', 'installing', 'contributing' ) as $key ) { if ( isset( $readme_sections[ $key ] ) ) { $readme_sections[ $key ]['body'] = dirname( dirname( __FILE__ ) ) . '/templates/readme-' . $key . '.mustache'; } } $readme_sections['package_description'] = array( 'body' => $composer_obj['description'], ); $readme_args['quick_links'] = ''; foreach( $readme_sections as $key => $section ) { if ( ! empty( $section['heading'] ) ) { $readme_args['quick_links'] .= '[' . $section['heading'] . '](#' . $key . ') | '; } } if ( ! empty( $readme_args['quick_links'] ) ) { $readme_args['quick_links'] = 'Quick links: ' . rtrim( $readme_args['quick_links'], '| ' ); } $readme_args['sections'] = array(); $ext_regex = '#\.(md|mustache)$#i'; foreach( $readme_sections as $section => $section_args ) { $value = array(); foreach( array( 'pre', 'body', 'post' ) as $k ) { $v = ''; if ( isset( $composer_obj['extra']['readme'][ $section ][ $k ] ) ) { $v = $composer_obj['extra']['readme'][ $section][ $k ]; if ( false !== stripos( $v, '://' ) ) { $response = Utils\http_request( 'GET', $v ); $v = $response->body; } else if ( preg_match( $ext_regex, $v ) ) { $v = $package_dir . '/' . $v; } } else if ( isset( $section_args[ $k ] ) ) { $v = $section_args[ $k ]; } if ( $v ) { if ( preg_match( $ext_regex, $v ) ) { $v = Utils\mustache_render( $v, $readme_args ); } $value[] = trim( $v ); } } $value = trim( implode( PHP_EOL . PHP_EOL, $value ) ); if ( 'package_description' === $section ) { $readme_args['package_description'] = $value; } else { $readme_args['sections'][] = array( 'heading' => $section_args['heading'], 'body' => $value, ); } } $files_written = $this->create_files( array( "{$package_dir}/README.md" => Utils\mustache_render( "{$template_path}/readme.mustache", $readme_args ), ), $force ); if ( empty( $files_written ) ) { WP_CLI::log( 'Package readme generation skipped.' ); } else { WP_CLI::success( 'Created package readme.' ); } } /** * Generate files needed for writing Behat tests for your command. * * WP-CLI makes use of a Behat-based testing framework, which you should use * too. Functional tests are an integral ingredient of highly-quality, * maintainable commands. Behat is a great choice as a testing framework * because: * * * It’s easy to write new tests, which means they’ll actually get written. * * The tests interface with your command in the same manner as your users * interface with your command, and they describe how the command is * expected to work in human-readable terms. * * Behat tests live in the `features/` directory of your project. When you * use this command, it will generate a default test that looks like this: * * ``` * Feature: Test that WP-CLI loads. * * Scenario: WP-CLI loads for your tests * Given a WP install * * When I run `wp eval 'echo "Hello world.";'` * Then STDOUT should contain: * """ * Hello world. * """ * ``` * * Functional tests typically follow this pattern: * * * **Given** some background, * * **When** a user performs a specific action, * * **Then** the end result should be X (and Y and Z). * * This command generates all of the files needed for you to write Behat * tests for your own command. Specifically: * * * `.travis.yml` is the configuration file for Travis CI. * * `bin/install-package-tests.sh` will configure your environment to run * the tests. * * `features/load-wp-cli.feature` is a basic test to confirm WP-CLI can * load. * * `features/bootstrap`, `features/steps`, `features/extra` are Behat * configuration files. * * After running `bin/install-package-tests.sh`, you can run the tests with * `./vendor/bin/behat`. If you find yourself using Behat on a number of * projects and don't want to install a copy with each one, you can * `composer global require behat/behat` to install Behat globally on your * machine. Make sure `~/.composer/vendor/bin` has also been added to your * `$PATH`. Once you've done so, you can run the tests for a project by * calling `behat`. * * ## ENVIRONMENT * * The `features/bootstrap/FeatureContext.php` file expects the * WP_CLI_BIN_DIR environment variable. * * WP-CLI Behat framework uses Behat ~2.5, which is installed with Composer. * * ## OPTIONS * * * : The package directory to generate tests for. * * [--ci=] * : Create a configuration file for a specific CI provider. * --- * default: travis * options: * - travis * - circle * --- * * [--force] * : Overwrite files that already exist. * * ## EXAMPLE * * wp scaffold package-tests /path/to/command/dir/ * * @when before_wp_load * @subcommand package-tests */ public function package_tests( $args, $assoc_args ) { list( $package_dir ) = $args; if ( is_file( $package_dir ) ) { $package_dir = dirname( $package_dir ); } else if ( is_dir( $package_dir ) ) { $package_dir = rtrim( $package_dir, '/' ); } if ( ! is_dir( $package_dir ) || ! file_exists( $package_dir . '/composer.json' ) ) { WP_CLI::error( "Invalid package directory. composer.json file must be present." ); } $package_dir .= '/'; $bin_dir = $package_dir . 'bin/'; $utils_dir = $package_dir . 'utils/'; $features_dir = $package_dir . 'features/'; $bootstrap_dir = $features_dir . 'bootstrap/'; $steps_dir = $features_dir . 'steps/'; $extra_dir = $features_dir . 'extra/'; foreach ( array( $features_dir, $bootstrap_dir, $steps_dir, $extra_dir, $utils_dir, $bin_dir ) as $dir ) { if ( ! is_dir( $dir ) ) { Process::create( Utils\esc_cmd( 'mkdir %s', $dir ) )->run(); } } $wp_cli_root = WP_CLI_ROOT; $package_root = dirname( dirname( __FILE__ ) ); $copy_source = array( $wp_cli_root => array( 'features/bootstrap/FeatureContext.php' => $bootstrap_dir, 'features/bootstrap/support.php' => $bootstrap_dir, 'php/WP_CLI/Process.php' => $bootstrap_dir, 'php/WP_CLI/ProcessRun.php' => $bootstrap_dir, 'php/utils.php' => $bootstrap_dir, 'ci/behat-tags.php' => $utils_dir, 'features/steps/given.php' => $steps_dir, 'features/steps/when.php' => $steps_dir, 'features/steps/then.php' => $steps_dir, 'features/extra/no-mail.php' => $extra_dir, ), $package_root => array( 'templates/load-wp-cli.feature' => $features_dir, 'bin/install-package-tests.sh' => $bin_dir, ), ); if ( 'travis' === $assoc_args['ci'] ) { $copy_source[ $package_root ]['.travis.yml'] = $package_dir; } else if ( 'circle' === $assoc_args['ci'] ) { $copy_source[ $package_root ]['circle.yml'] = $package_dir; } $files_written = array(); foreach( $copy_source as $source => $to_copy ) { foreach ( $to_copy as $file => $dir ) { if ( 'php/WP_CLI/ProcessRun.php' === $file && ! file_exists( $source . "/{$file}" ) ) { continue; } // file_get_contents() works with Phar-archived files $contents = file_get_contents( $source . "/{$file}" ); $file_path = $dir . basename( $file ); $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force' ); $should_write_file = $this->prompt_if_files_will_be_overwritten( $file_path, $force ); if ( ! $should_write_file ) { continue; } $files_written[] = $file_path; $result = Process::create( Utils\esc_cmd( 'touch %s', $file_path ) )->run(); file_put_contents( $file_path, $contents ); if ( 'bin/install-package-tests.sh' === $file ) { Process::create( Utils\esc_cmd( 'chmod +x %s', $file_path ) )->run(); } } } if ( empty( $files_written ) ) { WP_CLI::log( 'All package test files were skipped.' ); } else { WP_CLI::success( 'Created package test files.' ); } } private static function rewrap_param_desc( $matches ) { $param = $matches[1]; $desc = self::indent( "\t\t", $matches[2] ); return "\t$param\n$desc\n\n"; } private static function indent( $whitespace, $text ) { $lines = explode( "\n", $text ); foreach ( $lines as &$line ) { $line = $whitespace . $line; } return implode( $lines, "\n" ); } private function prompt_if_files_will_be_overwritten( $filename, $force ) { $should_write_file = true; if ( ! file_exists( $filename ) ) { return true; } WP_CLI::warning( 'File already exists' ); WP_CLI::log( $filename ); if ( ! $force ) { do { $answer = \cli\prompt( 'Skip this file, or replace it with scaffolding?', $default = false, $marker = '[s/r]: ' ); } while ( ! in_array( $answer, array( 's', 'r' ) ) ); $should_write_file = 'r' === $answer; } $outcome = $should_write_file ? 'Replacing' : 'Skipping'; WP_CLI::log( $outcome . PHP_EOL ); return $should_write_file; } private function create_files( $files_and_contents, $force ) { $wrote_files = array(); foreach ( $files_and_contents as $filename => $contents ) { $should_write_file = $this->prompt_if_files_will_be_overwritten( $filename, $force ); if ( ! $should_write_file ) { continue; } if ( ! is_dir( dirname( $filename ) ) ) { Process::create( Utils\esc_cmd( 'mkdir -p %s', dirname( $filename ) ) )->run(); } if ( ! file_put_contents( $filename, $contents ) ) { WP_CLI::error( "Error creating file: $filename" ); } elseif ( $should_write_file ) { $wrote_files[] = $filename; } } return $wrote_files; } }