bots-garden/mini-mepublic Fork 0
d72271127802973540c648bfb372176cdaaa8e4f
Commits
Clone
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

textfile.go · 120 lines · 3.7 KBGo Blame HistoryRaw
💾 Saved. d722711 k33g 7h ago1// Package fileedit reads, writes and edits ONE text file by exact replacement.
2// It is a port of the core of the `edit` CLI (tools/edit: internal/edits,
3// internal/diff, internal/textfile) with the same rules and, as far as the
4// tool names allow, the same messages — so that the built-in tools of this part
5// and the CLI of part 05 refuse the same things, in the same words, and the
6// A/B comparison between them measures the MODEL, not the tool.
7//
8// Copied rather than imported: the CLI keeps its core under internal/, which Go
9// does not let another module import, and moving it out would change a module
10// this talk shows on stage. Four hundred lines of stdlib are cheaper than that.
11package fileedit
12
13import (
14 "fmt"
15 "io/fs"
16 "os"
17 "path/filepath"
18 "strings"
19)
20
21// bom is the UTF-8 byte order mark, as it appears at the head of a file.
22const bom = "\uFEFF"
23
24// file is a text file loaded in memory, ready to be edited.
25//
26// Two details invisible on screen would not survive a naive read/replace/write
27// round trip: the UTF-8 BOM some Windows editors put first — left in the body
28// it sticks to the first word and defeats an `old` that starts at the top of
29// the file — and CRLF line endings, which would defeat EVERY multi-line `old`
30// written with \n, that is, every one a model produces. Both are removed on
31// read and restored on write.
32type file struct {
33 path string
34 // body is the NORMALISED content: no BOM, \n line endings. Searches and
35 // diffs work on it.
36 body string
37 mode fs.FileMode
38 // isNew says the file did not exist yet.
39 isNew bool
40 hadBOM bool
41 crlf bool
42}
43
44// load reads a file. A missing file is not an error: it comes back empty and
45// marked isNew, which lets Write create it and Replace refuse with its own
46// message.
47func load(path string) (*file, error) {
48 f := &file{path: path, mode: 0o644, isNew: true}
49
50 data, err := os.ReadFile(path)
51 if err != nil {
52 if os.IsNotExist(err) {
53 return f, nil
54 }
55 return nil, err
56 }
57 if info, err := os.Stat(path); err == nil {
58 f.mode = info.Mode().Perm()
59 }
60 f.isNew = false
61
62 body := string(data)
63 if strings.HasPrefix(body, bom) {
64 f.hadBOM = true
65 body = strings.TrimPrefix(body, bom)
66 }
67 // The file's line-ending style is that of its FIRST line ending. A mixed
68 // file — it happens, usually by accident — is thus rewritten in that style:
69 // better a consistent file than a diff where every line seems changed.
70 if i := strings.IndexByte(body, '\n'); i > 0 && body[i-1] == '\r' {
71 f.crlf = true
72 }
73 f.body = strings.ReplaceAll(body, "\r\n", "\n")
74 return f, nil
75}
76
77// render puts the original shape (BOM, CRLF) back around a normalised body.
78func (f *file) render(body string) string {
79 if f.crlf {
80 body = strings.ReplaceAll(body, "\n", "\r\n")
81 }
82 if f.hadBOM {
83 body = bom + body
84 }
85 return body
86}
87
88// write replaces the file with a normalised body.
89//
90// It goes through a temporary file in the SAME directory, then a rename: a
91// full disk or a process killed at the wrong moment leaves the original
92// intact, where a direct os.WriteFile would have truncated it. The rename is
93// only atomic on one file system, hence the same directory.
94func (f *file) write(body string) error {
95 dir := filepath.Dir(f.path)
96 if err := os.MkdirAll(dir, 0o755); err != nil {
97 return err
98 }
99 tmp, err := os.CreateTemp(dir, ".edit-*.tmp")
100 if err != nil {
101 return err
102 }
103 defer os.Remove(tmp.Name()) // no effect once the rename succeeded
104
105 if _, err := tmp.WriteString(f.render(body)); err != nil {
106 tmp.Close()
107 return err
108 }
109 if err := tmp.Close(); err != nil {
110 return err
111 }
112 if err := os.Chmod(tmp.Name(), f.mode); err != nil {
113 return err
114 }
115 if err := os.Rename(tmp.Name(), f.path); err != nil {
116 return fmt.Errorf("%s: %w", f.path, err)
117 }
118 f.body, f.isNew = body, false
119 return nil
120}