Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions pkg/kvfile/kvfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
//
// # Interpolation, substitution, and escaping
//
// Both keys and values are used as-is; no interpolation, substitution or
// escaping is supported, and quotes are considered part of the key or value.
// Both keys and values are used as-is; no interpolation or substitution is supported.
// Quotes around values are removed if the value starts and ends with the same quote character.
// Whitespace in values (including leading and trailing) is preserved. Given
// that the file format is line-delimited, neither key, nor value, can contain
// newlines.
Expand Down Expand Up @@ -78,6 +78,20 @@ func ParseFromReader(r io.Reader, lookupFn func(key string) (value string, found

const whiteSpaces = " \t"

// trimQuotes removes the quotes from a string if the string starts and ends with the same quote character.
func trimQuotes(value string) string {
if len(value) < 2 {
return value
}
lastIndex := len(value) - 1
for _, char := range []byte{'\'', '"'} {
if value[0] == char && value[lastIndex] == char {
return value[1:lastIndex]
}
}
return value
}

func parseKeyValueFile(r io.Reader, lookupFn func(string) (string, bool)) ([]string, error) {
lines := []string{}
scanner := bufio.NewScanner(r)
Expand Down Expand Up @@ -113,15 +127,22 @@ func parseKeyValueFile(r io.Reader, lookupFn func(string) (string, bool)) ([]str
}

if hasValue {
// key/value pair is valid and has a value; add the line as-is.
lines = append(lines, line)
// key/value pair is valid and has a value
// Split the line to get the key and value
key, value, _ := strings.Cut(line, "=")
// Trim quotes from the value
value = trimQuotes(value)
// Reconstruct the line with the trimmed value
lines = append(lines, key+"="+value)
continue
}

if lookupFn != nil {
// No value given; try to look up the value. The value may be
// empty but if no value is found, the key is omitted.
if value, found := lookupFn(line); found {
// Trim quotes from the value
value = trimQuotes(value)
lines = append(lines, key+"="+value)
}
}
Expand Down
46 changes: 44 additions & 2 deletions pkg/kvfile/kvfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ VAR=VAR_VALUE
EMPTY_VAR=
UNDEFINED_VAR
DEFINED_VAR
QUOTED_VAR
QUOTED_VALUE="this should be quoted"
`
vars := map[string]string{
"DEFINED_VAR": "defined-value",
"QUOTED_VAR": "\"quoted value\"",
}
lookupFn := func(name string) (string, bool) {
v, ok := vars[name]
Expand All @@ -40,7 +43,13 @@ DEFINED_VAR
variables, err := Parse(fileName, lookupFn)
assert.NilError(t, err)

expectedLines := []string{"VAR=VAR_VALUE", "EMPTY_VAR=", "DEFINED_VAR=defined-value"}
expectedLines := []string{
"VAR=VAR_VALUE",
"EMPTY_VAR=",
"DEFINED_VAR=defined-value",
"QUOTED_VAR=quoted value",
"QUOTED_VALUE=this should be quoted",
}
assert.Check(t, is.DeepEqual(variables, expectedLines))
}

Expand Down Expand Up @@ -118,9 +127,11 @@ VAR=VAR_VALUE
EMPTY_VAR=
UNDEFINED_VAR
DEFINED_VAR
QUOTED_VAR
`
vars := map[string]string{
"DEFINED_VAR": "defined-value",
"QUOTED_VAR": "\"quoted value\"",
}
lookupFn := func(name string) (string, bool) {
v, ok := vars[name]
Expand All @@ -130,7 +141,12 @@ DEFINED_VAR
variables, err := ParseFromReader(strings.NewReader(content), lookupFn)
assert.NilError(t, err)

expectedLines := []string{"VAR=VAR_VALUE", "EMPTY_VAR=", "DEFINED_VAR=defined-value"}
expectedLines := []string{
"VAR=VAR_VALUE",
"EMPTY_VAR=",
"DEFINED_VAR=defined-value",
"QUOTED_VAR=quoted value",
}
assert.Check(t, is.DeepEqual(variables, expectedLines))
}

Expand All @@ -143,3 +159,29 @@ func TestParseFromReaderWithNoName(t *testing.T) {
const expectedMessage = "no variable name on line '=blank variable names are an error case'"
assert.Check(t, is.ErrorContains(err, expectedMessage))
}

// Test ParseFromReader with quoted values
func TestParseFromReaderWithQuotes(t *testing.T) {
content := `# Test with quotes
DOUBLE_QUOTES="double quoted value"
SINGLE_QUOTES='single quoted value'
MIXED_QUOTES="'mixed' quotes"
NESTED_QUOTES='"nested" quotes'
UNBALANCED_QUOTES="unbalanced quotes
UNBALANCED_QUOTES2=unbalanced quotes"
QUOTES_IN_MIDDLE=value "with" quotes
`
variables, err := ParseFromReader(strings.NewReader(content), nil)
assert.NilError(t, err)

expectedLines := []string{
"DOUBLE_QUOTES=double quoted value",
"SINGLE_QUOTES=single quoted value",
"MIXED_QUOTES='mixed' quotes",
"NESTED_QUOTES=\"nested\" quotes",
"UNBALANCED_QUOTES=\"unbalanced quotes",
"UNBALANCED_QUOTES2=unbalanced quotes\"",
"QUOTES_IN_MIDDLE=value \"with\" quotes",
}
assert.Check(t, is.DeepEqual(variables, expectedLines))
}