├── .distignore ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE ├── PULL_REQUEST_TEMPLATE └── settings.yml ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── bin ├── install-package-tests.sh └── test.sh ├── command.php ├── composer.json ├── features ├── bootstrap │ ├── FeatureContext.php │ ├── Process.php │ ├── ProcessRun.php │ ├── support.php │ └── utils.php ├── extra │ └── no-mail.php ├── load-wp-cli.feature └── steps │ ├── given.php │ ├── then.php │ └── when.php ├── utils └── behat-tags.php └── wp-cli.yml /.distignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .git 3 | .gitignore 4 | .gitlab-ci.yml 5 | .editorconfig 6 | .travis.yml 7 | behat.yml 8 | circle.yml 9 | bin/ 10 | features/ 11 | utils/ 12 | *.zip 13 | *.tar.gz 14 | *.swp 15 | *.txt 16 | *.log 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # https://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = tab 15 | 16 | [{.jshintrc,*.json,*.yml,*.feature}] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [{*.txt,wp-config-sample.php}] 21 | end_of_line = crlf 22 | 23 | [composer.json] 24 | indent_style = space 25 | indent_size = 4 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # Used by Probot Settings: https://probot.github.io/apps/settings/ 2 | repository: 3 | labels: 4 | - name: bug 5 | color: fc2929 6 | - name: scope:documentation 7 | color: 0e8a16 8 | - name: scope:testing 9 | color: 5319e7 10 | - name: good-first-issue 11 | color: eb6420 12 | - name: help-wanted 13 | color: 159818 14 | - name: maybelater 15 | color: c2e0c6 16 | - name: state:unconfirmed 17 | color: bfe5bf 18 | - name: state:unsupported 19 | color: bfe5bf 20 | - name: wontfix 21 | color: c2e0c6 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | wp-cli.local.yml 3 | node_modules/ 4 | vendor/ 5 | *.zip 6 | *.tar.gz 7 | *.swp 8 | *.txt 9 | *.log 10 | composer.lock 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | 4 | language: php 5 | 6 | notifications: 7 | email: 8 | on_success: never 9 | on_failure: change 10 | 11 | branches: 12 | only: 13 | - master 14 | 15 | cache: 16 | directories: 17 | - $HOME/.composer/cache 18 | 19 | env: 20 | global: 21 | - PATH="$TRAVIS_BUILD_DIR/vendor/bin:$PATH" 22 | - WP_CLI_BIN_DIR="$TRAVIS_BUILD_DIR/vendor/bin" 23 | 24 | matrix: 25 | include: 26 | - php: 7.2 27 | env: WP_VERSION=latest 28 | - php: 7.1 29 | env: WP_VERSION=latest 30 | - php: 7.0 31 | env: WP_VERSION=latest 32 | - php: 5.6 33 | env: WP_VERSION=latest 34 | - php: 5.6 35 | env: WP_VERSION=3.7.11 36 | - php: 5.6 37 | env: WP_VERSION=trunk 38 | - php: 5.3 39 | dist: precise 40 | env: WP_VERSION=latest 41 | 42 | before_install: 43 | - | 44 | # Remove Xdebug for a huge performance increase: 45 | if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then 46 | phpenv config-rm xdebug.ini 47 | else 48 | echo "xdebug.ini does not exist" 49 | fi 50 | 51 | install: 52 | - composer require wp-cli/wp-cli:dev-master 53 | - composer install 54 | - bash bin/install-package-tests.sh 55 | 56 | before_script: 57 | - composer validate 58 | 59 | script: 60 | - bash bin/test.sh 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | We appreciate you taking the initiative to contribute to this project. 5 | 6 | Contributing isn’t limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation. 7 | 8 | For a more thorough introduction, [check out WP-CLI's guide to contributing](https://make.wordpress.org/cli/handbook/contributing/). This package follows those policy and guidelines. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | miya0001/drone-command 2 | ====================== 3 | 4 | 5 | 6 | [![Build Status](https://travis-ci.org/miya0001/drone-command.svg?branch=master)](https://travis-ci.org/miya0001/drone-command) 7 | 8 | Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing) | [Support](#support) 9 | 10 | ## Using 11 | 12 | Control DJI Tello with the WP-CLI command. 13 | 14 | ``` 15 | $ wp drone takeoff 16 | $ wp drone cw 360 17 | $ wp drone flip left 18 | $ wp drone land 19 | ``` 20 | 21 | ## Installing 22 | 23 | Installing this package requires WP-CLI v1.1.0 or greater. Update to the latest stable release with `wp cli update`. 24 | 25 | Once you've done so, you can install this package with: 26 | 27 | wp package install git@github.com:miya0001/drone-command.git 28 | 29 | ## Contributing 30 | 31 | We appreciate you taking the initiative to contribute to this project. 32 | 33 | Contributing isn’t limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation. 34 | 35 | For a more thorough introduction, [check out WP-CLI's guide to contributing](https://make.wordpress.org/cli/handbook/contributing/). This package follows those policy and guidelines. 36 | 37 | ### Reporting a bug 38 | 39 | Think you’ve found a bug? We’d love for you to help us get it fixed. 40 | 41 | Before you create a new issue, you should [search existing issues](https://github.com/miya0001/drone-command/issues?q=label%3Abug%20) to see if there’s an existing resolution to it, or if it’s already been fixed in a newer version. 42 | 43 | Once you’ve done a bit of searching and discovered there isn’t an open or fixed issue for your bug, please [create a new issue](https://github.com/miya0001/drone-command/issues/new). Include as much detail as you can, and clear steps to reproduce if possible. For more guidance, [review our bug report documentation](https://make.wordpress.org/cli/handbook/bug-reports/). 44 | 45 | ### Creating a pull request 46 | 47 | Want to contribute a new feature? Please first [open a new issue](https://github.com/miya0001/drone-command/issues/new) to discuss whether the feature is a good fit for the project. 48 | 49 | Once you've decided to commit the time to seeing your pull request through, [please follow our guidelines for creating a pull request](https://make.wordpress.org/cli/handbook/pull-requests/) to make sure it's a pleasant experience. See "[Setting up](https://make.wordpress.org/cli/handbook/pull-requests/#setting-up)" for details specific to working on this package locally. 50 | 51 | ## Support 52 | 53 | Github issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support 54 | 55 | 56 | *This README.md is generated dynamically from the project's codebase using `wp scaffold package-readme` ([doc](https://github.com/wp-cli/scaffold-package-command#wp-scaffold-package-readme)). To suggest changes, please submit a pull request against the corresponding part of the codebase.* 57 | -------------------------------------------------------------------------------- /bin/install-package-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | install_db() { 6 | mysql -e 'CREATE DATABASE IF NOT EXISTS wp_cli_test;' -uroot 7 | mysql -e 'GRANT ALL PRIVILEGES ON wp_cli_test.* TO "wp_cli_test"@"localhost" IDENTIFIED BY "password1"' -uroot 8 | mysql -e 'GRANT ALL PRIVILEGES ON wp_cli_test_scaffold.* TO "wp_cli_test"@"localhost" IDENTIFIED BY "password1"' -uroot 9 | } 10 | 11 | install_db 12 | -------------------------------------------------------------------------------- /bin/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | # Run the unit tests, if they exist 6 | if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ] 7 | then 8 | phpunit 9 | fi 10 | 11 | # Run the functional tests 12 | BEHAT_TAGS=$(php utils/behat-tags.php) 13 | behat --format progress $BEHAT_TAGS --strict 14 | -------------------------------------------------------------------------------- /command.php: -------------------------------------------------------------------------------- 1 | ] 25 | * : Hostname of the guest machine. Default is `vccw.test`. 26 | * 27 | * [--port=] 28 | * : IP address of the guest machine. Default is `192.168.33.10`. 29 | * 30 | * 31 | * @when before_wp_load 32 | */ 33 | public function takeoff( $args, $assoc_args ) 34 | { 35 | $this->connect( $assoc_args ); 36 | $this->send_command('command'); 37 | $this->send_command( 'takeoff' ); 38 | } 39 | 40 | /** 41 | * Land the drone. 42 | * 43 | * ## OPTIONS 44 | * 45 | * [--ip=] 46 | * : Hostname of the guest machine. Default is `vccw.test`. 47 | * 48 | * [--port=] 49 | * : IP address of the guest machine. Default is `192.168.33.10`. 50 | * 51 | * 52 | * @when before_wp_load 53 | */ 54 | public function land( $args, $assoc_args ) 55 | { 56 | $this->connect( $assoc_args ); 57 | $this->send_command( 'land' ); 58 | } 59 | 60 | /** 61 | * Rotate the drone clockwise. 62 | * 63 | * ## OPTIONS 64 | * 65 | * 66 | * : Degree to rotate clockwise. 67 | * 68 | * [--ip=] 69 | * : Hostname of the guest machine. Default is `vccw.test`. 70 | * 71 | * [--port=] 72 | * : IP address of the guest machine. Default is `192.168.33.10`. 73 | * 74 | * @when before_wp_load 75 | */ 76 | public function cw( $args, $assoc_args ) 77 | { 78 | $this->connect( $assoc_args ); 79 | $this->send_command( 'cw ' . $args[0] ); 80 | } 81 | 82 | /** 83 | * Rotate the drone counter-clockwise. 84 | * 85 | * ## OPTIONS 86 | * 87 | * 88 | * : Degree to rotate counter-clockwise. 89 | * 90 | * [--ip=] 91 | * : Hostname of the guest machine. Default is `vccw.test`. 92 | * 93 | * [--port=] 94 | * : IP address of the guest machine. Default is `192.168.33.10`. 95 | * 96 | * @when before_wp_load 97 | */ 98 | public function ccw( $args, $assoc_args ) 99 | { 100 | $this->connect( $assoc_args ); 101 | $this->send_command( 'ccw ' . $args[0] ); 102 | } 103 | 104 | /** 105 | * Flip the drone. 106 | * 107 | * ## OPTIONS 108 | * 109 | * 110 | * : `left, `right`, `forward` or `back`. 111 | * 112 | * [--ip=] 113 | * : Hostname of the guest machine. Default is `vccw.test`. 114 | * 115 | * [--port=] 116 | * : IP address of the guest machine. Default is `192.168.33.10`. 117 | * 118 | * @when before_wp_load 119 | */ 120 | public function flip( $args, $assoc_args ) 121 | { 122 | $this->connect( $assoc_args ); 123 | $this->send_command( 'flip ' . substr( $args[0], 0, 1 ) ); 124 | } 125 | 126 | private function get_ip( $assoc_args ) 127 | { 128 | if ( empty( $assoc_args["ip"] ) ) { 129 | return "192.168.10.1"; 130 | } else { 131 | return $assoc_args["ip"]; 132 | } 133 | } 134 | 135 | private function get_port( $assoc_args ) 136 | { 137 | if ( empty( $assoc_args["port"] ) ) { 138 | return 8889; 139 | } else { 140 | return $assoc_args["port"]; 141 | } 142 | } 143 | 144 | private function connect( $assoc_args ) 145 | { 146 | $this->socket = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ); 147 | socket_bind( $this->socket, $this->localhost, $this->localport ); 148 | socket_connect( $this->socket, $this->get_ip( $assoc_args ), $this->get_port( $assoc_args ) ); 149 | } 150 | 151 | private function send_command( $command ) 152 | { 153 | WP_CLI::line( $command ); 154 | return socket_send( $this->socket, $command, strlen( $command ), 0 ); 155 | } 156 | } 157 | 158 | WP_CLI::add_command( 'drone', 'Drone_Command' ); 159 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miya0001/drone-command", 3 | "description": "", 4 | "type": "wp-cli-package", 5 | "homepage": "https://github.com/miya0001/drone-command", 6 | "license": "MIT", 7 | "authors": [], 8 | "minimum-stability": "dev", 9 | "prefer-stable": true, 10 | "autoload": { 11 | "files": [ "command.php" ] 12 | }, 13 | "require": { 14 | "wp-cli/wp-cli": "^1.1.0" 15 | }, 16 | "require-dev": { 17 | "behat/behat": "~2.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /features/bootstrap/FeatureContext.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavin-chua/drone-command/70670eda3598b2e5e473b6c5743fccaee02c49a4/features/bootstrap/FeatureContext.php -------------------------------------------------------------------------------- /features/bootstrap/Process.php: -------------------------------------------------------------------------------- 1 | STDIN, 31 | 1 => array( 'pipe', 'w' ), 32 | 2 => array( 'pipe', 'w' ), 33 | ); 34 | 35 | /** 36 | * @var bool Whether to log run time info or not. 37 | */ 38 | public static $log_run_times = false; 39 | 40 | /** 41 | * @var array Array of process run time info, keyed by process command, each a 2-element array containing run time and run count. 42 | */ 43 | public static $run_times = array(); 44 | 45 | /** 46 | * @param string $command Command to execute. 47 | * @param string $cwd Directory to execute the command in. 48 | * @param array $env Environment variables to set when running the command. 49 | * 50 | * @return Process 51 | */ 52 | public static function create( $command, $cwd = null, $env = array() ) { 53 | $proc = new self; 54 | 55 | $proc->command = $command; 56 | $proc->cwd = $cwd; 57 | $proc->env = $env; 58 | 59 | return $proc; 60 | } 61 | 62 | private function __construct() {} 63 | 64 | /** 65 | * Run the command. 66 | * 67 | * @return ProcessRun 68 | */ 69 | public function run() { 70 | $start_time = microtime( true ); 71 | 72 | $proc = Utils\proc_open_compat( $this->command, self::$descriptors, $pipes, $this->cwd, $this->env ); 73 | 74 | $stdout = stream_get_contents( $pipes[1] ); 75 | fclose( $pipes[1] ); 76 | 77 | $stderr = stream_get_contents( $pipes[2] ); 78 | fclose( $pipes[2] ); 79 | 80 | $return_code = proc_close( $proc ); 81 | 82 | $run_time = microtime( true ) - $start_time; 83 | 84 | if ( self::$log_run_times ) { 85 | if ( ! isset( self::$run_times[ $this->command ] ) ) { 86 | self::$run_times[ $this->command ] = array( 0, 0 ); 87 | } 88 | self::$run_times[ $this->command ][0] += $run_time; 89 | self::$run_times[ $this->command ][1]++; 90 | } 91 | 92 | return new ProcessRun( 93 | array( 94 | 'stdout' => $stdout, 95 | 'stderr' => $stderr, 96 | 'return_code' => $return_code, 97 | 'command' => $this->command, 98 | 'cwd' => $this->cwd, 99 | 'env' => $this->env, 100 | 'run_time' => $run_time, 101 | ) 102 | ); 103 | } 104 | 105 | /** 106 | * Run the command, but throw an Exception on error. 107 | * 108 | * @return ProcessRun 109 | */ 110 | public function run_check() { 111 | $r = $this->run(); 112 | 113 | // $r->STDERR is incorrect, but kept incorrect for backwards-compat 114 | if ( $r->return_code || ! empty( $r->STDERR ) ) { 115 | throw new \RuntimeException( $r ); 116 | } 117 | 118 | return $r; 119 | } 120 | 121 | /** 122 | * Run the command, but throw an Exception on error. 123 | * Same as `run_check()` above, but checks the correct stderr. 124 | * 125 | * @return ProcessRun 126 | */ 127 | public function run_check_stderr() { 128 | $r = $this->run(); 129 | 130 | if ( $r->return_code || ! empty( $r->stderr ) ) { 131 | throw new \RuntimeException( $r ); 132 | } 133 | 134 | return $r; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /features/bootstrap/ProcessRun.php: -------------------------------------------------------------------------------- 1 | $value ) { 49 | $this->$key = $value; 50 | } 51 | } 52 | 53 | /** 54 | * Return properties of executed command as a string. 55 | * 56 | * @return string 57 | */ 58 | public function __toString() { 59 | $out = "$ $this->command\n"; 60 | $out .= "$this->stdout\n$this->stderr"; 61 | $out .= "cwd: $this->cwd\n"; 62 | $out .= "run time: $this->run_time\n"; 63 | $out .= "exit status: $this->return_code"; 64 | 65 | return $out; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /features/bootstrap/support.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavin-chua/drone-command/70670eda3598b2e5e473b6c5743fccaee02c49a4/features/bootstrap/support.php -------------------------------------------------------------------------------- /features/bootstrap/utils.php: -------------------------------------------------------------------------------- 1 | config ) && ! empty( $composer->config->{'vendor-dir'} ) ) { 78 | array_unshift( $vendor_paths, WP_CLI_ROOT . '/../../../' . $composer->config->{'vendor-dir'} ); 79 | } 80 | } 81 | return $vendor_paths; 82 | } 83 | 84 | // Using require() directly inside a class grants access to private methods to the loaded code 85 | function load_file( $path ) { 86 | require_once $path; 87 | } 88 | 89 | function load_command( $name ) { 90 | $path = WP_CLI_ROOT . "/php/commands/$name.php"; 91 | 92 | if ( is_readable( $path ) ) { 93 | include_once $path; 94 | } 95 | } 96 | 97 | /** 98 | * Like array_map(), except it returns a new iterator, instead of a modified array. 99 | * 100 | * Example: 101 | * 102 | * $arr = array('Football', 'Socker'); 103 | * 104 | * $it = iterator_map($arr, 'strtolower', function($val) { 105 | * return str_replace('foo', 'bar', $val); 106 | * }); 107 | * 108 | * foreach ( $it as $val ) { 109 | * var_dump($val); 110 | * } 111 | * 112 | * @param array|object Either a plain array or another iterator 113 | * @param callback The function to apply to an element 114 | * @return object An iterator that applies the given callback(s) 115 | */ 116 | function iterator_map( $it, $fn ) { 117 | if ( is_array( $it ) ) { 118 | $it = new \ArrayIterator( $it ); 119 | } 120 | 121 | if ( ! method_exists( $it, 'add_transform' ) ) { 122 | $it = new Transform( $it ); 123 | } 124 | 125 | foreach ( array_slice( func_get_args(), 1 ) as $fn ) { 126 | $it->add_transform( $fn ); 127 | } 128 | 129 | return $it; 130 | } 131 | 132 | /** 133 | * Search for file by walking up the directory tree until the first file is found or until $stop_check($dir) returns true 134 | * @param string|array The files (or file) to search for 135 | * @param string|null The directory to start searching from; defaults to CWD 136 | * @param callable Function which is passed the current dir each time a directory level is traversed 137 | * @return null|string Null if the file was not found 138 | */ 139 | function find_file_upward( $files, $dir = null, $stop_check = null ) { 140 | $files = (array) $files; 141 | if ( is_null( $dir ) ) { 142 | $dir = getcwd(); 143 | } 144 | while ( is_readable( $dir ) ) { 145 | // Stop walking up when the supplied callable returns true being passed the $dir 146 | if ( is_callable( $stop_check ) && call_user_func( $stop_check, $dir ) ) { 147 | return null; 148 | } 149 | 150 | foreach ( $files as $file ) { 151 | $path = $dir . DIRECTORY_SEPARATOR . $file; 152 | if ( file_exists( $path ) ) { 153 | return $path; 154 | } 155 | } 156 | 157 | $parent_dir = dirname( $dir ); 158 | if ( empty( $parent_dir ) || $parent_dir === $dir ) { 159 | break; 160 | } 161 | $dir = $parent_dir; 162 | } 163 | return null; 164 | } 165 | 166 | function is_path_absolute( $path ) { 167 | // Windows 168 | if ( isset( $path[1] ) && ':' === $path[1] ) { 169 | return true; 170 | } 171 | 172 | return '/' === $path[0]; 173 | } 174 | 175 | /** 176 | * Composes positional arguments into a command string. 177 | * 178 | * @param array 179 | * @return string 180 | */ 181 | function args_to_str( $args ) { 182 | return ' ' . implode( ' ', array_map( 'escapeshellarg', $args ) ); 183 | } 184 | 185 | /** 186 | * Composes associative arguments into a command string. 187 | * 188 | * @param array 189 | * @return string 190 | */ 191 | function assoc_args_to_str( $assoc_args ) { 192 | $str = ''; 193 | 194 | foreach ( $assoc_args as $key => $value ) { 195 | if ( true === $value ) { 196 | $str .= " --$key"; 197 | } elseif ( is_array( $value ) ) { 198 | foreach ( $value as $_ => $v ) { 199 | $str .= assoc_args_to_str( 200 | array( 201 | $key => $v, 202 | ) 203 | ); 204 | } 205 | } else { 206 | $str .= " --$key=" . escapeshellarg( $value ); 207 | } 208 | } 209 | 210 | return $str; 211 | } 212 | 213 | /** 214 | * Given a template string and an arbitrary number of arguments, 215 | * returns the final command, with the parameters escaped. 216 | */ 217 | function esc_cmd( $cmd ) { 218 | if ( func_num_args() < 2 ) { 219 | trigger_error( 'esc_cmd() requires at least two arguments.', E_USER_WARNING ); 220 | } 221 | 222 | $args = func_get_args(); 223 | 224 | $cmd = array_shift( $args ); 225 | 226 | return vsprintf( $cmd, array_map( 'escapeshellarg', $args ) ); 227 | } 228 | 229 | function locate_wp_config() { 230 | static $path; 231 | 232 | if ( null === $path ) { 233 | $path = false; 234 | 235 | if ( file_exists( ABSPATH . 'wp-config.php' ) ) { 236 | $path = ABSPATH . 'wp-config.php'; 237 | } elseif ( file_exists( ABSPATH . '../wp-config.php' ) && ! file_exists( ABSPATH . '/../wp-settings.php' ) ) { 238 | $path = ABSPATH . '../wp-config.php'; 239 | } 240 | 241 | if ( $path ) { 242 | $path = realpath( $path ); 243 | } 244 | } 245 | 246 | return $path; 247 | } 248 | 249 | function wp_version_compare( $since, $operator ) { 250 | $wp_version = str_replace( '-src', '', $GLOBALS['wp_version'] ); 251 | $since = str_replace( '-src', '', $since ); 252 | return version_compare( $wp_version, $since, $operator ); 253 | } 254 | 255 | /** 256 | * Render a collection of items as an ASCII table, JSON, CSV, YAML, list of ids, or count. 257 | * 258 | * Given a collection of items with a consistent data structure: 259 | * 260 | * ``` 261 | * $items = array( 262 | * array( 263 | * 'key' => 'foo', 264 | * 'value' => 'bar', 265 | * ) 266 | * ); 267 | * ``` 268 | * 269 | * Render `$items` as an ASCII table: 270 | * 271 | * ``` 272 | * WP_CLI\Utils\format_items( 'table', $items, array( 'key', 'value' ) ); 273 | * 274 | * # +-----+-------+ 275 | * # | key | value | 276 | * # +-----+-------+ 277 | * # | foo | bar | 278 | * # +-----+-------+ 279 | * ``` 280 | * 281 | * Or render `$items` as YAML: 282 | * 283 | * ``` 284 | * WP_CLI\Utils\format_items( 'yaml', $items, array( 'key', 'value' ) ); 285 | * 286 | * # --- 287 | * # - 288 | * # key: foo 289 | * # value: bar 290 | * ``` 291 | * 292 | * @access public 293 | * @category Output 294 | * 295 | * @param string $format Format to use: 'table', 'json', 'csv', 'yaml', 'ids', 'count' 296 | * @param array $items An array of items to output. 297 | * @param array|string $fields Named fields for each item of data. Can be array or comma-separated list. 298 | * @return null 299 | */ 300 | function format_items( $format, $items, $fields ) { 301 | $assoc_args = compact( 'format', 'fields' ); 302 | $formatter = new \WP_CLI\Formatter( $assoc_args ); 303 | $formatter->display_items( $items ); 304 | } 305 | 306 | /** 307 | * Write data as CSV to a given file. 308 | * 309 | * @access public 310 | * 311 | * @param resource $fd File descriptor 312 | * @param array $rows Array of rows to output 313 | * @param array $headers List of CSV columns (optional) 314 | */ 315 | function write_csv( $fd, $rows, $headers = array() ) { 316 | if ( ! empty( $headers ) ) { 317 | fputcsv( $fd, $headers ); 318 | } 319 | 320 | foreach ( $rows as $row ) { 321 | if ( ! empty( $headers ) ) { 322 | $row = pick_fields( $row, $headers ); 323 | } 324 | 325 | fputcsv( $fd, array_values( $row ) ); 326 | } 327 | } 328 | 329 | /** 330 | * Pick fields from an associative array or object. 331 | * 332 | * @param array|object Associative array or object to pick fields from 333 | * @param array List of fields to pick 334 | * @return array 335 | */ 336 | function pick_fields( $item, $fields ) { 337 | $values = array(); 338 | 339 | if ( is_object( $item ) ) { 340 | foreach ( $fields as $field ) { 341 | $values[ $field ] = isset( $item->$field ) ? $item->$field : null; 342 | } 343 | } else { 344 | foreach ( $fields as $field ) { 345 | $values[ $field ] = isset( $item[ $field ] ) ? $item[ $field ] : null; 346 | } 347 | } 348 | 349 | return $values; 350 | } 351 | 352 | /** 353 | * Launch system's $EDITOR for the user to edit some text. 354 | * 355 | * @access public 356 | * @category Input 357 | * 358 | * @param string $content Some form of text to edit (e.g. post content) 359 | * @param string $title Title to display in the editor. 360 | * @param string $ext Extension to use with the temp file. 361 | * @return string|bool Edited text, if file is saved from editor; false, if no change to file. 362 | */ 363 | function launch_editor_for_input( $input, $title = 'WP-CLI', $ext = 'tmp' ) { 364 | 365 | check_proc_available( 'launch_editor_for_input' ); 366 | 367 | $tmpdir = get_temp_dir(); 368 | 369 | do { 370 | $tmpfile = basename( $title ); 371 | $tmpfile = preg_replace( '|\.[^.]*$|', '', $tmpfile ); 372 | $tmpfile .= '-' . substr( md5( mt_rand() ), 0, 6 ); 373 | $tmpfile = $tmpdir . $tmpfile . '.' . $ext; 374 | $fp = fopen( $tmpfile, 'xb' ); 375 | if ( ! $fp && is_writable( $tmpdir ) && file_exists( $tmpfile ) ) { 376 | $tmpfile = ''; 377 | continue; 378 | } 379 | if ( $fp ) { 380 | fclose( $fp ); 381 | } 382 | } while ( ! $tmpfile ); 383 | 384 | if ( ! $tmpfile ) { 385 | \WP_CLI::error( 'Error creating temporary file.' ); 386 | } 387 | 388 | $output = ''; 389 | file_put_contents( $tmpfile, $input ); 390 | 391 | $editor = getenv( 'EDITOR' ); 392 | if ( ! $editor ) { 393 | $editor = is_windows() ? 'notepad' : 'vi'; 394 | } 395 | 396 | $descriptorspec = array( STDIN, STDOUT, STDERR ); 397 | $process = proc_open_compat( "$editor " . escapeshellarg( $tmpfile ), $descriptorspec, $pipes ); 398 | $r = proc_close( $process ); 399 | if ( $r ) { 400 | exit( $r ); 401 | } 402 | 403 | $output = file_get_contents( $tmpfile ); 404 | 405 | unlink( $tmpfile ); 406 | 407 | if ( $output === $input ) { 408 | return false; 409 | } 410 | 411 | return $output; 412 | } 413 | 414 | /** 415 | * @param string MySQL host string, as defined in wp-config.php 416 | * @return array 417 | */ 418 | function mysql_host_to_cli_args( $raw_host ) { 419 | $assoc_args = array(); 420 | 421 | $host_parts = explode( ':', $raw_host ); 422 | if ( count( $host_parts ) == 2 ) { 423 | list( $assoc_args['host'], $extra ) = $host_parts; 424 | $extra = trim( $extra ); 425 | if ( is_numeric( $extra ) ) { 426 | $assoc_args['port'] = (int) $extra; 427 | $assoc_args['protocol'] = 'tcp'; 428 | } elseif ( '' !== $extra ) { 429 | $assoc_args['socket'] = $extra; 430 | } 431 | } else { 432 | $assoc_args['host'] = $raw_host; 433 | } 434 | 435 | return $assoc_args; 436 | } 437 | 438 | function run_mysql_command( $cmd, $assoc_args, $descriptors = null ) { 439 | check_proc_available( 'run_mysql_command' ); 440 | 441 | if ( ! $descriptors ) { 442 | $descriptors = array( STDIN, STDOUT, STDERR ); 443 | } 444 | 445 | if ( isset( $assoc_args['host'] ) ) { 446 | //@codingStandardsIgnoreStart 447 | $assoc_args = array_merge( $assoc_args, mysql_host_to_cli_args( $assoc_args['host'] ) ); 448 | //@codingStandardsIgnoreEnd 449 | } 450 | 451 | $pass = $assoc_args['pass']; 452 | unset( $assoc_args['pass'] ); 453 | 454 | $old_pass = getenv( 'MYSQL_PWD' ); 455 | putenv( 'MYSQL_PWD=' . $pass ); 456 | 457 | $final_cmd = force_env_on_nix_systems( $cmd ) . assoc_args_to_str( $assoc_args ); 458 | 459 | $proc = proc_open_compat( $final_cmd, $descriptors, $pipes ); 460 | if ( ! $proc ) { 461 | exit( 1 ); 462 | } 463 | 464 | $r = proc_close( $proc ); 465 | 466 | putenv( 'MYSQL_PWD=' . $old_pass ); 467 | 468 | if ( $r ) { 469 | exit( $r ); 470 | } 471 | } 472 | 473 | /** 474 | * Render PHP or other types of files using Mustache templates. 475 | * 476 | * IMPORTANT: Automatic HTML escaping is disabled! 477 | */ 478 | function mustache_render( $template_name, $data = array() ) { 479 | if ( ! file_exists( $template_name ) ) { 480 | $template_name = WP_CLI_ROOT . "/templates/$template_name"; 481 | } 482 | 483 | $template = file_get_contents( $template_name ); 484 | 485 | $m = new \Mustache_Engine( 486 | array( 487 | 'escape' => function ( $val ) { 488 | return $val; }, 489 | ) 490 | ); 491 | 492 | return $m->render( $template, $data ); 493 | } 494 | 495 | /** 496 | * Create a progress bar to display percent completion of a given operation. 497 | * 498 | * Progress bar is written to STDOUT, and disabled when command is piped. Progress 499 | * advances with `$progress->tick()`, and completes with `$progress->finish()`. 500 | * Process bar also indicates elapsed time and expected total time. 501 | * 502 | * ``` 503 | * # `wp user generate` ticks progress bar each time a new user is created. 504 | * # 505 | * # $ wp user generate --count=500 506 | * # Generating users 22 % [=======> ] 0:05 / 0:23 507 | * 508 | * $progress = \WP_CLI\Utils\make_progress_bar( 'Generating users', $count ); 509 | * for ( $i = 0; $i < $count; $i++ ) { 510 | * // uses wp_insert_user() to insert the user 511 | * $progress->tick(); 512 | * } 513 | * $progress->finish(); 514 | * ``` 515 | * 516 | * @access public 517 | * @category Output 518 | * 519 | * @param string $message Text to display before the progress bar. 520 | * @param integer $count Total number of ticks to be performed. 521 | * @param int $interval Optional. The interval in milliseconds between updates. Default 100. 522 | * @return cli\progress\Bar|WP_CLI\NoOp 523 | */ 524 | function make_progress_bar( $message, $count, $interval = 100 ) { 525 | if ( \cli\Shell::isPiped() ) { 526 | return new \WP_CLI\NoOp; 527 | } 528 | 529 | return new \cli\progress\Bar( $message, $count, $interval ); 530 | } 531 | 532 | function parse_url( $url ) { 533 | $url_parts = \parse_url( $url ); 534 | 535 | if ( ! isset( $url_parts['scheme'] ) ) { 536 | $url_parts = parse_url( 'http://' . $url ); 537 | } 538 | 539 | return $url_parts; 540 | } 541 | 542 | /** 543 | * Check if we're running in a Windows environment (cmd.exe). 544 | * 545 | * @return bool 546 | */ 547 | function is_windows() { 548 | return false !== ( $test_is_windows = getenv( 'WP_CLI_TEST_IS_WINDOWS' ) ) ? (bool) $test_is_windows : strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN'; 549 | } 550 | 551 | /** 552 | * Replace magic constants in some PHP source code. 553 | * 554 | * @param string $source The PHP code to manipulate. 555 | * @param string $path The path to use instead of the magic constants 556 | */ 557 | function replace_path_consts( $source, $path ) { 558 | $replacements = array( 559 | '__FILE__' => "'$path'", 560 | '__DIR__' => "'" . dirname( $path ) . "'", 561 | ); 562 | 563 | $old = array_keys( $replacements ); 564 | $new = array_values( $replacements ); 565 | 566 | return str_replace( $old, $new, $source ); 567 | } 568 | 569 | /** 570 | * Make a HTTP request to a remote URL. 571 | * 572 | * Wraps the Requests HTTP library to ensure every request includes a cert. 573 | * 574 | * ``` 575 | * # `wp core download` verifies the hash for a downloaded WordPress archive 576 | * 577 | * $md5_response = Utils\http_request( 'GET', $download_url . '.md5' ); 578 | * if ( 20 != substr( $md5_response->status_code, 0, 2 ) ) { 579 | * WP_CLI::error( "Couldn't access md5 hash for release (HTTP code {$response->status_code})" ); 580 | * } 581 | * ``` 582 | * 583 | * @access public 584 | * 585 | * @param string $method HTTP method (GET, POST, DELETE, etc.) 586 | * @param string $url URL to make the HTTP request to. 587 | * @param array $headers Add specific headers to the request. 588 | * @param array $options 589 | * @return object 590 | */ 591 | function http_request( $method, $url, $data = null, $headers = array(), $options = array() ) { 592 | 593 | $cert_path = '/rmccue/requests/library/Requests/Transport/cacert.pem'; 594 | $halt_on_error = ! isset( $options['halt_on_error'] ) || (bool) $options['halt_on_error']; 595 | if ( inside_phar() ) { 596 | // cURL can't read Phar archives 597 | $options['verify'] = extract_from_phar( 598 | WP_CLI_VENDOR_DIR . $cert_path 599 | ); 600 | } else { 601 | foreach ( get_vendor_paths() as $vendor_path ) { 602 | if ( file_exists( $vendor_path . $cert_path ) ) { 603 | $options['verify'] = $vendor_path . $cert_path; 604 | break; 605 | } 606 | } 607 | if ( empty( $options['verify'] ) ) { 608 | $error_msg = 'Cannot find SSL certificate.'; 609 | if ( $halt_on_error ) { 610 | WP_CLI::error( $error_msg ); 611 | } 612 | throw new \RuntimeException( $error_msg ); 613 | } 614 | } 615 | 616 | try { 617 | return \Requests::request( $url, $headers, $data, $method, $options ); 618 | } catch ( \Requests_Exception $ex ) { 619 | // CURLE_SSL_CACERT_BADFILE only defined for PHP >= 7. 620 | if ( 'curlerror' !== $ex->getType() || ! in_array( curl_errno( $ex->getData() ), array( CURLE_SSL_CONNECT_ERROR, CURLE_SSL_CERTPROBLEM, 77 /*CURLE_SSL_CACERT_BADFILE*/ ), true ) ) { 621 | $error_msg = sprintf( "Failed to get url '%s': %s.", $url, $ex->getMessage() ); 622 | if ( $halt_on_error ) { 623 | WP_CLI::error( $error_msg ); 624 | } 625 | throw new \RuntimeException( $error_msg, null, $ex ); 626 | } 627 | // Handle SSL certificate issues gracefully 628 | \WP_CLI::warning( sprintf( "Re-trying without verify after failing to get verified url '%s' %s.", $url, $ex->getMessage() ) ); 629 | $options['verify'] = false; 630 | try { 631 | return \Requests::request( $url, $headers, $data, $method, $options ); 632 | } catch ( \Requests_Exception $ex ) { 633 | $error_msg = sprintf( "Failed to get non-verified url '%s' %s.", $url, $ex->getMessage() ); 634 | if ( $halt_on_error ) { 635 | WP_CLI::error( $error_msg ); 636 | } 637 | throw new \RuntimeException( $error_msg, null, $ex ); 638 | } 639 | } 640 | } 641 | 642 | /** 643 | * Increments a version string using the "x.y.z-pre" format 644 | * 645 | * Can increment the major, minor or patch number by one 646 | * If $new_version == "same" the version string is not changed 647 | * If $new_version is not a known keyword, it will be used as the new version string directly 648 | * 649 | * @param string $current_version 650 | * @param string $new_version 651 | * @return string 652 | */ 653 | function increment_version( $current_version, $new_version ) { 654 | // split version assuming the format is x.y.z-pre 655 | $current_version = explode( '-', $current_version, 2 ); 656 | $current_version[0] = explode( '.', $current_version[0] ); 657 | 658 | switch ( $new_version ) { 659 | case 'same': 660 | // do nothing 661 | break; 662 | 663 | case 'patch': 664 | $current_version[0][2]++; 665 | 666 | $current_version = array( $current_version[0] ); // drop possible pre-release info 667 | break; 668 | 669 | case 'minor': 670 | $current_version[0][1]++; 671 | $current_version[0][2] = 0; 672 | 673 | $current_version = array( $current_version[0] ); // drop possible pre-release info 674 | break; 675 | 676 | case 'major': 677 | $current_version[0][0]++; 678 | $current_version[0][1] = 0; 679 | $current_version[0][2] = 0; 680 | 681 | $current_version = array( $current_version[0] ); // drop possible pre-release info 682 | break; 683 | 684 | default: // not a keyword 685 | $current_version = array( array( $new_version ) ); 686 | break; 687 | } 688 | 689 | // reconstruct version string 690 | $current_version[0] = implode( '.', $current_version[0] ); 691 | $current_version = implode( '-', $current_version ); 692 | 693 | return $current_version; 694 | } 695 | 696 | /** 697 | * Compare two version strings to get the named semantic version. 698 | * 699 | * @access public 700 | * 701 | * @param string $new_version 702 | * @param string $original_version 703 | * @return string $name 'major', 'minor', 'patch' 704 | */ 705 | function get_named_sem_ver( $new_version, $original_version ) { 706 | 707 | if ( ! Comparator::greaterThan( $new_version, $original_version ) ) { 708 | return ''; 709 | } 710 | 711 | $parts = explode( '-', $original_version ); 712 | $bits = explode( '.', $parts[0] ); 713 | $major = $bits[0]; 714 | if ( isset( $bits[1] ) ) { 715 | $minor = $bits[1]; 716 | } 717 | if ( isset( $bits[2] ) ) { 718 | $patch = $bits[2]; 719 | } 720 | 721 | if ( ! is_null( $minor ) && Semver::satisfies( $new_version, "{$major}.{$minor}.x" ) ) { 722 | return 'patch'; 723 | } 724 | 725 | if ( Semver::satisfies( $new_version, "{$major}.x.x" ) ) { 726 | return 'minor'; 727 | } 728 | 729 | return 'major'; 730 | } 731 | 732 | /** 733 | * Return the flag value or, if it's not set, the $default value. 734 | * 735 | * Because flags can be negated (e.g. --no-quiet to negate --quiet), this 736 | * function provides a safer alternative to using 737 | * `isset( $assoc_args['quiet'] )` or similar. 738 | * 739 | * @access public 740 | * @category Input 741 | * 742 | * @param array $assoc_args Arguments array. 743 | * @param string $flag Flag to get the value. 744 | * @param mixed $default Default value for the flag. Default: NULL 745 | * @return mixed 746 | */ 747 | function get_flag_value( $assoc_args, $flag, $default = null ) { 748 | return isset( $assoc_args[ $flag ] ) ? $assoc_args[ $flag ] : $default; 749 | } 750 | 751 | /** 752 | * Get the home directory. 753 | * 754 | * @access public 755 | * @category System 756 | * 757 | * @return string 758 | */ 759 | function get_home_dir() { 760 | $home = getenv( 'HOME' ); 761 | if ( ! $home ) { 762 | // In Windows $HOME may not be defined 763 | $home = getenv( 'HOMEDRIVE' ) . getenv( 'HOMEPATH' ); 764 | } 765 | 766 | return rtrim( $home, '/\\' ); 767 | } 768 | 769 | /** 770 | * Appends a trailing slash. 771 | * 772 | * @access public 773 | * @category System 774 | * 775 | * @param string $string What to add the trailing slash to. 776 | * @return string String with trailing slash added. 777 | */ 778 | function trailingslashit( $string ) { 779 | return rtrim( $string, '/\\' ) . '/'; 780 | } 781 | 782 | /** 783 | * Normalize a filesystem path. 784 | * 785 | * On Windows systems, replaces backslashes with forward slashes 786 | * and forces upper-case drive letters. 787 | * Allows for two leading slashes for Windows network shares, but 788 | * ensures that all other duplicate slashes are reduced to a single one. 789 | * Ensures upper-case drive letters on Windows systems. 790 | * 791 | * @access public 792 | * @category System 793 | * 794 | * @param string $path Path to normalize. 795 | * @return string Normalized path. 796 | */ 797 | function normalize_path( $path ) { 798 | $path = str_replace( '\\', '/', $path ); 799 | $path = preg_replace( '|(?<=.)/+|', '/', $path ); 800 | if ( ':' === substr( $path, 1, 1 ) ) { 801 | $path = ucfirst( $path ); 802 | } 803 | return $path; 804 | } 805 | 806 | 807 | /** 808 | * Convert Windows EOLs to *nix. 809 | * 810 | * @param string $str String to convert. 811 | * @return string String with carriage return / newline pairs reduced to newlines. 812 | */ 813 | function normalize_eols( $str ) { 814 | return str_replace( "\r\n", "\n", $str ); 815 | } 816 | 817 | /** 818 | * Get the system's temp directory. Warns user if it isn't writable. 819 | * 820 | * @access public 821 | * @category System 822 | * 823 | * @return string 824 | */ 825 | function get_temp_dir() { 826 | static $temp = ''; 827 | 828 | if ( $temp ) { 829 | return $temp; 830 | } 831 | 832 | // `sys_get_temp_dir()` introduced PHP 5.2.1. Will always return something. 833 | $temp = trailingslashit( sys_get_temp_dir() ); 834 | 835 | if ( ! is_writable( $temp ) ) { 836 | \WP_CLI::warning( "Temp directory isn't writable: {$temp}" ); 837 | } 838 | 839 | return $temp; 840 | } 841 | 842 | /** 843 | * Parse a SSH url for its host, port, and path. 844 | * 845 | * Similar to parse_url(), but adds support for defined SSH aliases. 846 | * 847 | * ``` 848 | * host OR host/path/to/wordpress OR host:port/path/to/wordpress 849 | * ``` 850 | * 851 | * @access public 852 | * 853 | * @return mixed 854 | */ 855 | function parse_ssh_url( $url, $component = -1 ) { 856 | preg_match( '#^((docker|docker\-compose|ssh|vagrant):)?(([^@:]+)@)?([^:/~]+)(:([\d]*))?((/|~)(.+))?$#', $url, $matches ); 857 | $bits = array(); 858 | foreach ( array( 859 | 2 => 'scheme', 860 | 4 => 'user', 861 | 5 => 'host', 862 | 7 => 'port', 863 | 8 => 'path', 864 | ) as $i => $key ) { 865 | if ( ! empty( $matches[ $i ] ) ) { 866 | $bits[ $key ] = $matches[ $i ]; 867 | } 868 | } 869 | 870 | // Find the hostname from `vagrant ssh-config` automatically. 871 | if ( preg_match( '/^vagrant:?/', $url ) ) { 872 | if ( 'vagrant' === $bits['host'] && empty( $bits['scheme'] ) ) { 873 | $ssh_config = shell_exec( 'vagrant ssh-config 2>/dev/null' ); 874 | if ( preg_match( '/Host\s(.+)/', $ssh_config, $matches ) ) { 875 | $bits['scheme'] = 'vagrant'; 876 | $bits['host'] = $matches[1]; 877 | } 878 | } 879 | } 880 | 881 | switch ( $component ) { 882 | case PHP_URL_SCHEME: 883 | return isset( $bits['scheme'] ) ? $bits['scheme'] : null; 884 | case PHP_URL_USER: 885 | return isset( $bits['user'] ) ? $bits['user'] : null; 886 | case PHP_URL_HOST: 887 | return isset( $bits['host'] ) ? $bits['host'] : null; 888 | case PHP_URL_PATH: 889 | return isset( $bits['path'] ) ? $bits['path'] : null; 890 | case PHP_URL_PORT: 891 | return isset( $bits['port'] ) ? $bits['port'] : null; 892 | default: 893 | return $bits; 894 | } 895 | } 896 | 897 | /** 898 | * Report the results of the same operation against multiple resources. 899 | * 900 | * @access public 901 | * @category Input 902 | * 903 | * @param string $noun Resource being affected (e.g. plugin) 904 | * @param string $verb Type of action happening to the noun (e.g. activate) 905 | * @param integer $total Total number of resource being affected. 906 | * @param integer $successes Number of successful operations. 907 | * @param integer $failures Number of failures. 908 | * @param null|integer $skips Optional. Number of skipped operations. Default null (don't show skips). 909 | */ 910 | function report_batch_operation_results( $noun, $verb, $total, $successes, $failures, $skips = null ) { 911 | $plural_noun = $noun . 's'; 912 | $past_tense_verb = past_tense_verb( $verb ); 913 | $past_tense_verb_upper = ucfirst( $past_tense_verb ); 914 | if ( $failures ) { 915 | $failed_skipped_message = null === $skips ? '' : " ({$failures} failed" . ( $skips ? ", {$skips} skipped" : '' ) . ')'; 916 | if ( $successes ) { 917 | WP_CLI::error( "Only {$past_tense_verb} {$successes} of {$total} {$plural_noun}{$failed_skipped_message}." ); 918 | } else { 919 | WP_CLI::error( "No {$plural_noun} {$past_tense_verb}{$failed_skipped_message}." ); 920 | } 921 | } else { 922 | $skipped_message = $skips ? " ({$skips} skipped)" : ''; 923 | if ( $successes || $skips ) { 924 | WP_CLI::success( "{$past_tense_verb_upper} {$successes} of {$total} {$plural_noun}{$skipped_message}." ); 925 | } else { 926 | $message = $total > 1 ? ucfirst( $plural_noun ) : ucfirst( $noun ); 927 | WP_CLI::success( "{$message} already {$past_tense_verb}." ); 928 | } 929 | } 930 | } 931 | 932 | /** 933 | * Parse a string of command line arguments into an $argv-esqe variable. 934 | * 935 | * @access public 936 | * @category Input 937 | * 938 | * @param string $arguments 939 | * @return array 940 | */ 941 | function parse_str_to_argv( $arguments ) { 942 | preg_match_all( '/(?<=^|\s)([\'"]?)(.+?)(? 'create', 1147 | 'check' => 'check-update', 1148 | 'capability' => 'cap', 1149 | 'clear' => 'flush', 1150 | 'decrement' => 'decr', 1151 | 'del' => 'delete', 1152 | 'directory' => 'dir', 1153 | 'exec' => 'eval', 1154 | 'exec-file' => 'eval-file', 1155 | 'increment' => 'incr', 1156 | 'language' => 'locale', 1157 | 'lang' => 'locale', 1158 | 'new' => 'create', 1159 | 'number' => 'count', 1160 | 'remove' => 'delete', 1161 | 'regen' => 'regenerate', 1162 | 'rep' => 'replace', 1163 | 'repl' => 'replace', 1164 | 'trash' => 'delete', 1165 | 'v' => 'version', 1166 | ); 1167 | 1168 | if ( array_key_exists( $target, $suggestion_map ) && in_array( $suggestion_map[ $target ], $options, true ) ) { 1169 | return $suggestion_map[ $target ]; 1170 | } 1171 | 1172 | if ( empty( $options ) ) { 1173 | return ''; 1174 | } 1175 | foreach ( $options as $option ) { 1176 | $distance = levenshtein( $option, $target ); 1177 | $levenshtein[ $option ] = $distance; 1178 | } 1179 | 1180 | // Sort known command strings by distance to user entry. 1181 | asort( $levenshtein ); 1182 | 1183 | // Fetch the closest command string. 1184 | reset( $levenshtein ); 1185 | $suggestion = key( $levenshtein ); 1186 | 1187 | // Only return a suggestion if below a given threshold. 1188 | return $levenshtein[ $suggestion ] <= $threshold && $suggestion !== $target 1189 | ? (string) $suggestion 1190 | : ''; 1191 | } 1192 | 1193 | /** 1194 | * Get a Phar-safe version of a path. 1195 | * 1196 | * For paths inside a Phar, this strips the outer filesystem's location to 1197 | * reduce the path to what it needs to be within the Phar archive. 1198 | * 1199 | * Use the __FILE__ or __DIR__ constants as a starting point. 1200 | * 1201 | * @param string $path An absolute path that might be within a Phar. 1202 | * 1203 | * @return string A Phar-safe version of the path. 1204 | */ 1205 | function phar_safe_path( $path ) { 1206 | 1207 | if ( ! inside_phar() ) { 1208 | return $path; 1209 | } 1210 | 1211 | return str_replace( 1212 | PHAR_STREAM_PREFIX . WP_CLI_PHAR_PATH . '/', 1213 | PHAR_STREAM_PREFIX, 1214 | $path 1215 | ); 1216 | } 1217 | 1218 | /** 1219 | * Check whether a given Command object is part of the bundled set of 1220 | * commands. 1221 | * 1222 | * This function accepts both a fully qualified class name as a string as 1223 | * well as an object that extends `WP_CLI\Dispatcher\CompositeCommand`. 1224 | * 1225 | * @param \WP_CLI\Dispatcher\CompositeCommand|string $command 1226 | * 1227 | * @return bool 1228 | */ 1229 | function is_bundled_command( $command ) { 1230 | static $classes; 1231 | 1232 | if ( null === $classes ) { 1233 | $classes = array(); 1234 | // TODO: This needs to be rebuilt. 1235 | // $class_map = WP_CLI_VENDOR_DIR . '/composer/autoload_commands_classmap.php'; 1236 | // if ( file_exists( WP_CLI_VENDOR_DIR . '/composer/' ) ) { 1237 | // $classes = include $class_map; 1238 | // } 1239 | $classes = array( 'CLI_Command' => true ); 1240 | } 1241 | 1242 | if ( is_object( $command ) ) { 1243 | $command = get_class( $command ); 1244 | } 1245 | 1246 | return is_string( $command ) 1247 | ? array_key_exists( $command, $classes ) 1248 | : false; 1249 | } 1250 | 1251 | /** 1252 | * Maybe prefix command string with "/usr/bin/env". 1253 | * Removes (if there) if Windows, adds (if not there) if not. 1254 | * 1255 | * @param string $command 1256 | * 1257 | * @return string 1258 | */ 1259 | function force_env_on_nix_systems( $command ) { 1260 | $env_prefix = '/usr/bin/env '; 1261 | $env_prefix_len = strlen( $env_prefix ); 1262 | if ( is_windows() ) { 1263 | if ( 0 === strncmp( $command, $env_prefix, $env_prefix_len ) ) { 1264 | $command = substr( $command, $env_prefix_len ); 1265 | } 1266 | } else { 1267 | if ( 0 !== strncmp( $command, $env_prefix, $env_prefix_len ) ) { 1268 | $command = $env_prefix . $command; 1269 | } 1270 | } 1271 | return $command; 1272 | } 1273 | 1274 | /** 1275 | * Check that `proc_open()` and `proc_close()` haven't been disabled. 1276 | * 1277 | * @param string $context Optional. If set will appear in error message. Default null. 1278 | * @param bool $return Optional. If set will return false rather than error out. Default false. 1279 | * 1280 | * @return bool 1281 | */ 1282 | function check_proc_available( $context = null, $return = false ) { 1283 | if ( ! function_exists( 'proc_open' ) || ! function_exists( 'proc_close' ) ) { 1284 | if ( $return ) { 1285 | return false; 1286 | } 1287 | $msg = 'The PHP functions `proc_open()` and/or `proc_close()` are disabled. Please check your PHP ini directive `disable_functions` or suhosin settings.'; 1288 | if ( $context ) { 1289 | WP_CLI::error( sprintf( "Cannot do '%s': %s", $context, $msg ) ); 1290 | } else { 1291 | WP_CLI::error( $msg ); 1292 | } 1293 | } 1294 | return true; 1295 | } 1296 | 1297 | /** 1298 | * Returns past tense of verb, with limited accuracy. Only regular verbs catered for, apart from "reset". 1299 | * 1300 | * @param string $verb Verb to return past tense of. 1301 | * 1302 | * @return string 1303 | */ 1304 | function past_tense_verb( $verb ) { 1305 | static $irregular = array( 1306 | 'reset' => 'reset', 1307 | ); 1308 | if ( isset( $irregular[ $verb ] ) ) { 1309 | return $irregular[ $verb ]; 1310 | } 1311 | $last = substr( $verb, -1 ); 1312 | if ( 'e' === $last ) { 1313 | $verb = substr( $verb, 0, -1 ); 1314 | } elseif ( 'y' === $last && ! preg_match( '/[aeiou]y$/', $verb ) ) { 1315 | $verb = substr( $verb, 0, -1 ) . 'i'; 1316 | } elseif ( preg_match( '/^[^aeiou]*[aeiou][^aeiouhwxy]$/', $verb ) ) { 1317 | // Rule of thumb that most (all?) one-voweled regular verbs ending in vowel + consonant (excluding "h", "w", "x", "y") double their final consonant - misses many cases (eg "submit"). 1318 | $verb .= $last; 1319 | } 1320 | return $verb . 'ed'; 1321 | } 1322 | 1323 | /** 1324 | * Get the path to the PHP binary used when executing WP-CLI. 1325 | * 1326 | * Environment values permit specific binaries to be indicated. 1327 | * 1328 | * @access public 1329 | * @category System 1330 | * 1331 | * @return string 1332 | */ 1333 | function get_php_binary() { 1334 | if ( $wp_cli_php_used = getenv( 'WP_CLI_PHP_USED' ) ) { 1335 | return $wp_cli_php_used; 1336 | } 1337 | 1338 | if ( $wp_cli_php = getenv( 'WP_CLI_PHP' ) ) { 1339 | return $wp_cli_php; 1340 | } 1341 | 1342 | // Available since PHP 5.4. 1343 | if ( defined( 'PHP_BINARY' ) ) { 1344 | // @codingStandardsIgnoreLine 1345 | return PHP_BINARY; 1346 | } 1347 | 1348 | // @codingStandardsIgnoreLine 1349 | if ( @is_executable( PHP_BINDIR . '/php' ) ) { 1350 | return PHP_BINDIR . '/php'; 1351 | } 1352 | 1353 | // @codingStandardsIgnoreLine 1354 | if ( is_windows() && @is_executable( PHP_BINDIR . '/php.exe' ) ) { 1355 | return PHP_BINDIR . '/php.exe'; 1356 | } 1357 | 1358 | return 'php'; 1359 | } 1360 | 1361 | /** 1362 | * Windows compatible `proc_open()`. 1363 | * Works around bug in PHP, and also deals with *nix-like `ENV_VAR=blah cmd` environment variable prefixes. 1364 | * 1365 | * @access public 1366 | * 1367 | * @param string $command Command to execute. 1368 | * @param array $descriptorspec Indexed array of descriptor numbers and their values. 1369 | * @param array &$pipes Indexed array of file pointers that correspond to PHP's end of any pipes that are created. 1370 | * @param string $cwd Initial working directory for the command. 1371 | * @param array $env Array of environment variables. 1372 | * @param array $other_options Array of additional options (Windows only). 1373 | * 1374 | * @return string Command stripped of any environment variable settings. 1375 | */ 1376 | function proc_open_compat( $cmd, $descriptorspec, &$pipes, $cwd = null, $env = null, $other_options = null ) { 1377 | if ( is_windows() ) { 1378 | // Need to encompass the whole command in double quotes - PHP bug https://bugs.php.net/bug.php?id=49139 1379 | $cmd = '"' . _proc_open_compat_win_env( $cmd, $env ) . '"'; 1380 | } 1381 | return proc_open( $cmd, $descriptorspec, $pipes, $cwd, $env, $other_options ); 1382 | } 1383 | 1384 | /** 1385 | * For use by `proc_open_compat()` only. Separated out for ease of testing. Windows only. 1386 | * Turns *nix-like `ENV_VAR=blah command` environment variable prefixes into stripped `cmd` with prefixed environment variables added to passed in environment array. 1387 | * 1388 | * @access private 1389 | * 1390 | * @param string $command Command to execute. 1391 | * @param array &$env Array of existing environment variables. Will be modified if any settings in command. 1392 | * 1393 | * @return string Command stripped of any environment variable settings. 1394 | */ 1395 | function _proc_open_compat_win_env( $cmd, &$env ) { 1396 | if ( false !== strpos( $cmd, '=' ) ) { 1397 | while ( preg_match( '/^([A-Za-z_][A-Za-z0-9_]*)=("[^"]*"|[^ ]*) /', $cmd, $matches ) ) { 1398 | $cmd = substr( $cmd, strlen( $matches[0] ) ); 1399 | if ( null === $env ) { 1400 | $env = array(); 1401 | } 1402 | $env[ $matches[1] ] = isset( $matches[2][0] ) && '"' === $matches[2][0] ? substr( $matches[2], 1, -1 ) : $matches[2]; 1403 | } 1404 | } 1405 | return $cmd; 1406 | } 1407 | 1408 | /** 1409 | * First half of escaping for LIKE special characters % and _ before preparing for MySQL. 1410 | * 1411 | * Use this only before wpdb::prepare() or esc_sql(). Reversing the order is very bad for security. 1412 | * 1413 | * Copied from core "wp-includes/wp-db.php". Avoids dependency on WP 4.4 wpdb. 1414 | * 1415 | * @access public 1416 | * 1417 | * @param string $text The raw text to be escaped. The input typed by the user should have no 1418 | * extra or deleted slashes. 1419 | * @return string Text in the form of a LIKE phrase. The output is not SQL safe. Call $wpdb::prepare() 1420 | * or real_escape next. 1421 | */ 1422 | function esc_like( $text ) { 1423 | return addcslashes( $text, '_%\\' ); 1424 | } 1425 | 1426 | /** 1427 | * Escapes (backticks) MySQL identifiers (aka schema object names) - i.e. column names, table names, and database/index/alias/view etc names. 1428 | * See https://dev.mysql.com/doc/refman/5.5/en/identifiers.html 1429 | * 1430 | * @param string|array $idents A single identifier or an array of identifiers. 1431 | * @return string|array An escaped string if given a string, or an array of escaped strings if given an array of strings. 1432 | */ 1433 | function esc_sql_ident( $idents ) { 1434 | $backtick = function ( $v ) { 1435 | // Escape any backticks in the identifier by doubling. 1436 | return '`' . str_replace( '`', '``', $v ) . '`'; 1437 | }; 1438 | if ( is_string( $idents ) ) { 1439 | return $backtick( $idents ); 1440 | } 1441 | return array_map( $backtick, $idents ); 1442 | } 1443 | 1444 | /** 1445 | * Check whether a given string is a valid JSON representation. 1446 | * 1447 | * @param string $argument String to evaluate. 1448 | * @param bool $ignore_scalars Optional. Whether to ignore scalar values. 1449 | * Defaults to true. 1450 | * 1451 | * @return bool Whether the provided string is a valid JSON representation. 1452 | */ 1453 | function is_json( $argument, $ignore_scalars = true ) { 1454 | if ( ! is_string( $argument ) || '' === $argument ) { 1455 | return false; 1456 | } 1457 | 1458 | if ( $ignore_scalars && ! in_array( $argument[0], array( '{', '[' ), true ) ) { 1459 | return false; 1460 | } 1461 | 1462 | json_decode( $argument, $assoc = true ); 1463 | 1464 | return json_last_error() === JSON_ERROR_NONE; 1465 | } 1466 | 1467 | /** 1468 | * Parse known shell arrays included in the $assoc_args array. 1469 | * 1470 | * @param array $assoc_args Associative array of arguments. 1471 | * @param array $array_arguments Array of argument keys that should receive an 1472 | * array through the shell. 1473 | * 1474 | * @return array 1475 | */ 1476 | function parse_shell_arrays( $assoc_args, $array_arguments ) { 1477 | if ( empty( $assoc_args ) || empty( $array_arguments ) ) { 1478 | return $assoc_args; 1479 | } 1480 | 1481 | foreach ( $array_arguments as $key ) { 1482 | if ( array_key_exists( $key, $assoc_args ) && is_json( $assoc_args[ $key ] ) ) { 1483 | $assoc_args[ $key ] = json_decode( $assoc_args[ $key ], $assoc = true ); 1484 | } 1485 | } 1486 | 1487 | return $assoc_args; 1488 | } 1489 | 1490 | /** 1491 | * Describe a callable as a string. 1492 | * 1493 | * @param callable $callable The callable to describe. 1494 | * 1495 | * @return string String description of the callable. 1496 | */ 1497 | function describe_callable( $callable ) { 1498 | try { 1499 | if ( $callable instanceof \Closure ) { 1500 | $reflection = new \ReflectionFunction( $callable ); 1501 | 1502 | return (string) "Closure in file {$reflection->getFileName()} at line {$reflection->getStartLine()}"; 1503 | } 1504 | 1505 | if ( is_array( $callable ) ) { 1506 | if ( is_object( $callable[0] ) ) { 1507 | return (string) sprintf( 1508 | '%s->%s()', 1509 | get_class( $callable[0] ), 1510 | $callable[1] 1511 | ); 1512 | } 1513 | 1514 | return (string) sprintf( '%s::%s()', $callable[0], $callable[1] ); 1515 | } 1516 | 1517 | return (string) gettype( $callable ); 1518 | } catch ( \Exception $exception ) { 1519 | return 'Callable of unknown type'; 1520 | } 1521 | } 1522 | 1523 | /** 1524 | * Pluralizes a noun in a grammatically correct way. 1525 | * 1526 | * @param string $noun Noun to be pluralized. Needs to be in singular form. 1527 | * @param int|null $count Optional. Count of the nouns, to decide whether to 1528 | * pluralize. Will pluralize unconditionally if none 1529 | * provided. 1530 | * 1531 | * @return string Pluralized noun. 1532 | */ 1533 | function pluralize( $noun, $count = null ) { 1534 | if ( 1 === $count ) { 1535 | return $noun; 1536 | } 1537 | 1538 | return Inflector::pluralize( $noun ); 1539 | } 1540 | -------------------------------------------------------------------------------- /features/extra/no-mail.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavin-chua/drone-command/70670eda3598b2e5e473b6c5743fccaee02c49a4/features/extra/no-mail.php -------------------------------------------------------------------------------- /features/load-wp-cli.feature: -------------------------------------------------------------------------------- 1 | Feature: Test that WP-CLI loads. 2 | 3 | Scenario: WP-CLI loads for your tests 4 | Given a WP install 5 | 6 | When I run `wp eval 'echo "Hello world.";'` 7 | Then STDOUT should contain: 8 | """ 9 | Hello world. 10 | """ 11 | -------------------------------------------------------------------------------- /features/steps/given.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavin-chua/drone-command/70670eda3598b2e5e473b6c5743fccaee02c49a4/features/steps/given.php -------------------------------------------------------------------------------- /features/steps/then.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavin-chua/drone-command/70670eda3598b2e5e473b6c5743fccaee02c49a4/features/steps/then.php -------------------------------------------------------------------------------- /features/steps/when.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavin-chua/drone-command/70670eda3598b2e5e473b6c5743fccaee02c49a4/features/steps/when.php -------------------------------------------------------------------------------- /utils/behat-tags.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavin-chua/drone-command/70670eda3598b2e5e473b6c5743fccaee02c49a4/utils/behat-tags.php -------------------------------------------------------------------------------- /wp-cli.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - command.php --------------------------------------------------------------------------------