135 lines
2.8 KiB
Go
135 lines
2.8 KiB
Go
package fileops
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type ReplaceResult struct {
|
|
File string
|
|
ChangedLines []int
|
|
}
|
|
|
|
// ReplaceInFile replaces all occurrences of from with to in the file at path.
|
|
// Returns the 1-indexed line numbers that were changed. If dryRun is true,
|
|
// the file is not written.
|
|
func ReplaceInFile(path, from, to string, dryRun bool) (ReplaceResult, error) {
|
|
result := ReplaceResult{File: path}
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
var lines []string
|
|
scanner := bufio.NewScanner(f)
|
|
lineNum := 0
|
|
for scanner.Scan() {
|
|
lineNum++
|
|
line := scanner.Text()
|
|
replaced := strings.ReplaceAll(line, from, to)
|
|
if replaced != line {
|
|
result.ChangedLines = append(result.ChangedLines, lineNum)
|
|
}
|
|
lines = append(lines, replaced)
|
|
}
|
|
f.Close()
|
|
if err := scanner.Err(); err != nil {
|
|
return result, err
|
|
}
|
|
|
|
if len(result.ChangedLines) == 0 || dryRun {
|
|
return result, nil
|
|
}
|
|
|
|
return result, atomicWrite(path, strings.Join(lines, "\n")+trailingNewline(lines))
|
|
}
|
|
|
|
// ReplaceInFileRegex replaces regex matches of pattern with repl in the file at path.
|
|
func ReplaceInFileRegex(path, pattern, repl string, dryRun bool) (ReplaceResult, error) {
|
|
result := ReplaceResult{File: path}
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
re, err := compilePattern(pattern)
|
|
if err != nil {
|
|
f.Close()
|
|
return result, err
|
|
}
|
|
|
|
var lines []string
|
|
scanner := bufio.NewScanner(f)
|
|
lineNum := 0
|
|
for scanner.Scan() {
|
|
lineNum++
|
|
line := scanner.Text()
|
|
replaced := re.ReplaceAllString(line, repl)
|
|
if replaced != line {
|
|
result.ChangedLines = append(result.ChangedLines, lineNum)
|
|
}
|
|
lines = append(lines, replaced)
|
|
}
|
|
f.Close()
|
|
if err := scanner.Err(); err != nil {
|
|
return result, err
|
|
}
|
|
|
|
if len(result.ChangedLines) == 0 || dryRun {
|
|
return result, nil
|
|
}
|
|
|
|
return result, atomicWrite(path, strings.Join(lines, "\n")+trailingNewline(lines))
|
|
}
|
|
|
|
func PrintReplaceResults(results []ReplaceResult) {
|
|
total := 0
|
|
for _, r := range results {
|
|
if len(r.ChangedLines) == 0 {
|
|
continue
|
|
}
|
|
total++
|
|
nums := formatLineNums(r.ChangedLines)
|
|
fmt.Printf("%s: lines %s\n", r.File, nums)
|
|
}
|
|
fmt.Printf("%d file(s) modified.\n", total)
|
|
}
|
|
|
|
func atomicWrite(path, content string) error {
|
|
tmp, err := os.CreateTemp(filepath.Dir(path), ".text-shark-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmpName := tmp.Name()
|
|
if _, err := tmp.WriteString(content); err != nil {
|
|
tmp.Close()
|
|
os.Remove(tmpName)
|
|
return err
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
os.Remove(tmpName)
|
|
return err
|
|
}
|
|
return os.Rename(tmpName, path)
|
|
}
|
|
|
|
func trailingNewline(lines []string) string {
|
|
if len(lines) > 0 {
|
|
return "\n"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func formatLineNums(nums []int) string {
|
|
parts := make([]string, len(nums))
|
|
for i, n := range nums {
|
|
parts[i] = fmt.Sprintf("%d", n)
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|