From a70a3781e58dacf362096f941a7ac081ad2b3af2 Mon Sep 17 00:00:00 2001 From: Peter Bourgon Date: Thu, 8 Feb 2024 23:55:25 +0100 Subject: [PATCH] Allow env var split delimiter escaping (#129) * Allow env var split delimiter escaping * Update options.go --- options.go | 6 ++++++ parse.go | 14 +++++++++++++- parse_test.go | 12 ++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/options.go b/options.go index 9891959..ebd3405 100644 --- a/options.go +++ b/options.go @@ -114,6 +114,12 @@ func WithEnvVarPrefix(prefix string) Option { // one time, with the value `a,b,c`. Providing WithEnvVarSplit with a comma // delimiter would set `foo` multiple times, with the values `a`, `b`, and `c`. // +// If an env var value contains the delimiter prefixed by a single backslash, +// that occurrence will be treated as a literal string, and not as a split +// point. For example, `FOO=a,b\,c` with a delimiter of `,` would yield values +// `a` and `b,c`. Or, `FOO=axxxb\xxxc` with a delimiter of `xxx` would yield +// values `a` and `bxxxc`. +// // By default, no splitting of environment variable values occurs. func WithEnvVarSplit(delimiter string) Option { return func(pc *ParseContext) { diff --git a/parse.go b/parse.go index 6ef293c..bdfb2c7 100644 --- a/parse.go +++ b/parse.go @@ -99,7 +99,7 @@ func parse(fs Flags, args []string, options ...Option) error { // The value may need to be split. vals := []string{val} if pc.envVarSplit != "" { - vals = strings.Split(val, pc.envVarSplit) + vals = splitEscape(val, pc.envVarSplit) } // Set the flag to the value(s). @@ -291,6 +291,18 @@ func maybePrefix(key string, prefix string) string { return key } +func splitEscape(s string, separator string) []string { + escape := `\` + tokens := strings.Split(s, separator) + for i := len(tokens) - 2; i >= 0; i-- { + if strings.HasSuffix(tokens[i], escape) { + tokens[i] = tokens[i][:len(tokens[i])-len(escape)] + separator + tokens[i+1] + tokens = append(tokens[:i+1], tokens[i+2:]...) + } + } + return tokens +} + // // // diff --git a/parse_test.go b/parse_test.go index abb8dfa..62f9b3e 100644 --- a/parse_test.go +++ b/parse_test.go @@ -122,6 +122,18 @@ func TestParse(t *testing.T) { Options: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE"), ff.WithEnvVarSplit(",")}, Want: fftest.Vars{S: " three ", X: []string{"one", " two", " three "}}, }, + { + Name: "env var split escaping", + Environment: map[string]string{"TEST_PARSE_S": `a\,b`, "TEST_PARSE_X": `one,two\,three`}, + Options: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE"), ff.WithEnvVarSplit(",")}, + Want: fftest.Vars{S: `a,b`, X: []string{`one`, `two,three`}}, + }, + { + Name: "env var split escaping multichar", + Environment: map[string]string{"TEST_PARSE_S": `a\xxb`, "TEST_PARSE_X": `onexxtwo\xxthree`}, + Options: []ff.Option{ff.WithEnvVarPrefix("TEST_PARSE"), ff.WithEnvVarSplit("xx")}, + Want: fftest.Vars{S: `axxb`, X: []string{`one`, `twoxxthree`}}, + }, } testcases.Run(t)