diff --git a/features/option-pluck-patch.feature b/features/option-pluck-patch.feature new file mode 100644 index 000000000..fa790e406 --- /dev/null +++ b/features/option-pluck-patch.feature @@ -0,0 +1,271 @@ +Feature: Option commands have pluck and patch. + + @pluck + Scenario: Nested values can be retrieved. + Given a WP install + And an input.json file: + """ + { + "foo": "bar" + } + """ + And I run `wp option update option_name --format=json < input.json` + + When I run `wp option pluck option_name foo` + Then STDOUT should be: + """ + bar + """ + + @pluck @pluck-deep + Scenario: A nested value can be retrieved at any depth. + Given a WP install + And an input.json file: + """ + { + "foo": { + "bar": { + "baz": "some value" + } + }, + "foo.com": { + "visitors": 999 + } + } + """ + And I run `wp option update option_name --format=json < input.json` + + When I run `wp option pluck option_name foo bar baz` + Then STDOUT should be: + """ + some value + """ + + When I run `wp option pluck option_name foo.com visitors` + Then STDOUT should be: + """ + 999 + """ + + @pluck @pluck-fail + Scenario: Attempting to pluck a non-existent nested value fails. + Given a WP install + And I run `wp option update option_name '{ "key": "value" }' --format=json` + + When I run `wp option pluck option_name key` + Then STDOUT should be: + """ + value + """ + + When I try `wp option pluck option_name foo` + Then STDOUT should be empty + And the return code should be 1 + + @pluck @pluck-fail + Scenario: Attempting to pluck from a primitive value fails. + Given a WP install + And I run `wp option update option_name simple-value` + + When I try `wp option pluck option_name foo` + Then STDOUT should be empty + And the return code should be 1 + + @pluck @pluck-numeric + Scenario: A nested value can be retrieved from an integer key. + Given a WP install + And I run `wp option update option_name '[ "foo", "bar" ]' --format=json` + + When I run `wp option pluck option_name 0` + Then STDOUT should be: + """ + foo + """ + + @patch @patch-update @patch-arg + Scenario: Nested values can be changed. + Given a WP install + And an input.json file: + """ + { + "foo": "bar" + } + """ + And I run `wp option update option_name --format=json < input.json` + + When I run `wp option patch update option_name foo baz` + Then STDOUT should be: + """ + Success: Updated 'option_name' option. + """ + + When I run `wp option get option_name --format=json` + Then STDOUT should be JSON containing: + """ + { + "foo": "baz" + } + """ + + @patch @patch-update @patch-stdin + Scenario: Nested values can be set with a value from STDIN. + Given a WP install + And an input.json file: + """ + { + "foo": { + "bar": "baz" + }, + "bar": "bad" + } + """ + And a patch file: + """ + new value + """ + And I run `wp option update option_name --format=json < input.json` + + When I run `wp option patch update option_name foo bar < patch` + Then STDOUT should be: + """ + Success: Updated 'option_name' option. + """ + + When I run `wp option get option_name --format=json` + Then STDOUT should be JSON containing: + """ + { + "foo": { + "bar": "new value" + }, + "bar": "bad" + } + """ + + @patch @patch-update @patch-fail + Scenario: Attempting to update a nested value fails if a parent's key does not exist. + Given a WP install + And an input.json file: + """ + { + "foo": { + "bar": "baz" + }, + "bar": "bad" + } + """ + And I run `wp option update option_name --format=json < input.json` + + When I try `wp option patch update option_name foo not-a-key new-value` + Then STDOUT should be empty + And STDERR should contain: + """ + No data exists for key "not-a-key" + """ + And the return code should be 1 + + @patch @patch-delete + Scenario: A key can be deleted from a nested value. + Given a WP install + And an input.json file: + """ + { + "foo": { + "bar": "baz", + "abe": "lincoln" + } + } + """ + And I run `wp option update option_name --format=json < input.json` + + When I run `wp option patch delete option_name foo bar` + Then STDOUT should be: + """ + Success: Updated 'option_name' option. + """ + + When I run `wp option get option_name --format=json` + Then STDOUT should be JSON containing: + """ + { + "foo": { + "abe": "lincoln" + } + } + """ + + @patch @patch-fail @patch-delete @patch-delete-fail + Scenario: A key cannot be deleted from a nested value from a non-existent key. + Given a WP install + And an input.json file: + """ + { + "foo": { + "bar": "baz" + } + } + """ + And I run `wp option update option_name --format=json < input.json` + + When I try `wp option patch delete option_name foo not-a-key` + Then STDOUT should be empty + And STDERR should contain: + """ + No data exists for key "not-a-key" + """ + And the return code should be 1 + + @patch @patch-insert + Scenario: A new key can be inserted into a nested value. + Given a WP install + And I run `wp option update option_name '{}' --format=json` + + When I run `wp option patch insert option_name foo bar` + Then STDOUT should be: + """ + Success: Updated 'option_name' option. + """ + + When I run `wp option get option_name --format=json` + Then STDOUT should be JSON containing: + """ + { + "foo": "bar" + } + """ + + @patch @patch-fail @patch-insert @patch-insert-fail + Scenario: A new key cannot be inserted into a non-nested value. + Given a WP install + And I run `wp option update option_name 'a simple value'` + + When I try `wp option patch insert option_name foo bar` + Then STDOUT should be empty + And STDERR should contain: + """ + Cannot create key "foo" + """ + And the return code should be 1 + + When I run `wp option get option_name` + Then STDOUT should be: + """ + a simple value + """ + + @patch @patch-numeric + Scenario: A nested value can be updated using an integer key. + Given a WP install + And I run `wp option update option_name '[ "foo", "bar" ]' --format=json` + + When I run `wp option patch update option_name 0 new` + Then STDOUT should be: + """ + Success: Updated 'option_name' option. + """ + + When I run `wp option get option_name --format=json` + Then STDOUT should be JSON containing: + """ + [ "new", "bar" ] + """ diff --git a/features/site-option-pluck-patch.feature b/features/site-option-pluck-patch.feature new file mode 100644 index 000000000..c4bd3d56e --- /dev/null +++ b/features/site-option-pluck-patch.feature @@ -0,0 +1,271 @@ +Feature: Site option commands have pluck and patch. + + @pluck + Scenario: Nested values can be retrieved. + Given a WP multisite install + And an input.json file: + """ + { + "foo": "bar" + } + """ + And I run `wp site option update option_name --format=json < input.json` + + When I run `wp site option pluck option_name foo` + Then STDOUT should be: + """ + bar + """ + + @pluck @pluck-deep + Scenario: A nested value can be retrieved at any depth. + Given a WP multisite install + And an input.json file: + """ + { + "foo": { + "bar": { + "baz": "some value" + } + }, + "foo.com": { + "visitors": 999 + } + } + """ + And I run `wp site option update option_name --format=json < input.json` + + When I run `wp site option pluck option_name foo bar baz` + Then STDOUT should be: + """ + some value + """ + + When I run `wp site option pluck option_name foo.com visitors` + Then STDOUT should be: + """ + 999 + """ + + @pluck @pluck-fail + Scenario: Attempting to pluck a non-existent nested value fails. + Given a WP multisite install + And I run `wp site option update option_name '{ "key": "value" }' --format=json` + + When I run `wp site option pluck option_name key` + Then STDOUT should be: + """ + value + """ + + When I try `wp site option pluck option_name foo` + Then STDOUT should be empty + And the return code should be 1 + + @pluck @pluck-fail + Scenario: Attempting to pluck from a primitive value fails. + Given a WP multisite install + And I run `wp site option update option_name simple-value` + + When I try `wp site option pluck option_name foo` + Then STDOUT should be empty + And the return code should be 1 + + @pluck @pluck-numeric + Scenario: A nested value can be retrieved from an integer key. + Given a WP multisite install + And I run `wp site option update option_name '[ "foo", "bar" ]' --format=json` + + When I run `wp site option pluck option_name 0` + Then STDOUT should be: + """ + foo + """ + + @patch @patch-update @patch-arg + Scenario: Nested values can be changed. + Given a WP multisite install + And an input.json file: + """ + { + "foo": "bar" + } + """ + And I run `wp site option update option_name --format=json < input.json` + + When I run `wp site option patch update option_name foo baz` + Then STDOUT should be: + """ + Success: Updated 'option_name' site option. + """ + + When I run `wp site option get option_name --format=json` + Then STDOUT should be JSON containing: + """ + { + "foo": "baz" + } + """ + + @patch @patch-update @patch-stdin + Scenario: Nested values can be set with a value from STDIN. + Given a WP multisite install + And an input.json file: + """ + { + "foo": { + "bar": "baz" + }, + "bar": "bad" + } + """ + And a patch file: + """ + new value + """ + And I run `wp site option update option_name --format=json < input.json` + + When I run `wp site option patch update option_name foo bar < patch` + Then STDOUT should be: + """ + Success: Updated 'option_name' site option. + """ + + When I run `wp site option get option_name --format=json` + Then STDOUT should be JSON containing: + """ + { + "foo": { + "bar": "new value" + }, + "bar": "bad" + } + """ + + @patch @patch-update @patch-fail + Scenario: Attempting to update a nested value fails if a parent's key does not exist. + Given a WP multisite install + And an input.json file: + """ + { + "foo": { + "bar": "baz" + }, + "bar": "bad" + } + """ + And I run `wp site option update option_name --format=json < input.json` + + When I try `wp site option patch update option_name foo not-a-key new-value` + Then STDOUT should be empty + And STDERR should contain: + """ + No data exists for key "not-a-key" + """ + And the return code should be 1 + + @patch @patch-delete + Scenario: A key can be deleted from a nested value. + Given a WP multisite install + And an input.json file: + """ + { + "foo": { + "bar": "baz", + "abe": "lincoln" + } + } + """ + And I run `wp site option update option_name --format=json < input.json` + + When I run `wp site option patch delete option_name foo bar` + Then STDOUT should be: + """ + Success: Updated 'option_name' site option. + """ + + When I run `wp site option get option_name --format=json` + Then STDOUT should be JSON containing: + """ + { + "foo": { + "abe": "lincoln" + } + } + """ + + @patch @patch-fail @patch-delete @patch-delete-fail + Scenario: A key cannot be deleted from a nested value from a non-existent key. + Given a WP multisite install + And an input.json file: + """ + { + "foo": { + "bar": "baz" + } + } + """ + And I run `wp site option update option_name --format=json < input.json` + + When I try `wp site option patch delete option_name foo not-a-key` + Then STDOUT should be empty + And STDERR should contain: + """ + No data exists for key "not-a-key" + """ + And the return code should be 1 + + @patch @patch-insert + Scenario: A new key can be inserted into a nested value. + Given a WP multisite install + And I run `wp site option update option_name '{}' --format=json` + + When I run `wp site option patch insert option_name foo bar` + Then STDOUT should be: + """ + Success: Updated 'option_name' site option. + """ + + When I run `wp site option get option_name --format=json` + Then STDOUT should be JSON containing: + """ + { + "foo": "bar" + } + """ + + @patch @patch-fail @patch-insert @patch-insert-fail + Scenario: A new key cannot be inserted into a non-nested value. + Given a WP multisite install + And I run `wp site option update option_name 'a simple value'` + + When I try `wp site option patch insert option_name foo bar` + Then STDOUT should be empty + And STDERR should contain: + """ + Cannot create key "foo" + """ + And the return code should be 1 + + When I run `wp site option get option_name` + Then STDOUT should be: + """ + a simple value + """ + + @patch @patch-numeric + Scenario: A nested value can be updated using an integer key. + Given a WP multisite install + And I run `wp site option update option_name '[ "foo", "bar" ]' --format=json` + + When I run `wp site option patch update option_name 0 new` + Then STDOUT should be: + """ + Success: Updated 'option_name' site option. + """ + + When I run `wp site option get option_name --format=json` + Then STDOUT should be JSON containing: + """ + [ "new", "bar" ] + """ diff --git a/src/Option_Command.php b/src/Option_Command.php index d9b1abe48..0ea109eaf 100644 --- a/src/Option_Command.php +++ b/src/Option_Command.php @@ -1,5 +1,6 @@ + * : The option name. + * + * ... + * : The name(s) of the keys within the value to locate the value to pluck. + * + * [--format=] + * : The output format of the value. + * --- + * default: plaintext + * options: + * - plaintext + * - json + * - yaml + * --- + */ + public function pluck( $args, $assoc_args ) { + list( $key ) = $args; + + $value = get_option( $key ); + + if ( false === $value ) { + WP_CLI::halt( 1 ); + } + + $key_path = array_map( function( $key ) { + if ( is_numeric( $key ) && ( $key === (string) intval( $key ) ) ) { + return (int) $key; + } + return $key; + }, array_slice( $args, 1 ) ); + + $traverser = new RecursiveDataStructureTraverser( $value ); + + try { + $value = $traverser->get( $key_path ); + } catch ( \Exception $e ) { + die( 1 ); + } + + WP_CLI::print_value( $value, $assoc_args ); + } + + /** + * Update a nested value in an option. + * + * ## OPTIONS + * + * + * : Patch action to perform. + * --- + * options: + * - insert + * - update + * - delete + * --- + * + * + * : The option name. + * + * ... + * : The name(s) of the keys within the value to locate the value to patch. + * + * [] + * : The new value. If omitted, the value is read from STDIN. + * + * [--format=] + * : The serialization format for the value. + * --- + * default: plaintext + * options: + * - plaintext + * - json + * --- + */ + public function patch( $args, $assoc_args ) { + list( $action, $key ) = $args; + $key_path = array_map( function( $key ) { + if ( is_numeric( $key ) && ( $key === (string) intval( $key ) ) ) { + return (int) $key; + } + return $key; + }, array_slice( $args, 2 ) ); + + if ( 'delete' == $action ) { + $patch_value = null; + } elseif ( \WP_CLI\Entity\Utils::has_stdin() ) { + $stdin_value = WP_CLI::get_value_from_arg_or_stdin( $args, -1 ); + $patch_value = WP_CLI::read_value( trim( $stdin_value ), $assoc_args ); + } else { + // Take the patch value as the last positional argument. Mutates $key_path to be 1 element shorter! + $patch_value = WP_CLI::read_value( array_pop( $key_path ), $assoc_args ); + } + + /* Need to make a copy of $current_value here as it is modified by reference */ + $old_value = $current_value = sanitize_option( $key, get_option( $key ) ); + + $traverser = new RecursiveDataStructureTraverser( $current_value ); + + try { + $traverser->$action( $key_path, $patch_value ); + } catch ( \Exception $e ) { + WP_CLI::error( $e->getMessage() ); + } + + $patched_value = sanitize_option( $key, $traverser->value() ); + + if ( $patched_value === $old_value ) { + WP_CLI::success( "Value passed for '$key' option is unchanged." ); + } else { + if ( update_option( $key, $patched_value ) ) { + WP_CLI::success( "Updated '$key' option." ); + } else { + WP_CLI::error( "Could not update option '$key'." ); + } + } + } + private static function esc_like( $old ) { global $wpdb; diff --git a/src/Site_Option_Command.php b/src/Site_Option_Command.php index 5c3476073..c5f985f5c 100644 --- a/src/Site_Option_Command.php +++ b/src/Site_Option_Command.php @@ -1,5 +1,6 @@ + * : The option name. + * + * ... + * : The name(s) of the keys within the value to locate the value to pluck. + * + * [--format=] + * : The output format of the value. + * --- + * default: plaintext + * options: + * - plaintext + * - json + * - yaml + */ + public function pluck( $args, $assoc_args ) { + list( $key ) = $args; + + $value = get_site_option( $key ); + + if ( false === $value ) { + WP_CLI::halt( 1 ); + } + + $key_path = array_map( function( $key ) { + if ( is_numeric( $key ) && ( $key === (string) intval( $key ) ) ) { + return (int) $key; + } + return $key; + }, array_slice( $args, 1 ) ); + + $traverser = new RecursiveDataStructureTraverser( $value ); + + try { + $value = $traverser->get( $key_path ); + } catch ( \Exception $e ) { + die( 1 ); + } + + WP_CLI::print_value( $value, $assoc_args ); + } + + /** + * Update a nested value in an option. + * + * ## OPTIONS + * + * + * : Patch action to perform. + * --- + * options: + * - insert + * - update + * - delete + * --- + * + * + * : The option name. + * + * ... + * : The name(s) of the keys within the value to locate the value to patch. + * + * [] + * : The new value. If omitted, the value is read from STDIN. + * + * [--format=] + * : The serialization format for the value. + * --- + * default: plaintext + * options: + * - plaintext + * - json + * --- + */ + public function patch( $args, $assoc_args ) { + list( $action, $key ) = $args; + $key_path = array_map( function( $key ) { + if ( is_numeric( $key ) && ( $key === (string) intval( $key ) ) ) { + return (int) $key; + } + return $key; + }, array_slice( $args, 2 ) ); + + if ( 'delete' == $action ) { + $patch_value = null; + } elseif ( \WP_CLI\Entity\Utils::has_stdin() ) { + $stdin_value = WP_CLI::get_value_from_arg_or_stdin( $args, -1 ); + $patch_value = WP_CLI::read_value( trim( $stdin_value ), $assoc_args ); + } else { + // Take the patch value as the last positional argument. Mutates $key_path to be 1 element shorter! + $patch_value = WP_CLI::read_value( array_pop( $key_path ), $assoc_args ); + } + + /* Need to make a copy of $current_value here as it is modified by reference */ + $old_value = $current_value = sanitize_option( $key, get_site_option( $key ) ); + + $traverser = new RecursiveDataStructureTraverser( $current_value ); + + try { + $traverser->$action( $key_path, $patch_value ); + } catch ( \Exception $e ) { + WP_CLI::error( $e->getMessage() ); + } + + $patched_value = sanitize_option( $key, $traverser->value() ); + + if ( $patched_value === $old_value ) { + WP_CLI::success( "Value passed for '$key' site option is unchanged." ); + } else { + if ( update_site_option( $key, $patched_value ) ) { + WP_CLI::success( "Updated '$key' site option." ); + } else { + WP_CLI::error( "Could not update site option '$key'." ); + } + } + } + private static function esc_like( $old ) { global $wpdb;