GetEnvFile returned the first line for a key and SetEnvFile replaced the first one. Gemini CLI reads ~/.gemini/.env with dotenv.parse, where a later line overrides an earlier one, so in a file with GEMINI_API_KEY twice the Gemini CLI uses the last, while magpie read and wrote the first. Wiring Gemini CLI to the gateway then changed a line the CLI ignored, and the check for whether it was wired read a value the CLI did not use. GetEnvFile now returns the last line for the key and setLine replaces the last matching line (setLine is only used by SetEnvFile). A key that is set once behaves as before. TestGetEnvFileLastDuplicateWins and TestSetEnvFileReplacesLastDuplicate fail on the old code. go vet and go test ./internal/edit ./internal/agent pass.
124 lines
2.9 KiB
Go
124 lines
2.9 KiB
Go
package edit
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// TOML and top-level YAML entries are located by their parsers (toml.go and
|
|
// yaml_top.go), then edited without reformatting the rest of the file.
|
|
|
|
var (
|
|
// tomlTable and tomlKV only read a TOML file its parser refuses; see
|
|
// GetTOMLTop.
|
|
tomlTable = regexp.MustCompile(`^\s*\[`)
|
|
tomlKV = regexp.MustCompile(`^\s*([A-Za-z0-9_.-]+|"[^"]*")\s*=\s*(.*?)\s*$`)
|
|
)
|
|
|
|
// setLine replaces the line whose key matches, or inserts newLine after the
|
|
// last key line in the header section (before the first line matching stop).
|
|
func setLine(lines []string, key, newLine string, stop *regexp.Regexp, keyOf func(string) (string, bool)) []string {
|
|
lastKey, same := -1, -1
|
|
for i, line := range lines {
|
|
if stop != nil && stop.MatchString(line) {
|
|
break
|
|
}
|
|
k, ok := keyOf(line)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if k == key {
|
|
same = i
|
|
}
|
|
lastKey = i
|
|
}
|
|
if same >= 0 {
|
|
// The last line for a key that is set twice is the one a reader uses.
|
|
lines[same] = newLine
|
|
return lines
|
|
}
|
|
at := lastKey + 1
|
|
out := make([]string, 0, len(lines)+1)
|
|
out = append(out, lines[:at]...)
|
|
out = append(out, newLine)
|
|
out = append(out, lines[at:]...)
|
|
return out
|
|
}
|
|
|
|
// unquote strips a leading quoted scalar (and anything after it, such as an
|
|
// inline comment); ok is false when v does not start with a quote.
|
|
func unquote(v string) (string, bool) {
|
|
if v == "" || (v[0] != '"' && v[0] != '\'') {
|
|
return "", false
|
|
}
|
|
q := v[0]
|
|
for i := 1; i < len(v); i++ {
|
|
if q == '"' && v[i] == '\\' {
|
|
i++
|
|
continue
|
|
}
|
|
if v[i] == q {
|
|
if q == '"' {
|
|
if s, err := strconv.Unquote(v[:i+1]); err == nil {
|
|
return s, true
|
|
}
|
|
}
|
|
return v[1:i], true
|
|
}
|
|
}
|
|
return v[1:], true
|
|
}
|
|
|
|
func tomlValue(v string) string {
|
|
if s, ok := unquote(v); ok {
|
|
return s
|
|
}
|
|
if i := strings.Index(v, "#"); i >= 0 {
|
|
v = strings.TrimSpace(v[:i])
|
|
}
|
|
return v
|
|
}
|
|
|
|
func toString(v any) string {
|
|
switch x := v.(type) {
|
|
case string:
|
|
return x
|
|
case bool:
|
|
return strconv.FormatBool(x)
|
|
case int:
|
|
return strconv.Itoa(x)
|
|
}
|
|
return fmt.Sprint(v)
|
|
}
|
|
|
|
// splitLines splits on '\n' but keeps a trailing newline as an empty final
|
|
// element so joinLines can restore the file exactly.
|
|
func splitLines(s string) []string {
|
|
if s == "" {
|
|
return []string{""}
|
|
}
|
|
return strings.Split(s, "\n")
|
|
}
|
|
|
|
// joinLinesLike is joinLines for lines edited out of orig: when orig used
|
|
// \r\n for every line break, the lines added or replaced get \r\n too, so the
|
|
// file never ends up with mixed endings. Untouched lines already carry their
|
|
// \r, and a file that mixed endings is left as it was.
|
|
func joinLinesLike(lines []string, orig string) string {
|
|
s := joinLines(lines)
|
|
if n := strings.Count(orig, "\n"); n > 0 && strings.Count(orig, "\r\n") == n {
|
|
s = strings.ReplaceAll(strings.ReplaceAll(s, "\r\n", "\n"), "\n", "\r\n")
|
|
}
|
|
return s
|
|
}
|
|
|
|
func joinLines(lines []string) string {
|
|
s := strings.Join(lines, "\n")
|
|
if !strings.HasSuffix(s, "\n") {
|
|
s += "\n"
|
|
}
|
|
return s
|
|
}
|