Skip to content

Commit ece3aff

Browse files
authored
feat(internal/postprocessing): add replace and replaceRegex functions (#6412)
Add the replace and replaceRegex functions to the postprocessing package. They perform exact string and regex replacements in files and return an error if the target text is not found. Fixes #6297
1 parent e08889a commit ece3aff

2 files changed

Lines changed: 192 additions & 0 deletions

File tree

internal/postprocessing/fileops.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,19 @@
1616
package postprocessing
1717

1818
import (
19+
"bytes"
20+
"errors"
21+
"fmt"
1922
"os"
23+
"regexp"
24+
"strings"
2025

2126
"github.com/googleapis/librarian/internal/filesystem"
2227
)
2328

29+
// errTextNotFound is returned when the target text or pattern is not found in the file.
30+
var errTextNotFound = errors.New("text not found")
31+
2432
// CopyFile copies a single file from the src path to the dst path.
2533
// It acts as a wrapper around filesystem.CopyFile to provide a unified
2634
// interface for all postprocessing file operations.
@@ -32,3 +40,40 @@ func CopyFile(src, dst string) error {
3240
func RemoveFile(path string) error {
3341
return os.Remove(path)
3442
}
43+
44+
// Replace finds and replaces exact text in a file.
45+
// It returns an error if the target file does not exist or if the text is not found.
46+
func Replace(path, original, replacement string) error {
47+
content, err := os.ReadFile(path)
48+
if err != nil {
49+
return err
50+
}
51+
oldBytes := []byte(original)
52+
if !bytes.Contains(content, oldBytes) {
53+
return fmt.Errorf("%w: %q in file %s", errTextNotFound, original, path)
54+
}
55+
newContent := bytes.ReplaceAll(content, oldBytes, []byte(replacement))
56+
return os.WriteFile(path, newContent, 0644)
57+
}
58+
59+
// ReplaceRegex finds and replaces text in a file using a regular expression.
60+
// It returns an error if the target file does not exist or if the pattern matches no text.
61+
func ReplaceRegex(path, pattern, replacement string) error {
62+
// Default to multiline mode so ^ and $ match per-line.
63+
if !strings.HasPrefix(pattern, "(?") {
64+
pattern = "(?m)" + pattern
65+
}
66+
re, err := regexp.Compile(pattern)
67+
if err != nil {
68+
return err
69+
}
70+
content, err := os.ReadFile(path)
71+
if err != nil {
72+
return err
73+
}
74+
if !re.Match(content) {
75+
return fmt.Errorf("%w: pattern %q in file %s", errTextNotFound, pattern, path)
76+
}
77+
newContent := re.ReplaceAll(content, []byte(replacement))
78+
return os.WriteFile(path, newContent, 0644)
79+
}

internal/postprocessing/fileops_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,150 @@ func TestRemoveFile_Error(t *testing.T) {
9393
t.Errorf("RemoveFile() error = %v, wantErr %v", err, syscall.ENOTEMPTY)
9494
}
9595
}
96+
97+
func TestReplace(t *testing.T) {
98+
dir := t.TempDir()
99+
path := filepath.Join(dir, "test.txt")
100+
content := "Hello World"
101+
original := "World"
102+
replacement := "Go"
103+
want := "Hello Go"
104+
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
105+
t.Fatal(err)
106+
}
107+
if err := Replace(path, original, replacement); err != nil {
108+
t.Fatal(err)
109+
}
110+
gotBytes, err := os.ReadFile(path)
111+
if err != nil {
112+
t.Fatal(err)
113+
}
114+
got := string(gotBytes)
115+
if diff := cmp.Diff(want, got); diff != "" {
116+
t.Errorf("mismatch (-want +got):\n%s", diff)
117+
}
118+
}
119+
120+
func TestReplaceRegex(t *testing.T) {
121+
t.Parallel()
122+
for _, test := range []struct {
123+
name string
124+
content string
125+
pattern string
126+
replacement string
127+
want string
128+
}{
129+
{
130+
name: "simple replacement",
131+
content: "Hello World",
132+
pattern: "World",
133+
replacement: "Go",
134+
want: "Hello Go",
135+
},
136+
{
137+
name: "regex replacement",
138+
content: "Hello 123 World",
139+
pattern: `\d+`,
140+
replacement: "Number",
141+
want: "Hello Number World",
142+
},
143+
} {
144+
t.Run(test.name, func(t *testing.T) {
145+
t.Parallel()
146+
dir := t.TempDir()
147+
path := filepath.Join(dir, "test.txt")
148+
if err := os.WriteFile(path, []byte(test.content), 0644); err != nil {
149+
t.Fatal(err)
150+
}
151+
if err := ReplaceRegex(path, test.pattern, test.replacement); err != nil {
152+
t.Fatal(err)
153+
}
154+
gotBytes, err := os.ReadFile(path)
155+
if err != nil {
156+
t.Fatal(err)
157+
}
158+
got := string(gotBytes)
159+
if diff := cmp.Diff(test.want, got); diff != "" {
160+
t.Errorf("mismatch (-want +got):\n%s", diff)
161+
}
162+
})
163+
}
164+
}
165+
166+
func TestReplace_Error(t *testing.T) {
167+
for _, test := range []struct {
168+
name string
169+
content string
170+
original string
171+
replacement string
172+
wantErr error
173+
}{
174+
{
175+
name: "file does not exist",
176+
original: "old",
177+
replacement: "new",
178+
wantErr: fs.ErrNotExist,
179+
},
180+
{
181+
name: "text not found",
182+
content: "Hello World",
183+
original: "Apple",
184+
replacement: "Go",
185+
wantErr: errTextNotFound,
186+
},
187+
} {
188+
t.Run(test.name, func(t *testing.T) {
189+
dir := t.TempDir()
190+
path := filepath.Join(dir, "nonexistent.txt")
191+
if test.content != "" {
192+
path = filepath.Join(dir, "test.txt")
193+
if err := os.WriteFile(path, []byte(test.content), 0644); err != nil {
194+
t.Fatal(err)
195+
}
196+
}
197+
err := Replace(path, test.original, test.replacement)
198+
if !errors.Is(err, test.wantErr) {
199+
t.Errorf("Replace() returned unexpected error: got %v, want %v", err, test.wantErr)
200+
}
201+
})
202+
}
203+
}
204+
205+
func TestReplaceRegex_Error(t *testing.T) {
206+
for _, test := range []struct {
207+
name string
208+
content string
209+
pattern string
210+
replacement string
211+
wantErr error
212+
}{
213+
{
214+
name: "file does not exist",
215+
pattern: "old",
216+
replacement: "new",
217+
wantErr: fs.ErrNotExist,
218+
},
219+
{
220+
name: "pattern not found",
221+
content: "Hello World",
222+
pattern: `\d+`,
223+
replacement: "Number",
224+
wantErr: errTextNotFound,
225+
},
226+
} {
227+
t.Run(test.name, func(t *testing.T) {
228+
dir := t.TempDir()
229+
path := filepath.Join(dir, "nonexistent.txt")
230+
if test.content != "" {
231+
path = filepath.Join(dir, "test.txt")
232+
if err := os.WriteFile(path, []byte(test.content), 0644); err != nil {
233+
t.Fatal(err)
234+
}
235+
}
236+
err := ReplaceRegex(path, test.pattern, test.replacement)
237+
if !errors.Is(err, test.wantErr) {
238+
t.Errorf("ReplaceRegex() returned unexpected error: got %v, want %v", err, test.wantErr)
239+
}
240+
})
241+
}
242+
}

0 commit comments

Comments
 (0)