Skip to content

Commit 5260d25

Browse files
committed
Fix markup bug and simplify wildcard detection logic
- Fix: Escape patterns and keys using Markup.Escape() to prevent Spectre.Console markup interpretation - Fix: Apply escaping to all user-provided strings that appear in markup (patterns, keys, paths) - Simplify: Remove smart detection logic - --regex flag is the sole determinant - Update: Tests to reflect simplified detection logic (wildcards detected regardless of regex syntax) The --regex flag takes precedence over automatic wildcard detection, so we don't need to check for regex-specific syntax in IsWildcardPattern(). This makes the logic clearer and more predictable.
1 parent 25020cd commit 5260d25

6 files changed

Lines changed: 477 additions & 26 deletions

File tree

COMMANDS.md

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ Summary Table:
163163

164164
**Modes:**
165165
- **Exact match (default):** View a single specific key
166-
- **Regex mode (with --regex):** View all keys matching a pattern
166+
- **Wildcard mode (automatic):** Use `*` and `?` for simple pattern matching
167+
- **Regex mode (with --regex):** View all keys matching a regex pattern
167168

168169
**Examples:**
169170

@@ -182,6 +183,40 @@ lrm view SaveButton --format json
182183
lrm view SaveButton --format simple
183184
```
184185

186+
**Wildcard Patterns (Automatic Detection):**
187+
```bash
188+
# View all Error keys (App.* → App followed by anything)
189+
lrm view "Error.*"
190+
191+
# View all keys ending with .Text
192+
lrm view "*.Text"
193+
194+
# View all Button keys
195+
lrm view "Button.*"
196+
197+
# View all keys (match everything)
198+
lrm view "*"
199+
200+
# View numbered items with single digit (Item1, Item2, etc.)
201+
lrm view "Item?"
202+
203+
# View keys with exactly 4 characters
204+
lrm view "????"
205+
206+
# View all keys containing "Error" anywhere
207+
lrm view "*Error*"
208+
209+
# Combine wildcards: keys starting with App and ending with Text
210+
lrm view "App.*Text"
211+
212+
# Escape wildcards to match literal * or ?
213+
lrm view "Special\*Key" # Matches literal asterisk
214+
lrm view "Test\?Value" # Matches literal question mark
215+
216+
# With sorting and limit
217+
lrm view "Button.*" --sort --limit 10
218+
```
219+
185220
**Multiple Keys (Regex Pattern):**
186221
```bash
187222
# View all Error keys
@@ -209,6 +244,22 @@ lrm view ".*Label.*" --regex --limit 50
209244
lrm view "Error\..*" --regex --format json
210245
```
211246

247+
**Wildcards vs Regex:**
248+
249+
Wildcards are simpler and more intuitive for most users:
250+
- `*` matches zero or more characters (like `.*` in regex)
251+
- `?` matches exactly one character (like `.` in regex)
252+
- Automatically detected - no flag needed
253+
- Special regex chars are escaped automatically
254+
255+
Use explicit `--regex` when you need:
256+
- Alternation: `(Error|Warning)\..*`
257+
- Character classes: `Item[0-9]+`
258+
- Anchors: `^Start` or `End$`
259+
- Quantifiers: `Item[0-9]{2,4}`
260+
261+
The tool automatically detects wildcards if the pattern contains `*` or `?` but doesn't have regex-specific syntax like `^`, `$`, `[`, `(`, `+`, or `|`.
262+
212263
**Output formats:**
213264

214265
**Table (single key):**
@@ -225,9 +276,9 @@ Key: SaveButton
225276
Present in 2/2 language(s), 0 empty value(s)
226277
```
227278

228-
**Table (multiple keys with regex):**
279+
**Table (multiple keys with wildcard):**
229280
```
230-
Pattern: Error\..*
281+
Pattern: Error.* (wildcard)
231282
Matched 3 key(s)
232283
233284
┌───────────────────┬──────────────┬─────────────────┐

Commands/ViewCommand.cs

Lines changed: 141 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public override int Execute(CommandContext context, Settings settings, Cancellat
7474
var format = settings.GetOutputFormat();
7575
if (format == OutputFormat.Table)
7676
{
77-
AnsiConsole.MarkupLine($"[blue]Scanning:[/] {resourcePath}");
77+
AnsiConsole.MarkupLine($"[blue]Scanning:[/] {Markup.Escape(resourcePath)}");
7878
AnsiConsole.WriteLine();
7979
}
8080

@@ -116,6 +116,16 @@ public override int Execute(CommandContext context, Settings settings, Cancellat
116116

117117
// Find matching keys
118118
List<string> matchedKeys;
119+
bool usedWildcards = false;
120+
string originalPattern = settings.Key;
121+
122+
// Auto-detect and convert wildcard patterns to regex
123+
if (!settings.UseRegex && IsWildcardPattern(settings.Key))
124+
{
125+
settings.Key = ConvertWildcardToRegex(settings.Key);
126+
settings.UseRegex = true;
127+
usedWildcards = true;
128+
}
119129

120130
if (settings.UseRegex)
121131
{
@@ -149,7 +159,7 @@ public override int Execute(CommandContext context, Settings settings, Cancellat
149159
var existingEntry = defaultFile.Entries.FirstOrDefault(e => e.Key == settings.Key);
150160
if (existingEntry == null)
151161
{
152-
AnsiConsole.MarkupLine($"[red]✗ Key '{settings.Key}' not found![/]");
162+
AnsiConsole.MarkupLine($"[red]✗ Key '{Markup.Escape(settings.Key)}' not found![/]");
153163
return 1;
154164
}
155165
matchedKeys = new List<string> { settings.Key };
@@ -178,23 +188,25 @@ public override int Execute(CommandContext context, Settings settings, Cancellat
178188
// Check if we have any matches
179189
if (matchedKeys.Count == 0)
180190
{
181-
var patternType = settings.UseRegex ? "pattern" : "key";
182-
AnsiConsole.MarkupLine($"[red]✗ No keys match {patternType} '{settings.Key}'[/]");
191+
var patternType = usedWildcards ? "wildcard" : (settings.UseRegex ? "pattern" : "key");
192+
var displayPattern = usedWildcards ? originalPattern : settings.Key;
193+
// Escape pattern to prevent Spectre.Console markup interpretation
194+
AnsiConsole.MarkupLine($"[red]✗ No keys match {patternType} '{Markup.Escape(displayPattern)}'[/]");
183195
return 1;
184196
}
185197

186198
// Display based on format
187199
switch (format)
188200
{
189201
case OutputFormat.Json:
190-
DisplayJson(matchedKeys, resourceFiles, settings.ShowComments, settings);
202+
DisplayJson(matchedKeys, resourceFiles, settings.ShowComments, settings, usedWildcards, originalPattern);
191203
break;
192204
case OutputFormat.Simple:
193-
DisplaySimple(matchedKeys, resourceFiles, settings.ShowComments, settings);
205+
DisplaySimple(matchedKeys, resourceFiles, settings.ShowComments, settings, usedWildcards, originalPattern);
194206
break;
195207
case OutputFormat.Table:
196208
default:
197-
DisplayTable(matchedKeys, resourceFiles, settings.ShowComments, settings);
209+
DisplayTable(matchedKeys, resourceFiles, settings.ShowComments, settings, usedWildcards, originalPattern);
198210
break;
199211
}
200212

@@ -221,7 +233,7 @@ private void DisplayConfigNotice(Settings settings)
221233
}
222234
}
223235

224-
private void DisplayTable(List<string> keys, List<Core.Models.ResourceFile> resourceFiles, bool showComments, Settings settings)
236+
private void DisplayTable(List<string> keys, List<Core.Models.ResourceFile> resourceFiles, bool showComments, Settings settings, bool usedWildcards, string originalPattern)
225237
{
226238
DisplayConfigNotice(settings);
227239

@@ -233,13 +245,13 @@ private void DisplayTable(List<string> keys, List<Core.Models.ResourceFile> reso
233245
else
234246
{
235247
// Multiple keys - new grouped format
236-
DisplayMultipleKeysTable(keys, resourceFiles, showComments, settings);
248+
DisplayMultipleKeysTable(keys, resourceFiles, showComments, settings, usedWildcards, originalPattern);
237249
}
238250
}
239251

240252
private void DisplaySingleKeyTable(string key, List<Core.Models.ResourceFile> resourceFiles, bool showComments)
241253
{
242-
AnsiConsole.MarkupLine($"[yellow]Key:[/] [bold]{key}[/]");
254+
AnsiConsole.MarkupLine($"[yellow]Key:[/] [bold]{Markup.Escape(key)}[/]");
243255
AnsiConsole.WriteLine();
244256

245257
var table = new Table();
@@ -303,9 +315,24 @@ private void DisplaySingleKeyTable(string key, List<Core.Models.ResourceFile> re
303315
AnsiConsole.MarkupLine($"[dim]Present in {present}/{total} language(s), {empty} empty value(s)[/]");
304316
}
305317

306-
private void DisplayMultipleKeysTable(List<string> keys, List<Core.Models.ResourceFile> resourceFiles, bool showComments, Settings settings)
318+
private void DisplayMultipleKeysTable(List<string> keys, List<Core.Models.ResourceFile> resourceFiles, bool showComments, Settings settings, bool usedWildcards, string originalPattern)
307319
{
308-
var patternDisplay = settings.UseRegex ? $"Pattern: {settings.Key}" : $"Keys: {keys.Count}";
320+
string patternDisplay;
321+
if (usedWildcards)
322+
{
323+
// Escape pattern to prevent Spectre.Console markup interpretation
324+
patternDisplay = $"Pattern: {Markup.Escape(originalPattern)} [dim](wildcard)[/]";
325+
}
326+
else if (settings.UseRegex)
327+
{
328+
// Escape pattern to prevent Spectre.Console markup interpretation
329+
patternDisplay = $"Pattern: {Markup.Escape(originalPattern)} [dim](regex)[/]";
330+
}
331+
else
332+
{
333+
patternDisplay = $"Keys: {keys.Count}";
334+
}
335+
309336
AnsiConsole.MarkupLine($"[yellow]{patternDisplay}[/]");
310337
AnsiConsole.MarkupLine($"[dim]Matched {keys.Count} key(s)[/]");
311338
AnsiConsole.WriteLine();
@@ -353,7 +380,7 @@ private void DisplayMultipleKeysTable(List<string> keys, List<Core.Models.Resour
353380
AnsiConsole.MarkupLine($"[dim]Showing {keys.Count} key(s) across {resourceFiles.Count} language(s)[/]");
354381
}
355382

356-
private void DisplayJson(List<string> keys, List<Core.Models.ResourceFile> resourceFiles, bool showComments, Settings settings)
383+
private void DisplayJson(List<string> keys, List<Core.Models.ResourceFile> resourceFiles, bool showComments, Settings settings, bool usedWildcards, string originalPattern)
357384
{
358385
if (keys.Count == 1)
359386
{
@@ -363,7 +390,7 @@ private void DisplayJson(List<string> keys, List<Core.Models.ResourceFile> resou
363390
else
364391
{
365392
// Multiple keys - array format
366-
DisplayMultipleKeysJson(keys, resourceFiles, showComments, settings);
393+
DisplayMultipleKeysJson(keys, resourceFiles, showComments, settings, usedWildcards, originalPattern);
367394
}
368395
}
369396

@@ -399,7 +426,7 @@ private void DisplaySingleKeyJson(string key, List<Core.Models.ResourceFile> res
399426
Console.WriteLine(OutputFormatter.FormatJson(output));
400427
}
401428

402-
private void DisplayMultipleKeysJson(List<string> keys, List<Core.Models.ResourceFile> resourceFiles, bool showComments, Settings settings)
429+
private void DisplayMultipleKeysJson(List<string> keys, List<Core.Models.ResourceFile> resourceFiles, bool showComments, Settings settings, bool usedWildcards, string originalPattern)
403430
{
404431
var keyObjects = new List<object>();
405432

@@ -434,15 +461,16 @@ private void DisplayMultipleKeysJson(List<string> keys, List<Core.Models.Resourc
434461

435462
var output = new
436463
{
437-
pattern = settings.UseRegex ? settings.Key : (string?)null,
464+
pattern = settings.UseRegex || usedWildcards ? originalPattern : (string?)null,
465+
patternType = usedWildcards ? "wildcard" : (settings.UseRegex ? "regex" : (string?)null),
438466
matchCount = keys.Count,
439467
keys = keyObjects
440468
};
441469

442470
Console.WriteLine(OutputFormatter.FormatJson(output));
443471
}
444472

445-
private void DisplaySimple(List<string> keys, List<Core.Models.ResourceFile> resourceFiles, bool showComments, Settings settings)
473+
private void DisplaySimple(List<string> keys, List<Core.Models.ResourceFile> resourceFiles, bool showComments, Settings settings, bool usedWildcards, string originalPattern)
446474
{
447475
if (keys.Count == 1)
448476
{
@@ -452,7 +480,7 @@ private void DisplaySimple(List<string> keys, List<Core.Models.ResourceFile> res
452480
else
453481
{
454482
// Multiple keys
455-
DisplayMultipleKeysSimple(keys, resourceFiles, showComments, settings);
483+
DisplayMultipleKeysSimple(keys, resourceFiles, showComments, settings, usedWildcards, originalPattern);
456484
}
457485
}
458486

@@ -476,9 +504,22 @@ private void DisplaySingleKeySimple(string key, List<Core.Models.ResourceFile> r
476504
}
477505
}
478506

479-
private void DisplayMultipleKeysSimple(List<string> keys, List<Core.Models.ResourceFile> resourceFiles, bool showComments, Settings settings)
507+
private void DisplayMultipleKeysSimple(List<string> keys, List<Core.Models.ResourceFile> resourceFiles, bool showComments, Settings settings, bool usedWildcards, string originalPattern)
480508
{
481-
var patternDisplay = settings.UseRegex ? $"Pattern: {settings.Key}" : $"Keys: {keys.Count}";
509+
string patternDisplay;
510+
if (usedWildcards)
511+
{
512+
patternDisplay = $"Pattern: {originalPattern} (wildcard)";
513+
}
514+
else if (settings.UseRegex)
515+
{
516+
patternDisplay = $"Pattern: {originalPattern} (regex)";
517+
}
518+
else
519+
{
520+
patternDisplay = $"Keys: {keys.Count}";
521+
}
522+
482523
Console.WriteLine(patternDisplay);
483524
Console.WriteLine($"Matched {keys.Count} key(s)");
484525
Console.WriteLine();
@@ -511,4 +552,84 @@ private void DisplayMultipleKeysSimple(List<string> keys, List<Core.Models.Resou
511552
}
512553
}
513554
}
555+
556+
/// <summary>
557+
/// Detects if a pattern contains wildcard characters (* or ?) that should be converted to regex.
558+
/// Handles backslash escaping for literal wildcard characters.
559+
/// </summary>
560+
internal static bool IsWildcardPattern(string pattern)
561+
{
562+
// Simply check if pattern contains unescaped wildcards
563+
// The --regex flag takes precedence, so we don't need "smart" detection
564+
for (int i = 0; i < pattern.Length; i++)
565+
{
566+
char c = pattern[i];
567+
568+
// Check if this is an escaped character
569+
if (c == '\\' && i + 1 < pattern.Length)
570+
{
571+
i++; // Skip next character
572+
continue;
573+
}
574+
575+
// Check for unescaped wildcards
576+
if (c == '*' || c == '?')
577+
{
578+
return true;
579+
}
580+
}
581+
582+
return false;
583+
}
584+
585+
/// <summary>
586+
/// Converts a wildcard pattern to a regex pattern.
587+
/// Supports:
588+
/// - * for zero or more characters
589+
/// - ? for exactly one character
590+
/// - \* and \? for literal asterisk and question mark
591+
/// </summary>
592+
internal static string ConvertWildcardToRegex(string wildcardPattern)
593+
{
594+
var result = new System.Text.StringBuilder();
595+
596+
for (int i = 0; i < wildcardPattern.Length; i++)
597+
{
598+
char c = wildcardPattern[i];
599+
600+
if (c == '\\' && i + 1 < wildcardPattern.Length)
601+
{
602+
char next = wildcardPattern[i + 1];
603+
if (next == '*' || next == '?')
604+
{
605+
// Escaped wildcard - treat as literal
606+
result.Append('\\').Append(next);
607+
i++; // Skip next character
608+
}
609+
else
610+
{
611+
// Other escaped character - escape for regex
612+
result.Append(Regex.Escape(c.ToString()));
613+
}
614+
}
615+
else if (c == '*')
616+
{
617+
// Wildcard: match any characters
618+
result.Append(".*");
619+
}
620+
else if (c == '?')
621+
{
622+
// Wildcard: match single character
623+
result.Append('.');
624+
}
625+
else
626+
{
627+
// Regular character - escape for regex
628+
result.Append(Regex.Escape(c.ToString()));
629+
}
630+
}
631+
632+
// Anchor to match entire string
633+
return "^" + result.ToString() + "$";
634+
}
514635
}

0 commit comments

Comments
 (0)