-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfind_replace.go
124 lines (107 loc) · 3.57 KB
/
find_replace.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
package main
import (
"errors"
"log"
"math/rand"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
// findReplace is a struct used to provide context to all find & replace
// operations, including the strings to search for & replace.
type findReplace struct {
find string
replace string
}
// main processes command line arguments, builds the context struct, and begins
// the process of walking the current working directory.
//
// Variable terminology used throughout this module:
//
// • dirName: the name of a directory, without a trailing separator
// • baseName: the relative name of a file, without a directory
// • path: the relative path to a specific file or directory, including both dirName and baseName
func main() {
// Remove date/time from logging output
log.SetFlags(0)
rand.Seed(time.Now().UnixNano())
if len(os.Args) != 3 {
log.Fatal("Usage: find-replace FIND REPLACE")
}
find := os.Args[1]
replace := os.Args[2]
fr := findReplace{find: find, replace: replace}
// Recursively explore the hierarchy depth first, rewrite files as needed,
// and rename files last (after we don't have to revisit them).
// path.filepath.WalkDir() won't work here because it walks files
// alphabetically, breadth-first (and you'd be renaming files that you
// haven't explored yet).
fr.WalkDir(NewFile("."))
}
// Walks files in the directory given by dirName, which is a relative path to a
// directory. Calls HandleFile for each file it finds, if it's not ignored.
func (fr *findReplace) WalkDir(f *File) {
var wg sync.WaitGroup
// List the files in this directory.
files, err := os.ReadDir(f.Path)
if err != nil {
log.Fatalf("Unable to read directory: %v", err)
}
for _, file := range files {
childFile := NewFile(filepath.Join(f.Path, file.Name()))
wg.Add(1)
go func() {
defer wg.Done()
fr.HandleFile(childFile)
}()
}
wg.Wait() // for (potentially recursive) calls to return
}
// HandleFile immediately recurses depth-first into directories it finds,
// otherwise calls ReplaceContents for regular files. When either operation is
// complete, the file is renamed (if necessary) since no subsequent operations
// will need to access it again.
func (fr *findReplace) HandleFile(f *File) {
// If file is a directory, recurse immediately (depth-first).
if f.Info().IsDir() {
// Ignore certain directories
if f.Base() == ".git" {
return
}
fr.WalkDir(f)
} else {
// Replace the contents of regular files
fr.ReplaceContents(f)
}
// Rename the file now that we're otherwise done with it
fr.RenameFile(f)
}
// RenameFile renames a file if the destination file name does not already
// exist.
func (fr *findReplace) RenameFile(f *File) {
newBaseName := strings.Replace(f.Base(), fr.find, fr.replace, -1)
newPath := filepath.Join(f.Dir(), newBaseName)
if f.Base() != newBaseName {
if _, err := os.Stat(newPath); errors.Is(err, os.ErrNotExist) {
log.Printf("Renaming %v to %v", f.Path, newBaseName)
if err := os.Rename(f.Path, newPath); err != nil {
log.Fatalf("Unable to rename %v to %v: %v", f.Path, newBaseName, err)
}
} else {
log.Fatalf("Refusing to rename %v to %v: %v already exists", f.Path, newBaseName, newPath)
}
}
}
// Replaces the contents of the given file, using the find & replace values in
// context.
func (fr *findReplace) ReplaceContents(f *File) {
// Find & replace the contents of text files. Binary-looking files return
// an empty string and will be skipped here.
content := f.Read()
if strings.Contains(content, fr.find) {
newContent := strings.Replace(content, fr.find, fr.replace, -1)
f.Write(newContent)
}
}