Skip to content

Commit c51d92f

Browse files
authored
fix(cli): decode keys before minimatch comparison in --key filtering (#2017)
* fix(cli): decode URL-encoded keys before minimatch comparison in --key filtering * fix(cli): safely decode keys
1 parent de1d172 commit c51d92f

File tree

4 files changed

+24
-6
lines changed

4 files changed

+24
-6
lines changed

.changeset/salty-sails-sell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"lingo.dev": patch
3+
---
4+
5+
Fixed --key pattern matching in run and purge commands by decoding URL-encoded keys before minimatch comparison

packages/cli/src/cli/cmd/purge.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getBuckets } from "../utils/buckets";
66
import { resolveOverriddenLocale } from "@lingo.dev/_spec";
77
import createBucketLoader from "../loaders";
88
import { minimatch } from "minimatch";
9+
import { safeDecode } from "../utils/key-matching";
910
import { confirm } from "@inquirer/prompts";
1011

1112
interface PurgeOptions {
@@ -123,7 +124,7 @@ export default new Command()
123124
if (options.key) {
124125
// minimatch for key patterns
125126
keysToRemove = Object.keys(newData).filter((k) =>
126-
minimatch(k, options.key!),
127+
minimatch(safeDecode(k), safeDecode(options.key!)),
127128
);
128129
} else {
129130
// No key specified: remove all keys
@@ -133,7 +134,7 @@ export default new Command()
133134
// Show what will be deleted
134135
if (options.key) {
135136
bucketOra.info(
136-
`About to delete ${keysToRemove.length} key(s) matching '${options.key}' from ${bucketPath.pathPattern} [${targetLocale}]:\n ${keysToRemove.slice(0, 10).join(", ")}${keysToRemove.length > 10 ? ", ..." : ""}`,
137+
`About to delete ${keysToRemove.length} key(s) matching '${safeDecode(options.key)}' from ${bucketPath.pathPattern} [${targetLocale}]:\n ${keysToRemove.slice(0, 10).join(", ")}${keysToRemove.length > 10 ? ", ..." : ""}`,
137138
);
138139
} else {
139140
bucketOra.info(
@@ -161,7 +162,7 @@ export default new Command()
161162
await bucketLoader.push(targetLocale, newData);
162163
if (options.key) {
163164
bucketOra.succeed(
164-
`Removed ${keysToRemove.length} key(s) matching '${options.key}' from ${bucketPath.pathPattern} [${targetLocale}]`,
165+
`Removed ${keysToRemove.length} key(s) matching '${safeDecode(options.key)}' from ${bucketPath.pathPattern} [${targetLocale}]`,
165166
);
166167
} else {
167168
bucketOra.succeed(
@@ -170,7 +171,7 @@ export default new Command()
170171
}
171172
} else if (options.key) {
172173
bucketOra.info(
173-
`No keys matching '${options.key}' found in ${bucketPath.pathPattern} [${targetLocale}]`,
174+
`No keys matching '${safeDecode(options.key)}' found in ${bucketPath.pathPattern} [${targetLocale}]`,
174175
);
175176
} else {
176177
bucketOra.info("No keys to remove.");

packages/cli/src/cli/cmd/run/execute.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import pLimit, { LimitFunction } from "p-limit";
44
import _ from "lodash";
55
import { minimatch } from "minimatch";
66

7+
import { safeDecode } from "../../utils/key-matching";
78
import { colors } from "../../constants";
89
import { CmdRunContext, CmdRunTask, CmdRunTaskResult } from "./_types";
910
import { commonTaskRendererOptions } from "./_const";
@@ -231,7 +232,7 @@ function createWorkerTask(args: {
231232
([key]) =>
232233
!assignedTask.onlyKeys.length ||
233234
assignedTask.onlyKeys?.some((pattern) =>
234-
minimatch(key, pattern),
235+
minimatch(safeDecode(key), safeDecode(pattern)),
235236
),
236237
)
237238
.fromPairs()

packages/cli/src/cli/utils/key-matching.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
import { minimatch } from "minimatch";
22

3+
/**
4+
* Safely decodes a URI component, returning the original string if decoding fails.
5+
*/
6+
export function safeDecode(value: string): string {
7+
try {
8+
return decodeURIComponent(value);
9+
} catch {
10+
return value;
11+
}
12+
}
13+
314
/**
415
* Checks if a key matches any of the provided patterns using exact, separator-bounded prefix, or glob matching.
516
* Separator-bounded means the key must equal the pattern exactly, or continue with a ".", "/", or "-" separator.
6-
* This prevents "inbox" from matching "inbox_url" while still matching "inbox.title", "inbox/details", or "heading-0".
17+
* This prevents "inbox" from matching "inbox_url" while still matching "inbox.title", "inbox/details", or "inbox-0".
718
*/
819
export function matchesKeyPattern(key: string, patterns: string[]): boolean {
920
return patterns.some(

0 commit comments

Comments
 (0)