From b1a696ea7a464a1291b27fb718d202be01156a56 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 29 Apr 2026 20:48:51 +0200 Subject: [PATCH 1/3] Add dedicated URL replacement behavior --- features/search-replace-url.feature | 548 +++++++++++++++++++ features/search-replace.feature | 31 ++ src/Search_Replace_Command.php | 135 ++++- src/WP_CLI/SearchReplace/Non_URL_Columns.php | 238 ++++++++ 4 files changed, 951 insertions(+), 1 deletion(-) create mode 100644 features/search-replace-url.feature create mode 100644 src/WP_CLI/SearchReplace/Non_URL_Columns.php diff --git a/features/search-replace-url.feature b/features/search-replace-url.feature new file mode 100644 index 00000000..b18281b8 --- /dev/null +++ b/features/search-replace-url.feature @@ -0,0 +1,548 @@ +Feature: URL-optimized search/replace with smart column skipping + + @require-mysql + Scenario: Basic URL search/replace with smart column skipping + Given a WP install + + When I run `wp post create --post_title="Test Post" --post_content="Visit http://example.test for more" --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp search-replace 'http://example.test' 'http://example.com' --type=url --dry-run --verbose` + Then STDOUT should contain: + """ + Smart URL mode + """ + And STDOUT should contain: + """ + wp_posts + """ + And STDOUT should contain: + """ + post_content + """ + + @require-mysql + Scenario: Smart mode skips non-URL columns + Given a WP install + + When I run `wp search-replace --type=url 'http://example.test' 'http://example.com' --dry-run --verbose` + Then STDOUT should contain: + """ + Smart URL mode: Skipping + """ + And STDOUT should contain: + """ + columns: + """ + + @require-mysql + Scenario: Non-URL search does not trigger smart mode by default + Given a WP install + + When I run `wp search-replace 'http://example.test' 'http://example.com' --dry-run --verbose` + Then STDOUT should not contain: + """ + Smart URL mode + """ + + @require-mysql + Scenario: URL replacement in post content + Given a WP install + + When I run `wp post create --post_title="Test Post" --post_content="Visit http://oldsite.test for more info" --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp search-replace --type=url 'http://oldsite.test' 'http://newsite.com'` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp post get {POST_ID} --field=post_content` + Then STDOUT should contain: + """ + http://newsite.com + """ + And STDOUT should not contain: + """ + http://oldsite.test + """ + + @require-mysql + Scenario: URL replacement with skip-columns + Given a WP install + + When I run `wp post create --post_title="Test" --post_content="http://example.test" --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp search-replace --type=url 'http://example.test' 'http://example.com' --skip-columns=guid --dry-run` + Then STDOUT should not contain: + """ + | wp_posts | guid | + """ + + @require-mysql + Scenario: URL replacement with include-columns + Given a WP install + + When I run `wp search-replace --type=url 'http://example.test' 'http://example.com' --include-columns=post_content --dry-run` + Then STDOUT should be a table containing rows: + | Table | Column | Replacements | Type | + | wp_posts | post_content | 0 | SQL | + + @require-mysql + Scenario: Multisite URL replacement + Given a WP multisite install + And I run `wp site create --slug="foo" --title="foo" --email="foo@example.com"` + + When I run `wp search-replace --type=url 'http://example.test' 'http://example.com' --network --dry-run` + Then STDOUT should contain: + """ + wp_blogs + """ + + @require-mysql + Scenario: URL replacement with export + Given a WP install + And an empty cache + + When I run `wp post create --post_title="Test" --post_content="http://oldurl.test" --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp search-replace --type=url 'http://oldurl.test' 'http://newurl.com' --export` + Then STDOUT should contain: + """ + INSERT INTO + """ + And STDOUT should contain: + """ + http://newurl.com + """ + + @require-mysql + Scenario: URL replacement in options table + Given a WP install + + When I run `wp option add test_url 'http://testsite.test/page' --autoload=no` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp search-replace --type=url 'http://testsite.test' 'http://testsite.com'` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp option get test_url` + Then STDOUT should be: + """ + http://testsite.com/page + """ + + @require-mysql + Scenario: URL replacement in post meta + Given a WP install + + When I run `wp post create --post_title="Test" --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp post meta add {POST_ID} custom_url 'http://meta.test/path'` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp search-replace --type=url 'http://meta.test' 'http://meta.com'` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp post meta get {POST_ID} custom_url` + Then STDOUT should be: + """ + http://meta.com/path + """ + + @require-mysql + Scenario: URL replacement in comments + Given a WP install + + When I run `wp post create --post_title="Test" --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp comment create --comment_post_ID={POST_ID} --comment_content="Check http://comment.test" --comment_author="Test" --comment_author_email="test@test.com" --porcelain` + Then save STDOUT as {COMMENT_ID} + + When I run `wp search-replace --type=url 'http://comment.test' 'http://comment.com'` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp comment get {COMMENT_ID} --field=comment_content` + Then STDOUT should contain: + """ + http://comment.com + """ + + @require-mysql + Scenario: Dry run does not modify database + Given a WP install + + When I run `wp post create --post_title="Test" --post_content="http://dryrun.test" --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp search-replace --type=url 'http://dryrun.test' 'http://dryrun.com' --dry-run` + Then STDOUT should match /replacement(s)? to be made/ + + When I run `wp post get {POST_ID} --field=post_content` + Then STDOUT should contain: + """ + http://dryrun.test + """ + + @require-mysql + Scenario: Report changed only + Given a WP install + + When I run `wp post create --post_title="Test" --post_content="http://report.test" --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp search-replace --type=url 'http://report.test' 'http://report.com' --report-changed-only --dry-run` + Then STDOUT should contain: + """ + post_content + """ + And STDOUT should not contain: + """ + | wp_posts | post_type | 0 | + """ + + @require-mysql + Scenario: Skip tables option + Given a WP install + + When I run `wp search-replace --type=url 'http://example.test' 'http://example.com' --skip-tables=wp_posts --dry-run` + Then STDOUT should not contain: + """ + wp_posts + """ + And STDOUT should contain: + """ + wp_options + """ + + @require-mysql + Scenario: Specific tables only + Given a WP install + + When I run `wp search-replace --type=url 'http://example.test' 'http://example.com' wp_posts --dry-run` + Then STDOUT should contain: + """ + wp_posts + """ + And STDOUT should not contain: + """ + wp_options + """ + + @require-mysql + Scenario: HTTP to HTTPS conversion + Given a WP install + + When I run `wp post create --post_title="Test" --post_content="Visit http://secure.test" --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp search-replace --type=url 'http://secure.test' 'https://secure.test'` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp post get {POST_ID} --field=post_content` + Then STDOUT should contain: + """ + https://secure.test + """ + + @require-mysql + Scenario: Advanced table analysis mode + Given a WP install + + When I run `wp search-replace --type=url 'http://example.test' 'http://example.com' --analyze-tables --dry-run --verbose` + Then STDOUT should contain: + """ + Analyzing table structures + """ + And STDOUT should contain: + """ + Smart URL mode with table analysis + """ + + @require-mysql + Scenario: Table analysis skips integer columns + Given a WP install + And I run `wp db query "CREATE TABLE wp_test_table (id INT PRIMARY KEY, name VARCHAR(255), count BIGINT, url TEXT)"` + + When I run `wp db query "INSERT INTO wp_test_table VALUES (1, 'test', 100, 'http://test.url')"` + Then STDERR should be empty + + When I run `wp search-replace --type=url 'http://test.url' 'http://new.url' wp_test_table --analyze-tables --all-tables-with-prefix` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp db query "SELECT url FROM wp_test_table WHERE id = 1" --skip-column-names` + Then STDOUT should contain: + """ + http://new.url + """ + + When I run `wp db query "DROP TABLE wp_test_table"` + Then STDERR should be empty + + @require-mysql + Scenario: Table analysis skips enum columns + Given a WP install + And I run `wp db query "CREATE TABLE wp_test_enum (id INT PRIMARY KEY, status ENUM('active','inactive'), data TEXT)"` + + When I run `wp db query "INSERT INTO wp_test_enum VALUES (1, 'active', 'http://enum.test')"` + Then STDERR should be empty + + When I run `wp search-replace --type=url 'http://enum.test' 'http://enum.com' wp_test_enum --analyze-tables --all-tables-with-prefix` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp db query "SELECT data FROM wp_test_enum WHERE id = 1" --skip-column-names` + Then STDOUT should contain: + """ + http://enum.com + """ + + When I run `wp db query "DROP TABLE wp_test_enum"` + Then STDERR should be empty + + @require-mysql + Scenario: Table analysis skips date columns + Given a WP install + And I run `wp db query "CREATE TABLE wp_test_dates (id INT PRIMARY KEY, created_date DATE, url VARCHAR(255))"` + + When I run `wp db query "INSERT INTO wp_test_dates VALUES (1, '2024-01-01', 'http://date.test')"` + Then STDERR should be empty + + When I run `wp search-replace --type=url 'http://date.test' 'http://date.com' wp_test_dates --analyze-tables --all-tables-with-prefix` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp db query "SELECT url FROM wp_test_dates WHERE id = 1" --skip-column-names` + Then STDOUT should contain: + """ + http://date.com + """ + + When I run `wp db query "DROP TABLE wp_test_dates"` + Then STDERR should be empty + + @require-mysql + Scenario: Table analysis with pattern matching + Given a WP install + And I run `wp db query "CREATE TABLE wp_test_patterns (order_id INT PRIMARY KEY, order_count INT, order_status VARCHAR(20), order_url TEXT)"` + + When I run `wp db query "INSERT INTO wp_test_patterns VALUES (1, 5, 'pending', 'http://pattern.test')"` + Then STDERR should be empty + + When I run `wp search-replace --type=url 'http://pattern.test' 'http://pattern.com' wp_test_patterns --analyze-tables --all-tables-with-prefix --verbose` + Then STDOUT should contain: + """ + Analyzing table structures + """ + + When I run `wp db query "SELECT order_url FROM wp_test_patterns WHERE order_id = 1" --skip-column-names` + Then STDOUT should contain: + """ + http://pattern.com + """ + + When I run `wp db query "DROP TABLE wp_test_patterns"` + Then STDERR should be empty + + @require-mysql + Scenario: Serialized data handling + Given a WP install + + When I run `wp post create --post_title="Test" --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp post meta add {POST_ID} serialized_data 'a:2:{s:3:"url";s:18:"http://serial.test";s:4:"name";s:4:"test";}'` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp db query "SELECT meta_value FROM wp_postmeta WHERE meta_key = 'serialized_data' AND post_id = {POST_ID}" --skip-column-names` + Then STDOUT should contain: + """ + http://serial.test + """ + + When I run `wp search-replace --type=url 'http://serial.test' 'http://serial.com'` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp db query "SELECT meta_value FROM wp_postmeta WHERE meta_key = 'serialized_data' AND post_id = {POST_ID}" --skip-column-names` + Then STDOUT should contain: + """ + http://serial.com + """ + + @require-mysql + Scenario: Large content replacement + Given a WP install + + When I run `wp post create --post_title="Large Post" --post_content="$(printf 'http://large.test %.0s' {1..1000})" --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp search-replace --type=url 'http://large.test' 'http://large.com'` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp post get {POST_ID} --field=post_content` + Then STDOUT should contain: + """ + http://large.com + """ + And STDOUT should not contain: + """ + http://large.test + """ + + @require-mysql + Scenario: Multiple URL replacements in same content + Given a WP install + + When I run `wp post create --post_title="Multi URL" --post_content="Visit http://multi.test and also http://multi.test/page" --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp search-replace --type=url 'http://multi.test' 'http://multi.com'` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp post get {POST_ID} --field=post_content` + Then STDOUT should contain: + """ + http://multi.com + """ + And STDOUT should contain: + """ + http://multi.com/page + """ + + @require-mysql + Scenario: Verbose output shows progress + Given a WP install + + When I run `wp post create --post_title="Test" --post_content="http://verbose.test" --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp search-replace --type=url 'http://verbose.test' 'http://verbose.com' --verbose` + Then STDOUT should contain: + """ + Checking: + """ + + @require-mysql + Scenario: Count format output + Given a WP install + + When I run `wp post create --post_title="Test" --post_content="http://count.test http://count.test" --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp search-replace --type=url 'http://count.test' 'http://count.com' --format=count` + Then STDOUT should be a number + + @require-mysql + Scenario: All tables with prefix + Given a WP install + + When I run `wp search-replace --type=url 'http://example.test' 'http://example.com' --all-tables-with-prefix --dry-run` + Then STDOUT should contain: + """ + wp_ + """ + + @require-mysql + Scenario: Recurse objects option + Given a WP install + + When I run `wp post create --post_title="Test" --porcelain` + Then save STDOUT as {POST_ID} + + When I run `wp post meta add {POST_ID} object_data '{"url":"http://object.test","nested":{"url":"http://object.test/nested"}}'` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp search-replace --type=url 'http://object.test' 'http://object.com' --recurse-objects` + Then STDOUT should contain: + """ + Success: + """ + + @require-mysql + Scenario: Explicit --type=url flag allows non-schemed domains + Given a WP install + + When I run `wp search-replace 'explicit.test' 'explicit.com' --type=url --dry-run` + Then STDOUT should contain: + """ + replacements to be made + """ + + @require-mysql + Scenario: Error when --analyze-tables used without --type=url + Given a WP install + + When I try `wp search-replace 'foo' 'bar' --analyze-tables` + Then STDERR should contain: + """ + Error: The --analyze-tables flag requires --type=url to be enabled. + """ + And the return code should be 1 + + @require-mysql + Scenario: Table analysis skips SET columns + Given a WP install + And I run `wp db query "CREATE TABLE wp_test_set (id INT PRIMARY KEY, permissions SET('read','write','delete'), data TEXT)"` + + When I run `wp db query "INSERT INTO wp_test_set VALUES (1, 'read,write', 'http://set.test')"` + Then STDERR should be empty + + When I run `wp search-replace --type=url 'http://set.test' 'http://set.com' wp_test_set --analyze-tables --all-tables-with-prefix` + Then STDOUT should contain: + """ + Success: + """ + + When I run `wp db query "SELECT data FROM wp_test_set WHERE id = 1" --skip-column-names` + Then STDOUT should contain: + """ + http://set.com + """ + + When I run `wp db query "DROP TABLE wp_test_set"` + Then STDERR should be empty diff --git a/features/search-replace.feature b/features/search-replace.feature index 2bccd606..4ad32d0d 100644 --- a/features/search-replace.feature +++ b/features/search-replace.feature @@ -275,6 +275,37 @@ Feature: Do global search/replace | key | value | | header_image_data | {"url":"https:\/\/example.com\/foo.jpg"} | + @require-mysql + Scenario: Search and replace prevents malformed URL replacements + Given a WP install + + When I try `wp search-replace "https://example.com" "http;//newdomain.com"` + Then STDERR should contain: + """ + Error: The replacement string contains characters that are invalid in a URL (e.g., ';'). + """ + And the return code should be 1 + + When I try `wp search-replace "https://example.com" "http://newdomain.com, /subdir"` + Then STDERR should contain: + """ + Error: The replacement string contains characters that are invalid in a URL (e.g., ','). + """ + And the return code should be 1 + + When I run `wp search-replace "https://example.com" "https://newdomain.com"` + Then STDERR should be empty + + When I try `wp search-replace "example.com" "new;domain.com" --type=url` + Then STDERR should contain: + """ + Error: The replacement string contains characters that are invalid in a URL (e.g., ';'). + """ + And the return code should be 1 + + When I run `wp search-replace "example.com" "new;domain.com"` + Then STDERR should be empty + @require-mysql Scenario: Search and replace handles JSON-encoded URLs in post content Given a WP install diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index 8423560d..69c82ee2 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -230,6 +230,21 @@ class Search_Replace_Command extends WP_CLI_Command { * [--regex-limit=] * : The maximum possible replacements for the regex per row (or per unserialized data bit per row). Defaults to -1 (no limit). * + * [--type=] + * : Enable smart replacement mode for specific data types. Validates the replacement string and optimizes performance by skipping unrelated columns. + * --- + * options: + * - url + * --- + * + * [--analyze-tables] + * : Enable advanced table analysis mode. Analyzes MySQL column datatypes + * to automatically skip non-text columns (integers, dates, enums, etc.) + * and columns matching common WordPress-style naming patterns (e.g. `*_id`, + * `*_count`, `*_status`, etc.) in addition to the static skip list. Useful + * for plugin tables with custom schemas. Requires `--type=url` to be enabled. + * Note: Adds a small overhead for table introspection. + * * [--format=] * : Render output in a particular format. * --- @@ -328,6 +343,24 @@ public function __invoke( $args, $assoc_args ) { WP_CLI::error( $error_msg ); } + // Handle smart URL mode + $is_url_mode = 'url' === Utils\get_flag_value( $assoc_args, 'type' ); + $analyze_tables = Utils\get_flag_value( $assoc_args, 'analyze-tables', false ); + + if ( $analyze_tables && ! $is_url_mode ) { + WP_CLI::error( 'The --analyze-tables flag requires --type=url to be enabled.' ); + } + + if ( $is_url_mode ) { + // Issue #231: Validate replacement URL for illegal cookie path characters. + // We do not strictly validate the search string, as users often search for non-schemed domains (e.g. 'example.com'). + if ( preg_match( '/[;,\s\t\r\n]/', $new, $matches ) ) { + WP_CLI::error( sprintf( "The replacement string contains characters that are invalid in a URL (e.g., '%s'). This can cause fatal errors in PHP 8.0+.", $matches[0] ) ); + } + + $this->apply_smart_url_mode( $args, $assoc_args, $analyze_tables ); + } + $total = 0; $report = array(); $this->dry_run = Utils\get_flag_value( $assoc_args, 'dry-run', false ); @@ -379,7 +412,7 @@ public function __invoke( $args, $assoc_args ) { } } - $this->skip_columns = explode( ',', Utils\get_flag_value( $assoc_args, 'skip-columns', '' ) ); + $this->skip_columns = array_filter( explode( ',', Utils\get_flag_value( $assoc_args, 'skip-columns', '' ) ) ); $this->skip_tables = explode( ',', Utils\get_flag_value( $assoc_args, 'skip-tables', '' ) ); $this->include_columns = array_filter( explode( ',', Utils\get_flag_value( $assoc_args, 'include-columns', '' ) ) ); @@ -1304,4 +1337,104 @@ private function log_write( $col, $keys, $table, $old_bits, $new_bits ) { fwrite( $this->log_handle, "{$table_column_id_log}\n{$old_log}\n{$new_log}\n" ); } + + /** + * Apply smart URL mode to automatically skip non-URL columns. + * + * @param array $args Positional arguments. + * @param array $assoc_args Associative arguments (passed by reference). + * @param bool $analyze_tables Whether to analyze tables for additional skips. + */ + private function apply_smart_url_mode( $args, &$assoc_args, $analyze_tables ) { + // Get existing skip columns + $existing_skip_columns = Utils\get_flag_value( $assoc_args, 'skip-columns', '' ); + $skip_columns_array = array_filter( explode( ',', $existing_skip_columns ) ); + + // Start with our static non-URL columns + $all_skip_columns = WP_CLI\SearchReplace\Non_URL_Columns::get_core_columns(); + + // If analyze-tables is enabled, add datatype-based skipping + if ( $analyze_tables ) { + $tables = Utils\wp_get_table_names( $args, $assoc_args ); + + if ( $this->verbose && ! WP_CLI::get_config( 'quiet' ) && 'count' !== $this->format ) { + WP_CLI::log( 'Analyzing table structures for additional columns to skip...' ); + } + + $analyzed_columns = $this->analyze_tables_for_skip_columns( $tables ); + $all_skip_columns = array_merge( $all_skip_columns, $analyzed_columns ); + } + + // Merge with user-provided skip columns + $all_skip_columns = array_unique( array_merge( $skip_columns_array, $all_skip_columns ) ); + + // Update the assoc_args with the merged skip columns + $assoc_args['skip-columns'] = implode( ',', $all_skip_columns ); + + // Inform the user about the optimization + if ( ! WP_CLI::get_config( 'quiet' ) && 'count' !== $this->format ) { + if ( $this->verbose ) { + $mode_text = $analyze_tables ? 'Smart URL mode with table analysis' : 'Smart URL mode'; + WP_CLI::log( + sprintf( + '%s: Skipping %d columns: %s', + $mode_text, + count( $all_skip_columns ), + implode( ', ', array_slice( $all_skip_columns, 0, 10 ) ) . ( count( $all_skip_columns ) > 10 ? '...' : '' ) + ) + ); + } + } + } + + /** + * Analyze tables to find additional columns to skip based on datatypes. + * + * This method examines the MySQL column definitions to identify columns + * that cannot contain URLs based on their datatype (integers, dates, enums, etc.) + * or naming patterns. + * + * @param array $tables List of table names to analyze. + * @return array List of column names to skip. + */ + private function analyze_tables_for_skip_columns( $tables ) { + global $wpdb; + + $skip_columns = array(); + + foreach ( $tables as $table ) { + // Get column information from INFORMATION_SCHEMA + $columns = $wpdb->get_results( + $wpdb->prepare( + 'SELECT COLUMN_NAME AS column_name, DATA_TYPE AS data_type, COLUMN_TYPE AS column_type + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = %s', + $table + ) + ); + + if ( empty( $columns ) ) { + continue; // @codeCoverageIgnore + } + + foreach ( $columns as $col ) { + $column_name = $col->column_name; + $data_type = $col->data_type; + $column_type = $col->column_type; + + // Skip columns based on datatype + if ( WP_CLI\SearchReplace\Non_URL_Columns::is_non_text_datatype( $data_type, $column_type ) ) { + $skip_columns[] = $column_name; + } + + // Skip columns based on naming patterns + if ( WP_CLI\SearchReplace\Non_URL_Columns::matches_non_url_pattern( $column_name ) ) { + $skip_columns[] = $column_name; + } + } + } + + return array_unique( $skip_columns ); + } } diff --git a/src/WP_CLI/SearchReplace/Non_URL_Columns.php b/src/WP_CLI/SearchReplace/Non_URL_Columns.php new file mode 100644 index 00000000..b26b71ee --- /dev/null +++ b/src/WP_CLI/SearchReplace/Non_URL_Columns.php @@ -0,0 +1,238 @@ + Date: Wed, 29 Apr 2026 21:07:22 +0200 Subject: [PATCH 2/3] Initialize early --- src/Search_Replace_Command.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index 69c82ee2..486acce7 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -343,6 +343,15 @@ public function __invoke( $args, $assoc_args ) { WP_CLI::error( $error_msg ); } + $total = 0; + $report = array(); + $this->dry_run = Utils\get_flag_value( $assoc_args, 'dry-run', false ); + $php_only = Utils\get_flag_value( $assoc_args, 'precise', false ); + $this->recurse_objects = Utils\get_flag_value( $assoc_args, 'recurse-objects', true ); + $this->verbose = Utils\get_flag_value( $assoc_args, 'verbose', false ); + $this->format = Utils\get_flag_value( $assoc_args, 'format' ); + $this->regex = Utils\get_flag_value( $assoc_args, 'regex', false ); + // Handle smart URL mode $is_url_mode = 'url' === Utils\get_flag_value( $assoc_args, 'type' ); $analyze_tables = Utils\get_flag_value( $assoc_args, 'analyze-tables', false ); @@ -361,15 +370,6 @@ public function __invoke( $args, $assoc_args ) { $this->apply_smart_url_mode( $args, $assoc_args, $analyze_tables ); } - $total = 0; - $report = array(); - $this->dry_run = Utils\get_flag_value( $assoc_args, 'dry-run', false ); - $php_only = Utils\get_flag_value( $assoc_args, 'precise', false ); - $this->recurse_objects = Utils\get_flag_value( $assoc_args, 'recurse-objects', true ); - $this->verbose = Utils\get_flag_value( $assoc_args, 'verbose', false ); - $this->format = Utils\get_flag_value( $assoc_args, 'format' ); - $this->regex = Utils\get_flag_value( $assoc_args, 'regex', false ); - $default_regex_delimiter = false; if ( null !== $this->regex ) { From bcd4da6cb9e76d8f0920932dddf1f7bde64357ca Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 29 Apr 2026 21:07:33 +0200 Subject: [PATCH 3/3] Fix tests --- features/search-replace.feature | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/search-replace.feature b/features/search-replace.feature index 4ad32d0d..22f80039 100644 --- a/features/search-replace.feature +++ b/features/search-replace.feature @@ -279,21 +279,21 @@ Feature: Do global search/replace Scenario: Search and replace prevents malformed URL replacements Given a WP install - When I try `wp search-replace "https://example.com" "http;//newdomain.com"` + When I try `wp search-replace "https://example.com" "http;//newdomain.com" --type=url` Then STDERR should contain: """ Error: The replacement string contains characters that are invalid in a URL (e.g., ';'). """ And the return code should be 1 - When I try `wp search-replace "https://example.com" "http://newdomain.com, /subdir"` + When I try `wp search-replace "https://example.com" "http://newdomain.com, /subdir" --type=url` Then STDERR should contain: """ Error: The replacement string contains characters that are invalid in a URL (e.g., ','). """ And the return code should be 1 - When I run `wp search-replace "https://example.com" "https://newdomain.com"` + When I run `wp search-replace "https://example.com" "https://newdomain.com" --type=url` Then STDERR should be empty When I try `wp search-replace "example.com" "new;domain.com" --type=url`