Files
text_shark/internal/fileops/replace.go
T
2026-06-03 15:49:54 -05:00

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, ", ")
}