Skip to content

Commit 0a8cd93

Browse files
authored
✨ Make find-replace safer, more useful. (#694)
Before, if you omitted --replace, the tool would proceed with replacing whatever the --find option was with "null", it seems. Now, it is documented to be safe to omit "--replace". If so, you can get either a summary or --verbose report of what matched. Also, Using --replace with no argument is not considered a mistake of converting the value to null. If you want to replace something with an empty string, you have to explicitly give an empty string.
1 parent 2ad93f8 commit 0a8cd93

File tree

3 files changed

+61
-5
lines changed

3 files changed

+61
-5
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,12 +414,18 @@ gctools delete-empty-tags <apiURL> <adminAPIKey> --maxPostCount 5 --delayBetween
414414

415415
### find-replace
416416

417-
Find & replace strings of text within Ghost posts
417+
Find & replace strings of text within Ghost posts. If `--replace` is omitted, the command runs in dry-run mode and reports the number of matches without making any changes.
418418

419419
```sh
420420
# See all available options
421421
gctools find-replace --help
422422

423+
# Dry run: find matches without replacing anything
424+
gctools find-replace <apiURL> <adminAPIKey> --find 'Old text'
425+
426+
# Dry run with detailed per-post, per-field match report
427+
gctools find-replace <apiURL> <adminAPIKey> --find 'Old text' --where all -V
428+
423429
# Replace a string but only in the `mobiledoc` and `title`
424430
gctools find-replace <apiURL> <adminAPIKey> --find 'Old text' --replace 'New text' --where mobiledoc,title
425431

@@ -433,6 +439,8 @@ gctools find-replace <apiURL> <adminAPIKey> --tag world-news --find 'Old text' -
433439
gctools find-replace <apiURL> <adminAPIKey> --find 'Old text' --replace 'New text' --delayBetweenCalls 100
434440
```
435441

442+
Use `-V` (`--verbose`) for detailed output showing which fields matched or were replaced in each post.
443+
436444
Available `where` fields are:
437445

438446
* `all`

commands/find-replace.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const setup = (sywac) => {
3030
});
3131
sywac.string('--replace', {
3232
defaultValue: null,
33-
desc: 'Replace with'
33+
desc: 'Replace with (omit to do a dry run)'
3434
});
3535
sywac.array('--where', {
3636
defaultValue: 'mobiledoc',
@@ -52,6 +52,13 @@ const run = async (argv) => {
5252
let timer = Date.now();
5353
let context = {errors: []};
5454

55+
// Validate --replace: if the flag is present but has no argument,
56+
// sywac may coerce it to a non-string value (e.g. boolean true).
57+
if (argv.replace !== null && typeof argv.replace !== 'string') {
58+
ui.log.error(`--replace requires an argument. Provide '' to replace text with an empty string.`);
59+
return;
60+
}
61+
5562
if (argv.where.includes('all')) {
5663
argv.where = ['mobiledoc', 'html', 'lexical', 'title', 'slug', 'custom_excerpt', 'meta_title', 'meta_description', 'twitter_title', 'twitter_description', 'og_title', 'og_description', 'feature_image', 'codeinjection_head', 'codeinjection_foot'];
5764
}
@@ -66,8 +73,10 @@ const run = async (argv) => {
6673
ui.log.error('Done with errors', context.errors);
6774
}
6875

69-
// Report success
70-
ui.log.ok(`Successfully updated ${context.updated.length} strings in ${Date.now() - timer}ms.`);
76+
if (argv.replace !== null) {
77+
// Report success for replace mode
78+
ui.log.ok(`Successfully updated ${context.updated.length} strings in ${Date.now() - timer}ms.`);
79+
}
7180
};
7281

7382
export default {

tasks/find-replace.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Promise from 'bluebird';
22
import GhostAdminAPI from '@tryghost/admin-api';
33
import {makeTaskRunner} from '@tryghost/listr-smart-renderer';
4+
import {ui} from '@tryghost/pretty-cli';
45
import _ from 'lodash';
56
import {transformToCommaString} from '../lib/utils.js';
67
import {discover} from '../lib/batch-ghost-discover.js';
@@ -36,6 +37,8 @@ const initialise = (options) => {
3637
};
3738

3839
const getFullTaskList = (options) => {
40+
const dryRun = options.replace === null;
41+
3942
return [
4043
initialise(options),
4144
{
@@ -66,25 +69,60 @@ const getFullTaskList = (options) => {
6669
task: async (ctx) => {
6770
await Promise.mapSeries(ctx.posts, async (post) => {
6871
let matches = [];
72+
let matchesByField = {};
6973

7074
ctx.args.where.forEach((key) => {
7175
if (post[key]) {
7276
let match = post[key].match(ctx.regex);
7377
if (match) {
7478
matches.push(...match);
79+
matchesByField[key] = match.length;
7580
}
7681
}
7782
});
7883

7984
if (matches.length > 0) {
8085
post.matches = matches;
86+
post.matchesByField = matchesByField;
8187
ctx.toUpdate.push(post);
8288
}
8389
});
8490
}
8591
},
92+
{
93+
title: 'Reporting matches',
94+
enabled: () => dryRun,
95+
task: async (ctx, task) => {
96+
if (ctx.toUpdate.length === 0) {
97+
task.title = 'No matches found';
98+
return;
99+
}
100+
101+
let totalMatches = 0;
102+
103+
for (const post of ctx.toUpdate) {
104+
for (const [, count] of Object.entries(post.matchesByField)) {
105+
totalMatches += count;
106+
}
107+
}
108+
109+
if (ctx.args.verbose) {
110+
ui.log.info('');
111+
for (const post of ctx.toUpdate) {
112+
for (const [field, count] of Object.entries(post.matchesByField)) {
113+
ui.log.info(` ${count}:${field}:${post.title}`);
114+
}
115+
}
116+
ui.log.info('');
117+
}
118+
119+
task.title = `Found ${totalMatches} matches across ${ctx.toUpdate.length} posts`;
120+
task.output = `Add --replace '<string>' to replace them.`;
121+
}
122+
},
86123
{
87124
title: 'Replacing text',
125+
enabled: () => !dryRun,
88126
task: async (ctx) => {
89127
let tasks = [];
90128

@@ -98,8 +136,9 @@ const getFullTaskList = (options) => {
98136
}
99137
});
100138

101-
// Delete the matches object or else the request gets denied
139+
// Delete the matches objects or else the request gets denied
102140
delete post.matches;
141+
delete post.matchesByField;
103142

104143
try {
105144
let result = await ctx.api.posts.edit(post);

0 commit comments

Comments
 (0)