-
Notifications
You must be signed in to change notification settings - Fork 143
/
yaml.go
138 lines (129 loc) · 4 KB
/
yaml.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package yaml
import (
"bufio"
"bytes"
"os"
"strconv"
"strings"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
// SetStringsInFile overwrites the specified file with the changes specified by
// the changes map applied. The changes map maps keys to new values. Keys are of
// the form <key 0>.<key 1>...<key n>. Integers may be used as keys in cases
// where a specific node needs to be selected from a sequence. Individual
// changes are ignored without error if their key is not found or if their key
// is found not to address a scalar node. Importantly, all comments and style
// choices in the input bytes are preserved in the output.
func SetStringsInFile(file string, changes map[string]string) error {
inBytes, err := os.ReadFile(file)
if err != nil {
return errors.Wrapf(
err,
"error reading file %q",
file,
)
}
outBytes, err := SetStringsInBytes(inBytes, changes)
if err != nil {
return errors.Wrap(err, "error mutating bytes")
}
return errors.Wrapf(
// This file should always exist already, so the permissions we choose here
// don't really matter. (They only matter when this function call creates
// the file, which it never will.) We went with 0600 just to appease the
// gosec linter.
os.WriteFile(file, outBytes, 0600),
"error writing mutated bytes to file %q",
file,
)
}
// SetStringsInBytes returns a copy of the provided bytes with the changes
// specified by the changes map applied. The changes map maps keys to new
// values. Keys are of the form <key 0>.<key 1>...<key n>. Integers may be used
// as keys in cases where a specific node needs to be selected from a sequence.
// Individual changes are ignored without error if their key is not found or
// if their key is found not to address a scalar node. Importantly, all comments
// and style choices in the input bytes are preserved in the output.
func SetStringsInBytes(
inBytes []byte,
changes map[string]string,
) ([]byte, error) {
doc := &yaml.Node{}
if err := yaml.Unmarshal(inBytes, doc); err != nil {
return nil, errors.Wrap(err, "error unmarshaling input")
}
type change struct {
col int
value string
}
changesByLine := map[int]change{}
for k, v := range changes {
keyPath := strings.Split(k, ".")
if found, line, col := findScalarNode(doc, keyPath); found {
changesByLine[line] = change{
col: col,
value: v,
}
}
}
outBuf := &bytes.Buffer{}
scanner := bufio.NewScanner(bytes.NewBuffer(inBytes))
scanner.Split(bufio.ScanLines)
var line int
for scanner.Scan() {
const errMsg = "error writing to byte buffer"
change, found := changesByLine[line]
if !found {
if _, err := outBuf.WriteString(scanner.Text()); err != nil {
return nil, errors.Wrap(err, errMsg)
}
if _, err := outBuf.WriteString("\n"); err != nil {
return nil, errors.Wrap(err, errMsg)
}
} else {
unchanged := scanner.Text()[0:change.col]
if _, err := outBuf.WriteString(unchanged); err != nil {
return nil, errors.Wrap(err, errMsg)
}
if !strings.HasSuffix(unchanged, " ") {
if _, err := outBuf.WriteString(" "); err != nil {
return nil, errors.Wrap(err, errMsg)
}
}
if _, err := outBuf.WriteString(change.value); err != nil {
return nil, errors.Wrap(err, errMsg)
}
if _, err := outBuf.WriteString("\n"); err != nil {
return nil, errors.Wrap(err, errMsg)
}
}
line++
}
return outBuf.Bytes(), nil
}
func findScalarNode(node *yaml.Node, keyPath []string) (bool, int, int) {
if len(keyPath) == 0 {
if node.Kind == yaml.ScalarNode {
return true, node.Line - 1, node.Column - 1
}
return false, 0, 0
}
switch node.Kind {
case yaml.DocumentNode:
return findScalarNode(node.Content[0], keyPath)
case yaml.MappingNode:
for i := 0; i < len(node.Content); i += 2 {
if node.Content[i].Value == keyPath[0] {
return findScalarNode(node.Content[i+1], keyPath[1:])
}
}
case yaml.SequenceNode:
index, err := strconv.Atoi(keyPath[0])
if err != nil {
return false, 0, 0
}
return findScalarNode(node.Content[index], keyPath[1:])
}
return false, 0, 0
}