Skip to content

Commit

Permalink
Fix quoting of $ ` etc (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
ldemailly authored Nov 6, 2023
1 parent 3270095 commit ba71163
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 16 deletions.
24 changes: 18 additions & 6 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,25 @@ func CamelCaseToLowerKebabCase(s string) string {
return strings.ToLower(strings.Join(words, "-"))
}

// Intermediate result list from StructToEnvVars(), both the Key and QuotedValue
// must be shell safe/non adversarial as they are emitted as is by String() with = in between.
// Using StructToEnvVars produces safe values even with adversarial input (length notwithstanding).
type KeyValue struct {
Key string
Value string // Already quoted/escaped.
Key string // Must be safe (is when coming from Go struct names but could be bad with env:).
QuotedValue string // (Must be) Already quoted/escaped.
}

// Escape characters such as the result string can be embedded as a single argument in a shell fragment
// e.g for ENV_VAR=<value> such as <value> is safe (no $(cmd...) no ` etc`).
func ShellQuote(input string) string {
// To emit a single quote in a single quote enclosed string you have to close the current ' then emit a quote (\'),
// then reopen the single quote sequence to finish. Note that when the string ends with a quote there is an unnecessary
// trailing ''.
return "'" + strings.ReplaceAll(input, "'", `'\''`) + "'"
}

func (kv KeyValue) String() string {
return fmt.Sprintf("%s=%s", kv.Key, kv.Value)
return fmt.Sprintf("%s=%s", kv.Key, kv.QuotedValue)
}

func ToShell(kvl []KeyValue) string {
Expand Down Expand Up @@ -126,9 +138,9 @@ func SerializeValue(value interface{}) string {
}
return res
case string:
return strconv.Quote(v)
return ShellQuote(v)
default:
return strconv.Quote(fmt.Sprint(value))
return ShellQuote(fmt.Sprint(value))
}
}

Expand Down Expand Up @@ -191,7 +203,7 @@ func structToEnvVars(envVars []KeyValue, allErrors []error, prefix string, s int
value := fieldValue.Interface()
stringValue = SerializeValue(value)
}
envVars = append(envVars, KeyValue{Key: prefix + tag, Value: stringValue})
envVars = append(envVars, KeyValue{Key: prefix + tag, QuotedValue: stringValue})
}
return envVars, allErrors
}
Expand Down
21 changes: 11 additions & 10 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ type FooConfig struct {
func TestStructToEnvVars(t *testing.T) {
intV := 199
foo := FooConfig{
Foo: "a\nfoo with \" quotes and \\ and '",
Foo: "a\nfoo with $X, `backticks`, \" quotes and \\ and ' in middle and end '",
Bar: "42str",
Blah: 42,
ABool: true,
Expand Down Expand Up @@ -154,17 +154,18 @@ func TestStructToEnvVars(t *testing.T) {
}
str := ToShellWithPrefix("TST_", envVars)
//nolint:lll
expected := `TST_FOO="a\nfoo with \" quotes and \\ and '"
TST_BAR="42str"
TST_A_SPECIAL_BLAH="42"
expected := `TST_FOO='a
foo with $X, ` + "`backticks`" + `, " quotes and \ and '\'' in middle and end '\'''
TST_BAR='42str'
TST_A_SPECIAL_BLAH='42'
TST_A_BOOL=true
TST_HTTP_SERVER="http://localhost:8080"
TST_INT_POINTER="199"
TST_HTTP_SERVER='http://localhost:8080'
TST_INT_POINTER='199'
TST_FLOAT_POINTER=
TST_INNER_A="inner a"
TST_INNER_B="inner b"
TST_RECURSE_HERE_INNER_A="rec a"
TST_RECURSE_HERE_INNER_B="rec b"
TST_INNER_A='inner a'
TST_INNER_B='inner b'
TST_RECURSE_HERE_INNER_A='rec a'
TST_RECURSE_HERE_INNER_B='rec b'
export TST_FOO TST_BAR TST_A_SPECIAL_BLAH TST_A_BOOL TST_HTTP_SERVER TST_INT_POINTER TST_FLOAT_POINTER TST_INNER_A TST_INNER_B TST_RECURSE_HERE_INNER_A TST_RECURSE_HERE_INNER_B
`
if str != expected {
Expand Down

0 comments on commit ba71163

Please sign in to comment.