PluginProbe ʕ •ᴥ•ʔ
Search Regex / trunk
Search Regex vtrunk
trunk 1.4.12 1.4.13 1.4.14 1.4.15 1.4.16 2.0 2.0.1 2.1 2.2 2.2.1 2.3 2.3.1 2.3.2 2.3.3 2.4 2.4.1 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1 3.1.1 3.1.2 3.2 3.3 3.3.0 3.3.1 3.4 3.4.1 3.4.2
search-regex / includes / cli / class-search-regex-cli.php
search-regex / includes / cli Last commit date
class-search-regex-cli.php 6 months ago
class-search-regex-cli.php
514 lines
1 <?php
2
3 namespace SearchRegex\Cli;
4
5 use SearchRegex\Source;
6 use SearchRegex\Search;
7 use SearchRegex\Filter;
8 use SearchRegex\Action;
9 use SearchRegex\Schema;
10 use WP_CLI;
11 use WP_Error;
12
13 /**
14 * Search Regex WP CLI commands
15 */
16 class Search_Regex_CLI {
17 /**
18 * Perform a search using Search Regex
19 *
20 * ## OPTIONS
21 *
22 * <search>
23 * : The search phrase to find
24 *
25 * [--source=<source>]
26 * : The source to search in. Available: posts, comment, user, options, post-meta, comment-meta, user-meta, terms
27 * ---
28 * default: posts
29 * ---
30 *
31 * [--regex]
32 * : Use regular expression search
33 *
34 * [--case-insensitive]
35 * : Perform case-insensitive search
36 *
37 * [--per-page=<number>]
38 * : Number of results per page
39 * ---
40 * default: 25
41 * ---
42 *
43 * [--page=<number>]
44 * : Page number to retrieve
45 * ---
46 * default: 0
47 * ---
48 *
49 * [--limit=<number>]
50 * : Maximum number of results to return (0 for unlimited)
51 * ---
52 * default: 0
53 * ---
54 *
55 * [--format=<format>]
56 * : Output format
57 * ---
58 * default: table
59 * options:
60 * - table
61 * - json
62 * - csv
63 * - yaml
64 * - count
65 * ---
66 *
67 * ## EXAMPLES
68 *
69 * # Search for "hello world" in posts
70 * $ wp search-regex search "hello world"
71 *
72 * # Search for a pattern using regex in comments
73 * $ wp search-regex search "email.*@.*\.com" --source=comment --regex
74 *
75 * # Case-insensitive search in options
76 * $ wp search-regex search "siteurl" --source=options --case-insensitive
77 *
78 * # Get JSON output
79 * $ wp search-regex search "test" --format=json
80 *
81 * # Get just the count of matches
82 * $ wp search-regex search "test" --format=count
83 *
84 * @param array<string> $args Positional arguments.
85 * @param array<string, mixed> $assoc_args Associative arguments.
86 * @return void
87 * @when after_wp_load
88 */
89 public function search( $args, $assoc_args ): void {
90 $search_phrase = $args[0];
91 $source = $assoc_args['source'] ?? 'posts';
92 $per_page = isset( $assoc_args['per-page'] ) ? intval( $assoc_args['per-page'] ) : 25;
93 $page = isset( $assoc_args['page'] ) ? intval( $assoc_args['page'] ) : 0;
94 $limit = isset( $assoc_args['limit'] ) ? intval( $assoc_args['limit'] ) : 0;
95 $format = $assoc_args['format'] ?? 'table';
96
97 // Build search flags
98 $search_flags = [];
99 if ( isset( $assoc_args['regex'] ) ) {
100 $search_flags[] = 'regex';
101 }
102 if ( isset( $assoc_args['case-insensitive'] ) ) {
103 $search_flags[] = 'case';
104 }
105
106 // Validate source
107 $allowed_sources = Source\Manager::get_all_source_names();
108 if ( ! in_array( $source, $allowed_sources, true ) ) {
109 WP_CLI::error( sprintf( 'Invalid source "%s". Available sources: %s', $source, implode( ', ', $allowed_sources ) ) );
110 }
111
112 WP_CLI::line( sprintf( 'Searching for "%s" in %s...', $search_phrase, $source ) );
113
114 // Perform the search
115 $params = [
116 'searchPhrase' => $search_phrase,
117 'source' => [ $source ],
118 'searchFlags' => $search_flags,
119 'page' => $page,
120 'perPage' => $per_page,
121 'limit' => $limit,
122 'action' => 'nothing',
123 'actionOption' => [],
124 ];
125
126 try {
127 $schema = new Schema\Schema( Source\Manager::get_schema( $params['source'] ) );
128 $filters = [];
129
130 // Add global search filter
131 if ( $params['searchPhrase'] !== '' ) {
132 $filters[] = new Filter\Global_Filter( $params['searchPhrase'], $params['searchFlags'] );
133 }
134
135 // Create the action
136 $action = Action\Action::create( 'nothing', [], $schema );
137
138 // Get sources
139 $sources = Source\Manager::get( $params['source'], $filters );
140
141 // Create the search
142 $search = new Search\Search( $sources );
143
144 // Execute search
145 $results = $search->get_search_results( $action, $params['page'], $params['perPage'], $params['limit'] );
146
147 if ( is_wp_error( $results ) ) {
148 WP_CLI::error( $results->get_error_message() );
149 return;
150 }
151
152 $result_data = $action->get_results( $results );
153
154 // Handle format
155 if ( $format === 'count' ) {
156 WP_CLI::line( sprintf( 'Found %d results', count( $result_data['results'] ) ) );
157 return;
158 }
159
160 if ( count( $result_data['results'] ) === 0 ) {
161 WP_CLI::success( 'No results found.' );
162 return;
163 }
164
165 // Format results for output
166 $output_items = [];
167 foreach ( $result_data['results'] as $result ) {
168 $match_count = 0;
169 $columns_text = [];
170 $matched_texts = $this->extract_matched_texts( $result );
171
172 if ( isset( $result['columns'] ) ) {
173 foreach ( $result['columns'] as $column ) {
174 $match_count += isset( $column['match_count'] ) ? intval( $column['match_count'] ) : 0;
175 $columns_text[] = $column['column_label'];
176 }
177 }
178
179 $output_items[] = [
180 'row_id' => $result['row_id'],
181 'source' => $result['source_name'],
182 'title' => $result['title'] ?? '',
183 'matches' => $match_count,
184 'columns' => implode( ', ', $columns_text ),
185 'matched_text' => implode( ' | ', array_unique( $matched_texts ) ),
186 ];
187 }
188
189 // Output results
190 if ( $format === 'json' ) {
191 $json_output = json_encode( $result_data, JSON_PRETTY_PRINT );
192 WP_CLI::line( $json_output !== false ? $json_output : '{}' );
193 } else {
194 WP_CLI\Utils\format_items( $format, $output_items, [ 'row_id', 'source', 'title', 'matches', 'columns', 'matched_text' ] );
195 }
196
197 // Show totals
198 if ( isset( $result_data['totals'] ) ) {
199 $totals = $result_data['totals'];
200 WP_CLI::line( sprintf(
201 "\nShowing %d results. Total rows in source: %d",
202 count( $result_data['results'] ),
203 $totals['rows']
204 ) );
205 }
206 } catch ( \Exception $e ) {
207 WP_CLI::error( 'Search failed: ' . $e->getMessage() );
208 }
209 }
210
211 /**
212 * Extract matched text from result data
213 *
214 * @param array<string, mixed> $result Result data.
215 * @return string[] Array of matched text strings.
216 */
217 private function extract_matched_texts( $result ) {
218 $matched_texts = [];
219
220 if ( isset( $result['columns'] ) ) {
221 foreach ( $result['columns'] as $column ) {
222 // Extract actual matched text from contexts
223 if ( isset( $column['contexts'] ) ) {
224 foreach ( $column['contexts'] as $context ) {
225 if ( isset( $context['matches'] ) ) {
226 foreach ( $context['matches'] as $match ) {
227 if ( isset( $match['match'] ) ) {
228 $matched_text = $match['match'];
229 // Truncate if too long for table display
230 if ( strlen( $matched_text ) > 50 ) {
231 $matched_text = substr( $matched_text, 0, 47 ) . '...';
232 }
233 $matched_texts[] = $matched_text;
234 }
235 }
236 }
237 }
238 }
239 }
240 }
241
242 return $matched_texts;
243 }
244
245 /**
246 * Perform a search and replace using Search Regex
247 *
248 * ## OPTIONS
249 *
250 * <search>
251 * : The search phrase to find
252 *
253 * <replace>
254 * : The replacement text
255 *
256 * [--source=<source>]
257 * : The source to search in. Available: posts, comment, user, options, post-meta, comment-meta, user-meta, terms
258 * ---
259 * default: posts
260 * ---
261 *
262 * [--regex]
263 * : Use regular expression search
264 *
265 * [--case-insensitive]
266 * : Perform case-insensitive search
267 *
268 * [--per-page=<number>]
269 * : Number of results per page to process
270 * ---
271 * default: 25
272 * ---
273 *
274 * [--limit=<number>]
275 * : Maximum number of results to replace (0 for unlimited)
276 * ---
277 * default: 0
278 * ---
279 *
280 * [--dry-run]
281 * : Show what would be replaced without actually saving changes
282 *
283 * [--format=<format>]
284 * : Output format
285 * ---
286 * default: table
287 * options:
288 * - table
289 * - json
290 * - csv
291 * - yaml
292 * - count
293 * ---
294 *
295 * ## EXAMPLES
296 *
297 * # Replace "hello world" with "hi everyone" in posts (dry run)
298 * $ wp search-regex replace "hello world" "hi everyone" --dry-run
299 *
300 * # Replace using regex in comments
301 * $ wp search-regex replace "email@.*\.com" "email@example.com" --source=comment --regex
302 *
303 * # Case-insensitive replace in options and save changes
304 * $ wp search-regex replace "siteurl" "site_url" --source=options --case-insensitive
305 *
306 * # Replace with a limit
307 * $ wp search-regex replace "test" "production" --limit=10
308 *
309 * @param array<string> $args Positional arguments.
310 * @param array<string, mixed> $assoc_args Associative arguments.
311 * @return void
312 * @when after_wp_load
313 */
314 public function replace( $args, $assoc_args ): void {
315 if ( count( $args ) < 2 ) {
316 WP_CLI::error( 'Both search and replace arguments are required.' );
317 }
318
319 $search_phrase = $args[0];
320 $replace_phrase = $args[1];
321 $source = $assoc_args['source'] ?? 'posts';
322 $per_page = isset( $assoc_args['per-page'] ) ? intval( $assoc_args['per-page'] ) : 25;
323 $limit = isset( $assoc_args['limit'] ) ? intval( $assoc_args['limit'] ) : 0;
324 $format = $assoc_args['format'] ?? 'table';
325 $dry_run = isset( $assoc_args['dry-run'] );
326
327 // Build search flags
328 $search_flags = [];
329 if ( isset( $assoc_args['regex'] ) ) {
330 $search_flags[] = 'regex';
331 }
332 if ( isset( $assoc_args['case-insensitive'] ) ) {
333 $search_flags[] = 'case';
334 }
335
336 // Validate source
337 $allowed_sources = Source\Manager::get_all_source_names();
338 if ( ! in_array( $source, $allowed_sources, true ) ) {
339 WP_CLI::error( sprintf( 'Invalid source "%s". Available sources: %s', $source, implode( ', ', $allowed_sources ) ) );
340 }
341
342 if ( $dry_run ) {
343 WP_CLI::line( sprintf( 'DRY RUN: Searching for "%s" to replace with "%s" in %s...', $search_phrase, $replace_phrase, $source ) );
344 } else {
345 WP_CLI::line( sprintf( 'Replacing "%s" with "%s" in %s...', $search_phrase, $replace_phrase, $source ) );
346 }
347
348 // Perform the search and replace
349 $params = [
350 'searchPhrase' => $search_phrase,
351 'replacement' => $replace_phrase,
352 'source' => [ $source ],
353 'searchFlags' => $search_flags,
354 'page' => 0,
355 'perPage' => $per_page,
356 'limit' => $limit,
357 'action' => 'replace',
358 'actionOption' => [],
359 ];
360
361 try {
362 $schema = new Schema\Schema( Source\Manager::get_schema( $params['source'] ) );
363 $filters = [];
364
365 // Add global search filter
366 if ( $params['searchPhrase'] !== '' ) {
367 $filters[] = new Filter\Global_Filter( $params['searchPhrase'], $params['searchFlags'] );
368 }
369
370 // Create the action
371 $action = Action\Action::create(
372 'replace',
373 [
374 'search' => $params['searchPhrase'],
375 'replacement' => $params['replacement'],
376 'flags' => $params['searchFlags'],
377 ],
378 $schema
379 );
380
381 // Set save mode based on dry-run flag
382 if ( ! $dry_run ) {
383 $action->set_save_mode( true );
384 }
385
386 // Get sources
387 $sources = Source\Manager::get( $params['source'], $filters );
388
389 // Create the search
390 $search = new Search\Search( $sources );
391
392 // Execute search and replace
393 $results = $search->get_search_results( $action, $params['page'], $params['perPage'], $params['limit'] );
394
395 if ( is_wp_error( $results ) ) {
396 WP_CLI::error( $results->get_error_message() );
397 return;
398 }
399
400 $result_data = $action->get_results( $results );
401
402 // Handle format
403 if ( $format === 'count' ) {
404 WP_CLI::line( sprintf( '%s %d results', $dry_run ? 'Would replace' : 'Replaced', count( $result_data['results'] ) ) );
405 return;
406 }
407
408 if ( count( $result_data['results'] ) === 0 ) {
409 WP_CLI::success( 'No results found.' );
410 return;
411 }
412
413 // Format results for output
414 $output_items = [];
415 foreach ( $result_data['results'] as $result ) {
416 $match_count = 0;
417 $columns_text = [];
418 $matched_texts = $this->extract_matched_texts( $result );
419
420 if ( isset( $result['columns'] ) ) {
421 foreach ( $result['columns'] as $column ) {
422 $match_count += isset( $column['match_count'] ) ? intval( $column['match_count'] ) : 0;
423 $columns_text[] = $column['column_label'];
424 }
425 }
426
427 $output_items[] = [
428 'row_id' => $result['row_id'],
429 'source' => $result['source_name'],
430 'title' => $result['title'] ?? '',
431 'matches' => $match_count,
432 'columns' => implode( ', ', $columns_text ),
433 'matched_text' => implode( ' | ', array_unique( $matched_texts ) ),
434 ];
435 }
436
437 // Output results
438 if ( $format === 'json' ) {
439 $json_output = json_encode( $result_data, JSON_PRETTY_PRINT );
440 WP_CLI::line( $json_output !== false ? $json_output : '{}' );
441 } else {
442 WP_CLI\Utils\format_items( $format, $output_items, [ 'row_id', 'source', 'title', 'matches', 'columns', 'matched_text' ] );
443 }
444
445 // Show totals
446 if ( isset( $result_data['totals'] ) ) {
447 $totals = $result_data['totals'];
448 $status_message = sprintf(
449 "\n%s %d results. Total rows in source: %d",
450 $dry_run ? 'Would replace' : 'Replaced',
451 count( $result_data['results'] ),
452 $totals['rows']
453 );
454
455 if ( $dry_run ) {
456 WP_CLI::warning( $status_message . ' (No changes saved - dry run mode)' );
457 } else {
458 WP_CLI::success( $status_message );
459 }
460 }
461 } catch ( \Exception $e ) {
462 WP_CLI::error( 'Replace failed: ' . $e->getMessage() );
463 }
464 }
465
466 /**
467 * List available search sources
468 *
469 * ## OPTIONS
470 *
471 * [--format=<format>]
472 * : Output format
473 * ---
474 * default: table
475 * options:
476 * - table
477 * - json
478 * - csv
479 * - yaml
480 * ---
481 *
482 * ## EXAMPLES
483 *
484 * # List all available sources
485 * $ wp search-regex sources
486 *
487 * @param array<string> $_args Positional arguments.
488 * @param array<string, mixed> $assoc_args Associative arguments.
489 * @return void
490 * @when after_wp_load
491 */
492 public function sources( $_args, $assoc_args ): void {
493 $format = $assoc_args['format'] ?? 'table';
494
495 $sources = Source\Manager::get_all_sources();
496 $output_items = [];
497
498 foreach ( $sources as $source ) {
499 $output_items[] = [
500 'name' => $source['name'],
501 'label' => $source['label'],
502 'type' => $source['type'],
503 ];
504 }
505
506 WP_CLI\Utils\format_items( $format, $output_items, [ 'name', 'label', 'type' ] );
507 }
508 }
509
510 // Register the WP CLI command
511 if ( defined( 'WP_CLI' ) && WP_CLI ) {
512 WP_CLI::add_command( 'search-regex', \SearchRegex\Cli\Search_Regex_CLI::class );
513 }
514