Files
helmfile/pkg/config/config.go
T
000a6fa660 fix: support escaped commas in --state-values-set (#2813)
* feat: support comma escape for values overwrites

Signed-off-by: zhengbayi <zhengbaiyi@sensetime.com>

* fix: harden state-values-set parsing and support \, in state-values-set-string

- Return a descriptive error instead of panicking when a --state-values-set
  assignment is missing '=', has an empty key, or ends with a trailing comma
- Support escaped commas in --state-values-set-string and preserve other
  backslashes (including trailing ones), aligning it with --state-values-set
  and Helm's --set/--set-string; quoted values keep working
- Allow empty values in --state-values-set-string for parity with
  --state-values-set
- Restructure parsing into guard-clause helpers with single-level
  conditionals; parse assignments via strings.Cut
- Extend table-driven tests: error cases, --state-values-set-string
  coverage, combined flags, and unit tests for the split/unescape helpers

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: zhengbayi <zhengbaiyi@sensetime.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: zhengbayi <zhengbaiyi@sensetime.com>
Co-authored-by: yxxhero <aiopsclub@163.com>
2026-10-03 15:31:04 +08:00

142 lines
3.6 KiB
Go

package config
import (
"fmt"
"regexp"
"strings"
"github.com/helmfile/helmfile/pkg/maputil"
)
// stateValuesSetStringRE matches a single key=value assignment within one
// --state-values-set-string flag value. The value is either a quoted string,
// which may contain commas, or a run of plain or backslash-escaped characters
// ending at the next unescaped comma.
var stateValuesSetStringRE = regexp.MustCompile(`(?s)(?:,|^)([^\s=]+)=(['"][^'"]*['"]|(?:\\.|[^,\\])*\\?)`)
// NewCLIConfigImpl parses the raw --state-values-set-string and
// --state-values-set flag values into state value overrides on g.
func NewCLIConfigImpl(g *GlobalImpl) error {
if err := parseStateValuesSetString(g); err != nil {
return err
}
return parseStateValuesSet(g)
}
// parseStateValuesSetString applies the --state-values-set-string flag values
// to g. Unlike --state-values-set, values are kept as strings without type
// conversion.
func parseStateValuesSetString(g *GlobalImpl) error {
optsSet := g.RawStateValuesSetString()
if len(optsSet) == 0 {
return nil
}
set := map[string]any{}
for _, raw := range optsSet {
for _, match := range stateValuesSetStringRE.FindAllStringSubmatch(raw, -1) {
key, value := match[1], match[2]
maputil.Set(set, maputil.ParseKey(key), unescapeCommas(value), true)
}
}
g.SetSet(set)
return nil
}
// parseStateValuesSet applies the --state-values-set flag values to g. Values
// are type-converted like Helm's --set.
func parseStateValuesSet(g *GlobalImpl) error {
optsSet := g.RawStateValuesSet()
if len(optsSet) == 0 {
return nil
}
set := map[string]any{}
for _, raw := range optsSet {
for _, assignment := range splitOnUnescapedCommas(raw) {
if err := setAssignment(set, assignment); err != nil {
return err
}
}
}
g.SetSet(set)
return nil
}
// setAssignment parses a single key=value assignment into set, returning an
// error instead of panicking when the assignment is malformed.
func setAssignment(set map[string]any, assignment string) error {
key, value, found := strings.Cut(unescapeCommas(assignment), "=")
if !found || key == "" {
return fmt.Errorf("--state-values-set: invalid assignment %q: expected <key>=<value>", assignment)
}
maputil.Set(set, maputil.ParseKey(key), value, false)
return nil
}
// splitOnUnescapedCommas splits the input on commas that are not escaped by a
// preceding backslash, keeping every backslash intact so that escapes remain
// available to key and value parsing.
func splitOnUnescapedCommas(input string) []string {
segments := make([]string, 0, 1+strings.Count(input, ","))
var current strings.Builder
escaped := false
for i := range len(input) {
char := input[i]
switch {
case escaped:
current.WriteByte('\\')
current.WriteByte(char)
escaped = false
case char == '\\':
escaped = true
case char == ',':
segments = append(segments, current.String())
current.Reset()
default:
current.WriteByte(char)
}
}
if escaped {
current.WriteByte('\\')
}
return append(segments, current.String())
}
// unescapeCommas turns every backslash-escaped comma into a literal comma,
// preserving all other backslashes, including a trailing one.
func unescapeCommas(input string) string {
if !strings.Contains(input, `\,`) {
return input
}
var out strings.Builder
out.Grow(len(input))
escaped := false
for i := range len(input) {
char := input[i]
switch {
case escaped && char == ',':
out.WriteByte(',')
escaped = false
case escaped:
out.WriteByte('\\')
out.WriteByte(char)
escaped = false
case char == '\\':
escaped = true
default:
out.WriteByte(char)
}
}
if escaped {
out.WriteByte('\\')
}
return out.String()
}