From 281d37d008b2161090c4c6d9168bf6e3a5d63551 Mon Sep 17 00:00:00 2001 From: Jussi Maki Date: Mon, 6 Oct 2025 15:41:56 +0200 Subject: [PATCH] script: Add json and yaml commands This provides simple "jq" style facility for transforming a JSON/YAML document to make it easier to write assertions against more complex data. While a Go implementation of jq does exist it is very large, so opting instead for this much simpler one. Signed-off-by: Jussi Maki --- go.mod | 7 +- go.sum | 12 +++ script/cmds.go | 109 +++++++++++++++++++++++++++- script/scripttest/testdata/jmes.txt | 39 ++++++++++ 4 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 script/scripttest/testdata/jmes.txt diff --git a/go.mod b/go.mod index 95dbe35..2703237 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.0 require ( github.com/cilium/stream v0.0.0-20240209152734-a0792b51812d github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc + github.com/jmespath/go-jmespath v0.4.0 github.com/mitchellh/mapstructure v1.5.0 github.com/spf13/cast v1.6.0 github.com/spf13/cobra v1.8.0 @@ -13,8 +14,11 @@ require ( github.com/stretchr/testify v1.8.4 go.uber.org/dig v1.17.1 go.uber.org/goleak v1.3.0 + golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb + golang.org/x/sys v0.15.0 golang.org/x/term v0.15.0 golang.org/x/tools v0.16.0 + sigs.k8s.io/yaml v1.6.0 ) require ( @@ -32,8 +36,7 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect - golang.org/x/sys v0.15.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 7f49c23..5f3c2e4 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,10 @@ 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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 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= @@ -62,6 +66,10 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= +go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= @@ -79,6 +87,10 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/script/cmds.go b/script/cmds.go index 208a9e5..c3ef6f3 100644 --- a/script/cmds.go +++ b/script/cmds.go @@ -5,6 +5,7 @@ package script import ( + "encoding/json" "errors" "fmt" "io/fs" @@ -21,9 +22,12 @@ import ( "sync" "time" - "github.com/cilium/hive/script/internal/diff" + "github.com/jmespath/go-jmespath" "github.com/spf13/pflag" "golang.org/x/term" + "sigs.k8s.io/yaml" + + "github.com/cilium/hive/script/internal/diff" ) // DefaultCmds returns a set of broadly useful script commands. @@ -57,6 +61,8 @@ func DefaultCmds() map[string]Cmd { "symlink": Symlink(), "wait": Wait(), "break": Break(), + "json": Json(), + "yaml": Yaml(), } } @@ -1575,3 +1581,104 @@ func dirEntryIsExec(entry os.DirEntry) bool { } return info.Mode()&execAny != 0 } + +func Json() Cmd { + return jsonOrYaml(false) +} + +func Yaml() Cmd { + return jsonOrYaml(true) +} + +func jsonOrYaml(yamlMode bool) Cmd { + which := "JSON" + if yamlMode { + which = "YAML" + } + return Command( + CmdUsage{ + Summary: "Transform a "+which+" document with a JMESPath expression", + Args: "expression", + Detail: []string{ + "See https://jmespath.org/tutorial.html for tutorial on JMESPath expressions.", + }, + Flags: func(fs *pflag.FlagSet) { + fs.StringP("input", "i", "", "File to read from instead of stdout buffer") + fs.StringP("output", "o", "", "File to write to instead of stdout buffer") + fs.BoolP("quiet", "q", false, "Do not output the transformed document") + fs.BoolP("fail", "f", false, "Fail on null (no match)") + }, + }, + func(s *State, args ...string) (WaitFunc, error) { + if len(args) != 1 { + return nil, fmt.Errorf("%s: expected expression", ErrUsage) + } + expr := args[0] + return func(s *State) (stdout string, stderr string, err error) { + input := []byte(s.Stdout()) + if inputFile, err := s.Flags.GetString("input"); err != nil { + return "", "", err + } else if inputFile != "" { + b, err := os.ReadFile(s.Path(inputFile)) + if err != nil { + return "", "", err + } + input = b + } + + if yamlMode { + input, err = yaml.YAMLToJSON(input) + if err != nil { + return "", "", err + } + } + + var data any + if err := json.Unmarshal(input, &data); err != nil { + return "", "", err + } + result, err := jmespath.Search(expr, data) + if err != nil { + return "", "", err + } + + if fail, err := s.Flags.GetBool("fail"); err != nil { + return "", "", err + } else if fail && result == nil { + return "", "", errors.New("No match") + } + + if quiet, err := s.Flags.GetBool("quiet"); err != nil { + return "", "", err + } else if quiet { + return "", "", nil + } + + var out []byte + if yamlMode { + out, err = yaml.Marshal(result) + if err != nil { + return "", "", err + } + } else { + out, err = json.Marshal(result) + if err != nil { + return "", "", err + } + } + + if outputFile, err := s.Flags.GetString("output"); err != nil { + return "", "", err + } else if outputFile != "" { + err := os.WriteFile(s.Path(outputFile), out, 0644) + return "", "", err + } + + return string(out), "", nil + }, nil + + }, + ) +} + + diff --git a/script/scripttest/testdata/jmes.txt b/script/scripttest/testdata/jmes.txt new file mode 100644 index 0000000..1a7929b --- /dev/null +++ b/script/scripttest/testdata/jmes.txt @@ -0,0 +1,39 @@ +# Tests for the json/yaml commands for transforming a JSON/YAML document using +# a JMESPath expression. + +# stdout +echo '{"a": "b"}' +json a +stdout "b" + +echo 'a: b' +yaml a +stdout b + +# from input file +json -i test.json a +stdout ^{"aa":"foo"}$ + +json -i test.json a.aa +stdout "foo" + +json -i test.json c[?v>`1`].v +stdout [2] + +yaml -i test.yaml c[?v>`1`].v +stdout '\- 2' + +# fail when no matches +! json --fail --input test.json 'c[2]' +! json --fail --input test.json 'd' + +-- test.json -- +{"a": {"aa": "foo"}, "b": 10, "c": [{"v": 1}, {"v": 2}]} + +-- test.yaml -- +a: + aa: foo +b: 10 +c: + - v: 1 + - v: 2