diff --git a/README.md b/README.md index d900456..cbb0d61 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,20 @@ -# YAML CLI processor -A CLI tool for querying and transforming YAML data: Grep matching objects, join YAML documents, get/add/edit/delete YAML nodes matching given selector, loop over objects and/or data arrays etc. +# Streaming YAML CLI processor +A CLI tool for querying and transforming YAML stream data: +- Grep matching documents (ie. K8s objects) +- Join multiple YAML files +- Get/add/edit/delete YAML nodes matching given selector +- Loop over documents and/or data arrays +- etc. `[input.yml] => [query or transformations] => [output.yml]` -*Note: The input YAML data might contain multiple YAML documents separated by `---`.* +*Note: The input YAML documents in a [YAML stream](https://yaml.org/spec/1.2/spec.html#id2801681) are separated by `---`.* - [One-liner commands](#one-liner-commands) - [yaml get selector](#yaml-get-selector) + - [yaml get selector --print-key](#yaml-get-selector---print-key) + - [yaml get array[*] --print-key](#yaml-get-array---print-key) + - [yaml get spec.containers[*].image --no-separator](#yaml-get-speccontainersimage---no-separator) - [yaml set "selector: value"](#yaml-set-%22selector-value%22) - [yaml default "selector: value"](#yaml-default-%22selector-value%22) - [yaml delete selector](#yaml-delete-selector) @@ -42,6 +50,8 @@ $ kubectl get pod/nats-8576dfb67-vg6v7 -o yaml | yaml get spec.containers[0].ima nats-streaming:0.10.0 ``` +### yaml get selector --print-key + Since we're printing a value, the output might not necesarilly be a valid YAML. If we're printing value of a primitive type (ie. string) and we need the output in a valid YAML format, so it can be processed further, we can explicitly print the node key in front of the value: @@ -60,6 +70,8 @@ image: nats-streaming:0.10.0 image: sidecar:1.0.1 ``` +### yaml get array[*] --print-key + We can print all array items at once with a wildcard (`array[*]`) too: ```bash @@ -69,7 +81,9 @@ image: nats-streaming:0.10.0 image: sidecar:1.0.1 ``` -Need to get list of values only? +### yaml get spec.containers[*].image --no-separator + +Need to print values only? ```bash $ kubectl get pod/nats-8576dfb67-vg6v7 -o yaml | yaml get spec.containers[*].image --no-separator diff --git a/delete_test.go b/delete_test.go index 38abbb0..9ee8d85 100644 --- a/delete_test.go +++ b/delete_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/VojtechVitek/yaml" + "github.com/VojtechVitek/yaml-cli" "github.com/google/go-cmp/cmp" ) diff --git a/match.go b/match.go index 510cff1..1533c31 100644 --- a/match.go +++ b/match.go @@ -1,6 +1,7 @@ package yaml import ( + "fmt" "regexp" "strings" @@ -10,12 +11,24 @@ import ( func (t *Transformation) MustMatchAll(doc *yaml.Node) (bool, error) { for path, want := range t.Matches { - if want.Kind != yaml.ScalarNode { - return false, errors.Errorf("TODO: Support non-scalar match values?") + var regexString string + + switch want.Kind { + case yaml.ScalarNode: // Single value. Ok. + regexString = want.Value + case yaml.SequenceNode: // Obsolete syntax, ie. "kind: [Deployment, Pod]". Convert to regex. + var values []string + for _, node := range want.Content { + values = append(values, node.Value) + } + regexString = fmt.Sprintf("^%v$", strings.Join(values, "|")) + default: + panic(errors.Errorf("Unexpected match kind %v (expected: single value or array)", want.Kind)) } - re, err := regexp.Compile(want.Value) + + re, err := regexp.Compile(regexString) if err != nil { - return false, errors.Errorf("%q is not a valid regex, see https://github.com/google/re2/wiki/Syntax", want.Content) + return false, errors.Errorf("%q is not a valid regex, see https://github.com/google/re2/wiki/Syntax", regexString) } selectors := strings.Split(path, ".") diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 1f050e6..d6bab9e 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -19,12 +19,13 @@ import ( var ( // TODO: Move these vars to a struct, they shouldn't be global. A left-over from "main" pkg. - flags = flag.NewFlagSet("yaml", flag.ExitOnError) - from = flags.String("from", "yaml", "input data format [json]") - to = flags.String("to", "yaml", "output data format [json]") - printKey = flags.Bool("print-key", false, "yaml get: print node key in front of the value, so the output is valid YAML") - noSeparator = flags.Bool("no-separator", false, "yaml get: don't print `---' separator between YAML documents") - invert = flags.BoolP("invert-match", "v", false, "yaml grep -v: select non-matching documents") + flags = flag.NewFlagSet("yaml", flag.ExitOnError) + from = flags.String("from", "yaml", "input data format [json]") + to = flags.String("to", "yaml", "output data format [json]") + ignoreMissing = flags.Bool("ignore-missing", false, "yaml get: ignore missing nodes") + printKey = flags.Bool("print-key", false, "yaml get: print node key in front of the value, so the output is valid YAML") + noSeparator = flags.Bool("no-separator", false, "yaml get: don't print `---' separator between YAML documents") + invert = flags.BoolP("invert-match", "v", false, "yaml grep -v: select non-matching documents") ) // TODO: Split into multiple files/functions. This function grew too much @@ -237,19 +238,21 @@ func run(out io.Writer, in io.Reader, args []string) error { return nil case "get": + useTopLevelEnc := true + + var allMatchedNodes []*yamlv3.Node + for _, selector := range args[1:] { selectors := strings.Split(selector, ".") lastSelector := selectors[len(selectors)-1] nodes, err := yaml.Get(&doc, selectors) if err != nil { - return errors.Wrapf(err, "failed to get %q", selector) + if !*ignoreMissing { + return errors.Wrapf(err, "failed to get %q", selector) + } } - // // Don't reuse top level encoder; we don't want to render - // // multiple YAML documents separated by `---`. - // enc = yamlv3.NewEncoder(out) - // enc.SetIndent(2) for _, node := range nodes { if *printKey { node = &yamlv3.Node{ @@ -265,10 +268,21 @@ func run(out io.Writer, in io.Reader, args []string) error { } } - if *noSeparator { - enc = yamlv3.NewEncoder(out) - enc.SetIndent(2) + allMatchedNodes = append(allMatchedNodes, node) + } + } + + for _, node := range allMatchedNodes { + if useTopLevelEnc { + useTopLevelEnc = false + // Top level enc will print separator between documents. + if err := enc.Encode(node); err != nil { + return errors.Wrap(err, "failed to encode YAML node") } + } else { + // Omit separator between one YAML document's nodes. + enc := yamlv3.NewEncoder(out) + enc.SetIndent(2) if err := enc.Encode(node); err != nil { return errors.Wrap(err, "failed to encode YAML node") } diff --git a/pkg/cli/cli_test.go b/pkg/cli/cli_test.go index 33a2c66..238e6d6 100644 --- a/pkg/cli/cli_test.go +++ b/pkg/cli/cli_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/VojtechVitek/yaml/pkg/cli" + "github.com/VojtechVitek/yaml-cli/pkg/cli" "github.com/google/go-cmp/cmp" ) diff --git a/pkg/cli/get_test.go b/pkg/cli/get_test.go index a813d81..17e2f80 100644 --- a/pkg/cli/get_test.go +++ b/pkg/cli/get_test.go @@ -7,8 +7,6 @@ import ( func TestGet(t *testing.T) { var kubectlGetPod = readFile("_testfiles/kubectl-get-pod.yml") - // On single input object/document, get and print should behave the same. - tt := []*cliTestCase{ { in: kubectlGetPod, @@ -46,3 +44,44 @@ func TestGet(t *testing.T) { tc.runTest(t) } } + +func TestGetPrintKey(t *testing.T) { + var kubectlGetPod = readFile("_testfiles/kubectl-get-pod.yml") + + tt := []*cliTestCase{ + { + in: kubectlGetPod, + args: []string{"get", "--print-key", "status.containerStatuses[0].name"}, + out: "name: goose-metrixdb\n", + }, + { + in: kubectlGetPod, + args: []string{"get", "--print-key", "status.containerStatuses[0].state.terminated.finishedAt"}, + out: "finishedAt: \"2019-08-18T12:23:29Z\"\n", + }, + { + in: kubectlGetPod, + args: []string{"get", "--print-key", "status.containerStatuses[1].name"}, + out: "name: linkerd-proxy\n", + }, + { + in: kubectlGetPod, + args: []string{"get", "--print-key", "status.containerStatuses[1].state.running.startedAt"}, + out: "startedAt: \"2019-08-18T12:23:30Z\"\n", + }, + { + in: kubectlGetPod, + args: []string{"get", "--print-key", "status.containerStatuses[2].name"}, + err: true, + }, + { + in: kubectlGetPod, + args: []string{"get", "--print-key", "status.containerStatuses[2].state.terminated.finishedAt"}, + err: true, + }, + } + + for _, tc := range tt { + tc.runTest(t) + } +} diff --git a/pkg/cli/print_test.go b/pkg/cli/print_test.go deleted file mode 100644 index c88de04..0000000 --- a/pkg/cli/print_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package cli_test - -import ( - "testing" -) - -func TestPrint(t *testing.T) { - var kubectlGetPod = readFile("_testfiles/kubectl-get-pod.yml") - - // On single input object/document, get and print should behave the same. - - tt := []*cliTestCase{ - { - in: kubectlGetPod, - args: []string{"print", "status.containerStatuses[0].name"}, - out: "name: goose-metrixdb\n", - }, - { - in: kubectlGetPod, - args: []string{"print", "status.containerStatuses[0].state.terminated.finishedAt"}, - out: "finishedAt: \"2019-08-18T12:23:29Z\"\n", - }, - { - in: kubectlGetPod, - args: []string{"print", "status.containerStatuses[1].name"}, - out: "name: linkerd-proxy\n", - }, - { - in: kubectlGetPod, - args: []string{"print", "status.containerStatuses[1].state.running.startedAt"}, - out: "startedAt: \"2019-08-18T12:23:30Z\"\n", - }, - { - in: kubectlGetPod, - args: []string{"print", "status.containerStatuses[2].name"}, - err: true, - }, - { - in: kubectlGetPod, - args: []string{"print", "status.containerStatuses[2].state.terminated.finishedAt"}, - err: true, - }, - } - - for _, tc := range tt { - tc.runTest(t) - } -}