Add non-string slice env var support ([]int, []bool, etc.) #8

Closed
opened 2026-09-20 15:05:10 +00:00 by agent · 0 comments
Member

Problem

env.go:95-103 — setSlice only handles []string slices. All other slice types ([]int, []bool, []float64) silently return nil:

func setSlice(field reflect.Value, meta fieldMeta, val string) error {
    if meta.Type.Elem().Kind() == reflect.String {
        parts := splitEnvSep(val, meta.EnvSep)
        field.Set(reflect.ValueOf(parts))
        return nil
    }
    // Non-string slices not supported in v1.
    return nil
}

A user setting MYAPP__PORTS=8080,8443,9090 with Ports []int conf:"ports" envsep:","`` would get an empty slice with no error.

Fix

Extend setSlice to handle common element types:

func setSlice(field reflect.Value, meta fieldMeta, val string) error {
    parts := splitEnvSep(val, meta.EnvSep)
    
    switch meta.Type.Elem().Kind() {
    case reflect.String:
        field.Set(reflect.ValueOf(parts))
    case reflect.Int, reflect.Int64:
        ints := make([]int, 0, len(parts))
        for _, p := range parts {
            n, err := strconv.Atoi(p)
            if err != nil {
                return fmt.Errorf("env: parse int %q: %w", p, err)
            }
            ints = append(ints, n)
        }
        field.Set(reflect.ValueOf(ints))
    case reflect.Float32, reflect.Float64:
        floats := make([]float64, 0, len(parts))
        for _, p := range parts {
            n, err := strconv.ParseFloat(p, 64)
            if err != nil {
                return fmt.Errorf("env: parse float %q: %w", p, err)
            }
            floats = append(floats, n)
        }
        field.Set(reflect.ValueOf(floats))
    case reflect.Bool:
        bools := make([]bool, 0, len(parts))
        for _, p := range parts {
            b, err := strconv.ParseBool(p)
            if err != nil {
                return fmt.Errorf("env: parse bool %q: %w", p, err)
            }
            bools = append(bools, b)
        }
        field.Set(reflect.ValueOf(bools))
    default:
        // Unsupported element type — skip silently.
    }
    return nil
}

Files

  • core/env.go — extend setSlice
  • core/env_test.go — add tests for []int, []bool, []float64 env vars

Tests

func TestEnvSliceInt(t *testing.T) {
    t.Setenv("TEST__PORTS", "8080,8443,9090")
    type cfg struct { Ports []int `conf:"ports" envsep:","` }
    var c cfg
    applyEnvOverlay(&c, "TEST")
    // expect c.Ports == []int{8080, 8443, 9090}
}

func TestEnvSliceBool(t *testing.T) {
    t.Setenv("TEST__FLAGS", "true,false,true")
    type cfg struct { Flags []bool `conf:"flags" envsep:","` }
    var c cfg
    applyEnvOverlay(&c, "TEST")
    // expect c.Flags == []bool{true, false, true}
}
## Problem `env.go:95-103` — `setSlice` only handles `[]string` slices. All other slice types (`[]int`, `[]bool`, `[]float64`) silently return nil: ```go func setSlice(field reflect.Value, meta fieldMeta, val string) error { if meta.Type.Elem().Kind() == reflect.String { parts := splitEnvSep(val, meta.EnvSep) field.Set(reflect.ValueOf(parts)) return nil } // Non-string slices not supported in v1. return nil } ``` A user setting `MYAPP__PORTS=8080,8443,9090` with `Ports []int `conf:"ports" envsep:","`` would get an empty slice with no error. ## Fix Extend `setSlice` to handle common element types: ```go func setSlice(field reflect.Value, meta fieldMeta, val string) error { parts := splitEnvSep(val, meta.EnvSep) switch meta.Type.Elem().Kind() { case reflect.String: field.Set(reflect.ValueOf(parts)) case reflect.Int, reflect.Int64: ints := make([]int, 0, len(parts)) for _, p := range parts { n, err := strconv.Atoi(p) if err != nil { return fmt.Errorf("env: parse int %q: %w", p, err) } ints = append(ints, n) } field.Set(reflect.ValueOf(ints)) case reflect.Float32, reflect.Float64: floats := make([]float64, 0, len(parts)) for _, p := range parts { n, err := strconv.ParseFloat(p, 64) if err != nil { return fmt.Errorf("env: parse float %q: %w", p, err) } floats = append(floats, n) } field.Set(reflect.ValueOf(floats)) case reflect.Bool: bools := make([]bool, 0, len(parts)) for _, p := range parts { b, err := strconv.ParseBool(p) if err != nil { return fmt.Errorf("env: parse bool %q: %w", p, err) } bools = append(bools, b) } field.Set(reflect.ValueOf(bools)) default: // Unsupported element type — skip silently. } return nil } ``` ## Files - `core/env.go` — extend `setSlice` - `core/env_test.go` — add tests for `[]int`, `[]bool`, `[]float64` env vars ## Tests ```go func TestEnvSliceInt(t *testing.T) { t.Setenv("TEST__PORTS", "8080,8443,9090") type cfg struct { Ports []int `conf:"ports" envsep:","` } var c cfg applyEnvOverlay(&c, "TEST") // expect c.Ports == []int{8080, 8443, 9090} } func TestEnvSliceBool(t *testing.T) { t.Setenv("TEST__FLAGS", "true,false,true") type cfg struct { Flags []bool `conf:"flags" envsep:","` } var c cfg applyEnvOverlay(&c, "TEST") // expect c.Flags == []bool{true, false, true} } ```
agent closed this issue 2026-09-20 15:49:32 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
residual/config#8
No description provided.