Skip to content

Commit c48f281

Browse files
committed
Add support for limiting replacements
- Also update string mode replacement implementation
1 parent 49194d3 commit c48f281

File tree

7 files changed

+377
-310
lines changed

7 files changed

+377
-310
lines changed

src/app.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@ func GetApp() *cli.App {
9898
Aliases: []string{"r"},
9999
Usage: "Replacement `<string>`. If omitted, defaults to an empty string. Supports built-in and regex capture variables",
100100
},
101+
&cli.UintFlag{
102+
Name: "replace-limit",
103+
Aliases: []string{"l"},
104+
Usage: "Limit the number of replacements to be made (replaces all matches if set to 0)",
105+
Value: 0,
106+
DefaultText: "0",
107+
},
108+
&cli.BoolFlag{
109+
Name: "string-mode",
110+
Aliases: []string{"s"},
111+
Usage: "Opt into string literal mode by treating find expressions as non-regex strings",
112+
},
101113
&cli.StringSliceFlag{
102114
Name: "exclude",
103115
Aliases: []string{"E"},
@@ -113,7 +125,7 @@ func GetApp() *cli.App {
113125
Aliases: []string{"R"},
114126
Usage: "Rename files recursively",
115127
},
116-
&cli.IntFlag{
128+
&cli.UintFlag{
117129
Name: "max-depth",
118130
Aliases: []string{"m"},
119131
Usage: "positive `<integer>` indicating the maximum depth for a recursive search (set to 0 for no limit)",
@@ -168,11 +180,6 @@ func GetApp() *cli.App {
168180
Aliases: []string{"F"},
169181
Usage: "Fix any detected conflicts with auto indexing",
170182
},
171-
&cli.BoolFlag{
172-
Name: "string-mode",
173-
Aliases: []string{"s"},
174-
Usage: "Opt into string literal mode by treating find expressions as non-regex strings",
175-
},
176183
},
177184
UseShortOptionHandling: true,
178185
Action: func(c *cli.Context) error {

src/operation.go

Lines changed: 36 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -61,32 +61,33 @@ type renameError struct {
6161

6262
// Operation represents a batch renaming operation
6363
type Operation struct {
64-
paths []Change
65-
matches []Change
66-
conflicts map[conflict][]Conflict
67-
findString string
68-
replacement string
69-
startNumber int
70-
exec bool
71-
fixConflicts bool
72-
includeHidden bool
73-
includeDir bool
74-
onlyDir bool
75-
ignoreCase bool
76-
ignoreExt bool
77-
searchRegex *regexp.Regexp
78-
directories []string
79-
recursive bool
80-
workingDir string
81-
stringMode bool
82-
excludeFilter []string
83-
maxDepth int
84-
sort string
85-
reverseSort bool
86-
quiet bool
87-
errors []renameError
88-
revert bool
89-
numberOffset []int
64+
paths []Change
65+
matches []Change
66+
conflicts map[conflict][]Conflict
67+
findString string
68+
replacement string
69+
startNumber int
70+
exec bool
71+
fixConflicts bool
72+
includeHidden bool
73+
includeDir bool
74+
onlyDir bool
75+
ignoreCase bool
76+
ignoreExt bool
77+
searchRegex *regexp.Regexp
78+
directories []string
79+
recursive bool
80+
workingDir string
81+
stringLiteralMode bool
82+
excludeFilter []string
83+
maxDepth int
84+
sort string
85+
reverseSort bool
86+
quiet bool
87+
errors []renameError
88+
revert bool
89+
numberOffset []int
90+
replaceLimit int
9091
}
9192

9293
type backupFile struct {
@@ -427,20 +428,6 @@ func (op *Operation) findMatches() error {
427428
f = filenameWithoutExtension(f)
428429
}
429430

430-
if op.stringMode {
431-
findStr := op.findString
432-
433-
if op.ignoreCase {
434-
f = strings.ToLower(f)
435-
findStr = strings.ToLower(findStr)
436-
}
437-
438-
if strings.Contains(f, findStr) {
439-
op.matches = append(op.matches, v)
440-
}
441-
continue
442-
}
443-
444431
matched := op.searchRegex.MatchString(f)
445432
if matched {
446433
op.matches = append(op.matches, v)
@@ -564,11 +551,12 @@ func setOptions(op *Operation, c *cli.Context) error {
564551
op.recursive = c.Bool("recursive")
565552
op.directories = c.Args().Slice()
566553
op.onlyDir = c.Bool("only-dir")
567-
op.stringMode = c.Bool("string-mode")
554+
op.stringLiteralMode = c.Bool("string-mode")
568555
op.excludeFilter = c.StringSlice("exclude")
569-
op.maxDepth = c.Int("max-depth")
556+
op.maxDepth = int(c.Uint("max-depth"))
570557
op.quiet = c.Bool("quiet")
571558
op.revert = c.Bool("undo")
559+
op.replaceLimit = int(c.Uint("replace-limit"))
572560

573561
// Sorting
574562
if c.String("sort") != "" {
@@ -583,6 +571,12 @@ func setOptions(op *Operation, c *cli.Context) error {
583571
}
584572

585573
findPattern := c.String("find")
574+
575+
// Escape all regular expression metacharacters in string literal mode
576+
if op.stringLiteralMode {
577+
findPattern = regexp.QuoteMeta(findPattern)
578+
}
579+
586580
// Match entire string if find pattern is empty
587581
if findPattern == "" {
588582
findPattern = ".*"

src/operation_linux_test.go

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ package f2
44

55
import (
66
"path/filepath"
7-
"regexp"
87
"testing"
98
)
109

@@ -59,41 +58,3 @@ func TestCaseConversion(t *testing.T) {
5958

6059
runFindReplace(t, cases)
6160
}
62-
63-
func TestTransformation(t *testing.T) {
64-
cases := []struct {
65-
input string
66-
transform string
67-
find string
68-
output string
69-
}{
70-
{
71-
input: `abc<>_{}*?\/\.epub`,
72-
transform: `\Twin`,
73-
find: `abc.*`,
74-
output: "abc_{}.epub",
75-
},
76-
{
77-
input: `abc<>_{}*:?\/\.epub`,
78-
transform: `\Tmac`,
79-
find: `abc.*`,
80-
output: `abc<>_{}*?\/\.epub`,
81-
},
82-
}
83-
84-
for _, v := range cases {
85-
op := &Operation{}
86-
op.replacement = v.transform
87-
regex, err := regexp.Compile(v.find)
88-
if err != nil {
89-
t.Fatalf("Unexpected error: %v", err)
90-
}
91-
92-
op.searchRegex = regex
93-
out := op.replaceString(v.input)
94-
95-
if out != v.output {
96-
t.Fatalf("Expected %s, but got: %s", v.output, out)
97-
}
98-
}
99-
}

src/operation_test.go

Lines changed: 0 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -236,192 +236,6 @@ func runFindReplace(t *testing.T, cases []testCase) {
236236
}
237237
}
238238

239-
func TestFindReplace(t *testing.T) {
240-
testDir := setupFileSystem(t)
241-
242-
cases := []testCase{
243-
{
244-
want: []Change{
245-
{
246-
Source: "No Pressure (2021) S1.E1.1080p.mkv",
247-
BaseDir: testDir,
248-
Target: "1.mkv",
249-
},
250-
{
251-
Source: "No Pressure (2021) S1.E2.1080p.mkv",
252-
BaseDir: testDir,
253-
Target: "2.mkv",
254-
},
255-
{
256-
Source: "No Pressure (2021) S1.E3.1080p.mkv",
257-
BaseDir: testDir,
258-
Target: "3.mkv",
259-
},
260-
},
261-
args: []string{
262-
"-f",
263-
".*E(\\d+).*",
264-
"-r",
265-
"$1.mkv",
266-
testDir,
267-
},
268-
},
269-
{
270-
want: []Change{
271-
{
272-
Source: "No Pressure (2021) S1.E1.1080p.mkv",
273-
BaseDir: testDir,
274-
Target: "No Pressure 98.mkv",
275-
},
276-
{
277-
Source: "No Pressure (2021) S1.E2.1080p.mkv",
278-
BaseDir: testDir,
279-
Target: "No Pressure 99.mkv",
280-
},
281-
{
282-
Source: "No Pressure (2021) S1.E3.1080p.mkv",
283-
BaseDir: testDir,
284-
Target: "No Pressure 100.mkv",
285-
},
286-
},
287-
args: []string{
288-
"-f",
289-
"(No Pressure).*",
290-
"-r",
291-
"$1 98%d.mkv",
292-
testDir,
293-
},
294-
},
295-
{
296-
want: []Change{
297-
{
298-
Source: "index.js",
299-
BaseDir: filepath.Join(testDir, "scripts"),
300-
Target: "index.ts",
301-
},
302-
{
303-
Source: "main.js",
304-
BaseDir: filepath.Join(testDir, "scripts"),
305-
Target: "main.ts",
306-
},
307-
},
308-
args: []string{
309-
"-f",
310-
"js",
311-
"-r",
312-
"ts",
313-
filepath.Join(testDir, "scripts"),
314-
},
315-
},
316-
{
317-
want: []Change{
318-
{
319-
Source: "index.js",
320-
BaseDir: filepath.Join(testDir, "scripts"),
321-
Target: "i n d e x .js",
322-
},
323-
{
324-
Source: "main.js",
325-
BaseDir: filepath.Join(testDir, "scripts"),
326-
Target: "m a i n .js",
327-
},
328-
},
329-
args: []string{
330-
"-f",
331-
"(.)",
332-
"-r",
333-
"$1 ",
334-
"-e",
335-
filepath.Join(testDir, "scripts"),
336-
},
337-
},
338-
{
339-
want: []Change{
340-
{
341-
Source: "a.jpg",
342-
BaseDir: filepath.Join(testDir, "images"),
343-
Target: "a.jpeg",
344-
},
345-
{
346-
Source: "b.jPg",
347-
BaseDir: filepath.Join(testDir, "images"),
348-
Target: "b.jpeg",
349-
},
350-
{
351-
Source: "123.JPG",
352-
BaseDir: filepath.Join(testDir, "images", "pics"),
353-
Target: "123.jpeg",
354-
},
355-
{
356-
Source: "free.jpg",
357-
BaseDir: filepath.Join(testDir, "images", "pics"),
358-
Target: "free.jpeg",
359-
},
360-
{
361-
Source: "img.jpg",
362-
BaseDir: filepath.Join(testDir, "morepics", "nested"),
363-
Target: "img.jpeg",
364-
},
365-
},
366-
args: []string{
367-
"-f",
368-
"jpg",
369-
"-r",
370-
"jpeg",
371-
"-R",
372-
"-i",
373-
testDir,
374-
},
375-
},
376-
{
377-
want: []Change{
378-
{
379-
Source: "pics",
380-
IsDir: true,
381-
BaseDir: filepath.Join(testDir, "images"),
382-
Target: "images",
383-
},
384-
{
385-
Source: "morepics",
386-
IsDir: true,
387-
BaseDir: testDir,
388-
Target: "moreimages",
389-
},
390-
{
391-
Source: "pic-1.avif",
392-
BaseDir: filepath.Join(testDir, "morepics"),
393-
Target: "image-1.avif",
394-
},
395-
{
396-
Source: "pic-2.avif",
397-
BaseDir: filepath.Join(testDir, "morepics"),
398-
Target: "image-2.avif",
399-
},
400-
},
401-
args: []string{"-f", "pic", "-r", "image", "-d", "-R", testDir},
402-
},
403-
{
404-
want: []Change{
405-
{
406-
Source: "pics",
407-
IsDir: true,
408-
BaseDir: filepath.Join(testDir, "images"),
409-
Target: "images",
410-
},
411-
{
412-
Source: "morepics",
413-
IsDir: true,
414-
BaseDir: testDir,
415-
Target: "moreimages",
416-
},
417-
},
418-
args: []string{"-f", "pic", "-r", "image", "-D", "-R", testDir},
419-
},
420-
}
421-
422-
runFindReplace(t, cases)
423-
}
424-
425239
func TestHidden(t *testing.T) {
426240
testDir := setupFileSystem(t)
427241
cases := []testCase{

0 commit comments

Comments
 (0)