pax_global_header00006660000000000000000000000064140215550030014505gustar00rootroot0000000000000052 comment=8c1192a09f860f4282bdd7093084352ffbec1929 kong-hcl-1.0.1/000077500000000000000000000000001402155500300132065ustar00rootroot00000000000000kong-hcl-1.0.1/.circleci/000077500000000000000000000000001402155500300150415ustar00rootroot00000000000000kong-hcl-1.0.1/.circleci/config.yml000066400000000000000000000020121402155500300170240ustar00rootroot00000000000000version: 2 jobs: build: environment: GO111MODULE: "on" GOBIN: "/go/bin" docker: - image: circleci/golang:1.14 working_directory: /go/src/github.com/alecthomas/kong-hcl steps: - checkout - run: name: Prepare command: | unset GOPATH go get -v github.com/jstemmer/go-junit-report curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s v1.23.8 mkdir ~/report when: always - run: name: Test command: | go test -v ./... 2>&1 | tee report.txt && /go/bin/go-junit-report < report.txt > ~/report/junit.xml (cd v2 && go test -v ./... 2>&1 | tee report.txt && /go/bin/go-junit-report < report.txt > ~/report/junit.xml) - run: name: Lint command: | unset GOPATH ./bin/golangci-lint run (cd v2 && ../bin/golangci-lint run) - store_test_results: path: ~/report kong-hcl-1.0.1/.github/000077500000000000000000000000001402155500300145465ustar00rootroot00000000000000kong-hcl-1.0.1/.github/FUNDING.yml000066400000000000000000000000251402155500300163600ustar00rootroot00000000000000github: [alecthomas] kong-hcl-1.0.1/.golangci.yml000066400000000000000000000016221402155500300155730ustar00rootroot00000000000000run: tests: true output: print-issued-lines: false linters: enable-all: true disable: - maligned - lll - gochecknoglobals - wsl - gomnd - funlen - stylecheck linters-settings: govet: check-shadowing: true gocyclo: min-complexity: 10 dupl: threshold: 100 goconst: min-len: 5 min-occurrences: 3 gocyclo: min-complexity: 20 issues: max-per-linter: 0 max-same: 0 exclude-use-default: false exclude: - '^(G104|G204):' # Very commonly not checked. - 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked' - 'exported method (.*\.MarshalJSON|.*\.UnmarshalJSON) should have comment or be unexported' - 'composite literal uses unkeyed fields' - 'bad syntax for struct tag pair' - 'bad syntax for struct tag key' kong-hcl-1.0.1/COPYING000066400000000000000000000020371402155500300142430ustar00rootroot00000000000000Copyright (C) 2019 Alec Thomas Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. kong-hcl-1.0.1/README.md000066400000000000000000000041141402155500300144650ustar00rootroot00000000000000# A Kong configuration loader for HCL [![](https://godoc.org/github.com/alecthomas/kong-hcl?status.svg)](http://godoc.org/github.com/alecthomas/kong-hcl) [![CircleCI](https://img.shields.io/circleci/project/github/alecthomas/kong-hcl.svg)](https://circleci.com/gh/alecthomas/kong-hcl) This is version 1.x of kong-hcl. [Version 2](https://github.com/alecthomas/kong-hcl/tree/master/v2) of this package uses the HCL2 library but is otherwise largely a drop-in replacement (see the README for details). Use it like so: ```go var cli struct { Config kong.ConfigFlag `help:"Load configuration."` } parser, err := kong.New(&cli, kong.Configuration(konghcl.Loader, "/etc/myapp/config.hcl", "~/.myapp.hcl)) ``` ## Mapping HCL fragments to a struct More complex structures can be loaded directly into flag values by implementing the `kong.MapperValue` interface, and calling `konghcl.DecodeValue`. The value can either be a HCL(/JSON) fragment, or a path to a HCL file that will be loaded. Both can be specified on the command-line or config file. eg. ```go type NestedConfig struct { Size int Name string } type ComplexConfig struct { Key bool Nested map[string]NestedConfig } func (c *ComplexConfig) Decode(ctx *kong.DecodeContext) error { return konghcl.DecodeValue(ctx, c) } // ... type Config struct { Complex ComplexConfig } ``` Then the following `.hcl` config fragment will be decoded into `Complex`: ```hcl complex { key = true nested first { size = 10 name = "first name" } nested second { size = 12 name = "second name" } } ``` ## Configuration layout Configuration keys are mapped directly to flags. Additionally, HCL block keys will be used as a hyphen-separated prefix when looking up flags. ## Example The following Kong CLI: ```go type CLI struct { Debug bool DB struct { DSN string Trace bool } `embed:"" prefix:"db-"` } ``` Maps to the following flags: ``` --debug --db-dsn= --db-trace ``` And can be configured via the following HCL configuration file... ```hcl debug = true db { dsn = "root@/database" trace = true } ``` kong-hcl-1.0.1/dump.go000066400000000000000000000041431402155500300145040ustar00rootroot00000000000000package konghcl import ( "fmt" "sort" "strings" "github.com/alecthomas/kong" ) var ( // DumpIgnoreFlags specifies a set of flags that should not be dumped. DumpIgnoreFlags = map[string]bool{ "help": true, "version": true, "dump-config": true, "env": true, "validate-config": true, } ) // DumpConfig can be added as a flag to dump HCL configuration. type DumpConfig bool func (f DumpConfig) BeforeApply(app *kong.Kong) error { // nolint: golint groups := map[string][]*kong.Flag{} standalone := []*kong.Flag{} for _, flags := range app.Model.AllFlags(true) { for _, flag := range flags { if DumpIgnoreFlags[flag.Name] { continue } parts := strings.SplitN(flag.Name, "-", 2) if len(parts) == 1 { standalone = append(standalone, flag) } else { groups[parts[0]] = append(groups[parts[0]], flag) } } } // Write non-grouped flags out at the top. for key, flags := range groups { if len(flags) == 1 { standalone = append(standalone, flags...) delete(groups, key) } } // Alphabetical ordering. sort.Slice(standalone, func(i, j int) bool { return standalone[i].Name < standalone[j].Name }) for _, flag := range standalone { formatFlag("", flag, false) fmt.Println() } delete(groups, "") // Alphabetically order the groups. keys := []string{} for key := range groups { keys = append(keys, key) } sort.Strings(keys) for block := range groups { flags := groups[block] if len(flags) == 1 { formatFlag("", flags[0], false) fmt.Println() continue } fmt.Printf("%s {\n", block) for i, flag := range flags { if i != 0 { fmt.Println() } formatFlag(" ", flag, true) } fmt.Printf("}\n\n") } app.Exit(0) return nil } func formatFlag(indent string, flag *kong.Flag, grouped bool) { fmt.Printf("%s// %s\n", indent, flag.Help) fmt.Print(indent) if grouped { parts := strings.SplitN(flag.Name, "-", 2) fmt.Printf("%s = ", parts[1]) } else { fmt.Printf("%s = ", flag.Name) } switch { case flag.IsSlice(): fmt.Println("[ ... ]") case flag.IsMap(): fmt.Println("{ ... }") default: fmt.Println(flag.FormatPlaceHolder()) } } kong-hcl-1.0.1/go.mod000066400000000000000000000003031402155500300143100ustar00rootroot00000000000000module github.com/alecthomas/kong-hcl go 1.14 require ( github.com/alecthomas/kong v0.2.16 github.com/hashicorp/hcl v1.0.0 github.com/pkg/errors v0.8.1 github.com/stretchr/testify v1.2.2 ) kong-hcl-1.0.1/go.sum000066400000000000000000000017761402155500300143540ustar00rootroot00000000000000github.com/alecthomas/kong v0.2.16 h1:F232CiYSn54Tnl1sJGTeHmx4vJDNLVP2b9yCVMOQwHQ= github.com/alecthomas/kong v0.2.16/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= 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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= kong-hcl-1.0.1/loader.go000066400000000000000000000113641402155500300150100ustar00rootroot00000000000000package konghcl import ( "encoding/json" "fmt" "io" "io/ioutil" "os" "strings" "github.com/alecthomas/kong" "github.com/hashicorp/hcl" "github.com/pkg/errors" ) // Resolver resolves kong Flags from configuration in HCL. type Resolver struct { config map[string]interface{} } var _ kong.ConfigurationLoader = Loader // DecodeValue decodes Kong values into a Go structure. func DecodeValue(ctx *kong.DecodeContext, dest interface{}) error { v := ctx.Scan.Pop().Value var ( data []byte err error ) switch v := v.(type) { case string: // Value is a string; it can either be a filename or a HCL fragment. filename := kong.ExpandPath(v) data, err = ioutil.ReadFile(filename) // nolint: gosec if os.IsNotExist(err) { data = []byte(v) } else if err != nil { return errors.Wrapf(err, "invalid HCL in %q", filename) } case []map[string]interface{}: merged := map[string]interface{}{} for _, m := range v { for k, v := range m { merged[k] = v } } data, err = json.Marshal(merged) if err != nil { return err } default: data, err = json.Marshal(v) if err != nil { return err } } return errors.Wrapf(hcl.Unmarshal(data, dest), "invalid HCL %q", data) } // Loader is a Kong configuration loader for HCL. func Loader(r io.Reader) (kong.Resolver, error) { data, err := ioutil.ReadAll(r) if err != nil { return nil, err } config := map[string]interface{}{} err = hcl.Unmarshal(data, &config) if err != nil { return nil, errors.Wrap(err, "invalid HCL") } return &Resolver{config: config}, nil } func (r *Resolver) Validate(app *kong.Application) error { // nolint: golint // Find all valid configuration keys from the Application. valid := map[string]bool{} rawPrefixes := []string{} path := []string{} _ = kong.Visit(app, func(node kong.Visitable, next kong.Next) error { switch node := node.(type) { case *kong.Node: path = append(path, node.Name) _ = next(nil) path = path[:len(path)-1] return nil case *kong.Flag: flagPath := append([]string{}, path...) key := strings.Join(append(flagPath, node.Name), "-") if _, ok := node.Target.Interface().(kong.MapperValue); ok { rawPrefixes = append(rawPrefixes, key) } else { valid[key] = true } default: return next(nil) } return nil }) // Then check all configuration keys against the Application keys. next: for key := range flattenConfig(valid, r.config) { if !valid[key] { for _, prefix := range rawPrefixes { if strings.HasPrefix(key, prefix) { continue next } } return errors.Errorf("unknown configuration key %q", key) } } return nil } func (r *Resolver) Resolve(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) { // nolint: golint path := r.pathForFlag(parent, flag) return find(r.config, path) } // Build a string path up to this flag. func (r *Resolver) pathForFlag(parent *kong.Path, flag *kong.Flag) []string { path := []string{} for n := parent.Node(); n != nil && n.Type != kong.ApplicationNode; n = n.Parent { path = append([]string{n.Name}, path...) } path = append(path, flag.Name) return path } // Find the value that path maps to. func find(config map[string]interface{}, path []string) (interface{}, error) { if len(path) == 0 { return config, nil } key := strings.Join(path, "-") if sub := config[key]; sub != nil { return sub, nil } parts := strings.SplitN(key, "-", -1) for i := len(parts); i > 0; i-- { prefix := strings.Join(parts[:i], "-") if sub := config[prefix]; sub != nil { if sub, ok := sub.([]map[string]interface{}); ok { if len(sub) > 1 { return sub, nil } return find(sub[0], parts[i:]) } } } return nil, nil } func flattenConfig(schema map[string]bool, config map[string]interface{}) map[string]bool { out := map[string]bool{} next: for _, path := range flattenNode(config) { for i := len(path) - 1; i >= 0; i-- { candidate := strings.Join(path[:i], "-") if schema[candidate] { out[candidate] = true continue next } } out[strings.Join(path, "-")] = true } return out } func flattenNode(config interface{}) [][]string { out := [][]string{} switch config := config.(type) { case []map[string]interface{}: for _, group := range config { out = append(out, flattenNode(group)...) } case map[string]interface{}: for key, value := range config { children := flattenNode(value) if len(children) == 0 { out = append(out, []string{key}) } else { for _, childValue := range children { out = append(out, append([]string{key}, childValue...)) } } } case []interface{}: for _, el := range config { out = flattenNode(el) } case bool, float64, int, string: return nil default: panic(fmt.Sprintf("unsupported type %T", config)) } return out } kong-hcl-1.0.1/loader_test.go000066400000000000000000000063261402155500300160510ustar00rootroot00000000000000// nolint: govet package konghcl import ( "io/ioutil" "os" "strings" "testing" "github.com/alecthomas/kong" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const testConfig = ` flag-name = "hello world" int-flag = 10 float-flag = 10.5 slice-flag = [1, 2, 3] prefix { prefixed-flag = "prefixed flag" } group { grouped-flag = "grouped flag" embedded-flag = "embedded flag" } map-flag = { key = "value" } // Multiple keys are merged. mapped = { left = "left" } mapped = { right = "right" } prefix-block { embedded-flag = "yes" } ` type mapperValue struct { Left string Right string } func (m *mapperValue) Decode(ctx *kong.DecodeContext) error { return DecodeValue(ctx, m) } func TestHCL(t *testing.T) { type Embedded struct { EmbeddedFlagTwo string EmbeddedFlag string } type CLI struct { FlagName string IntFlag int FloatFlag float64 SliceFlag []int GroupedFlag string `group:"group"` PrefixedFlag string `prefix:"prefix-"` Embedded `group:"group"` PrefixedBlock Embedded `embed:"" prefix:"prefix-block-"` MapFlag map[string]string Mapped mapperValue } t.Run("FromResolver", func(t *testing.T) { var cli CLI r := strings.NewReader(testConfig) resolver, err := Loader(r) require.NoError(t, err) parser, err := kong.New(&cli, kong.Resolvers(resolver)) require.NoError(t, err) _, err = parser.Parse(nil) require.NoError(t, err) assert.Equal(t, "hello world", cli.FlagName) assert.Equal(t, "grouped flag", cli.GroupedFlag) assert.Equal(t, "prefixed flag", cli.PrefixedFlag) assert.Equal(t, "embedded flag", cli.EmbeddedFlag) assert.Equal(t, 10, cli.IntFlag) assert.Equal(t, 10.5, cli.FloatFlag) assert.Equal(t, []int{1, 2, 3}, cli.SliceFlag) assert.Equal(t, map[string]string{"key": "value"}, cli.MapFlag) assert.Equal(t, mapperValue{Left: "left", Right: "right"}, cli.Mapped) assert.Equal(t, Embedded{EmbeddedFlag: "yes"}, cli.PrefixedBlock) }) t.Run("FragmentFromFlag", func(t *testing.T) { var cli CLI parser, err := kong.New(&cli) require.NoError(t, err) _, err = parser.Parse([]string{"--mapped", ` left = "LEFT" right = "RIGHT" `}) require.NoError(t, err) require.Equal(t, mapperValue{Left: "LEFT", Right: "RIGHT"}, cli.Mapped) }) t.Run("FragmentFromFile", func(t *testing.T) { w, err := ioutil.TempFile("", "kong-hcl-") require.NoError(t, err) _, err = w.Write([]byte(` left = "LEFT" right = "RIGHT" `)) require.NoError(t, err) _ = w.Close() defer os.Remove(w.Name()) var cli CLI parser, err := kong.New(&cli) require.NoError(t, err) _, err = parser.Parse([]string{"--mapped", w.Name()}) require.NoError(t, err) require.Equal(t, mapperValue{Left: "LEFT", Right: "RIGHT"}, cli.Mapped) }) } func TestHCLValidation(t *testing.T) { type command struct { CommandFlag string } var cli struct { Command command `cmd:""` Flag string } resolver, err := Loader(strings.NewReader(` invalid-flag = true `)) require.NoError(t, err) parser, err := kong.New(&cli, kong.Resolvers(resolver)) require.NoError(t, err) _, err = parser.Parse([]string{"command"}) require.EqualError(t, err, "unknown configuration key \"invalid-flag\"") } kong-hcl-1.0.1/v2/000077500000000000000000000000001402155500300135355ustar00rootroot00000000000000kong-hcl-1.0.1/v2/COPYING000066400000000000000000000020371402155500300145720ustar00rootroot00000000000000Copyright (C) 2019 Alec Thomas Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. kong-hcl-1.0.1/v2/README.md000066400000000000000000000044751402155500300150260ustar00rootroot00000000000000# A Kong configuration loader for HCL [![](https://godoc.org/github.com/alecthomas/kong-hcl?status.svg)](http://godoc.org/github.com/alecthomas/kong-hcl/v2) [![CircleCI](https://img.shields.io/circleci/project/github/alecthomas/kong-hcl.svg)](https://circleci.com/gh/alecthomas/kong-hcl) This is version 2.x of this package which uses the HCL2 library. For most config files it should be a drop-in replacement, but for any codebases using `konghcl.DecodeValue()` you will need to update your Go structs to include [HCL tags](https://pkg.go.dev/github.com/hashicorp/hcl/v2@v2.4.0/gohcl?tab=doc). Use it like so: ```go var cli struct { Config kong.ConfigFlag `help:"Load configuration."` } parser, err := kong.New(&cli, kong.Configuration(konghcl.Loader, "/etc/myapp/config.hcl", "~/.myapp.hcl)) ``` ## Mapping HCL fragments to a struct More complex structures can be loaded directly into flag values by implementing the `kong.MapperValue` interface, and calling `konghcl.DecodeValue`. The value can either be a HCL(/JSON) fragment, or a path to a HCL file that will be loaded. Both can be specified on the command-line or config file. Note that kong-hcl 2.x uses the HCL2 library, which is *much* stricter about Go tags. See the [HCL2 documentation](https://pkg.go.dev/github.com/hashicorp/hcl/v2@v2.4.0/gohcl?tab=doc) for details. eg. ```go type NestedConfig struct { Size int `hcl:"size,optional"` Name string `hcl:"name,optional"` } type ComplexConfig struct { Key bool `hcl:"key,optional"` Nested map[string]NestedConfig `hcl:"nested,optional"` } func (c *ComplexConfig) Decode(ctx *kong.DecodeContext) error { return konghcl.DecodeValue(ctx, c) } // ... type Config struct { Complex ComplexConfig } ``` Then the following `.hcl` config fragment will be decoded into `Complex`: ```hcl complex { key = true nested first { size = 10 name = "first name" } nested second { size = 12 name = "second name" } } ``` ## Configuration layout Configuration keys are mapped directly to flags. Additionally, HCL block keys will be used as a hyphen-separated prefix when looking up flags. ## Example The following HCL configuration file... ```hcl debug = true db { dsn = "root@/database" trace = true } ``` Maps to the following flags: ``` --debug --db-dsn= --db-trace ``` kong-hcl-1.0.1/v2/dump.go000066400000000000000000000041431402155500300150330ustar00rootroot00000000000000package konghcl import ( "fmt" "sort" "strings" "github.com/alecthomas/kong" ) var ( // DumpIgnoreFlags specifies a set of flags that should not be dumped. DumpIgnoreFlags = map[string]bool{ "help": true, "version": true, "dump-config": true, "env": true, "validate-config": true, } ) // DumpConfig can be added as a flag to dump HCL configuration. type DumpConfig bool func (f DumpConfig) BeforeApply(app *kong.Kong) error { // nolint: golint groups := map[string][]*kong.Flag{} standalone := []*kong.Flag{} for _, flags := range app.Model.AllFlags(true) { for _, flag := range flags { if DumpIgnoreFlags[flag.Name] { continue } parts := strings.SplitN(flag.Name, "-", 2) if len(parts) == 1 { standalone = append(standalone, flag) } else { groups[parts[0]] = append(groups[parts[0]], flag) } } } // Write non-grouped flags out at the top. for key, flags := range groups { if len(flags) == 1 { standalone = append(standalone, flags...) delete(groups, key) } } // Alphabetical ordering. sort.Slice(standalone, func(i, j int) bool { return standalone[i].Name < standalone[j].Name }) for _, flag := range standalone { formatFlag("", flag, false) fmt.Println() } delete(groups, "") // Alphabetically order the groups. keys := []string{} for key := range groups { keys = append(keys, key) } sort.Strings(keys) for block := range groups { flags := groups[block] if len(flags) == 1 { formatFlag("", flags[0], false) fmt.Println() continue } fmt.Printf("%s {\n", block) for i, flag := range flags { if i != 0 { fmt.Println() } formatFlag(" ", flag, true) } fmt.Printf("}\n\n") } app.Exit(0) return nil } func formatFlag(indent string, flag *kong.Flag, grouped bool) { fmt.Printf("%s// %s\n", indent, flag.Help) fmt.Print(indent) if grouped { parts := strings.SplitN(flag.Name, "-", 2) fmt.Printf("%s = ", parts[1]) } else { fmt.Printf("%s = ", flag.Name) } switch { case flag.IsSlice(): fmt.Println("[ ... ]") case flag.IsMap(): fmt.Println("{ ... }") default: fmt.Println(flag.FormatPlaceHolder()) } } kong-hcl-1.0.1/v2/go.mod000066400000000000000000000004511402155500300146430ustar00rootroot00000000000000module github.com/alecthomas/kong-hcl/v2 require ( github.com/alecthomas/kong v0.2.5 github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c github.com/hashicorp/hcl/v2 v2.0.0 github.com/pkg/errors v0.8.1 github.com/stretchr/testify v1.2.2 github.com/zclconf/go-cty v1.1.0 ) go 1.13 kong-hcl-1.0.1/v2/go.sum000066400000000000000000000115401402155500300146710ustar00rootroot00000000000000github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alecthomas/kong v0.2.5 h1:ebhzNAWR9tLmqkdnzaVCvtj65aGmM1xiEZPgM5vYWJM= github.com/alecthomas/kong v0.2.5/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c h1:MVVbswUlqicyj8P/JljoocA7AyCo62gzD0O7jfvrhtE= github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= 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/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/hashicorp/hcl/v2 v2.0.0 h1:efQznTz+ydmQXq3BOnRa3AXzvCeTq1P4dKj/z5GLlY8= github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/zclconf/go-cty v1.1.0 h1:uJwc9HiBOCpoKIObTQaLR+tsEXx1HBHnOsOOpcdhZgw= github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= kong-hcl-1.0.1/v2/loader.go000066400000000000000000000200521402155500300153310ustar00rootroot00000000000000package konghcl import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "os" "strings" "github.com/alecthomas/kong" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclparse" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/pkg/errors" "github.com/zclconf/go-cty/cty" ) // Resolver resolves kong Flags from configuration in HCL. type Resolver struct { config map[string]interface{} } var _ kong.ConfigurationLoader = Loader // DecodeValue decodes Kong values into a Go structure. func DecodeValue(ctx *kong.DecodeContext, dest interface{}) error { v := ctx.Scan.Pop().Value var ( data []byte err error ) filename := "config.hcl" switch v := v.(type) { case string: // Value is a string; it can either be a filename or a HCL fragment. filename = kong.ExpandPath(v) data, err = ioutil.ReadFile(filename) // nolint: gosec if os.IsNotExist(err) { data = []byte(v) } else if err != nil { return errors.Wrapf(err, "invalid HCL in %q", filename) } case []map[string]interface{}: merged := map[string]interface{}{} for _, m := range v { for k, v := range m { merged[k] = v } } data, err = json.Marshal(merged) if err != nil { return err } default: data, err = json.Marshal(v) if err != nil { return err } } parser := hclparse.NewParser() var ( ast *hcl.File diag hcl.Diagnostics ) if bytes.HasPrefix(data, []byte("{")) { ast, diag = parser.ParseJSON(data, filename) } else { ast, diag = parser.ParseHCL(data, filename) } if diag.HasErrors() { return errors.Errorf("invalid HCL %s: %s", data, diag[0].Summary) } diag = gohcl.DecodeBody(ast.Body, nil, dest) if diag.HasErrors() { return errors.Errorf("invalid HCL %s: %s", data, diag[0].Summary) } return nil } // Loader is a Kong configuration loader for HCL. func Loader(r io.Reader) (kong.Resolver, error) { filename := "config.hcl" if named, ok := r.(interface{ Name() string }); ok { filename = named.Name() } parser := hclparse.NewParser() source, err := ioutil.ReadAll(r) if err != nil { return nil, errors.WithStack(err) } ast, diag := parser.ParseHCL(source, filename) if diag.HasErrors() { return nil, errors.Wrap(diag, filename) } config := map[string]interface{}{} err = flattenHCL(nil, ast.Body.(*hclsyntax.Body), config) if err != nil { return nil, err } return &Resolver{config: config}, nil } func flattenHCL(key []string, node hclsyntax.Node, dest map[string]interface{}) (err error) { defer func() { if err != nil && len(key) > 0 { err = errors.Wrap(err, key[len(key)-1]) } }() switch node := node.(type) { case hclsyntax.Attributes: for attr, value := range node { if err := flattenHCL(append(key, attr), value, dest); err != nil { return err } } case *hclsyntax.Attribute: value, err := decodeHCLExpr(node.Expr) if err != nil { return err } dest[strings.Join(key, "-")] = value case hclsyntax.Blocks: for _, block := range node { if err := flattenHCL(key, block, dest); err != nil { return err } } case *hclsyntax.Block: sub := map[string]interface{}{} key = append(key, node.Type) for _, label := range node.Labels { next := map[string]interface{}{} sub[label] = []map[string]interface{}{next} sub = next } if err := flattenHCL(nil, node.Body, sub); err != nil { return err } dkey := strings.Join(key, "-") switch value := dest[dkey].(type) { case nil: dest[dkey] = []map[string]interface{}{sub} case []map[string]interface{}: value = append(value, sub) dest[dkey] = value } case *hclsyntax.Body: if err := flattenHCL(key, node.Attributes, dest); err != nil { return err } if err := flattenHCL(key, node.Blocks, dest); err != nil { return err } default: panic(fmt.Sprintf("%T", node)) } return nil } func decodeHCLExpr(expr hclsyntax.Expression) (interface{}, error) { value, diag := expr.Value(nil) if diag.HasErrors() { return nil, errors.WithStack(diag) } return decodeCTYValue(value), nil } func decodeCTYValue(value cty.Value) interface{} { switch value.Type() { case cty.String: return value.AsString() case cty.Bool: return value.True() case cty.Number: f, _ := value.AsBigFloat().Float64() return f default: if value.Type().IsListType() || value.Type().IsTupleType() { out := []interface{}{} value.ForEachElement(func(key cty.Value, val cty.Value) (stop bool) { out = append(out, decodeCTYValue(val)) return false }) return out } else if value.Type().IsMapType() || value.Type().IsObjectType() { out := map[string]interface{}{} value.ForEachElement(func(key cty.Value, val cty.Value) (stop bool) { out[key.AsString()] = decodeCTYValue(val) return false }) return out } } panic(value.Type().GoString()) } func (r *Resolver) Validate(app *kong.Application) error { // nolint: golint // Find all valid configuration keys from the Application. valid := map[string]bool{} rawPrefixes := []string{} path := []string{} _ = kong.Visit(app, func(node kong.Visitable, next kong.Next) error { switch node := node.(type) { case *kong.Node: path = append(path, node.Name) _ = next(nil) path = path[:len(path)-1] return nil case *kong.Flag: flagPath := append([]string{}, path...) if node.Group != "" { flagPath = append(flagPath, node.Group) } key := strings.Join(append(flagPath, node.Name), "-") if _, ok := node.Target.Interface().(kong.MapperValue); ok { rawPrefixes = append(rawPrefixes, key) } else { valid[key] = true } default: return next(nil) } return nil }) // Then check all configuration keys against the Application keys. next: for key := range flattenConfig(valid, r.config) { if !valid[key] { for _, prefix := range rawPrefixes { if strings.HasPrefix(key, prefix) { continue next } } return errors.Errorf("unknown configuration key %q", key) } } return nil } func (r *Resolver) Resolve(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) { // nolint: golint path := r.pathForFlag(parent, flag) return find(r.config, path) } // Build a string path up to this flag. func (r *Resolver) pathForFlag(parent *kong.Path, flag *kong.Flag) []string { path := []string{} for n := parent.Node(); n != nil && n.Type != kong.ApplicationNode; n = n.Parent { path = append([]string{n.Name}, path...) } if flag.Group != "" { path = append([]string{flag.Group}, path...) } path = append(path, flag.Name) return path } // Find the value that path maps to. func find(config map[string]interface{}, path []string) (interface{}, error) { if len(path) == 0 { return config, nil } key := strings.Join(path, "-") parts := strings.SplitN(key, "-", -1) for i := len(parts); i > 0; i-- { prefix := strings.Join(parts[:i], "-") if sub := config[prefix]; sub != nil { if sub, ok := sub.([]map[string]interface{}); ok { if len(sub) > 1 { return sub, nil } return find(sub[0], parts[i:]) } return sub, nil } } return nil, nil } func flattenConfig(schema map[string]bool, config map[string]interface{}) map[string]bool { out := map[string]bool{} next: for _, path := range flattenNode(config) { for i := len(path) - 1; i >= 0; i-- { candidate := strings.Join(path[:i], "-") if schema[candidate] { out[candidate] = true continue next } } out[strings.Join(path, "-")] = true } return out } func flattenNode(config interface{}) [][]string { out := [][]string{} switch config := config.(type) { case []map[string]interface{}: for _, group := range config { out = append(out, flattenNode(group)...) } case map[string]interface{}: for key, value := range config { children := flattenNode(value) if len(children) == 0 { out = append(out, []string{key}) } else { for _, childValue := range children { out = append(out, append([]string{key}, childValue...)) } } } case []interface{}: for _, el := range config { out = flattenNode(el) } case bool, float64, int, string: return nil default: panic(fmt.Sprintf("unsupported type %T", config)) } return out } kong-hcl-1.0.1/v2/loader_test.go000066400000000000000000000063431402155500300163770ustar00rootroot00000000000000// nolint: govet package konghcl import ( "io/ioutil" "os" "strings" "testing" "github.com/alecthomas/kong" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const testConfig = ` flag-name = "hello world" int-flag = 10 float-flag = 10.5 slice-flag = [1, 2, 3] prefix { prefixed-flag = "prefixed flag" } group { grouped-flag = "grouped flag" embedded-flag = "embedded flag" } map-flag = { key = "value" } // Multiple keys are merged. mapped { left = "left" } mapped { right = "right" } prefix-block { embedded-flag = "yes" } ` type mapperValue struct { Left string `hcl:"left,optional"` Right string `hcl:"right,optional"` } func (m *mapperValue) Decode(ctx *kong.DecodeContext) error { return DecodeValue(ctx, m) } func TestHCL(t *testing.T) { type Embedded struct { EmbeddedFlag string } type CLI struct { FlagName string IntFlag int FloatFlag float64 SliceFlag []int GroupedFlag string `group:"group"` PrefixedFlag string `prefix:"prefix-"` Embedded `group:"group"` PrefixedBlock Embedded `embed:"" prefix:"prefix-block-"` MapFlag map[string]string Mapped mapperValue } t.Run("FromResolver", func(t *testing.T) { var cli CLI r := strings.NewReader(testConfig) resolver, err := Loader(r) require.NoError(t, err) parser, err := kong.New(&cli, kong.Resolvers(resolver)) require.NoError(t, err) _, err = parser.Parse(nil) require.NoError(t, err) assert.Equal(t, "hello world", cli.FlagName) assert.Equal(t, "grouped flag", cli.GroupedFlag) assert.Equal(t, "prefixed flag", cli.PrefixedFlag) assert.Equal(t, "embedded flag", cli.EmbeddedFlag) assert.Equal(t, 10, cli.IntFlag) assert.Equal(t, 10.5, cli.FloatFlag) assert.Equal(t, []int{1, 2, 3}, cli.SliceFlag) assert.Equal(t, map[string]string{"key": "value"}, cli.MapFlag) assert.Equal(t, mapperValue{Left: "left", Right: "right"}, cli.Mapped) assert.Equal(t, Embedded{EmbeddedFlag: "yes"}, cli.PrefixedBlock) }) t.Run("FragmentFromFlag", func(t *testing.T) { var cli CLI parser, err := kong.New(&cli) require.NoError(t, err) _, err = parser.Parse([]string{"--mapped", ` left = "LEFT" right = "RIGHT" `}) require.NoError(t, err) require.Equal(t, mapperValue{Left: "LEFT", Right: "RIGHT"}, cli.Mapped) }) t.Run("FragmentFromFile", func(t *testing.T) { w, err := ioutil.TempFile("", "kong-hcl-") require.NoError(t, err) _, err = w.Write([]byte(` left = "LEFT" right = "RIGHT" `)) require.NoError(t, err) _ = w.Close() defer os.Remove(w.Name()) var cli CLI parser, err := kong.New(&cli) require.NoError(t, err) _, err = parser.Parse([]string{"--mapped", w.Name()}) require.NoError(t, err) require.Equal(t, mapperValue{Left: "LEFT", Right: "RIGHT"}, cli.Mapped) }) } func TestHCLValidation(t *testing.T) { type command struct { CommandFlag string } var cli struct { Command command `cmd:""` Flag string } resolver, err := Loader(strings.NewReader(` invalid-flag = true `)) require.NoError(t, err) parser, err := kong.New(&cli, kong.Resolvers(resolver)) require.NoError(t, err) _, err = parser.Parse([]string{"command"}) require.EqualError(t, err, "unknown configuration key \"invalid-flag\"") }