-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
expand.go
180 lines (165 loc) · 4.58 KB
/
expand.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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package expand
import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"github.com/buildkite/interpolate"
"github.com/goccy/go-yaml/lexer"
"github.com/goccy/go-yaml/token"
)
type Mapper struct {
mapping func(string) (string, bool)
}
// Implement Env
var _ interpolate.Env = Mapper{}
func (m Mapper) Get(key string) (string, bool) {
if m.mapping == nil {
return "", false
}
return m.mapping(key)
}
// ReplaceYAML replaces the tokens of YAML (string) using repFn.
func ReplaceYAML(s string, repFn func(s string) (string, error), replaceMapKey bool) (string, error) {
var err error
tokens := lexer.Tokenize(s)
if len(tokens) == 0 {
return "", nil
}
texts := []string{}
for _, tk := range tokens {
lines := strings.Split(tk.Origin, "\n")
isMapKey := tk.NextType() == token.MappingValueType
nte := false // Need to expand
qt := false // Quote target
if replaceMapKey || !isMapKey {
switch tk.Type {
case token.StringType, token.SingleQuoteType, token.DoubleQuoteType:
nte = true
if len(lines) == 1 {
qt = true
} else if len(lines) == 2 && strings.Trim(lines[1], " ") == "" {
if tk.Prev != nil && tk.Prev.Type == token.LiteralType && token.Type(tk.Prev.Indicator) == token.Type(token.BlockScalarIndicator) {
// Block scalars does not quote
qt = false
} else {
qt = true
}
}
}
}
if len(lines) == 1 {
line := lines[0]
if nte && line != "" {
line, err = repFn(line)
if err != nil {
return "", err
}
if isNeedQuoted(qt, isMapKey, line) {
line = quoteLine(line)
}
}
if len(texts) == 0 {
texts = append(texts, line)
} else {
text := texts[len(texts)-1]
texts[len(texts)-1] = text + line
}
} else {
for idx, src := range lines {
line := src
if nte && line != "" {
line, err = repFn(line)
if err != nil {
return "", err
}
if isNeedQuoted(qt, isMapKey, line) {
line = quoteLine(line)
}
}
if idx == 0 {
if len(texts) == 0 {
texts = append(texts, line)
} else {
text := texts[len(texts)-1]
texts[len(texts)-1] = text + line
}
} else {
texts = append(texts, line)
}
}
}
}
if strings.HasSuffix(s, "\n") && !strings.HasSuffix(tokens[len(tokens)-1].Value, "\n") {
return fmt.Sprintf("%s\n", strings.Join(texts, "\n")), nil
}
return strings.Join(texts, "\n"), nil
}
// ExpandYAML replaces ${var} or $var in the values of YAML (string) based on the mapping function.
func ExpandYAML(s string, mapping func(string) (string, bool)) string {
repFn := InterpolateRepFn(mapping)
rep, _ := ReplaceYAML(s, repFn, false)
return rep
}
// ExpandYAML replaces ${var} or $var in the values of YAML ([]byte) based on the mapping function.
func ExpandYAMLBytes(b []byte, mapping func(string) (string, bool)) []byte {
return []byte(ExpandYAML(string(b), mapping))
}
// ExpandenvYAML replaces ${var} or $var in the values of YAML (string) according to the values
// of the current environment variables.
func ExpandenvYAML(s string) string {
return ExpandYAML(s, os.LookupEnv)
}
// ExpandenvYAML replaces ${var} or $var in the values of YAML ([]byte) according to the values
// of the current environment variables.
func ExpandenvYAMLBytes(b []byte) []byte {
return ExpandYAMLBytes(b, os.LookupEnv)
}
func quoteOnce(s string) string {
u, err := strconv.Unquote(s)
if err != nil {
return strconv.Quote(s)
}
return strconv.Quote(u)
}
func quoteLine(line string) string {
old := strings.Trim(line, " ")
new := quoteOnce(old)
// Avoid duplicate quotes heuristically.
switch {
case strings.HasPrefix(new, `"'`) && strings.HasSuffix(new, `'"`):
// no quote
return line
case strings.HasPrefix(new, `"\"`) && strings.HasSuffix(new, `\""`):
new = fmt.Sprintf(`"%s"`, strings.TrimSuffix(strings.TrimPrefix(new, `"\"`), `\""`))
return strings.Replace(line, old, new, 1)
default:
return strings.Replace(line, old, new, 1)
}
}
func isNeedQuoted(quoteTarget bool, isMapKey bool, line string) bool {
if quoteTarget && token.IsNeedQuoted(line) ||
// If there is a line break in the result of the conversion of what was one line, quote it.
strings.Contains(line, "\n") {
if isJSONString(line) && !isMapKey {
// Not quoting to be interpreted as inline YAML
return false
}
return true
}
return false
}
func isJSONString(line string) bool {
if !strings.Contains(line, "{") && !strings.Contains(line, "[") {
return false
}
var v any
if err := json.Unmarshal([]byte(strings.Trim(line, " ")), &v); err == nil {
switch v.(type) {
case []any, map[any]any, map[string]any:
return true
}
}
return false
}