Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6b308fd
Initial plan
Copilot Nov 10, 2025
41bbda5
Add progress bar support to search-replace command
Copilot Nov 10, 2025
cd31f29
Add Behat tests for progress bar functionality
Copilot Nov 10, 2025
61e02c0
Remove unused progress_bar property
Copilot Nov 10, 2025
77534f5
Refactor progress tick to reduce code duplication
Copilot Nov 10, 2025
59217ca
Lint fix
swissspidy Nov 10, 2025
681d4c0
Revert to early continue pattern and fix progress tick to count by batch
Copilot Nov 10, 2025
1f1bb20
Suppress progress bar when logging is enabled
Copilot Nov 13, 2025
8d65105
Update test to include search string in generated post content
Copilot Nov 13, 2025
b5da516
Fix test by explicitly setting option with search string
Copilot Nov 14, 2025
46ba4ed
Fix test to check STDERR for progress bar output
Copilot Nov 14, 2025
3e0e5a0
Add WP_CLI::log message before progress bar to show processing status
Copilot Nov 14, 2025
3efdf0a
Fix progress bar sprintf format strings
Copilot Nov 14, 2025
0d531c9
Move progress bar initialization into while loop
Copilot Nov 14, 2025
077285b
Merge branch 'main' into copilot/add-progress-bar-indicator
swissspidy Jan 20, 2026
b0eeefa
Merge branch 'main' into copilot/add-progress-bar-indicator
swissspidy Mar 11, 2026
b6b7e3b
Apply suggestion from @swissspidy
swissspidy Mar 11, 2026
b3f2da1
Remove WP_CLI::log calls that break existing tests
Copilot Mar 11, 2026
9a2facf
Merge branch 'main' into copilot/add-progress-bar-indicator
swissspidy Mar 22, 2026
f81af96
Merge branch 'main' into copilot/add-progress-bar-indicator
swissspidy May 19, 2026
9abe70a
Flush persistent object cache after replacements
Copilot May 19, 2026
146a8ca
Remove redundant function_exists check for wp_cache_flush
Copilot May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion features/search-replace.feature
Original file line number Diff line number Diff line change
Expand Up @@ -1476,7 +1476,6 @@ Feature: Do global search/replace
Success: Made 0 replacements.
"""

@require-mysql
Scenario: Search/replace strings starting with hyphens using --old and --new flags
Given a WP install
And I run `wp post create --post_title="Test Post" --post_content="This is --old-content and more text" --porcelain`
Expand All @@ -1499,6 +1498,33 @@ Feature: Do global search/replace
--old-content
"""

@require-mysql
Scenario: Progress bar shows when not in verbose mode
Given a WP install
And I run `wp post generate --count=100`
And I run `wp option set test_url 'Visit http://example.com for more'`

When I run `wp search-replace http://example.com http://example.org --precise`
Then STDERR should contain:
"""
Processing
"""

@require-mysql
Scenario: Progress bar does not show in verbose mode
Given a WP install
And I run `wp post generate --count=10`

When I run `wp search-replace http://example.com http://example.org --verbose`
Then STDOUT should contain:
"""
Checking:
"""
And STDERR should not contain:
"""
Processing
"""

