diff --git a/README.md b/README.md index 19c4b2e..df065b3 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,10 @@ Type `php srdb.cli.php` to run the program. Type `php srdb.cli.php None empty string to replace search with or `preg_replace()` style replacement. + -f, --file + File that contains touples of search/replace strings/patterns + separated by new lines: search1\nreplace1\nsearch2\nreplace2 + -t, --tables If set only runs the script on the specified table, comma separate for multiple values. diff --git a/srdb.class.php b/srdb.class.php index ee9b392..5c2339b 100644 --- a/srdb.class.php +++ b/srdb.class.php @@ -75,6 +75,11 @@ class icit_srdb { */ public $replace = false; + /** + * @var string Path to file containing tuples of search => replace + */ + public $file = false; + /** * @var bool Use regular expressions to perform search and replace */ @@ -219,6 +224,7 @@ public function __construct( $args ) { 'port' => 3306, 'search' => '', 'replace' => '', + 'file' => '', 'tables' => array(), 'exclude_tables' => array(), 'exclude_cols' => array(), @@ -314,15 +320,16 @@ public function __construct( $args ) { } // update collation elseif ( $this->alter_collation ) { $report = $this->update_collation( $this->alter_collation, $this->tables ); + } elseif ( $this->file ) { + $searchReplaceTuples = $this->parseTuplesFromFile( $this->file ); + + $report = $this->replacer( $searchReplaceTuples, $this->tables, $this->exclude_tables ); } elseif ( is_array( $this->search ) ) { - $report = array(); - for ( $i = 0; $i < count( $this->search ); $i ++ ) { - $report[ $i ] = $this->replacer( $this->search[ $i ], $this->replace[ $i ], $this->tables, - $this->exclude_tables ); - } + $searchReplaceTuples = $this->parseTuplesFromArray( $this->search, $this->replace ); + $report = $this->replacer( $searchReplaceTuples, $this->tables, $this->exclude_tables ); } else { - $report = $this->replacer( $this->search, $this->replace, $this->tables, $this->exclude_tables ); + $report = $this->replacer( [ $this->search, $this->replace ], $this->tables, $this->exclude_tables ); } } else { @@ -898,18 +905,13 @@ public function preg_fix_serialised_count( $matches ) { * We split large tables into 50,000 row blocks when dealing with them to save * on memmory consumption. * - * @param string $search What we want to replace - * @param string $replace What we want to replace it with. + * @param array $searchReplaceTuples Array of Tuples of What we want to replace and What we want to replace it with. * @param array $tables The tables we want to look at. * * @return array|bool Collection of information gathered during the run. */ - public function replacer( $search = '', $replace = '', $tables = array(), $exclude_tables = array() ) { - $search = (string) $search; - // check we have a search string, bail if not - if ( '' === $search ) { - $this->add_error( 'Search string is empty', 'search' ); - + public function replacer( $searchReplaceTuples, $tables = array(), $exclude_tables = array() ) { + if ( empty ( $searchReplaceTuples ) ) { return false; } @@ -988,7 +990,7 @@ public function replacer( $search = '', $replace = '', $tables = array(), $exclu $new_table_report = $table_report; $new_table_report['start'] = microtime( true ); - $this->log( 'search_replace_table_start', $table, $search, $replace ); + $this->log( 'search_replace_table_start', $table, $searchReplaceTuples ); // Count the number of rows we have in the table if large we'll split into blocks, This is a mod from Simon Wheatley $row_count = $this->db_query( "SELECT COUNT(*) FROM `{$table}`" ); @@ -1036,16 +1038,32 @@ public function replacer( $search = '', $replace = '', $tables = array(), $exclu if ( ! empty( $this->include_cols ) && ! in_array( $column, $this->include_cols ) ) { continue; } + + foreach ( $searchReplaceTuples as $searchReplaceTuple ) { + $search = (string) $searchReplaceTuple[0]; + $replace = (string) $searchReplaceTuple[1]; + + // check we have a search string, bail if not + if ( '' === $search ) { + $this->add_error( 'Search string is empty', 'search' ); + + return false; + } - // Run a search replace on the data that'll respect the serialisation. - $edited_data = $this->recursive_unserialize_replace( $search, $replace, $data_to_fix ); + $previous_data = $edited_data; + + // Run a search replace on the data that'll respect the serialisation. + $edited_data = $this->recursive_unserialize_replace( $search, $replace, $edited_data ); + + if ( $previous_data !== $edited_data ) { + $report['change'] ++; + $new_table_report['change'] ++; + } + } // Something was changed if ( $edited_data != $data_to_fix ) { - $report['change'] ++; - $new_table_report['change'] ++; - // log first x changes if ( $new_table_report['change'] <= $this->report_change_num ) { $new_table_report['changes'][] = array( @@ -1103,7 +1121,7 @@ public function replacer( $search = '', $replace = '', $tables = array(), $exclu $report['end'] = microtime( true ); - $this->log( 'search_replace_end', $search, $replace, $report ); + $this->log( 'search_replace_end', $searchReplaceTuples, $report ); return $report; } @@ -1331,7 +1349,60 @@ public function charset_decode_utf_8( $string ) { return $string; } + /** + * Parses the file containing search/replacement tuples separated by new lines + * to an array + * + * @param string $filePath + * + * @return array + */ + public function parseTuplesFromFile( $filePath ) + { + if (!is_file( $filePath ) || !is_readable( $filePath )) { + $this->add_error( 'The file path provided is either not a file or it is not readable.' ); + + return []; + } + + $fileContent = file_get_contents( $filePath ); + $expandedContent = preg_split("/\R/", $fileContent); + + if (count( $expandedContent ) % 2 === 1) { + $this->add_error( 'The file provided does not contain pairs of search and replacement' + . ' tuples separated by new lines, the number of lines in the file is odd.' ); + + return []; + } + + $result = []; + for ( $i = 0; $i < count( $expandedContent ); $i += 2 ) { + if ( empty( $expandedContent[ $i ] ) ) { + $this->add_error( 'The file provided contains an empty line where it should be a search string' ); + + return []; + } + + $result []= [ $expandedContent[ $i ], $expandedContent[ $i + 1 ] ]; + } + + return $result; + } + + public function parseTuplesFromArray( $searchArray, $replaceArrayOrString ) + { + $result = []; + + foreach ( $searchArray as $i => $search ) { + if ( is_array( $replaceArrayOrString ) ) { + $result []= [ $search, $replaceArrayOrString[ $i ] ]; + } else { + $result []= [ $search, $replaceArrayOrString ]; + } + } + return $result; + } } diff --git a/srdb.cli.php b/srdb.cli.php index 2c86323..0eea145 100755 --- a/srdb.cli.php +++ b/srdb.cli.php @@ -39,6 +39,11 @@ [ 'P:', 'port:', 'Optional. Port on database server to connect to. The default is 3306. (MySQL default port).', ], [ 's:', 'search:', 'String to search for or `preg_replace()` style regular expression.', ], [ 'r:', 'replace:', 'None empty string to replace search with or `preg_replace()` style replacement.', ], + [ + 'f:', + 'file:', + 'File with strings or regular expressions along with their intended replacements, separated by line endings.', + ], [ 't:', 'tables:', 'If set only runs the script on the specified table, comma separate for multiple values.', ], [ 'w:', 'exclude-tables:', 'If set excluded the specified tables, comma separate for multuple values.', ], [ @@ -246,15 +251,12 @@ public function log( $type = '' ) { $output .= "$error_type: $error"; break; case 'search_replace_table_start': - list( $table, $search, $replace ) = $args; + list( $table, $searchReplaceTuples ) = $args; - if ( is_array( $search ) ) { - $search = implode( ' or ', $search ); - } - if ( is_array( $replace ) ) { - $replace = implode( ' or ', $replace ); - } - $output .= "{$table}: replacing {$search} with {$replace}"; + $output .= "{$table}: replacing " . implode( ', ', array_map( + function ( $searchReplaceTuple ) { return implode( ' => ', $searchReplaceTuple ); }, + $searchReplaceTuples + )); break; case 'search_replace_table_end': @@ -266,20 +268,18 @@ public function log( $type = '' ) { $output .= "{$table}: {$report['rows']} rows, {$report['change']} changes found, {$report['updates']} updates made in {$time} seconds"; break; case 'search_replace_end': - list( $search, $replace, $report ) = $args; - if ( is_array( $search ) ) { - $search = implode( ' or ', $search ); - } - if ( is_array( $replace ) ) { - $replace = implode( ' or ', $replace ); - } + list( $searchReplaceTuples, $report ) = $args; + $time = number_format( floatval( $report['end'] ) - floatval( $report['start'] ), 8 ); if ( $time < 0 ) { $time = $time * - 1; } $dry_run_string = $this->dry_run ? "would have been" : "were"; - $output .= " -Replacing {$search} with {$replace} on {$report['tables']} tables with {$report['rows']} rows + + $output .= "Replacing " . implode( ', ', array_map( + function ($searchReplaceTuple) { return implode(' => ', $searchReplaceTuple); }, + $searchReplaceTuples + )) . " on {$report['tables']} tables with {$report['rows']} rows {$report['change']} changes {$dry_run_string} made {$report['updates']} updates were actually made It took {$time} seconds";