2
0
forked from mirror/oddmu
Files
oddmu/replace_cmd.go
2025-12-06 14:10:20 +01:00

118 lines
3.0 KiB
Go

package main
import (
"context"
"flag"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"github.com/google/subcommands"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
)
type replaceCmd struct {
confirm bool
regexp bool
}
func (cmd *replaceCmd) SetFlags(f *flag.FlagSet) {
f.BoolVar(&cmd.confirm, "confirm", false, "do the replacement instead of just doing a dry run")
f.BoolVar(&cmd.regexp, "regexp", false, "the search string is a regular expression")
}
func (*replaceCmd) Name() string { return "replace" }
func (*replaceCmd) Synopsis() string { return "search and replace in all the pages" }
func (*replaceCmd) Usage() string {
return `replace [-confirm] [-regexp] <term> <replacement>:
Search a string or a regular expression and replace it. By default,
this is a dry run and nothing is saved. If this is a regular
expression, the replacement can use $1, $2, etc. to refer to capture
groups in the regular expression.
`
}
func (cmd *replaceCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
return replaceCli(os.Stdout, cmd.confirm, cmd.regexp, f.Args())
}
func replaceCli(w io.Writer, isConfirmed bool, isRegexp bool, args []string) subcommands.ExitStatus {
if len(args) != 2 {
fmt.Fprintln(os.Stderr, "Replace takes exactly two arguments.")
return subcommands.ExitFailure
}
var re *regexp.Regexp
if isRegexp {
re = regexp.MustCompile(args[0])
} else {
re = regexp.MustCompile(regexp.QuoteMeta(args[0]))
}
repl := []byte(args[1])
changes := 0
err := filepath.Walk(".", func(fp string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
// skip hidden directories and files
if fp != "." && strings.HasPrefix(filepath.Base(fp), ".") {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
// skipp all but page files
if !strings.HasSuffix(fp, ".md") {
return nil
}
body, err := os.ReadFile(fp)
if err != nil {
return err
}
result := re.ReplaceAll(body, repl)
if !slices.Equal(result, body) {
changes++
if isConfirmed {
fmt.Fprintln(w, fp)
_ = os.Rename(fp, fp+"~")
err = os.WriteFile(fp, result, 0644)
if err != nil {
return err
}
} else {
edits := myers.ComputeEdits(span.URIFromPath(fp+"~"), string(body), string(result))
diff := fmt.Sprint(gotextdiff.ToUnified(fp+"~", fp, string(body), edits))
fmt.Fprintln(w, diff)
}
}
return nil
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
return subcommands.ExitFailure
}
if changes == 1 {
if isConfirmed {
fmt.Fprintln(w, "1 file was changed.")
} else {
fmt.Fprintln(w, "1 file would be changed.")
}
} else {
if isConfirmed {
fmt.Fprintf(w, "%d files were changed.\n", changes)
} else {
fmt.Fprintf(w, "%d files would be changed.\n", changes)
}
}
if !isConfirmed && changes > 0 {
fmt.Fprintln(w, "This is a dry run. Use -confirm to make it happen.")
}
return subcommands.ExitSuccess
}