Exemplo
github.com/spf13/viper v1.21.0: Pin spf13/viper's configuration precedence order and the env, IsSet and case-folding traps around it, without reading a config file from disk
Amostra verificada para golang github.com/spf13/viper v1.21.0: Pin spf13/viper's configuration precedence order and the env, IsSet and case-folding traps…
sha256:ea36c1c79c3a4263305e79d2403494634711a275d2fd41a2d8df834ac7e9101b
Esta rede oferece uma coisa: uma amostra que compila. Ela a executou em um sandbox e guardou o recibo assinado. Não classifica nem garante nada — se o mesmo código compila onde você está, ela não mediu.
Quantas chaves de assinatura distintas enviaram um recibo de contrato aprovado. Uma é só o autor; mais de uma significa que outra pessoa também o compilou. Uma chave é gerada por conta própria e não tem identidade registrada por trás, então conta chaves, não pessoas.
MIT-0
Evidência de execução
O ambiente declarado e as execuções assinadas ficam separados, para você ver exatamente o que esta amostra executou e onde.
- Base da evidência
- Contrato assinado aprovado
- Recibos de verificação
- 3
- Chaves de assinatura que o compilaram
- 3
Ambiente declarado
go 1.26 linux x64 go 1.26 go go
Ambientes das execuções de verificação
| Ambiente | Contrato | Etapas | Execução |
|---|---|---|---|
| go 1.26 · linux alpine/x64 · docker ed25519:a2ec939a4c60e243 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · golang@1 |
2026-08-14 |
| go 1.26 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · golang@1 |
2026-08-18 |
| go 1.26 · linux alpine/x64 · docker ed25519:c1973797be207ac4 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · golang@1golang:1.26-alpine@sha256:28d89ee9cc0f… |
2026-09-08 |
Caso
HOW- Objetivo
- Pin spf13/viper's configuration precedence order and the env, IsSet and case-folding traps around it, without reading a config file from disk
- Símbolos
-
- viper.New
- viper.SetDefault
- viper.Set
- viper.BindPFlag
- viper.AutomaticEnv
- viper.SetEnvPrefix
- viper.SetEnvKeyReplacer
- viper.BindEnv
- viper.AllowEmptyEnv
- viper.ReadConfig
- viper.AddRemoteProvider
- viper.RemoteConfig
- viper.ReadRemoteConfig
- viper.IsSet
- viper.Get
- viper.AllKeys
- viper.Unmarshal
- viper.ExperimentalBindStruct
- Ambiente
- go 1.26
- Criado
- 2026-08-14T13:24:24Z
Contrato
- assert the documented ladder top down by writing the same key at every rung at once: explicit Set beats a changed flag beats env beats config file beats key/value store beats default, each rung checked with everything below it populated
- assert the key/value rung is reachable offline through viper.RemoteConfig, whose interface type is unexported but whose three methods are not, so the one rung nobody ever sees is observable without an etcd server
- assert a bound flag only sits at flag height once pflag records it as Changed, and that its default is consulted last of all, below SetDefault, so a flag default loses to a config file
- assert IsSet searches with that flag-default pass switched off, so it returns false for a key whose Get returns the flag's default
- assert AutomaticEnv computes the name as uppercase(prefix + underscore + key) by exporting the literal name CSX_LOG.LEVEL and watching a dotted key find it, which no shell could have exported
- assert that without SetEnvKeyReplacer the expected CSX_LOG_LEVEL is never consulted for the key log.level, and that installing the replacer reaches it
- assert the underscore between prefix and key is added by viper, so SetEnvPrefix("csx_") looks up CSX__LOG_LEVEL
- assert the replacer runs over the finished name rather than the key alone, so SetEnvPrefix("csx.app") with a dot replacer looks up CSX_APP_LOG_LEVEL
- assert an env var exported empty is treated as unset and falls through to the config file, until AllowEmptyEnv(true) makes the empty string a value that IsSet agrees is set
- assert BindEnv with one argument means prefix plus key, while a second argument is the env name verbatim with no prefix, no uppercasing and no replacer
- assert Get, GetString, GetInt, GetBool and GetDuration return zero values rather than errors on a missing key, so a typo is indistinguishable from a zero, and IsSet is the only thing that tells them apart
- assert, measured, that IsSet is true for a key that only has a default, while Set(key, nil) is invisible to the search and does not even shadow the default underneath it
- assert keys are case-insensitive and stored folded: Get agrees across spellings, AllKeys reports only the lower-case form, and a document's keys are folded on read while its values are left alone
- assert Unmarshal enumerates AllKeys rather than searching per key, so a value AutomaticEnv makes Get return is absent from both AllSettings and the struct, and that either a default for the key or viper.ExperimentalBindStruct brings it back
Arquivos
- csx.json
- go.mod
- go.sum
- src/config.go
- test/main.go
Código-fonte
{"case":{"caseId":"case:sha256:49a00062ac22ba403d7fad6003e0e7adb4f024adef22bf71a527a801c0d9a459","constraints":{"runtime":"go"},"contract":["assert the documented ladder top down by writing the same key at every rung at once: explicit Set beats a changed flag beats env beats config file beats key/value store beats default, each rung checked with everything below it populated","assert the key/value rung is reachable offline through viper.RemoteConfig, whose interface type is unexported but whose three methods are not, so the one rung nobody ever sees is observable without an etcd server","assert a bound flag only sits at flag height once pflag records it as Changed, and that its default is consulted last of all, below SetDefault, so a flag default loses to a config file","assert IsSet searches with that flag-default pass switched off, so it returns false for a key whose Get returns the flag's default","assert AutomaticEnv computes the name as uppercase(prefix + underscore + key) by exporting the literal name CSX_LOG.LEVEL and watching a dotted key find it, which no shell could have exported","assert that without SetEnvKeyReplacer the expected CSX_LOG_LEVEL is never consulted for the key log.level, and that installing the replacer reaches it","assert the underscore between prefix and key is added by viper, so SetEnvPrefix(\"csx_\") looks up CSX__LOG_LEVEL","assert the replacer runs over the finished name rather than the key alone, so SetEnvPrefix(\"csx.app\") with a dot replacer looks up CSX_APP_LOG_LEVEL","assert an env var exported empty is treated as unset and falls through to the config file, until AllowEmptyEnv(true) makes the empty string a value that IsSet agrees is set","assert BindEnv with one argument means prefix plus key, while a second argument is the env name verbatim with no prefix, no uppercasing and no replacer","assert Get, GetString, GetInt, GetBool and GetDuration return zero values rather than errors on a missing key, so a typo is indistinguishable from a zero, and IsSet is the only thing that tells them apart","assert, measured, that IsSet is true for a key that only has a default, while Set(key, nil) is invisible to the search and does not even shadow the default underneath it","assert keys are case-insensitive and stored folded: Get agrees across spellings, AllKeys reports only the lower-case form, and a document's keys are folded on read while its values are left alone","assert Unmarshal enumerates AllKeys rather than searching per key, so a value AutomaticEnv makes Get return is absent from both AllSettings and the struct, and that either a default for the key or viper.ExperimentalBindStruct brings it back"],"goal":"Pin spf13/viper's configuration precedence order and the env, IsSet and case-folding traps around it, without reading a config file from disk","kind":"HOW","packages":["pkg:golang/github.com/spf13/viper@1.21.0","pkg:golang/github.com/spf13/pflag@1.0.10"],"schemaVersion":1,"symbols":["viper.New","viper.SetDefault","viper.Set","viper.BindPFlag","viper.AutomaticEnv","viper.SetEnvPrefix","viper.SetEnvKeyReplacer","viper.BindEnv","viper.AllowEmptyEnv","viper.ReadConfig","viper.AddRemoteProvider","viper.RemoteConfig","viper.ReadRemoteConfig","viper.IsSet","viper.Get","viper.AllKeys","viper.Unmarshal","viper.ExperimentalBindStruct"]},"contractCommand":["go","run","./test"],"environment":{"arch":"x64","ecosystem":"golang","executionContext":"go","language":"go","os":"linux","packageManager":"go","runtime":"go","runtimeVersion":"1.26","schemaVersion":1},"license":"MIT-0","packages":["pkg:golang/github.com/spf13/viper@1.21.0","pkg:golang/github.com/spf13/pflag@1.0.10"],"schemaVersion":1,"symbols":["viper.New","viper.SetDefault","viper.Set","viper.BindPFlag","viper.AutomaticEnv","viper.SetEnvPrefix","viper.SetEnvKeyReplacer","viper.BindEnv","viper.AllowEmptyEnv","viper.ReadConfig","viper.AddRemoteProvider","viper.RemoteConfig","viper.ReadRemoteConfig","viper.IsSet","viper.Get","viper.AllKeys","viper.Unmarshal","viper.ExperimentalBindStruct"],"verifierAdapter":"golang@1"}
module codesamplex.dev/sample/goviper
go 1.24
require (
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0
)
require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.28.0 // indirect
)
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
// Package config wires spf13/viper the way a program does, and exposes the
// pieces needed to watch its precedence ladder decide a value.
//
// The documented order is: explicit Set, then flag, then environment, then
// config file, then key/value store, then default. What that sentence hides
// is that "flag" means a flag the user actually changed. viper checks
// flag.HasChanged() near the top of the search, and consults the flag's
// *default* only after everything else has failed — below SetDefault. So a
// flag default never overrides a config file, which is the opposite of what
// most people assume when they wire BindPFlag once and move on.
//
// Nothing here reads a path from disk. SetConfigType plus ReadConfig(io.Reader)
// is the whole file-loading path minus the file, which is also the cheapest
// way to test config handling without a fixture directory.
package config
import (
"bytes"
"io"
"strings"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
// Key is the key every rung of the ladder writes. It is dotted on purpose: a
// dot is legal in a viper key and no shell can export a variable whose name
// contains one, and that mismatch is where the env lookup silently stops
// working.
const Key = "log.level"
// EnvPrefix is glued to the key as strings.ToUpper(prefix + "_" + key), so
// the prefix is written without its own trailing underscore.
const EnvPrefix = "csx"
// Level names one rung, highest priority first.
type Level int
const (
LevelSet Level = iota
LevelFlag
LevelEnv
LevelConfig
LevelKV
LevelDefault
)
func (l Level) String() string {
return [...]string{"Set", "flag", "env", "config file", "key/value store", "default"}[l]
}
// New returns the viper an application ends up with: a format for readers
// that have no filename, a prefix so the process cannot be steered by a bare
// PATH-like name, a replacer so dotted keys have a legal env spelling, and
// AutomaticEnv so every Get consults the environment.
//
// Order does not matter among these four — each sets one field that is read
// at lookup time, not at call time — but all four are load-bearing: drop the
// replacer and "log.level" is looked up as CSX_LOG.LEVEL.
func New(opts ...viper.Option) *viper.Viper {
v := viper.NewWithOptions(opts...)
v.SetConfigType("yaml")
v.SetEnvPrefix(EnvPrefix)
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
return v
}
// ReadDocument fills the config-file rung from memory. ReadConfig needs
// SetConfigType because there is no extension to guess from.
func ReadDocument(v *viper.Viper, document string) error {
return v.ReadConfig(bytes.NewBufferString(document))
}
// FlagSet is the flag rung. The flag is spelled log-level and the viper key
// is log.level; BindPFlag is the translation between them, which is why the
// two spellings never have to agree.
func FlagSet(defaultValue string) *pflag.FlagSet {
fs := pflag.NewFlagSet("csxtool", pflag.ContinueOnError)
fs.String("log-level", defaultValue, "logging level")
return fs
}
// offlineKV stands in for etcd or consul. viper reaches its key/value rung
// through the package-level viper.RemoteConfig hook, whose interface type is
// unexported but whose three methods are not — so any type carrying them can
// be plugged in, and the rung nobody ever sees becomes observable without a
// server. Being package-level, this affects every *Viper in the process.
type offlineKV struct{ document string }
func (k offlineKV) Get(viper.RemoteProvider) (io.Reader, error) {
return strings.NewReader(k.document), nil
}
func (k offlineKV) Watch(viper.RemoteProvider) (io.Reader, error) {
return strings.NewReader(k.document), nil
}
func (k offlineKV) WatchChannel(viper.RemoteProvider) (<-chan *viper.RemoteResponse, chan bool) {
return nil, nil
}
// ReadKeyValue fills the key/value rung. The payload is parsed with the same
// codec as the config file, so SetConfigType has to be in place first.
func ReadKeyValue(v *viper.Viper, document string) error {
viper.RemoteConfig = offlineKV{document: document}
if err := v.AddRemoteProvider("etcd3", "http://127.0.0.1:2379", "/csx"); err != nil {
return err
}
return v.ReadRemoteConfig()
}
// Settings is the struct half of the API. Unmarshal builds its input from
// AllKeys, which is a different question than the one Get answers.
type Settings struct {
Log struct {
Level string `mapstructure:"level"`
} `mapstructure:"log"`
}
package main
import (
"fmt"
"os"
"slices"
"strings"
"github.com/spf13/viper"
"codesamplex.dev/sample/goviper/src"
)
const document = `log:
level: config
timeout: 30s
`
const kvDocument = `log:
level: kv
`
// The env name viper computes for "log.level" under prefix "csx": uppercase
// the prefix, an underscore, then the key with the replacer applied.
const envName = "CSX_LOG_LEVEL"
func main() {
precedence()
flagDefaultIsLast()
envNaming()
emptyEnvIsNoEnv()
bindEnvNaming()
missingKeys()
caseFolding()
unmarshalSeesDifferentKeys()
fmt.Println("contract ok")
}
// ladder builds a viper with every rung from top down to the default filled
// in, each writing the name of its own rung to the same key. Whatever Get
// returns is the name of the rung that won.
func ladder(top config.Level) *viper.Viper {
v := config.New()
v.SetDefault(config.Key, "default")
if top <= config.LevelKV {
must(config.ReadKeyValue(v, kvDocument))
}
if top <= config.LevelConfig {
must(config.ReadDocument(v, document))
}
if top <= config.LevelEnv {
must(os.Setenv(envName, "env"))
} else {
must(os.Unsetenv(envName))
}
if top <= config.LevelFlag {
fs := config.FlagSet("flag default")
must(fs.Parse([]string{"--log-level=flag"}))
must(v.BindPFlag(config.Key, fs.Lookup("log-level")))
}
if top <= config.LevelSet {
v.Set(config.Key, "set")
}
return v
}
func precedence() {
for _, tc := range []struct {
top config.Level
want string
}{
{config.LevelSet, "set"},
{config.LevelFlag, "flag"},
{config.LevelEnv, "env"},
{config.LevelConfig, "config"},
{config.LevelKV, "kv"},
{config.LevelDefault, "default"},
} {
got := ladder(tc.top).GetString(config.Key)
check(got == tc.want, "%v should outrank everything below it: got %q, want %q",
tc.top, got, tc.want)
}
must(os.Unsetenv(envName))
}
// The rung that is not where people put it. A bound flag only enters the
// search at flag height once pflag records it as Changed; until then its
// default sits at the very bottom, below SetDefault.
func flagDefaultIsLast() {
v := config.New()
fs := config.FlagSet("flag default")
must(v.BindPFlag(config.Key, fs.Lookup("log-level")))
// Nothing else is set, so the flag default is all there is.
check(v.GetString(config.Key) == "flag default", "unparsed flag: %q", v.GetString(config.Key))
// IsSet searches with the flag-default pass switched off, so it disagrees
// with Get about whether this key has a value. A guard written as
// `if !v.IsSet(k) { return errors.New("required") }` rejects a key whose
// Get would have returned the flag's default perfectly well.
check(!v.IsSet(config.Key), "IsSet must ignore a flag default that was never changed")
// A viper default outranks it, which is the wrong way round if you think
// of the flag as living at flag height.
v.SetDefault(config.Key, "default")
check(v.GetString(config.Key) == "default", "SetDefault should beat an unchanged flag: %q",
v.GetString(config.Key))
check(v.IsSet(config.Key), "a default is enough for IsSet")
// Parse the flag and it jumps the whole ladder.
must(fs.Parse([]string{"--log-level=flag"}))
check(v.GetString(config.Key) == "flag", "a changed flag should win: %q", v.GetString(config.Key))
// The consequence, spelled out rather than inferred from the two steps
// above: bind a flag, never parse it, and the config file buries its
// default, even though the flag sits above the file in the documented order.
buried := config.New()
unparsed := config.FlagSet("flag default")
must(buried.BindPFlag(config.Key, unparsed.Lookup("log-level")))
must(config.ReadDocument(buried, document))
check(buried.GetString(config.Key) == "config",
"a config file should bury an unparsed flag's default: %q", buried.GetString(config.Key))
}
// AutomaticEnv does not look for CSX_LOG_LEVEL because someone decided it
// should. It builds the name mechanically, and the replacer is the only part
// of that name a dotted key can supply.
func envNaming() {
bare := viper.New()
bare.SetConfigType("yaml")
bare.SetEnvPrefix(config.EnvPrefix)
bare.AutomaticEnv()
must(config.ReadDocument(bare, document))
must(os.Setenv(envName, "env"))
check(bare.GetString(config.Key) == "config",
"without SetEnvKeyReplacer, %s is never consulted: %q", envName, bare.GetString(config.Key))
// The name it does consult keeps the dot, which no shell can export. Go's
// os.Setenv only refuses '=' and NUL, so the unreachable name is reachable
// from here, and that is the proof the lookup happens at all.
must(os.Setenv("CSX_LOG.LEVEL", "dotted"))
check(bare.GetString(config.Key) == "dotted",
"the computed name is uppercase(prefix + _ + key), dot intact: %q", bare.GetString(config.Key))
must(os.Unsetenv("CSX_LOG.LEVEL"))
// With the replacer installed, the spelling people expect is the spelling
// that works.
check(config.New().GetString(config.Key) == "env", "replacer should reach %s", envName)
// The replacer runs over the finished name, prefix included: viper glues
// the prefix on and uppercases, then replaces. A prefix carrying a
// character the replacer rewrites gets rewritten too, which is easy to miss
// when the replacer was added for the key's sake. Were it applied to the
// key alone, the lookup here would be CSX.APP_LOG_LEVEL and find nothing.
dottedPrefix := viper.New()
dottedPrefix.SetEnvPrefix("csx.app")
dottedPrefix.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
dottedPrefix.AutomaticEnv()
must(os.Setenv("CSX_APP_LOG_LEVEL", "prefix replaced"))
check(dottedPrefix.GetString(config.Key) == "prefix replaced",
"the replacer rewrites the prefix too: %q", dottedPrefix.GetString(config.Key))
must(os.Unsetenv("CSX_APP_LOG_LEVEL"))
// The underscore between prefix and key is added by viper, so a prefix
// written with one of its own doubles it.
doubled := viper.New()
doubled.SetEnvPrefix(config.EnvPrefix + "_")
doubled.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
doubled.AutomaticEnv()
must(os.Setenv("CSX__LOG_LEVEL", "doubled"))
check(doubled.GetString(config.Key) == "doubled",
"SetEnvPrefix(%q) should look up CSX__LOG_LEVEL: %q", config.EnvPrefix+"_", doubled.GetString(config.Key))
must(os.Unsetenv("CSX__LOG_LEVEL"))
must(os.Unsetenv(envName))
}
// An exported-but-empty variable is a real state for a shell, and viper
// throws it away unless told not to.
func emptyEnvIsNoEnv() {
v := config.New()
must(config.ReadDocument(v, document))
must(os.Setenv(envName, ""))
check(v.GetString(config.Key) == "config",
"an empty env var falls through by default: %q", v.GetString(config.Key))
allow := config.New()
allow.AllowEmptyEnv(true)
must(config.ReadDocument(allow, document))
check(allow.GetString(config.Key) == "",
"AllowEmptyEnv(true) makes the empty string a value: %q", allow.GetString(config.Key))
check(allow.IsSet(config.Key), "and IsSet agrees it is set, even though the value is empty")
// With nothing underneath to fall through to, the same difference decides
// whether the key exists at all.
must(os.Setenv("CSX_TRACE_ID", ""))
check(!v.IsSet("trace.id"), "an env var exported empty leaves its key unset")
check(allow.IsSet("trace.id"), "and set once empty values are allowed")
must(os.Unsetenv("CSX_TRACE_ID"))
must(os.Unsetenv(envName))
}
// BindEnv is the explicit half of the same machinery, and its two forms do
// not agree about the prefix.
func bindEnvNaming() {
must(os.Setenv("CSX_PORT", "8080"))
must(os.Setenv("SERVICE_PORT", "9090"))
prefixed := viper.New()
prefixed.SetEnvPrefix(config.EnvPrefix)
must(prefixed.BindEnv("port"))
// Values arrive from the environment as strings; the Get* family casts.
check(prefixed.GetInt("port") == 8080, "one argument means prefix + key: %v", prefixed.Get("port"))
explicit := viper.New()
explicit.SetEnvPrefix(config.EnvPrefix)
must(explicit.BindEnv("port", "SERVICE_PORT"))
check(explicit.GetInt("port") == 9090,
"a second argument is the env name verbatim: no prefix, no uppercasing, no replacer: %v",
explicit.Get("port"))
must(os.Unsetenv("CSX_PORT"))
must(os.Unsetenv("SERVICE_PORT"))
}
// Nothing in the Get family returns an error, so a typo reads as a zero.
func missingKeys() {
v := config.New()
check(v.Get("nope") == nil, "Get on a missing key returns a nil any: %v", v.Get("nope"))
check(v.GetString("nope") == "", "GetString: %q", v.GetString("nope"))
check(v.GetInt("nope") == 0, "GetInt: %d", v.GetInt("nope"))
check(!v.GetBool("nope"), "GetBool")
check(v.GetDuration("nope") == 0, "GetDuration: %v", v.GetDuration("nope"))
check(!v.IsSet("nope"), "IsSet is the only one that can tell you it was missing")
// So a key really set to its zero value is indistinguishable from a typo
// by Get alone, and IsSet is the whole answer.
v.Set("retries", 0)
check(v.GetInt("retries") == 0 && v.IsSet("retries"), "an explicit zero is set")
// Measured, and worth knowing before writing "IsSet means the user chose
// it": a key that only has a default is set. IsSet skips just one rung of
// the search, the flag default, and defaults are not that rung.
d := config.New()
d.SetDefault("timeout", "30s")
check(d.IsSet("timeout"), "IsSet is true for a key that only has a default")
// The exception at the other end: Set(key, nil) stores nothing findable,
// because nil is also how the search says "not here". It does not even
// shadow the default underneath it.
n := config.New()
n.SetDefault("retries", 3)
n.Set("retries", nil)
check(n.GetInt("retries") == 3, "Set(key, nil) is invisible to the search: %v", n.Get("retries"))
}
func caseFolding() {
v := config.New()
v.Set("Feature.Flag", "ON")
check(v.Get("feature.flag") == "ON", "lower: %v", v.Get("feature.flag"))
check(v.Get("FEATURE.FLAG") == "ON", "upper: %v", v.Get("FEATURE.FLAG"))
check(v.Get("Feature.Flag") == "ON", "as written: %v", v.Get("Feature.Flag"))
// One key, stored once, under the folded spelling.
check(slices.Contains(v.AllKeys(), "feature.flag"), "AllKeys: %v", v.AllKeys())
check(!slices.Contains(v.AllKeys(), "Feature.Flag"), "AllKeys keeps no original spelling: %v", v.AllKeys())
// Keys read from a document are folded on the way in. Values are not
// touched, so only half of a case-sensitive comparison changes under you.
mixed := config.New()
must(config.ReadDocument(mixed, "Log:\n Level: Config\n"))
check(mixed.GetString(config.Key) == "Config", "keys fold, values do not: %q", mixed.GetString(config.Key))
}
// Get and Unmarshal ask different questions. Get searches the rungs for one
// key; Unmarshal enumerates AllKeys and reads each one. AutomaticEnv
// contributes no keys to that enumeration — it cannot, since it would have to
// guess names — so a value Get returns can be missing from the struct.
func unmarshalSeesDifferentKeys() {
must(os.Setenv(envName, "env"))
v := config.New()
check(v.GetString(config.Key) == "env", "Get sees it: %q", v.GetString(config.Key))
check(len(v.AllSettings()) == 0, "AllSettings does not: %v", v.AllSettings())
var s config.Settings
must(v.Unmarshal(&s))
check(s.Log.Level == "", "Unmarshal walks AllKeys, so the field stays empty: %q", s.Log.Level)
// Fix one: make the key known by any rung at all. The default never wins
// the lookup — it only puts the key in AllKeys so Unmarshal asks for it.
known := config.New()
known.SetDefault(config.Key, "default")
var s2 config.Settings
must(known.Unmarshal(&s2))
check(s2.Log.Level == "env", "a default makes the key enumerable: %q", s2.Log.Level)
// Fix two: let viper take the key list from the destination struct.
bound := config.New(viper.ExperimentalBindStruct())
var s3 config.Settings
must(bound.Unmarshal(&s3))
check(s3.Log.Level == "env", "ExperimentalBindStruct adds the struct's keys: %q", s3.Log.Level)
must(os.Unsetenv(envName))
}
func must(err error) {
check(err == nil, "unexpected error: %v", err)
}
func check(ok bool, format string, args ...any) {
if !ok {
fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(1)
}
}