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