◐ Off-By-One · answer catalog

go-cli-flag-overrides

1 answer(s)godocker

go-cli-flag-overrides

📦 Source in repository (JSON)

Answer

Gap (UHLP U-GAP-002): the spec promised --port, --docker-sock, and --log-level; the CLI only had --config, so operators could not override YAML settings at launch. The naive fix (defaults of 0/"" applied unconditionally) would clobber YAML values with zero sentinels on every run.

Fix — explicit-pass-only overrides via flag.Visit. flag.Visit invokes its callback only for flags actually present on the command line, so absent flags never touch the config. The sentinel guards (port != 0, strings != "") additionally make even an explicit --port 0 / --docker-sock "" a no-op instead of a wipe. The log level is threaded through framework.New(cfg, logLevel) with info as the final fallback, keeping the CLI the single source of truth.

// Flag registration — full spec surface, sentinel defaults (0 / "" = unset).
func defineFlags(fs *flag.FlagSet, cfgPath *string) (port *int, dockerSock *string, logLevel *string) {
    fs.StringVar(cfgPath, "config", "config.yaml", "path to configuration file")
    port = fs.Int("port", 0, "override config port (0 = keep config value)")
    dockerSock = fs.String("docker-sock", "", "override docker socket path (empty = keep config value)")
    logLevel = fs.String("log-level", "", "override log level (empty = keep config value)")
    return port, dockerSock, logLevel
}

// Merge: explicit flags only, sentinels never clobber YAML.
func applyOverrides(cfg *framework.Config, fs *flag.FlagSet, port *int, dockerSock *string, logLevel *string) error {
    fs.Visit(func(f *flag.Flag) {          // callback fires only for passed flags
        switch f.Name {
        case "port":
            if *port != 0 { cfg.Port = *port }
        case "docker-sock":
            if *dockerSock != "" { cfg.DockerSock = *dockerSock }
        case "log-level":
            if *logLevel != "" { cfg.LogLevel = *logLevel }
        }
    })
    return nil
}

// Defaulting: config file value, else "info". (An explicit flag has already
// overwritten cfg.LogLevel before this runs.)
func resolveLogLevel(cfg *framework.Config) string {
    if cfg.LogLevel != "" { return cfg.LogLevel }
    return "info"
}

// Wiring — single framework.New(cfg, logLevel) call site (low-risk signature change).
func run(args []string) error {
    fs := flag.NewFlagSet("cli", flag.ContinueOnError)
    var cfgPath string
    port, dockerSock, logLevel := defineFlags(fs, &cfgPath)
    if err := fs.Parse(args); err != nil { return err }

    cfg, err := loadFileConfig(cfgPath)               // YAML/JSON load
    if err != nil { return err }
    if err := applyOverrides(cfg, fs, port, dockerSock, logLevel); err != nil { return err }

    fw, err := framework.New(cfg, resolveLogLevel(cfg)) // log level threaded in
    if err != nil { return err }
    return fw.Serve()
}

framework.New accepts the resolved level directly and falls back to "info" only if still empty — the constructor no longer re-derives policy.

Evidence & signatures

Built a self-contained reproduction (`/tmp/go-cli-flag-overrides`, Go 1.26, `go vet` clean, `gofmt` clean) and verified both unit and live behavior:

**Automated — 18/18 test executions pass** (6 test functions incl. 12 table subtests): spec flag surface present; 9 override cases; 3 log-level defaulting cases; unknown-flag errors; missing-config fail-fast; live bind test.

| Case | Result |
|---|---|
| No flags → YAML untouched | `port=8080 sock=/var/run/docker.sock level=debug` preserved |
| `--port 9091` only | port overridden; socket + level still from YAML |
| `--docker-sock` / `--log-level` individually | only that field overridden |
| All three flags together | all overridden |
| Sentinels `--port 0`, `--docker-sock ""`, `--log-level ""` explicitly passed | YAML **not** clobbered (verified live: bound on YAML port, not 0) |
| `--log-level` absent everywhere | resolves to `info` |
| Unknown flag | clean usage error, no silent swallow |
| Missing config file | fail-fast error, no default fallback |
| Port conflict (env service on 8080) | fail-fast `bind: address already in use` — never silently hijacks |

**Live runs** (real TCP binds, health endpoint inspected via curl):
- `./cli --config cfg.yaml --port 9091` → `ok port=9091 log=debug sock=/var/run/docker.sock` — **`--port 9091` bound correctly**, flag wins over YAML 8080.
- No `--port` → YAML port binds; `--port 0`/`--docker-sock ""` → YAML values survive.
- `--port P --docker-sock /tmp/x.sock --log-level trace` → `ok port=P log=trace sock=/tmp/x.sock`.

**Signature-change risk:** `grep` confirms exactly one `framework.New(` call site (`main.go:106`) and **zero** test callers of `framework.New` — the added `logLevel` parameter compiles cleanly with no collateral test breakage.
{"model": "deepseek-v4-flash", "problem_class": "go-cli-flag-overrides", "result": "passed", "tests": 18}
Generated from the verified corpus · MIT licensedBack to the catalog