@require-mysql
Scenario: Error when neither positional args nor flags provided
Given a WP install
Expand Down
77 changes: 75 additions & 2 deletions src/Search_Replace_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -616,10 +616,11 @@ public function __invoke( $args, $assoc_args ) {
if ( ! empty( $assoc_args['export'] ) ) {
$success_message = 1 === $total ? "Made 1 replacement and exported to {$assoc_args['export']}." : "Made {$total} replacements and exported to {$assoc_args['export']}.";
} else {
$success_message = 1 === $total ? 'Made 1 replacement.' : "Made $total replacements.";
if ( $total && 'Default' !== Utils\wp_get_cache_type() ) {
$success_message .= ' Please remember to flush your persistent object cache with `wp cache flush`.';
wp_cache_flush();
}

$success_message = 1 === $total ? 'Made 1 replacement.' : "Made $total replacements.";
}
WP_CLI::success( $success_message );
} else {
Expand All @@ -645,6 +646,18 @@ private function php_export_table( $table, $old, $new ) {
WP_CLI::log( sprintf( 'Checking: %s', $table ) );
}

// Set up progress bar if appropriate
$progress = null;
if ( $this->should_show_progress_bar() ) {
global $wpdb;
$table_sql = self::esc_sql_ident( $table );
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
$total_rows = $wpdb->get_var( "SELECT COUNT(*) FROM {$table_sql}" );
if ( $total_rows > 0 ) {
$progress = \WP_CLI\Utils\make_progress_bar( sprintf( 'Processing %s', $table ), $total_rows );
}
}

$rows = array();
foreach ( new Iterators\Table( $args ) as $i => $row ) {
$row_fields = array();
Expand All @@ -665,9 +678,17 @@ private function php_export_table( $table, $old, $new ) {
$row_fields[ $col ] = $value;
}
$rows[] = $row_fields;

if ( $progress ) {
$progress->tick();
}
}
$this->write_sql_row_fields( $table, $rows );

if ( $progress ) {
$progress->finish();
}

$table_report = array();
$total_rows = 0;
$total_cols = 0;
Expand Down Expand Up @@ -769,12 +790,22 @@ static function ( $key ) {
);
$order_by_sql = 'ORDER BY ' . implode( ',', $order_by_keys );
$limit = 1000;
$progress = null;

// 2 errors:
// - WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
// - WordPress.CodeAnalysis.AssignmentInCondition -- no reason to do copy-paste for a single valid assignment in while
// phpcs:ignore
while ( $rows = $wpdb->get_results( "SELECT {$primary_keys_sql} FROM {$table_sql} {$where_key} {$order_by_sql} LIMIT {$limit}" ) ) {
// Set up progress bar on first iteration if we have rows to process
if ( null === $progress && $this->should_show_progress_bar() ) {
// Count total rows to process
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
$total_rows = $wpdb->get_var( "SELECT COUNT(*) FROM {$table_sql} {$where_key}" );
if ( $total_rows > 0 ) {
$progress = \WP_CLI\Utils\make_progress_bar( sprintf( 'Processing %s.%s', $table, $col ), $total_rows );
}
}
foreach ( $rows as $keys ) {
$where_sql = '';
foreach ( (array) $keys as $k => $v ) {
Expand Down Expand Up @@ -841,6 +872,10 @@ static function ( $key ) {
}
}

if ( $progress ) {
$progress->tick( count( $rows ) );
}

// Because we are ordering by primary keys from least to greatest,
// we can exclude previous chunks from consideration by adding greater-than conditions
// to insist the next chunk's keys must be greater than the last of this chunk's keys.
Expand Down Expand Up @@ -877,6 +912,10 @@ static function ( $key ) {
$where_key = 'WHERE ' . implode( ' AND ', $where_key_conditions );
}

if ( $progress ) {
$progress->finish();
}

if ( $this->verbose && 'table' === $this->format ) {
$time = round( microtime( true ) - $this->start_time, 3 );
WP_CLI::log( sprintf( '%d rows affected using PHP (in %ss).', $count, $time ) );
Expand Down Expand Up @@ -992,6 +1031,40 @@ private static function is_text_col( $type ) {
return false;
}

/**
* Determines whether a progress bar should be shown.
*
* @return bool True if progress bar should be shown.
*/
private function should_show_progress_bar() {
// Don't show progress bar if in quiet mode
if ( WP_CLI::get_config( 'quiet' ) ) {
return false;
}

// Don't show progress bar if exporting to STDOUT
if ( STDOUT === $this->export_handle ) {
return false;
}

// Don't show progress bar if logging is enabled (would interfere with log output)
if ( null !== $this->log_handle ) {
return false;
}

// Don't show progress bar if verbose mode is enabled (it shows row-by-row updates)
if ( $this->verbose ) {
return false;
}

// Don't show progress bar if format is 'count'
if ( 'count' === $this->format ) {
return false;
}

return true;
}

private static function esc_like( $old ) {
global $wpdb;

Expand Down
Loading