mirror of
https://github.com/taigrr/yq
synced 2025-01-18 04:53:17 -08:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f9f8665dd | ||
|
|
3dea1efc03 | ||
|
|
547787f3ae | ||
|
|
3e83ff7ac8 | ||
|
|
f18c5161e0 | ||
|
|
eeeeeffd7b | ||
|
|
27604289f4 | ||
|
|
3f36a18791 | ||
|
|
5fc13bdccd | ||
|
|
95fec2984e | ||
|
|
64d1e58f97 | ||
|
|
4b3fbb878f | ||
|
|
26a09e6ec0 | ||
|
|
ceafed30f9 | ||
|
|
b8b2c9de61 | ||
|
|
f5fdf98c38 | ||
|
|
29986db8f8 | ||
|
|
1f4e3a9cde | ||
|
|
b6da773dde | ||
|
|
8020d4253b | ||
|
|
3c701fe98e | ||
|
|
97d1aa2b26 | ||
|
|
d05391e244 | ||
|
|
d1cec1ad18 | ||
|
|
fe5842e5f9 | ||
|
|
e0d8cd6bf6 | ||
|
|
8f5ffe47ff | ||
|
|
5acc1e661e | ||
|
|
a9c0ef571c | ||
|
|
7a28531f2f | ||
|
|
5de2bea1b4 | ||
|
|
7320b8d3c9 | ||
|
|
2f70e6f27a | ||
|
|
a9e871ee00 | ||
|
|
bc4bab9380 | ||
|
|
665d9079fa | ||
|
|
7b54a44fcf | ||
|
|
94148e4398 | ||
|
|
f8553162ca | ||
|
|
be532bf2fe | ||
|
|
e9b8265ca3 | ||
|
|
0f8b864321 | ||
|
|
e5bcfedbe9 | ||
|
|
a5f5fb2562 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
bin
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,6 +8,7 @@ _obj
|
||||
_test
|
||||
bin
|
||||
build
|
||||
build-done
|
||||
.DS_Store
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
@@ -26,7 +27,7 @@ coverage.out
|
||||
*.test
|
||||
*.prof
|
||||
yaml
|
||||
vendor/*/
|
||||
vendor/
|
||||
tmp/
|
||||
cover/
|
||||
yq
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.11.x
|
||||
- 1.13.x
|
||||
script:
|
||||
- scripts/devtools.sh
|
||||
- make local build
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.11 as builder
|
||||
FROM golang:1.13 as builder
|
||||
|
||||
WORKDIR /go/src/mikefarah/yq
|
||||
|
||||
@@ -6,10 +6,6 @@ WORKDIR /go/src/mikefarah/yq
|
||||
COPY ./scripts/devtools.sh /go/src/mikefarah/yq/scripts/devtools.sh
|
||||
RUN ./scripts/devtools.sh
|
||||
|
||||
# cache vendor
|
||||
COPY ./vendor/vendor.json /go/src/mikefarah/yq/vendor/vendor.json
|
||||
RUN govendor sync
|
||||
|
||||
COPY . /go/src/mikefarah/yq
|
||||
|
||||
RUN CGO_ENABLED=0 make local build
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.9
|
||||
FROM golang:1.13
|
||||
|
||||
COPY scripts/devtools.sh /opt/devtools.sh
|
||||
|
||||
|
||||
5
Makefile
5
Makefile
@@ -14,7 +14,7 @@ help:
|
||||
@echo ' make build Build yq binary.'
|
||||
@echo ' make install Install yq.'
|
||||
@echo ' make xcompile Build cross-compiled binaries of yq.'
|
||||
@echo ' make vendor Install dependencies using govendor.'
|
||||
@echo ' make vendor Install dependencies to vendor directory.'
|
||||
@echo ' make format Run code formatter.'
|
||||
@echo ' make check Run static code analysis (lint).'
|
||||
@echo ' make test Run tests on project.'
|
||||
@@ -71,8 +71,7 @@ install: build
|
||||
# Each of the fetch should be an entry within vendor.json; not currently included within project
|
||||
.PHONY: vendor
|
||||
vendor: tmp/dev_image_id
|
||||
${DOCKRUN} govendor sync
|
||||
@chmod 664 vendor/vendor.json
|
||||
${DOCKRUN} go mod vendor
|
||||
|
||||
# ----------------------------------------------
|
||||
# develop and test
|
||||
|
||||
41
README.md
41
README.md
@@ -1,13 +1,27 @@
|
||||
# yq
|
||||
|
||||
[](https://travis-ci.org/mikefarah/yq)  
|
||||
[](https://travis-ci.org/mikefarah/yq)   
|
||||
|
||||
|
||||
a lightweight and portable command-line YAML processor
|
||||
|
||||
The aim of the project is to be the [jq](https://github.com/stedolan/jq) or sed of yaml files.
|
||||
|
||||
|
||||
## Major upgrade - V3 beta is out!
|
||||
|
||||
This addresses a number of features requests and issues that have been raised :)
|
||||
|
||||
Currently only available only available as a [binary release here](https://github.com/mikefarah/yq/releases/tag/3.0.0-beta) or via docker mikefarah/yq:3.0.0-beta!
|
||||
|
||||
It does have a few breaking changes listed on the [release page](https://github.com/mikefarah/yq/releases/tag/3.0.0-beta)
|
||||
|
||||
Looking forward to feedback - once this is out of beta it will be added to the remaining package managers, and be the default version downloaded (and merged into master).
|
||||
|
||||
V2 will no longer have any new features added, and will be moved to a branch (v2). It will have limited maintenance for bugs for a few months.
|
||||
|
||||
## Install
|
||||
|
||||
### On MacOS:
|
||||
```
|
||||
brew install yq
|
||||
@@ -21,16 +35,16 @@ snap install yq
|
||||
`yq` installs with with [_strict confinement_](https://docs.snapcraft.io/snap-confinement/6233) in snap, this means it doesn't have direct access to root files. To read root files you can:
|
||||
|
||||
```
|
||||
sudo cat /etc/myfile | yq -r - somecommand
|
||||
sudo cat /etc/myfile | yq r - a.path
|
||||
```
|
||||
|
||||
And to write to a root file you can either use [sponge](https://linux.die.net/man/1/sponge):
|
||||
```
|
||||
sudo cat /etc/myfile | yq -r - somecommand | sudo sponge /etc/myfile
|
||||
sudo cat /etc/myfile | yq w - a.path value | sudo sponge /etc/myfile
|
||||
```
|
||||
or write to a temporary file:
|
||||
```
|
||||
sudo cat /etc/myfile | yq -r - somecommand | sudo tee /etc/myfile.tmp
|
||||
sudo cat /etc/myfile | yq w - a.path value | sudo tee /etc/myfile.tmp
|
||||
sudo mv /etc/myfile.tmp /etc/myfile
|
||||
rm /etc/myfile.tmp
|
||||
```
|
||||
@@ -43,7 +57,7 @@ sudo apt install yq -y
|
||||
```
|
||||
### or, [Download latest binary](https://github.com/mikefarah/yq/releases/latest) or alternatively:
|
||||
```
|
||||
go get gopkg.in/mikefarah/yq.v2
|
||||
GO111MODULE=on go get github.com/mikefarah/yq/v2
|
||||
```
|
||||
|
||||
## Run with Docker
|
||||
@@ -51,13 +65,21 @@ go get gopkg.in/mikefarah/yq.v2
|
||||
Oneshot use:
|
||||
|
||||
```bash
|
||||
docker run -v ${PWD}:/workdir mikefarah/yq yq [flags] <command> FILE...
|
||||
docker run --rm -v ${PWD}:/workdir mikefarah/yq yq [flags] <command> FILE...
|
||||
```
|
||||
|
||||
Run commands interactively:
|
||||
|
||||
```bash
|
||||
docker run -it -v ${PWD}:/workdir mikefarah/yq sh
|
||||
docker run --rm -it -v ${PWD}:/workdir mikefarah/yq sh
|
||||
```
|
||||
|
||||
It can be useful to have a bash function to avoid typing the whole docker command:
|
||||
|
||||
```bash
|
||||
yq() {
|
||||
docker run --rm -i -v ${PWD}:/workdir mikefarah/yq yq $@
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
@@ -107,10 +129,13 @@ Use "yq [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
## Contribute
|
||||
|
||||
**Note: v3 is currently in progress - for the moment I won't be accepting new feature PRs until v3 is ready :)**
|
||||
|
||||
1. `scripts/devtools.sh`
|
||||
2. `make [local] vendor`
|
||||
3. add unit tests
|
||||
4. apply changes (use govendor with a preference to [gopkg](https://gopkg.in/) for package dependencies)
|
||||
4. apply changes to go.mod
|
||||
5. `make [local] build`
|
||||
6. If required, update the user documentation
|
||||
- Update README.md and/or documentation under the mkdocs folder
|
||||
|
||||
462
commands_test.go
462
commands_test.go
File diff suppressed because it is too large
Load Diff
@@ -1,349 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
yaml "gopkg.in/mikefarah/yaml.v2"
|
||||
)
|
||||
|
||||
func matchesKey(key string, actual interface{}) bool {
|
||||
var actualString = fmt.Sprintf("%v", actual)
|
||||
var prefixMatch = strings.TrimSuffix(key, "*")
|
||||
if prefixMatch != key {
|
||||
return strings.HasPrefix(actualString, prefixMatch)
|
||||
}
|
||||
return actualString == key
|
||||
}
|
||||
|
||||
func entriesInSlice(context yaml.MapSlice, key string) []*yaml.MapItem {
|
||||
var matches = make([]*yaml.MapItem, 0)
|
||||
for idx := range context {
|
||||
var entry = &context[idx]
|
||||
if matchesKey(key, entry.Key) {
|
||||
matches = append(matches, entry)
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
func getMapSlice(context interface{}) yaml.MapSlice {
|
||||
var mapSlice yaml.MapSlice
|
||||
switch context := context.(type) {
|
||||
case yaml.MapSlice:
|
||||
mapSlice = context
|
||||
default:
|
||||
mapSlice = make(yaml.MapSlice, 0)
|
||||
}
|
||||
return mapSlice
|
||||
}
|
||||
|
||||
func getArray(context interface{}) (array []interface{}, ok bool) {
|
||||
switch context := context.(type) {
|
||||
case []interface{}:
|
||||
array = context
|
||||
ok = true
|
||||
default:
|
||||
array = make([]interface{}, 0)
|
||||
ok = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func writeMap(context interface{}, paths []string, value interface{}) interface{} {
|
||||
log.Debugf("writeMap with path %v for %v to set value %v\n", paths, context, value)
|
||||
|
||||
mapSlice := getMapSlice(context)
|
||||
|
||||
if len(paths) == 0 {
|
||||
return context
|
||||
}
|
||||
|
||||
children := entriesInSlice(mapSlice, paths[0])
|
||||
|
||||
if len(children) == 0 && paths[0] == "*" {
|
||||
log.Debugf("\tNo matches, return map as is")
|
||||
return context
|
||||
}
|
||||
|
||||
if len(children) == 0 {
|
||||
newChild := yaml.MapItem{Key: paths[0]}
|
||||
mapSlice = append(mapSlice, newChild)
|
||||
children = entriesInSlice(mapSlice, paths[0])
|
||||
log.Debugf("\tAppended child at %v for mapSlice %v\n", paths[0], mapSlice)
|
||||
}
|
||||
|
||||
remainingPaths := paths[1:]
|
||||
for _, child := range children {
|
||||
child.Value = updatedChildValue(child.Value, remainingPaths, value)
|
||||
}
|
||||
log.Debugf("\tReturning mapSlice %v\n", mapSlice)
|
||||
return mapSlice
|
||||
}
|
||||
|
||||
func updatedChildValue(child interface{}, remainingPaths []string, value interface{}) interface{} {
|
||||
if len(remainingPaths) == 0 {
|
||||
return value
|
||||
}
|
||||
log.Debugf("updatedChildValue for child %v with path %v to set value %v", child, remainingPaths, value)
|
||||
log.Debugf("type of child is %v", reflect.TypeOf(child))
|
||||
|
||||
switch child := child.(type) {
|
||||
case nil:
|
||||
if remainingPaths[0] == "+" || remainingPaths[0] == "*" {
|
||||
return writeArray(child, remainingPaths, value)
|
||||
}
|
||||
case []interface{}:
|
||||
_, nextIndexErr := strconv.ParseInt(remainingPaths[0], 10, 64)
|
||||
arrayCommand := nextIndexErr == nil || remainingPaths[0] == "+" || remainingPaths[0] == "*"
|
||||
if arrayCommand {
|
||||
return writeArray(child, remainingPaths, value)
|
||||
}
|
||||
}
|
||||
return writeMap(child, remainingPaths, value)
|
||||
}
|
||||
|
||||
func writeArray(context interface{}, paths []string, value interface{}) []interface{} {
|
||||
log.Debugf("writeArray with path %v for %v to set value %v\n", paths, context, value)
|
||||
array, _ := getArray(context)
|
||||
|
||||
if len(paths) == 0 {
|
||||
return array
|
||||
}
|
||||
|
||||
log.Debugf("\tarray %v\n", array)
|
||||
|
||||
rawIndex := paths[0]
|
||||
remainingPaths := paths[1:]
|
||||
var index int64
|
||||
// the append array indicator
|
||||
if rawIndex == "+" {
|
||||
index = int64(len(array))
|
||||
} else if rawIndex == "*" {
|
||||
for index, oldChild := range array {
|
||||
array[index] = updatedChildValue(oldChild, remainingPaths, value)
|
||||
}
|
||||
return array
|
||||
} else {
|
||||
index, _ = strconv.ParseInt(rawIndex, 10, 64) // nolint
|
||||
// writeArray is only called by updatedChildValue which handles parsing the
|
||||
// index, as such this renders this dead code.
|
||||
}
|
||||
|
||||
for index >= int64(len(array)) {
|
||||
array = append(array, nil)
|
||||
}
|
||||
currentChild := array[index]
|
||||
|
||||
log.Debugf("\tcurrentChild %v\n", currentChild)
|
||||
|
||||
array[index] = updatedChildValue(currentChild, remainingPaths, value)
|
||||
log.Debugf("\tReturning array %v\n", array)
|
||||
return array
|
||||
}
|
||||
|
||||
func readMap(context yaml.MapSlice, head string, tail []string) (interface{}, error) {
|
||||
log.Debugf("readingMap %v with key %v\n", context, head)
|
||||
if head == "*" {
|
||||
return readMapSplat(context, tail)
|
||||
}
|
||||
|
||||
entries := entriesInSlice(context, head)
|
||||
if len(entries) == 1 {
|
||||
return calculateValue(entries[0].Value, tail)
|
||||
} else if len(entries) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var errInIdx error
|
||||
values := make([]interface{}, len(entries))
|
||||
for idx, entry := range entries {
|
||||
values[idx], errInIdx = calculateValue(entry.Value, tail)
|
||||
if errInIdx != nil {
|
||||
log.Errorf("Error updating index %v in %v", idx, context)
|
||||
return nil, errInIdx
|
||||
}
|
||||
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func readMapSplat(context yaml.MapSlice, tail []string) (interface{}, error) {
|
||||
var newArray = make([]interface{}, len(context))
|
||||
var i = 0
|
||||
for _, entry := range context {
|
||||
if len(tail) > 0 {
|
||||
val, err := recurse(entry.Value, tail[0], tail[1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newArray[i] = val
|
||||
} else {
|
||||
newArray[i] = entry.Value
|
||||
}
|
||||
i++
|
||||
}
|
||||
return newArray, nil
|
||||
}
|
||||
|
||||
func recurse(value interface{}, head string, tail []string) (interface{}, error) {
|
||||
switch value := value.(type) {
|
||||
case []interface{}:
|
||||
if head == "*" {
|
||||
return readArraySplat(value, tail)
|
||||
}
|
||||
index, err := strconv.ParseInt(head, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error accessing array: %v", err)
|
||||
}
|
||||
return readArray(value, index, tail)
|
||||
case yaml.MapSlice:
|
||||
return readMap(value, head, tail)
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func readArray(array []interface{}, head int64, tail []string) (interface{}, error) {
|
||||
if head >= int64(len(array)) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
value := array[head]
|
||||
return calculateValue(value, tail)
|
||||
}
|
||||
|
||||
func readArraySplat(array []interface{}, tail []string) (interface{}, error) {
|
||||
var newArray = make([]interface{}, len(array))
|
||||
for index, value := range array {
|
||||
val, err := calculateValue(value, tail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newArray[index] = val
|
||||
}
|
||||
return newArray, nil
|
||||
}
|
||||
|
||||
func calculateValue(value interface{}, tail []string) (interface{}, error) {
|
||||
if len(tail) > 0 {
|
||||
return recurse(value, tail[0], tail[1:])
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func deleteMap(context interface{}, paths []string) (yaml.MapSlice, error) {
|
||||
log.Debugf("deleteMap for %v for %v\n", paths, context)
|
||||
|
||||
mapSlice := getMapSlice(context)
|
||||
|
||||
if len(paths) == 0 {
|
||||
return mapSlice, nil
|
||||
}
|
||||
|
||||
var index int
|
||||
var child yaml.MapItem
|
||||
for index, child = range mapSlice {
|
||||
if matchesKey(paths[0], child.Key) {
|
||||
log.Debugf("\tMatched [%v] with [%v] at index %v", paths[0], child.Key, index)
|
||||
var badDelete error
|
||||
mapSlice, badDelete = deleteEntryInMap(mapSlice, child, index, paths)
|
||||
if badDelete != nil {
|
||||
return nil, badDelete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mapSlice, nil
|
||||
|
||||
}
|
||||
|
||||
func deleteEntryInMap(original yaml.MapSlice, child yaml.MapItem, index int, paths []string) (yaml.MapSlice, error) {
|
||||
remainingPaths := paths[1:]
|
||||
|
||||
var newSlice yaml.MapSlice
|
||||
if len(remainingPaths) > 0 {
|
||||
newChild := yaml.MapItem{Key: child.Key}
|
||||
var errorDeleting error
|
||||
newChild.Value, errorDeleting = deleteChildValue(child.Value, remainingPaths)
|
||||
if errorDeleting != nil {
|
||||
return nil, errorDeleting
|
||||
}
|
||||
|
||||
newSlice = make(yaml.MapSlice, len(original))
|
||||
for i := range original {
|
||||
item := original[i]
|
||||
if i == index {
|
||||
item = newChild
|
||||
}
|
||||
newSlice[i] = item
|
||||
}
|
||||
} else {
|
||||
// Delete item from slice at index
|
||||
newSlice = append(original[:index], original[index+1:]...)
|
||||
log.Debugf("\tDeleted item index %d from original", index)
|
||||
}
|
||||
|
||||
log.Debugf("\tReturning original %v\n", original)
|
||||
return newSlice, nil
|
||||
}
|
||||
|
||||
func deleteArraySplat(array []interface{}, tail []string) (interface{}, error) {
|
||||
log.Debugf("deleteArraySplat for %v for %v\n", tail, array)
|
||||
var newArray = make([]interface{}, len(array))
|
||||
for index, value := range array {
|
||||
val, err := deleteChildValue(value, tail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newArray[index] = val
|
||||
}
|
||||
return newArray, nil
|
||||
}
|
||||
|
||||
func deleteArray(array []interface{}, paths []string, index int64) (interface{}, error) {
|
||||
log.Debugf("deleteArray for %v for %v\n", paths, array)
|
||||
|
||||
if index >= int64(len(array)) {
|
||||
return array, nil
|
||||
}
|
||||
|
||||
remainingPaths := paths[1:]
|
||||
if len(remainingPaths) > 0 {
|
||||
// Recurse into the array element at index
|
||||
var errorDeleting error
|
||||
array[index], errorDeleting = deleteMap(array[index], remainingPaths)
|
||||
if errorDeleting != nil {
|
||||
return nil, errorDeleting
|
||||
}
|
||||
|
||||
} else {
|
||||
// Delete the array element at index
|
||||
array = append(array[:index], array[index+1:]...)
|
||||
log.Debugf("\tDeleted item index %d from array, leaving %v", index, array)
|
||||
}
|
||||
|
||||
log.Debugf("\tReturning array: %v\n", array)
|
||||
return array, nil
|
||||
}
|
||||
|
||||
func deleteChildValue(child interface{}, remainingPaths []string) (interface{}, error) {
|
||||
log.Debugf("deleteChildValue for %v for %v\n", remainingPaths, child)
|
||||
var head = remainingPaths[0]
|
||||
var tail = remainingPaths[1:]
|
||||
switch child := child.(type) {
|
||||
case yaml.MapSlice:
|
||||
return deleteMap(child, remainingPaths)
|
||||
case []interface{}:
|
||||
if head == "*" {
|
||||
return deleteArraySplat(child, tail)
|
||||
}
|
||||
index, err := strconv.ParseInt(head, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error accessing array: %v", err)
|
||||
}
|
||||
return deleteArray(child, remainingPaths, index)
|
||||
}
|
||||
return child, nil
|
||||
}
|
||||
@@ -1,399 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadMap_simple(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
got, _ := readMap(data, "b", []string{"c"})
|
||||
assertResult(t, 2, got)
|
||||
}
|
||||
|
||||
func TestReadMap_numberKey(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
200: things
|
||||
`)
|
||||
got, _ := readMap(data, "200", []string{})
|
||||
assertResult(t, "things", got)
|
||||
}
|
||||
|
||||
func TestReadMap_splat(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
mapSplat:
|
||||
item1: things
|
||||
item2: whatever
|
||||
otherThing: cat
|
||||
`)
|
||||
res, _ := readMap(data, "mapSplat", []string{"*"})
|
||||
assertResult(t, "[things whatever cat]", fmt.Sprintf("%v", res))
|
||||
}
|
||||
|
||||
func TestReadMap_prefixSplat(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
mapSplat:
|
||||
item1: things
|
||||
item2: whatever
|
||||
otherThing: cat
|
||||
`)
|
||||
res, _ := readMap(data, "mapSplat", []string{"item*"})
|
||||
assertResult(t, "[things whatever]", fmt.Sprintf("%v", res))
|
||||
}
|
||||
|
||||
func TestReadMap_deep_splat(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
mapSplatDeep:
|
||||
item1:
|
||||
cats: bananas
|
||||
item2:
|
||||
cats: apples
|
||||
`)
|
||||
|
||||
res, _ := readMap(data, "mapSplatDeep", []string{"*", "cats"})
|
||||
result := res.([]interface{})
|
||||
var actual = []string{result[0].(string), result[1].(string)}
|
||||
sort.Strings(actual)
|
||||
assertResult(t, "[apples bananas]", fmt.Sprintf("%v", actual))
|
||||
}
|
||||
|
||||
func TestReadMap_key_doesnt_exist(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
got, _ := readMap(data, "b.x.f", []string{"c"})
|
||||
assertResult(t, nil, got)
|
||||
}
|
||||
|
||||
func TestReadMap_recurse_against_string(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
a: cat
|
||||
`)
|
||||
got, _ := readMap(data, "a", []string{"b"})
|
||||
assertResult(t, nil, got)
|
||||
}
|
||||
|
||||
func TestReadMap_with_array(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
b:
|
||||
d:
|
||||
- 3
|
||||
- 4
|
||||
`)
|
||||
got, _ := readMap(data, "b", []string{"d", "1"})
|
||||
assertResult(t, 4, got)
|
||||
}
|
||||
|
||||
func TestReadMap_with_array_and_bad_index(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
b:
|
||||
d:
|
||||
- 3
|
||||
- 4
|
||||
`)
|
||||
_, err := readMap(data, "b", []string{"d", "x"})
|
||||
if err == nil {
|
||||
t.Fatal("Expected error due to invalid path")
|
||||
}
|
||||
expectedOutput := `error accessing array: strconv.ParseInt: parsing "x": invalid syntax`
|
||||
assertResult(t, expectedOutput, err.Error())
|
||||
}
|
||||
|
||||
func TestReadMap_with_mapsplat_array_and_bad_index(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
b:
|
||||
d:
|
||||
e:
|
||||
- 3
|
||||
- 4
|
||||
f:
|
||||
- 1
|
||||
- 2
|
||||
`)
|
||||
_, err := readMap(data, "b", []string{"d", "*", "x"})
|
||||
if err == nil {
|
||||
t.Fatal("Expected error due to invalid path")
|
||||
}
|
||||
expectedOutput := `error accessing array: strconv.ParseInt: parsing "x": invalid syntax`
|
||||
assertResult(t, expectedOutput, err.Error())
|
||||
}
|
||||
|
||||
func TestReadMap_with_arraysplat_map_array_and_bad_index(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
b:
|
||||
d:
|
||||
- names:
|
||||
- fred
|
||||
- smith
|
||||
- names:
|
||||
- sam
|
||||
- bo
|
||||
`)
|
||||
_, err := readMap(data, "b", []string{"d", "*", "names", "x"})
|
||||
if err == nil {
|
||||
t.Fatal("Expected error due to invalid path")
|
||||
}
|
||||
expectedOutput := `error accessing array: strconv.ParseInt: parsing "x": invalid syntax`
|
||||
assertResult(t, expectedOutput, err.Error())
|
||||
}
|
||||
|
||||
func TestReadMap_with_array_out_of_bounds(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
b:
|
||||
d:
|
||||
- 3
|
||||
- 4
|
||||
`)
|
||||
got, _ := readMap(data, "b", []string{"d", "3"})
|
||||
assertResult(t, nil, got)
|
||||
}
|
||||
|
||||
func TestReadMap_with_array_out_of_bounds_by_1(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
b:
|
||||
d:
|
||||
- 3
|
||||
- 4
|
||||
`)
|
||||
got, _ := readMap(data, "b", []string{"d", "2"})
|
||||
assertResult(t, nil, got)
|
||||
}
|
||||
|
||||
func TestReadMap_with_array_splat(t *testing.T) {
|
||||
var data = parseData(`
|
||||
e:
|
||||
-
|
||||
name: Fred
|
||||
thing: cat
|
||||
-
|
||||
name: Sam
|
||||
thing: dog
|
||||
`)
|
||||
got, _ := readMap(data, "e", []string{"*", "name"})
|
||||
assertResult(t, "[Fred Sam]", fmt.Sprintf("%v", got))
|
||||
}
|
||||
|
||||
func TestWrite_really_simple(t *testing.T) {
|
||||
var data = parseData(`
|
||||
b: 2
|
||||
`)
|
||||
|
||||
updated := writeMap(data, []string{"b"}, "4")
|
||||
assertResult(t, "[{b 4}]", fmt.Sprintf("%v", updated))
|
||||
}
|
||||
|
||||
func TestWrite_simple(t *testing.T) {
|
||||
var data = parseData(`
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
|
||||
updated := writeMap(data, []string{"b", "c"}, "4")
|
||||
assertResult(t, "[{b [{c 4}]}]", fmt.Sprintf("%v", updated))
|
||||
}
|
||||
|
||||
func TestWrite_new(t *testing.T) {
|
||||
var data = parseData(`
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
|
||||
updated := writeMap(data, []string{"b", "d"}, "4")
|
||||
assertResult(t, "[{b [{c 2} {d 4}]}]", fmt.Sprintf("%v", updated))
|
||||
}
|
||||
|
||||
func TestWrite_new_deep(t *testing.T) {
|
||||
var data = parseData(`
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
|
||||
updated := writeMap(data, []string{"b", "d", "f"}, "4")
|
||||
assertResult(t, "[{b [{c 2} {d [{f 4}]}]}]", fmt.Sprintf("%v", updated))
|
||||
}
|
||||
|
||||
func TestWrite_array(t *testing.T) {
|
||||
var data = parseData(`
|
||||
b:
|
||||
- aa
|
||||
`)
|
||||
|
||||
updated := writeMap(data, []string{"b", "0"}, "bb")
|
||||
|
||||
assertResult(t, "[{b [bb]}]", fmt.Sprintf("%v", updated))
|
||||
}
|
||||
|
||||
func TestWrite_new_array(t *testing.T) {
|
||||
var data = parseData(`
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
|
||||
updated := writeMap(data, []string{"b", "0"}, "4")
|
||||
assertResult(t, "[{b [{c 2} {0 4}]}]", fmt.Sprintf("%v", updated))
|
||||
}
|
||||
|
||||
func TestWrite_new_array_deep(t *testing.T) {
|
||||
var data = parseData(`
|
||||
a: apple
|
||||
`)
|
||||
|
||||
var expected = `a: apple
|
||||
b:
|
||||
- c: "4"`
|
||||
|
||||
updated := writeMap(data, []string{"b", "+", "c"}, "4")
|
||||
got, _ := yamlToString(updated)
|
||||
assertResult(t, expected, got)
|
||||
}
|
||||
|
||||
func TestWrite_new_map_array_deep(t *testing.T) {
|
||||
var data = parseData(`
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
var expected = `b:
|
||||
c: 2
|
||||
d:
|
||||
- "4"`
|
||||
|
||||
updated := writeMap(data, []string{"b", "d", "+"}, "4")
|
||||
got, _ := yamlToString(updated)
|
||||
assertResult(t, expected, got)
|
||||
}
|
||||
|
||||
func TestWrite_add_to_array(t *testing.T) {
|
||||
var data = parseData(`
|
||||
b:
|
||||
- aa
|
||||
`)
|
||||
|
||||
var expected = `b:
|
||||
- aa
|
||||
- bb`
|
||||
|
||||
updated := writeMap(data, []string{"b", "1"}, "bb")
|
||||
got, _ := yamlToString(updated)
|
||||
assertResult(t, expected, got)
|
||||
}
|
||||
|
||||
func TestWrite_with_no_tail(t *testing.T) {
|
||||
var data = parseData(`
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
updated := writeMap(data, []string{"b"}, "4")
|
||||
|
||||
assertResult(t, "[{b 4}]", fmt.Sprintf("%v", updated))
|
||||
}
|
||||
|
||||
func TestWriteMap_no_paths(t *testing.T) {
|
||||
var data = parseData(`
|
||||
b: 5
|
||||
`)
|
||||
|
||||
result := writeMap(data, []string{}, 4)
|
||||
assertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
|
||||
}
|
||||
|
||||
func TestWriteArray_no_paths(t *testing.T) {
|
||||
var data = make([]interface{}, 1)
|
||||
data[0] = "mike"
|
||||
result := writeArray(data, []string{}, 4)
|
||||
assertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
|
||||
}
|
||||
|
||||
func TestDelete_MapItem(t *testing.T) {
|
||||
var data = parseData(`
|
||||
a: 123
|
||||
b: 456
|
||||
`)
|
||||
var expected = parseData(`
|
||||
b: 456
|
||||
`)
|
||||
|
||||
result, _ := deleteMap(data, []string{"a"})
|
||||
assertResult(t, fmt.Sprintf("%v", expected), fmt.Sprintf("%v", result))
|
||||
}
|
||||
|
||||
// Ensure deleting an index into a string does nothing
|
||||
func TestDelete_index_to_string(t *testing.T) {
|
||||
var data = parseData(`
|
||||
a: mystring
|
||||
`)
|
||||
result, _ := deleteMap(data, []string{"a", "0"})
|
||||
assertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
|
||||
}
|
||||
|
||||
func TestDelete_list_index(t *testing.T) {
|
||||
var data = parseData(`
|
||||
a: [3, 4]
|
||||
`)
|
||||
var expected = parseData(`
|
||||
a: [3]
|
||||
`)
|
||||
result, _ := deleteMap(data, []string{"a", "1"})
|
||||
assertResult(t, fmt.Sprintf("%v", expected), fmt.Sprintf("%v", result))
|
||||
}
|
||||
|
||||
func TestDelete_list_index_beyond_bounds(t *testing.T) {
|
||||
var data = parseData(`
|
||||
a: [3, 4]
|
||||
`)
|
||||
result, _ := deleteMap(data, []string{"a", "5"})
|
||||
assertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
|
||||
}
|
||||
|
||||
func TestDelete_list_index_out_of_bounds_by_1(t *testing.T) {
|
||||
var data = parseData(`
|
||||
a: [3, 4]
|
||||
`)
|
||||
result, _ := deleteMap(data, []string{"a", "2"})
|
||||
assertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
|
||||
}
|
||||
|
||||
func TestDelete_no_paths(t *testing.T) {
|
||||
var data = parseData(`
|
||||
a: [3, 4]
|
||||
b:
|
||||
- name: test
|
||||
`)
|
||||
result, _ := deleteMap(data, []string{})
|
||||
assertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
|
||||
}
|
||||
|
||||
func TestDelete_array_map_item(t *testing.T) {
|
||||
var data = parseData(`
|
||||
b:
|
||||
- name: fred
|
||||
value: blah
|
||||
- name: john
|
||||
value: test
|
||||
`)
|
||||
var expected = parseData(`
|
||||
b:
|
||||
- value: blah
|
||||
- name: john
|
||||
value: test
|
||||
`)
|
||||
result, _ := deleteMap(data, []string{"b", "0", "name"})
|
||||
assertResult(t, fmt.Sprintf("%v", expected), fmt.Sprintf("%v", result))
|
||||
}
|
||||
@@ -458,7 +458,7 @@
|
||||
<p>Create scripts follow the same format as the update scripts.</p>
|
||||
<p>Given a script create_instructions.yaml of:</p>
|
||||
<pre><code class="yaml">b.c: 3
|
||||
b.e[0].name: Howdy Partner
|
||||
b.e[+].name: Howdy Partner
|
||||
</code></pre>
|
||||
|
||||
<p>then</p>
|
||||
|
||||
@@ -325,6 +325,27 @@
|
||||
Deleting nodes in-place
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#splat" title="Splat" class="md-nav__link">
|
||||
Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#prefix-splat" title="Prefix Splat" class="md-nav__link">
|
||||
Prefix Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#array-splat" title="Array Splat" class="md-nav__link">
|
||||
Array Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
@@ -446,6 +467,27 @@
|
||||
Deleting nodes in-place
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#splat" title="Splat" class="md-nav__link">
|
||||
Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#prefix-splat" title="Prefix Splat" class="md-nav__link">
|
||||
Prefix Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#array-splat" title="Array Splat" class="md-nav__link">
|
||||
Array Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
@@ -552,6 +594,91 @@
|
||||
</code></pre>
|
||||
|
||||
<p>will update the sample.yaml file so that the 'c' node is deleted</p>
|
||||
<h3 id="splat">Splat<a class="headerlink" href="#splat" title="Permanent link">¶</a></h3>
|
||||
<p>Given a sample.yaml file of:</p>
|
||||
<pre><code class="yaml">---
|
||||
bob:
|
||||
item1:
|
||||
cats: bananas
|
||||
dogs: woof
|
||||
item2:
|
||||
cats: apples
|
||||
dogs: woof2
|
||||
thing:
|
||||
cats: oranges
|
||||
dogs: woof3
|
||||
</code></pre>
|
||||
|
||||
<p>then</p>
|
||||
<pre><code class="bash">yq d sample.yaml bob.*.cats
|
||||
</code></pre>
|
||||
|
||||
<p>will output:</p>
|
||||
<pre><code class="yaml">---
|
||||
bob:
|
||||
item1:
|
||||
dogs: woof
|
||||
item2:
|
||||
dogs: woof2
|
||||
thing:
|
||||
dogs: woof3
|
||||
</code></pre>
|
||||
|
||||
<h3 id="prefix-splat">Prefix Splat<a class="headerlink" href="#prefix-splat" title="Permanent link">¶</a></h3>
|
||||
<p>Given a sample.yaml file of:</p>
|
||||
<pre><code class="yaml">---
|
||||
bob:
|
||||
item1:
|
||||
cats: bananas
|
||||
dogs: woof
|
||||
item2:
|
||||
cats: apples
|
||||
dogs: woof2
|
||||
thing:
|
||||
cats: oranges
|
||||
dogs: woof3
|
||||
</code></pre>
|
||||
|
||||
<p>then</p>
|
||||
<pre><code class="bash">yq d sample.yaml bob.item*.cats
|
||||
</code></pre>
|
||||
|
||||
<p>will output:</p>
|
||||
<pre><code class="yaml">---
|
||||
bob:
|
||||
item1:
|
||||
dogs: woof
|
||||
item2:
|
||||
dogs: woof2
|
||||
thing:
|
||||
cats: oranges
|
||||
dogs: woof3
|
||||
</code></pre>
|
||||
|
||||
<h3 id="array-splat">Array Splat<a class="headerlink" href="#array-splat" title="Permanent link">¶</a></h3>
|
||||
<p>Given a sample.yaml file of:</p>
|
||||
<pre><code class="yaml">---
|
||||
bob:
|
||||
- cats: bananas
|
||||
dogs: woof
|
||||
- cats: apples
|
||||
dogs: woof2
|
||||
- cats: oranges
|
||||
dogs: woof3
|
||||
</code></pre>
|
||||
|
||||
<p>then</p>
|
||||
<pre><code class="bash">yq d sample.yaml bob.[*].cats
|
||||
</code></pre>
|
||||
|
||||
<p>will output:</p>
|
||||
<pre><code class="yaml">---
|
||||
bob:
|
||||
- dogs: woof
|
||||
- dogs: woof2
|
||||
- dogs: woof3
|
||||
</code></pre>
|
||||
|
||||
<h3 id="multiple-documents-delete-from-single-document">Multiple Documents - delete from single document<a class="headerlink" href="#multiple-documents-delete-from-single-document" title="Permanent link">¶</a></h3>
|
||||
<p>Given a sample.yaml file of:</p>
|
||||
<pre><code class="yaml">something: else
|
||||
|
||||
@@ -282,6 +282,13 @@
|
||||
Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#prefix-splat" title="Prefix Splat" class="md-nav__link">
|
||||
Prefix Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
@@ -446,6 +453,13 @@
|
||||
Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#prefix-splat" title="Prefix Splat" class="md-nav__link">
|
||||
Prefix Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
@@ -540,12 +554,36 @@ bob:
|
||||
cats: bananas
|
||||
item2:
|
||||
cats: apples
|
||||
thing:
|
||||
cats: oranges
|
||||
</code></pre>
|
||||
|
||||
<p>then</p>
|
||||
<pre><code class="bash">yq r sample.yaml bob.*.cats
|
||||
</code></pre>
|
||||
|
||||
<p>will output</p>
|
||||
<pre><code class="yaml">- bananas
|
||||
- apples
|
||||
- oranges
|
||||
</code></pre>
|
||||
|
||||
<h3 id="prefix-splat">Prefix Splat<a class="headerlink" href="#prefix-splat" title="Permanent link">¶</a></h3>
|
||||
<p>Given a sample.yaml file of:</p>
|
||||
<pre><code class="yaml">---
|
||||
bob:
|
||||
item1:
|
||||
cats: bananas
|
||||
item2:
|
||||
cats: apples
|
||||
thing:
|
||||
cats: oranges
|
||||
</code></pre>
|
||||
|
||||
<p>then</p>
|
||||
<pre><code class="bash">yq r sample.yaml bob.item*.cats
|
||||
</code></pre>
|
||||
|
||||
<p>will output</p>
|
||||
<pre><code class="yaml">- bananas
|
||||
- apples
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,42 +2,42 @@
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>None</loc>
|
||||
<lastmod>2019-05-14</lastmod>
|
||||
<lastmod>2019-05-16</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>None</loc>
|
||||
<lastmod>2019-05-14</lastmod>
|
||||
<lastmod>2019-05-16</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>None</loc>
|
||||
<lastmod>2019-05-14</lastmod>
|
||||
<lastmod>2019-05-16</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>None</loc>
|
||||
<lastmod>2019-05-14</lastmod>
|
||||
<lastmod>2019-05-16</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>None</loc>
|
||||
<lastmod>2019-05-14</lastmod>
|
||||
<lastmod>2019-05-16</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>None</loc>
|
||||
<lastmod>2019-05-14</lastmod>
|
||||
<lastmod>2019-05-16</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>None</loc>
|
||||
<lastmod>2019-05-14</lastmod>
|
||||
<lastmod>2019-05-16</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
</url>
|
||||
<url>
|
||||
<loc>None</loc>
|
||||
<lastmod>2019-05-14</lastmod>
|
||||
<lastmod>2019-05-16</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
</url>
|
||||
</urlset>
|
||||
Binary file not shown.
@@ -294,6 +294,27 @@
|
||||
Adding new fields
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#splat" title="Splat" class="md-nav__link">
|
||||
Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#prefix-splat" title="Prefix Splat" class="md-nav__link">
|
||||
Prefix Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#array-splat" title="Array Splat" class="md-nav__link">
|
||||
Array Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
@@ -460,6 +481,27 @@
|
||||
Adding new fields
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#splat" title="Splat" class="md-nav__link">
|
||||
Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#prefix-splat" title="Prefix Splat" class="md-nav__link">
|
||||
Prefix Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#array-splat" title="Array Splat" class="md-nav__link">
|
||||
Array Splat
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
@@ -569,7 +611,7 @@
|
||||
</code></pre>
|
||||
|
||||
<p>then</p>
|
||||
<pre><code class="bash">yq w sample.yaml b.d[0] "new thing"
|
||||
<pre><code class="bash">yq w sample.yaml b.d[+] "new thing"
|
||||
</code></pre>
|
||||
|
||||
<p>will output:</p>
|
||||
@@ -579,6 +621,81 @@
|
||||
- new thing
|
||||
</code></pre>
|
||||
|
||||
<h3 id="splat">Splat<a class="headerlink" href="#splat" title="Permanent link">¶</a></h3>
|
||||
<p>Given a sample.yaml file of:</p>
|
||||
<pre><code class="yaml">---
|
||||
bob:
|
||||
item1:
|
||||
cats: bananas
|
||||
item2:
|
||||
cats: apples
|
||||
thing:
|
||||
cats: oranges
|
||||
</code></pre>
|
||||
|
||||
<p>then</p>
|
||||
<pre><code class="bash">yq w sample.yaml bob.*.cats meow
|
||||
</code></pre>
|
||||
|
||||
<p>will output:</p>
|
||||
<pre><code class="yaml">---
|
||||
bob:
|
||||
item1:
|
||||
cats: meow
|
||||
item2:
|
||||
cats: meow
|
||||
thing:
|
||||
cats: meow
|
||||
</code></pre>
|
||||
|
||||
<h3 id="prefix-splat">Prefix Splat<a class="headerlink" href="#prefix-splat" title="Permanent link">¶</a></h3>
|
||||
<p>Given a sample.yaml file of:</p>
|
||||
<pre><code class="yaml">---
|
||||
bob:
|
||||
item1:
|
||||
cats: bananas
|
||||
item2:
|
||||
cats: apples
|
||||
thing:
|
||||
cats: oranges
|
||||
</code></pre>
|
||||
|
||||
<p>then</p>
|
||||
<pre><code class="bash">yq w sample.yaml bob.item*.cats meow
|
||||
</code></pre>
|
||||
|
||||
<p>will output:</p>
|
||||
<pre><code class="yaml">---
|
||||
bob:
|
||||
item1:
|
||||
cats: meow
|
||||
item2:
|
||||
cats: meow
|
||||
thing:
|
||||
cats: oranges
|
||||
</code></pre>
|
||||
|
||||
<h3 id="array-splat">Array Splat<a class="headerlink" href="#array-splat" title="Permanent link">¶</a></h3>
|
||||
<p>Given a sample.yaml file of:</p>
|
||||
<pre><code class="yaml">---
|
||||
bob:
|
||||
- cats: bananas
|
||||
- cats: apples
|
||||
- cats: oranges
|
||||
</code></pre>
|
||||
|
||||
<p>then</p>
|
||||
<pre><code class="bash">yq w sample.yaml bob[*].cats meow
|
||||
</code></pre>
|
||||
|
||||
<p>will output:</p>
|
||||
<pre><code class="yaml">---
|
||||
bob:
|
||||
- cats: meow
|
||||
- cats: meow
|
||||
- cats: meow
|
||||
</code></pre>
|
||||
|
||||
<h3 id="appending-value-to-an-array-field">Appending value to an array field<a class="headerlink" href="#appending-value-to-an-array-field" title="Permanent link">¶</a></h3>
|
||||
<p>Given a sample.yaml file of:</p>
|
||||
<pre><code class="yaml">b:
|
||||
@@ -664,7 +781,7 @@ b:
|
||||
|
||||
<p>and a script update_instructions.yaml of:</p>
|
||||
<pre><code class="yaml">b.c: 3
|
||||
b.e[0].name: Howdy Partner
|
||||
b.e[+].name: Howdy Partner
|
||||
</code></pre>
|
||||
|
||||
<p>then</p>
|
||||
|
||||
12
go.mod
Normal file
12
go.mod
Normal file
@@ -0,0 +1,12 @@
|
||||
module github.com/mikefarah/yq/v2
|
||||
|
||||
require (
|
||||
github.com/mikefarah/yaml/v2 v2.4.0
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/spf13/cobra v0.0.5
|
||||
golang.org/x/tools v0.0.0-20191030203535-5e247c9ad0a0 // indirect
|
||||
gopkg.in/imdario/mergo.v0 v0.3.7
|
||||
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
|
||||
)
|
||||
|
||||
go 1.13
|
||||
50
go.sum
Normal file
50
go.sum
Normal file
@@ -0,0 +1,50 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mikefarah/yaml/v2 v2.4.0 h1:eYqfooY0BnvKTJxr7+ABJs13n3dg9n347GScDaU2Lww=
|
||||
github.com/mikefarah/yaml/v2 v2.4.0/go.mod h1:ahVqZF4n1W4NqwvVnZzC4es67xsW9uR/RRf2RRxieJU=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20191030203535-5e247c9ad0a0 h1:s5lp4ug7qHzUccgyFdjsX7OZDzHXRaePrF3B3vmUiuM=
|
||||
golang.org/x/tools v0.0.0-20191030203535-5e247c9ad0a0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/imdario/mergo.v0 v0.3.7 h1:QDotlIZtaO/p+Um0ok18HRTpq5i5/SAk/qprsor+9c8=
|
||||
gopkg.in/imdario/mergo.v0 v0.3.7/go.mod h1:9qPP6AGrlC1G2PTNXko614FwGZvorN7MiBU0Eppok+U=
|
||||
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE=
|
||||
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -1,46 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJsonToString(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
got, _ := jsonToString(data)
|
||||
assertResult(t, "{\"b\":{\"c\":2}}", got)
|
||||
}
|
||||
|
||||
func TestJsonToString_withIntKey(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
b:
|
||||
2: c
|
||||
`)
|
||||
got, _ := jsonToString(data)
|
||||
assertResult(t, `{"b":{"2":"c"}}`, got)
|
||||
}
|
||||
|
||||
func TestJsonToString_withBoolKey(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
b:
|
||||
false: c
|
||||
`)
|
||||
got, _ := jsonToString(data)
|
||||
assertResult(t, `{"b":{"false":"c"}}`, got)
|
||||
}
|
||||
|
||||
func TestJsonToString_withArray(t *testing.T) {
|
||||
var data = parseData(`
|
||||
---
|
||||
b:
|
||||
- item: one
|
||||
- item: two
|
||||
`)
|
||||
got, _ := jsonToString(data)
|
||||
assertResult(t, "{\"b\":[{\"item\":\"one\"},{\"item\":\"two\"}]}", got)
|
||||
}
|
||||
12
merge.go
12
merge.go
@@ -1,12 +0,0 @@
|
||||
package main
|
||||
|
||||
import mergo "gopkg.in/imdario/mergo.v0"
|
||||
|
||||
func merge(dst interface{}, src interface{}, overwrite bool, append bool) error {
|
||||
if overwrite {
|
||||
return mergo.Merge(dst, src, mergo.WithOverride)
|
||||
} else if append {
|
||||
return mergo.Merge(dst, src, mergo.WithAppendSlice)
|
||||
}
|
||||
return mergo.Merge(dst, src)
|
||||
}
|
||||
@@ -20,7 +20,7 @@ Create scripts follow the same format as the update scripts.
|
||||
Given a script create_instructions.yaml of:
|
||||
```yaml
|
||||
b.c: 3
|
||||
b.e[0].name: Howdy Partner
|
||||
b.e[+].name: Howdy Partner
|
||||
```
|
||||
then
|
||||
|
||||
|
||||
@@ -59,6 +59,93 @@ yq d -i sample.yaml b.c
|
||||
will update the sample.yaml file so that the 'c' node is deleted
|
||||
|
||||
|
||||
### Splat
|
||||
Given a sample.yaml file of:
|
||||
```yaml
|
||||
---
|
||||
bob:
|
||||
item1:
|
||||
cats: bananas
|
||||
dogs: woof
|
||||
item2:
|
||||
cats: apples
|
||||
dogs: woof2
|
||||
thing:
|
||||
cats: oranges
|
||||
dogs: woof3
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq d sample.yaml bob.*.cats
|
||||
```
|
||||
will output:
|
||||
```yaml
|
||||
---
|
||||
bob:
|
||||
item1:
|
||||
dogs: woof
|
||||
item2:
|
||||
dogs: woof2
|
||||
thing:
|
||||
dogs: woof3
|
||||
```
|
||||
|
||||
### Prefix Splat
|
||||
Given a sample.yaml file of:
|
||||
```yaml
|
||||
---
|
||||
bob:
|
||||
item1:
|
||||
cats: bananas
|
||||
dogs: woof
|
||||
item2:
|
||||
cats: apples
|
||||
dogs: woof2
|
||||
thing:
|
||||
cats: oranges
|
||||
dogs: woof3
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq d sample.yaml bob.item*.cats
|
||||
```
|
||||
will output:
|
||||
```yaml
|
||||
---
|
||||
bob:
|
||||
item1:
|
||||
dogs: woof
|
||||
item2:
|
||||
dogs: woof2
|
||||
thing:
|
||||
cats: oranges
|
||||
dogs: woof3
|
||||
```
|
||||
|
||||
### Array Splat
|
||||
Given a sample.yaml file of:
|
||||
```yaml
|
||||
---
|
||||
bob:
|
||||
- cats: bananas
|
||||
dogs: woof
|
||||
- cats: apples
|
||||
dogs: woof2
|
||||
- cats: oranges
|
||||
dogs: woof3
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq d sample.yaml bob.[*].cats
|
||||
```
|
||||
will output:
|
||||
```yaml
|
||||
---
|
||||
bob:
|
||||
- dogs: woof
|
||||
- dogs: woof2
|
||||
- dogs: woof3
|
||||
```
|
||||
|
||||
### Multiple Documents - delete from single document
|
||||
Given a sample.yaml file of:
|
||||
|
||||
@@ -32,6 +32,8 @@ bob:
|
||||
cats: bananas
|
||||
item2:
|
||||
cats: apples
|
||||
thing:
|
||||
cats: oranges
|
||||
```
|
||||
then
|
||||
```bash
|
||||
@@ -41,6 +43,29 @@ will output
|
||||
```yaml
|
||||
- bananas
|
||||
- apples
|
||||
- oranges
|
||||
```
|
||||
|
||||
### Prefix Splat
|
||||
Given a sample.yaml file of:
|
||||
```yaml
|
||||
---
|
||||
bob:
|
||||
item1:
|
||||
cats: bananas
|
||||
item2:
|
||||
cats: apples
|
||||
thing:
|
||||
cats: oranges
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq r sample.yaml bob.item*.cats
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
- bananas
|
||||
- apples
|
||||
```
|
||||
|
||||
### Multiple Documents - specify a single document
|
||||
|
||||
@@ -33,7 +33,7 @@ b:
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq w sample.yaml b.d[0] "new thing"
|
||||
yq w sample.yaml b.d[+] "new thing"
|
||||
```
|
||||
will output:
|
||||
```yaml
|
||||
@@ -43,6 +43,84 @@ b:
|
||||
- new thing
|
||||
```
|
||||
|
||||
### Splat
|
||||
Given a sample.yaml file of:
|
||||
```yaml
|
||||
---
|
||||
bob:
|
||||
item1:
|
||||
cats: bananas
|
||||
item2:
|
||||
cats: apples
|
||||
thing:
|
||||
cats: oranges
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq w sample.yaml bob.*.cats meow
|
||||
```
|
||||
will output:
|
||||
```yaml
|
||||
---
|
||||
bob:
|
||||
item1:
|
||||
cats: meow
|
||||
item2:
|
||||
cats: meow
|
||||
thing:
|
||||
cats: meow
|
||||
```
|
||||
|
||||
### Prefix Splat
|
||||
Given a sample.yaml file of:
|
||||
```yaml
|
||||
---
|
||||
bob:
|
||||
item1:
|
||||
cats: bananas
|
||||
item2:
|
||||
cats: apples
|
||||
thing:
|
||||
cats: oranges
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq w sample.yaml bob.item*.cats meow
|
||||
```
|
||||
will output:
|
||||
```yaml
|
||||
---
|
||||
bob:
|
||||
item1:
|
||||
cats: meow
|
||||
item2:
|
||||
cats: meow
|
||||
thing:
|
||||
cats: oranges
|
||||
```
|
||||
|
||||
### Array Splat
|
||||
Given a sample.yaml file of:
|
||||
```yaml
|
||||
---
|
||||
bob:
|
||||
- cats: bananas
|
||||
- cats: apples
|
||||
- cats: oranges
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq w sample.yaml bob[*].cats meow
|
||||
```
|
||||
will output:
|
||||
```yaml
|
||||
---
|
||||
bob:
|
||||
- cats: meow
|
||||
- cats: meow
|
||||
- cats: meow
|
||||
```
|
||||
|
||||
### Appending value to an array field
|
||||
Given a sample.yaml file of:
|
||||
```yaml
|
||||
@@ -136,7 +214,7 @@ b:
|
||||
and a script update_instructions.yaml of:
|
||||
```yaml
|
||||
b.c: 3
|
||||
b.e[0].name: Howdy Partner
|
||||
b.e[+].name: Howdy Partner
|
||||
```
|
||||
then
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package main
|
||||
|
||||
func parsePath(path string) []string {
|
||||
return parsePathAccum([]string{}, path)
|
||||
}
|
||||
|
||||
func parsePathAccum(paths []string, remaining string) []string {
|
||||
head, tail := nextYamlPath(remaining)
|
||||
if tail == "" {
|
||||
return append(paths, head)
|
||||
}
|
||||
return parsePathAccum(append(paths, head), tail)
|
||||
}
|
||||
|
||||
func nextYamlPath(path string) (pathElement string, remaining string) {
|
||||
switch path[0] {
|
||||
case '[':
|
||||
// e.g [0].blah.cat -> we need to return "0" and "blah.cat"
|
||||
return search(path[1:], []uint8{']'}, true)
|
||||
case '"':
|
||||
// e.g "a.b".blah.cat -> we need to return "a.b" and "blah.cat"
|
||||
return search(path[1:], []uint8{'"'}, true)
|
||||
default:
|
||||
// e.g "a.blah.cat" -> return "a" and "blah.cat"
|
||||
return search(path[0:], []uint8{'.', '['}, false)
|
||||
}
|
||||
}
|
||||
|
||||
func search(path string, matchingChars []uint8, skipNext bool) (pathElement string, remaining string) {
|
||||
for i := 0; i < len(path); i++ {
|
||||
var char = path[i]
|
||||
if contains(matchingChars, char) {
|
||||
var remainingStart = i + 1
|
||||
if skipNext {
|
||||
remainingStart = remainingStart + 1
|
||||
} else if !skipNext && char != '.' {
|
||||
remainingStart = i
|
||||
}
|
||||
if remainingStart > len(path) {
|
||||
remainingStart = len(path)
|
||||
}
|
||||
return path[0:i], path[remainingStart:]
|
||||
}
|
||||
}
|
||||
return path, ""
|
||||
}
|
||||
|
||||
func contains(matchingChars []uint8, candidate uint8) bool {
|
||||
for _, a := range matchingChars {
|
||||
if a == candidate {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var parsePathsTests = []struct {
|
||||
path string
|
||||
expectedPaths []string
|
||||
}{
|
||||
{"a.b", []string{"a", "b"}},
|
||||
{"a.b[0]", []string{"a", "b", "0"}},
|
||||
{"a.b.d[+]", []string{"a", "b", "d", "+"}},
|
||||
}
|
||||
|
||||
func TestParsePath(t *testing.T) {
|
||||
for _, tt := range parsePathsTests {
|
||||
assertResultComplex(t, tt.expectedPaths, parsePath(tt.path))
|
||||
}
|
||||
}
|
||||
|
||||
var nextYamlPathTests = []struct {
|
||||
path string
|
||||
expectedElement string
|
||||
expectedRemaining string
|
||||
}{
|
||||
{"a.b", "a", "b"},
|
||||
{"a", "a", ""},
|
||||
{"a.b.c", "a", "b.c"},
|
||||
{"\"a.b\".c", "a.b", "c"},
|
||||
{"a.\"b.c\".d", "a", "\"b.c\".d"},
|
||||
{"[1].a.d", "1", "a.d"},
|
||||
{"a[0].c", "a", "[0].c"},
|
||||
{"[0]", "0", ""},
|
||||
}
|
||||
|
||||
func TestNextYamlPath(t *testing.T) {
|
||||
for _, tt := range nextYamlPathTests {
|
||||
var element, remaining = nextYamlPath(tt.path)
|
||||
assertResultWithContext(t, tt.expectedElement, element, tt)
|
||||
assertResultWithContext(t, tt.expectedRemaining, remaining, tt)
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,38 @@
|
||||
package main
|
||||
package marshal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
yaml "gopkg.in/mikefarah/yaml.v2"
|
||||
yaml "github.com/mikefarah/yaml/v2"
|
||||
)
|
||||
|
||||
func jsonToString(context interface{}) (string, error) {
|
||||
out, err := json.Marshal(toJSON(context))
|
||||
type JsonConverter interface {
|
||||
JsonToString(context interface{}) (string, error)
|
||||
}
|
||||
|
||||
type jsonConverter struct{}
|
||||
|
||||
func NewJsonConverter() JsonConverter {
|
||||
return &jsonConverter{}
|
||||
}
|
||||
|
||||
func (j *jsonConverter) JsonToString(context interface{}) (string, error) {
|
||||
out, err := json.Marshal(j.toJSON(context))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error printing yaml as json: %v", err)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
func toJSON(context interface{}) interface{} {
|
||||
func (j *jsonConverter) toJSON(context interface{}) interface{} {
|
||||
switch context := context.(type) {
|
||||
case []interface{}:
|
||||
oldArray := context
|
||||
newArray := make([]interface{}, len(oldArray))
|
||||
for index, value := range oldArray {
|
||||
newArray[index] = toJSON(value)
|
||||
newArray[index] = j.toJSON(value)
|
||||
}
|
||||
return newArray
|
||||
case yaml.MapSlice:
|
||||
@@ -30,11 +40,11 @@ func toJSON(context interface{}) interface{} {
|
||||
newMap := make(map[string]interface{})
|
||||
for _, entry := range oldMap {
|
||||
if str, ok := entry.Key.(string); ok {
|
||||
newMap[str] = toJSON(entry.Value)
|
||||
newMap[str] = j.toJSON(entry.Value)
|
||||
} else if i, ok := entry.Key.(int); ok {
|
||||
newMap[strconv.Itoa(i)] = toJSON(entry.Value)
|
||||
newMap[strconv.Itoa(i)] = j.toJSON(entry.Value)
|
||||
} else if b, ok := entry.Key.(bool); ok {
|
||||
newMap[strconv.FormatBool(b)] = toJSON(entry.Value)
|
||||
newMap[strconv.FormatBool(b)] = j.toJSON(entry.Value)
|
||||
}
|
||||
}
|
||||
return newMap
|
||||
48
pkg/marshal/json_converter_test.go
Normal file
48
pkg/marshal/json_converter_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package marshal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mikefarah/yq/v2/test"
|
||||
)
|
||||
|
||||
func TestJsonToString(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
got, _ := NewJsonConverter().JsonToString(data)
|
||||
test.AssertResult(t, "{\"b\":{\"c\":2}}", got)
|
||||
}
|
||||
|
||||
func TestJsonToString_withIntKey(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
2: c
|
||||
`)
|
||||
got, _ := NewJsonConverter().JsonToString(data)
|
||||
test.AssertResult(t, `{"b":{"2":"c"}}`, got)
|
||||
}
|
||||
|
||||
func TestJsonToString_withBoolKey(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
false: c
|
||||
`)
|
||||
got, _ := NewJsonConverter().JsonToString(data)
|
||||
test.AssertResult(t, `{"b":{"false":"c"}}`, got)
|
||||
}
|
||||
|
||||
func TestJsonToString_withArray(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
- item: one
|
||||
- item: two
|
||||
`)
|
||||
got, _ := NewJsonConverter().JsonToString(data)
|
||||
test.AssertResult(t, "{\"b\":[{\"item\":\"one\"},{\"item\":\"two\"}]}", got)
|
||||
}
|
||||
43
pkg/marshal/yaml_converter.go
Normal file
43
pkg/marshal/yaml_converter.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package marshal
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
yaml "github.com/mikefarah/yaml/v2"
|
||||
errors "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type YamlConverter interface {
|
||||
YamlToString(context interface{}, trimOutput bool) (string, error)
|
||||
}
|
||||
|
||||
type yamlConverter struct{}
|
||||
|
||||
func NewYamlConverter() YamlConverter {
|
||||
return &yamlConverter{}
|
||||
}
|
||||
|
||||
func (y *yamlConverter) YamlToString(context interface{}, trimOutput bool) (string, error) {
|
||||
switch context := context.(type) {
|
||||
case string:
|
||||
return context, nil
|
||||
default:
|
||||
return y.marshalContext(context, trimOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func (y *yamlConverter) marshalContext(context interface{}, trimOutput bool) (string, error) {
|
||||
out, err := yaml.Marshal(context)
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error printing yaml")
|
||||
}
|
||||
|
||||
outStr := string(out)
|
||||
// trim the trailing new line as it's easier for a script to add
|
||||
// it in if required than to remove it
|
||||
if trimOutput {
|
||||
return strings.Trim(outStr, "\n "), nil
|
||||
}
|
||||
return outStr, nil
|
||||
}
|
||||
52
pkg/marshal/yaml_converter_test.go
Normal file
52
pkg/marshal/yaml_converter_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package marshal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mikefarah/yq/v2/test"
|
||||
)
|
||||
|
||||
func TestYamlToString(t *testing.T) {
|
||||
var raw = `b:
|
||||
c: 2
|
||||
`
|
||||
var data = test.ParseData(raw)
|
||||
got, _ := NewYamlConverter().YamlToString(data, false)
|
||||
test.AssertResult(t, raw, got)
|
||||
}
|
||||
|
||||
func TestYamlToString_withTrim(t *testing.T) {
|
||||
var raw = `b:
|
||||
c: 2`
|
||||
var data = test.ParseData(raw)
|
||||
got, _ := NewYamlConverter().YamlToString(data, true)
|
||||
test.AssertResult(t, raw, got)
|
||||
}
|
||||
|
||||
func TestYamlToString_withIntKey(t *testing.T) {
|
||||
var raw = `b:
|
||||
2: c
|
||||
`
|
||||
var data = test.ParseData(raw)
|
||||
got, _ := NewYamlConverter().YamlToString(data, false)
|
||||
test.AssertResult(t, raw, got)
|
||||
}
|
||||
|
||||
func TestYamlToString_withBoolKey(t *testing.T) {
|
||||
var raw = `b:
|
||||
false: c
|
||||
`
|
||||
var data = test.ParseData(raw)
|
||||
got, _ := NewYamlConverter().YamlToString(data, false)
|
||||
test.AssertResult(t, raw, got)
|
||||
}
|
||||
|
||||
func TestYamlToString_withArray(t *testing.T) {
|
||||
var raw = `b:
|
||||
- item: one
|
||||
- item: two
|
||||
`
|
||||
var data = test.ParseData(raw)
|
||||
got, _ := NewYamlConverter().YamlToString(data, false)
|
||||
test.AssertResult(t, raw, got)
|
||||
}
|
||||
376
pkg/yqlib/data_navigator.go
Normal file
376
pkg/yqlib/data_navigator.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
yaml "github.com/mikefarah/yaml/v2"
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
type DataNavigator interface {
|
||||
ReadChildValue(child interface{}, remainingPaths []string) (interface{}, error)
|
||||
UpdatedChildValue(child interface{}, remainingPaths []string, value interface{}) interface{}
|
||||
DeleteChildValue(child interface{}, remainingPaths []string) (interface{}, error)
|
||||
}
|
||||
|
||||
type navigator struct {
|
||||
log *logging.Logger
|
||||
}
|
||||
|
||||
func NewDataNavigator(l *logging.Logger) DataNavigator {
|
||||
return &navigator{
|
||||
log: l,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *navigator) ReadChildValue(child interface{}, remainingPaths []string) (interface{}, error) {
|
||||
if len(remainingPaths) == 0 {
|
||||
return child, nil
|
||||
}
|
||||
return n.recurse(child, remainingPaths[0], remainingPaths[1:])
|
||||
}
|
||||
|
||||
func (n *navigator) UpdatedChildValue(child interface{}, remainingPaths []string, value interface{}) interface{} {
|
||||
if len(remainingPaths) == 0 {
|
||||
return value
|
||||
}
|
||||
n.log.Debugf("UpdatedChildValue for child %v with path %v to set value %v", child, remainingPaths, value)
|
||||
n.log.Debugf("type of child is %v", reflect.TypeOf(child))
|
||||
|
||||
switch child := child.(type) {
|
||||
case nil:
|
||||
if remainingPaths[0] == "+" || remainingPaths[0] == "*" {
|
||||
return n.writeArray(child, remainingPaths, value)
|
||||
}
|
||||
case []interface{}:
|
||||
_, nextIndexErr := strconv.ParseInt(remainingPaths[0], 10, 64)
|
||||
arrayCommand := nextIndexErr == nil || remainingPaths[0] == "+" || remainingPaths[0] == "*"
|
||||
if arrayCommand {
|
||||
return n.writeArray(child, remainingPaths, value)
|
||||
}
|
||||
}
|
||||
return n.writeMap(child, remainingPaths, value)
|
||||
}
|
||||
|
||||
func (n *navigator) DeleteChildValue(child interface{}, remainingPaths []string) (interface{}, error) {
|
||||
n.log.Debugf("DeleteChildValue for %v for %v\n", remainingPaths, child)
|
||||
if len(remainingPaths) == 0 {
|
||||
return child, nil
|
||||
}
|
||||
var head = remainingPaths[0]
|
||||
var tail = remainingPaths[1:]
|
||||
switch child := child.(type) {
|
||||
case yaml.MapSlice:
|
||||
return n.deleteMap(child, remainingPaths)
|
||||
case []interface{}:
|
||||
if head == "*" {
|
||||
return n.deleteArraySplat(child, tail)
|
||||
}
|
||||
index, err := strconv.ParseInt(head, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error accessing array: %v", err)
|
||||
}
|
||||
return n.deleteArray(child, remainingPaths, index)
|
||||
}
|
||||
return child, nil
|
||||
}
|
||||
|
||||
func (n *navigator) recurse(value interface{}, head string, tail []string) (interface{}, error) {
|
||||
switch value := value.(type) {
|
||||
case []interface{}:
|
||||
if head == "*" {
|
||||
return n.readArraySplat(value, tail)
|
||||
}
|
||||
index, err := strconv.ParseInt(head, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error accessing array: %v", err)
|
||||
}
|
||||
return n.readArray(value, index, tail)
|
||||
case yaml.MapSlice:
|
||||
return n.readMap(value, head, tail)
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (n *navigator) matchesKey(key string, actual interface{}) bool {
|
||||
var actualString = fmt.Sprintf("%v", actual)
|
||||
var prefixMatch = strings.TrimSuffix(key, "*")
|
||||
if prefixMatch != key {
|
||||
return strings.HasPrefix(actualString, prefixMatch)
|
||||
}
|
||||
return actualString == key
|
||||
}
|
||||
|
||||
func (n *navigator) entriesInSlice(context yaml.MapSlice, key string) []*yaml.MapItem {
|
||||
var matches = make([]*yaml.MapItem, 0)
|
||||
for idx := range context {
|
||||
var entry = &context[idx]
|
||||
if n.matchesKey(key, entry.Key) {
|
||||
matches = append(matches, entry)
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
func (n *navigator) getMapSlice(context interface{}) yaml.MapSlice {
|
||||
var mapSlice yaml.MapSlice
|
||||
switch context := context.(type) {
|
||||
case yaml.MapSlice:
|
||||
mapSlice = context
|
||||
default:
|
||||
mapSlice = make(yaml.MapSlice, 0)
|
||||
}
|
||||
return mapSlice
|
||||
}
|
||||
|
||||
func (n *navigator) getArray(context interface{}) (array []interface{}, ok bool) {
|
||||
switch context := context.(type) {
|
||||
case []interface{}:
|
||||
array = context
|
||||
ok = true
|
||||
default:
|
||||
array = make([]interface{}, 0)
|
||||
ok = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (n *navigator) writeMap(context interface{}, paths []string, value interface{}) interface{} {
|
||||
n.log.Debugf("writeMap with path %v for %v to set value %v\n", paths, context, value)
|
||||
|
||||
mapSlice := n.getMapSlice(context)
|
||||
|
||||
if len(paths) == 0 {
|
||||
return context
|
||||
}
|
||||
|
||||
children := n.entriesInSlice(mapSlice, paths[0])
|
||||
|
||||
if len(children) == 0 && paths[0] == "*" {
|
||||
n.log.Debugf("\tNo matches, return map as is")
|
||||
return context
|
||||
}
|
||||
|
||||
if len(children) == 0 {
|
||||
newChild := yaml.MapItem{Key: paths[0]}
|
||||
mapSlice = append(mapSlice, newChild)
|
||||
children = n.entriesInSlice(mapSlice, paths[0])
|
||||
n.log.Debugf("\tAppended child at %v for mapSlice %v\n", paths[0], mapSlice)
|
||||
}
|
||||
|
||||
remainingPaths := paths[1:]
|
||||
for _, child := range children {
|
||||
child.Value = n.UpdatedChildValue(child.Value, remainingPaths, value)
|
||||
}
|
||||
n.log.Debugf("\tReturning mapSlice %v\n", mapSlice)
|
||||
return mapSlice
|
||||
}
|
||||
|
||||
func (n *navigator) writeArray(context interface{}, paths []string, value interface{}) []interface{} {
|
||||
n.log.Debugf("writeArray with path %v for %v to set value %v\n", paths, context, value)
|
||||
array, _ := n.getArray(context)
|
||||
|
||||
if len(paths) == 0 {
|
||||
return array
|
||||
}
|
||||
|
||||
n.log.Debugf("\tarray %v\n", array)
|
||||
|
||||
rawIndex := paths[0]
|
||||
remainingPaths := paths[1:]
|
||||
var index int64
|
||||
// the append array indicator
|
||||
if rawIndex == "+" {
|
||||
index = int64(len(array))
|
||||
} else if rawIndex == "*" {
|
||||
for index, oldChild := range array {
|
||||
array[index] = n.UpdatedChildValue(oldChild, remainingPaths, value)
|
||||
}
|
||||
return array
|
||||
} else {
|
||||
index, _ = strconv.ParseInt(rawIndex, 10, 64) // nolint
|
||||
// writeArray is only called by UpdatedChildValue which handles parsing the
|
||||
// index, as such this renders this dead code.
|
||||
}
|
||||
|
||||
for index >= int64(len(array)) {
|
||||
array = append(array, nil)
|
||||
}
|
||||
currentChild := array[index]
|
||||
|
||||
n.log.Debugf("\tcurrentChild %v\n", currentChild)
|
||||
|
||||
array[index] = n.UpdatedChildValue(currentChild, remainingPaths, value)
|
||||
n.log.Debugf("\tReturning array %v\n", array)
|
||||
return array
|
||||
}
|
||||
|
||||
func (n *navigator) readMap(context yaml.MapSlice, head string, tail []string) (interface{}, error) {
|
||||
n.log.Debugf("readingMap %v with key %v\n", context, head)
|
||||
if head == "*" {
|
||||
return n.readMapSplat(context, tail)
|
||||
}
|
||||
|
||||
entries := n.entriesInSlice(context, head)
|
||||
if len(entries) == 1 {
|
||||
return n.calculateValue(entries[0].Value, tail)
|
||||
} else if len(entries) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var errInIdx error
|
||||
values := make([]interface{}, len(entries))
|
||||
for idx, entry := range entries {
|
||||
values[idx], errInIdx = n.calculateValue(entry.Value, tail)
|
||||
if errInIdx != nil {
|
||||
n.log.Errorf("Error updating index %v in %v", idx, context)
|
||||
return nil, errInIdx
|
||||
}
|
||||
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (n *navigator) readMapSplat(context yaml.MapSlice, tail []string) (interface{}, error) {
|
||||
var newArray = make([]interface{}, len(context))
|
||||
var i = 0
|
||||
for _, entry := range context {
|
||||
if len(tail) > 0 {
|
||||
val, err := n.recurse(entry.Value, tail[0], tail[1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newArray[i] = val
|
||||
} else {
|
||||
newArray[i] = entry.Value
|
||||
}
|
||||
i++
|
||||
}
|
||||
return newArray, nil
|
||||
}
|
||||
|
||||
func (n *navigator) readArray(array []interface{}, head int64, tail []string) (interface{}, error) {
|
||||
if head >= int64(len(array)) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
value := array[head]
|
||||
return n.calculateValue(value, tail)
|
||||
}
|
||||
|
||||
func (n *navigator) readArraySplat(array []interface{}, tail []string) (interface{}, error) {
|
||||
var newArray = make([]interface{}, len(array))
|
||||
for index, value := range array {
|
||||
val, err := n.calculateValue(value, tail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newArray[index] = val
|
||||
}
|
||||
return newArray, nil
|
||||
}
|
||||
|
||||
func (n *navigator) calculateValue(value interface{}, tail []string) (interface{}, error) {
|
||||
if len(tail) > 0 {
|
||||
return n.recurse(value, tail[0], tail[1:])
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (n *navigator) deleteMap(context interface{}, paths []string) (yaml.MapSlice, error) {
|
||||
n.log.Debugf("deleteMap for %v for %v\n", paths, context)
|
||||
|
||||
mapSlice := n.getMapSlice(context)
|
||||
|
||||
if len(paths) == 0 {
|
||||
return mapSlice, nil
|
||||
}
|
||||
|
||||
var index int
|
||||
var child yaml.MapItem
|
||||
for index, child = range mapSlice {
|
||||
if n.matchesKey(paths[0], child.Key) {
|
||||
n.log.Debugf("\tMatched [%v] with [%v] at index %v", paths[0], child.Key, index)
|
||||
var badDelete error
|
||||
mapSlice, badDelete = n.deleteEntryInMap(mapSlice, child, index, paths)
|
||||
if badDelete != nil {
|
||||
return nil, badDelete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mapSlice, nil
|
||||
|
||||
}
|
||||
|
||||
func (n *navigator) deleteEntryInMap(original yaml.MapSlice, child yaml.MapItem, index int, paths []string) (yaml.MapSlice, error) {
|
||||
remainingPaths := paths[1:]
|
||||
|
||||
var newSlice yaml.MapSlice
|
||||
if len(remainingPaths) > 0 {
|
||||
newChild := yaml.MapItem{Key: child.Key}
|
||||
var errorDeleting error
|
||||
newChild.Value, errorDeleting = n.DeleteChildValue(child.Value, remainingPaths)
|
||||
if errorDeleting != nil {
|
||||
return nil, errorDeleting
|
||||
}
|
||||
|
||||
newSlice = make(yaml.MapSlice, len(original))
|
||||
for i := range original {
|
||||
item := original[i]
|
||||
if i == index {
|
||||
item = newChild
|
||||
}
|
||||
newSlice[i] = item
|
||||
}
|
||||
} else {
|
||||
// Delete item from slice at index
|
||||
newSlice = append(original[:index], original[index+1:]...)
|
||||
n.log.Debugf("\tDeleted item index %d from original", index)
|
||||
}
|
||||
|
||||
n.log.Debugf("\tReturning original %v\n", original)
|
||||
return newSlice, nil
|
||||
}
|
||||
|
||||
func (n *navigator) deleteArraySplat(array []interface{}, tail []string) (interface{}, error) {
|
||||
n.log.Debugf("deleteArraySplat for %v for %v\n", tail, array)
|
||||
var newArray = make([]interface{}, len(array))
|
||||
for index, value := range array {
|
||||
val, err := n.DeleteChildValue(value, tail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newArray[index] = val
|
||||
}
|
||||
return newArray, nil
|
||||
}
|
||||
|
||||
func (n *navigator) deleteArray(array []interface{}, paths []string, index int64) (interface{}, error) {
|
||||
n.log.Debugf("deleteArray for %v for %v\n", paths, array)
|
||||
|
||||
if index >= int64(len(array)) {
|
||||
return array, nil
|
||||
}
|
||||
|
||||
remainingPaths := paths[1:]
|
||||
if len(remainingPaths) > 0 {
|
||||
// recurse into the array element at index
|
||||
var errorDeleting error
|
||||
array[index], errorDeleting = n.deleteMap(array[index], remainingPaths)
|
||||
if errorDeleting != nil {
|
||||
return nil, errorDeleting
|
||||
}
|
||||
|
||||
} else {
|
||||
// Delete the array element at index
|
||||
array = append(array[:index], array[index+1:]...)
|
||||
n.log.Debugf("\tDeleted item index %d from array, leaving %v", index, array)
|
||||
}
|
||||
|
||||
n.log.Debugf("\tReturning array: %v\n", array)
|
||||
return array, nil
|
||||
}
|
||||
397
pkg/yqlib/data_navigator_test.go
Normal file
397
pkg/yqlib/data_navigator_test.go
Normal file
@@ -0,0 +1,397 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/mikefarah/yq/v2/test"
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
func TestDataNavigator(t *testing.T) {
|
||||
var log = logging.MustGetLogger("yq")
|
||||
subject := NewDataNavigator(log)
|
||||
|
||||
t.Run("TestReadMap_simple", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
got, _ := subject.ReadChildValue(data, []string{"b", "c"})
|
||||
test.AssertResult(t, 2, got)
|
||||
})
|
||||
|
||||
t.Run("TestReadMap_numberKey", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
200: things
|
||||
`)
|
||||
got, _ := subject.ReadChildValue(data, []string{"200"})
|
||||
test.AssertResult(t, "things", got)
|
||||
})
|
||||
|
||||
t.Run("TestReadMap_splat", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
mapSplat:
|
||||
item1: things
|
||||
item2: whatever
|
||||
otherThing: cat
|
||||
`)
|
||||
res, _ := subject.ReadChildValue(data, []string{"mapSplat", "*"})
|
||||
test.AssertResult(t, "[things whatever cat]", fmt.Sprintf("%v", res))
|
||||
})
|
||||
|
||||
t.Run("TestReadMap_prefixSplat", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
mapSplat:
|
||||
item1: things
|
||||
item2: whatever
|
||||
otherThing: cat
|
||||
`)
|
||||
res, _ := subject.ReadChildValue(data, []string{"mapSplat", "item*"})
|
||||
test.AssertResult(t, "[things whatever]", fmt.Sprintf("%v", res))
|
||||
})
|
||||
|
||||
t.Run("TestReadMap_deep_splat", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
mapSplatDeep:
|
||||
item1:
|
||||
cats: bananas
|
||||
item2:
|
||||
cats: apples
|
||||
`)
|
||||
|
||||
res, _ := subject.ReadChildValue(data, []string{"mapSplatDeep", "*", "cats"})
|
||||
result := res.([]interface{})
|
||||
var actual = []string{result[0].(string), result[1].(string)}
|
||||
sort.Strings(actual)
|
||||
test.AssertResult(t, "[apples bananas]", fmt.Sprintf("%v", actual))
|
||||
})
|
||||
|
||||
t.Run("TestReadMap_key_doesnt_exist", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
got, _ := subject.ReadChildValue(data, []string{"b", "x", "f", "c"})
|
||||
test.AssertResult(t, nil, got)
|
||||
})
|
||||
|
||||
t.Run("TestReadMap_recurse_against_string", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
a: cat
|
||||
`)
|
||||
got, _ := subject.ReadChildValue(data, []string{"a", "b"})
|
||||
test.AssertResult(t, nil, got)
|
||||
})
|
||||
|
||||
t.Run("TestReadMap_with_array", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
d:
|
||||
- 3
|
||||
- 4
|
||||
`)
|
||||
got, _ := subject.ReadChildValue(data, []string{"b", "d", "1"})
|
||||
test.AssertResult(t, 4, got)
|
||||
})
|
||||
|
||||
t.Run("TestReadMap_with_array_and_bad_index", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
d:
|
||||
- 3
|
||||
- 4
|
||||
`)
|
||||
_, err := subject.ReadChildValue(data, []string{"b", "d", "x"})
|
||||
if err == nil {
|
||||
t.Fatal("Expected error due to invalid path")
|
||||
}
|
||||
expectedOutput := `error accessing array: strconv.ParseInt: parsing "x": invalid syntax`
|
||||
test.AssertResult(t, expectedOutput, err.Error())
|
||||
})
|
||||
|
||||
t.Run("TestReadMap_with_mapsplat_array_and_bad_index", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
d:
|
||||
e:
|
||||
- 3
|
||||
- 4
|
||||
f:
|
||||
- 1
|
||||
- 2
|
||||
`)
|
||||
_, err := subject.ReadChildValue(data, []string{"b", "d", "*", "x"})
|
||||
if err == nil {
|
||||
t.Fatal("Expected error due to invalid path")
|
||||
}
|
||||
expectedOutput := `error accessing array: strconv.ParseInt: parsing "x": invalid syntax`
|
||||
test.AssertResult(t, expectedOutput, err.Error())
|
||||
})
|
||||
|
||||
t.Run("TestReadMap_with_arraysplat_map_array_and_bad_index", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
d:
|
||||
- names:
|
||||
- fred
|
||||
- smith
|
||||
- names:
|
||||
- sam
|
||||
- bo
|
||||
`)
|
||||
_, err := subject.ReadChildValue(data, []string{"b", "d", "*", "names", "x"})
|
||||
if err == nil {
|
||||
t.Fatal("Expected error due to invalid path")
|
||||
}
|
||||
expectedOutput := `error accessing array: strconv.ParseInt: parsing "x": invalid syntax`
|
||||
test.AssertResult(t, expectedOutput, err.Error())
|
||||
})
|
||||
|
||||
t.Run("TestReadMap_with_array_out_of_bounds", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
d:
|
||||
- 3
|
||||
- 4
|
||||
`)
|
||||
got, _ := subject.ReadChildValue(data, []string{"b", "d", "3"})
|
||||
test.AssertResult(t, nil, got)
|
||||
})
|
||||
|
||||
t.Run("TestReadMap_with_array_out_of_bounds_by_1", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
d:
|
||||
- 3
|
||||
- 4
|
||||
`)
|
||||
got, _ := subject.ReadChildValue(data, []string{"b", "d", "2"})
|
||||
test.AssertResult(t, nil, got)
|
||||
})
|
||||
|
||||
t.Run("TestReadMap_with_array_splat", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
e:
|
||||
-
|
||||
name: Fred
|
||||
thing: cat
|
||||
-
|
||||
name: Sam
|
||||
thing: dog
|
||||
`)
|
||||
got, _ := subject.ReadChildValue(data, []string{"e", "*", "name"})
|
||||
test.AssertResult(t, "[Fred Sam]", fmt.Sprintf("%v", got))
|
||||
})
|
||||
|
||||
t.Run("TestWrite_really_simple", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
b: 2
|
||||
`)
|
||||
|
||||
updated := subject.UpdatedChildValue(data, []string{"b"}, "4")
|
||||
test.AssertResult(t, "[{b 4}]", fmt.Sprintf("%v", updated))
|
||||
})
|
||||
|
||||
t.Run("TestWrite_simple", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
|
||||
updated := subject.UpdatedChildValue(data, []string{"b", "c"}, "4")
|
||||
test.AssertResult(t, "[{b [{c 4}]}]", fmt.Sprintf("%v", updated))
|
||||
})
|
||||
|
||||
t.Run("TestWrite_new", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
|
||||
updated := subject.UpdatedChildValue(data, []string{"b", "d"}, "4")
|
||||
test.AssertResult(t, "[{b [{c 2} {d 4}]}]", fmt.Sprintf("%v", updated))
|
||||
})
|
||||
|
||||
t.Run("TestWrite_new_deep", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
|
||||
updated := subject.UpdatedChildValue(data, []string{"b", "d", "f"}, "4")
|
||||
test.AssertResult(t, "[{b [{c 2} {d [{f 4}]}]}]", fmt.Sprintf("%v", updated))
|
||||
})
|
||||
|
||||
t.Run("TestWrite_array", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
b:
|
||||
- aa
|
||||
`)
|
||||
|
||||
updated := subject.UpdatedChildValue(data, []string{"b", "0"}, "bb")
|
||||
|
||||
test.AssertResult(t, "[{b [bb]}]", fmt.Sprintf("%v", updated))
|
||||
})
|
||||
|
||||
t.Run("TestWrite_new_array", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
|
||||
updated := subject.UpdatedChildValue(data, []string{"b", "0"}, "4")
|
||||
test.AssertResult(t, "[{b [{c 2} {0 4}]}]", fmt.Sprintf("%v", updated))
|
||||
})
|
||||
|
||||
t.Run("TestWrite_new_array_deep", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
a: apple
|
||||
`)
|
||||
|
||||
updated := subject.UpdatedChildValue(data, []string{"b", "+", "c"}, "4")
|
||||
test.AssertResult(t, "[{a apple} {b [[{c 4}]]}]", fmt.Sprintf("%v", updated))
|
||||
})
|
||||
|
||||
t.Run("TestWrite_new_map_array_deep", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
|
||||
updated := subject.UpdatedChildValue(data, []string{"b", "d", "+"}, "4")
|
||||
test.AssertResult(t, "[{b [{c 2} {d [4]}]}]", fmt.Sprintf("%v", updated))
|
||||
})
|
||||
|
||||
t.Run("TestWrite_add_to_array", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
b:
|
||||
- aa
|
||||
`)
|
||||
|
||||
updated := subject.UpdatedChildValue(data, []string{"b", "1"}, "bb")
|
||||
test.AssertResult(t, "[{b [aa bb]}]", fmt.Sprintf("%v", updated))
|
||||
})
|
||||
|
||||
t.Run("TestWrite_with_no_tail", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
b:
|
||||
c: 2
|
||||
`)
|
||||
updated := subject.UpdatedChildValue(data, []string{"b"}, "4")
|
||||
|
||||
test.AssertResult(t, "[{b 4}]", fmt.Sprintf("%v", updated))
|
||||
})
|
||||
|
||||
t.Run("TestWriteMap_no_paths", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
b: 5
|
||||
`)
|
||||
var new = test.ParseData(`
|
||||
c: 4
|
||||
`)
|
||||
result := subject.UpdatedChildValue(data, []string{}, new)
|
||||
test.AssertResult(t, fmt.Sprintf("%v", new), fmt.Sprintf("%v", result))
|
||||
})
|
||||
|
||||
t.Run("TestWriteArray_no_paths", func(t *testing.T) {
|
||||
var data = make([]interface{}, 1)
|
||||
data[0] = "mike"
|
||||
var new = test.ParseData(`
|
||||
c: 4
|
||||
`)
|
||||
result := subject.UpdatedChildValue(data, []string{}, new)
|
||||
test.AssertResult(t, fmt.Sprintf("%v", new), fmt.Sprintf("%v", result))
|
||||
})
|
||||
|
||||
t.Run("TestDelete_MapItem", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
a: 123
|
||||
b: 456
|
||||
`)
|
||||
var expected = test.ParseData(`
|
||||
b: 456
|
||||
`)
|
||||
|
||||
result, _ := subject.DeleteChildValue(data, []string{"a"})
|
||||
test.AssertResult(t, fmt.Sprintf("%v", expected), fmt.Sprintf("%v", result))
|
||||
})
|
||||
|
||||
// Ensure deleting an index into a string does nothing
|
||||
t.Run("TestDelete_index_to_string", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
a: mystring
|
||||
`)
|
||||
result, _ := subject.DeleteChildValue(data, []string{"a", "0"})
|
||||
test.AssertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
|
||||
})
|
||||
|
||||
t.Run("TestDelete_list_index", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
a: [3, 4]
|
||||
`)
|
||||
var expected = test.ParseData(`
|
||||
a: [3]
|
||||
`)
|
||||
result, _ := subject.DeleteChildValue(data, []string{"a", "1"})
|
||||
test.AssertResult(t, fmt.Sprintf("%v", expected), fmt.Sprintf("%v", result))
|
||||
})
|
||||
|
||||
t.Run("TestDelete_list_index_beyond_bounds", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
a: [3, 4]
|
||||
`)
|
||||
result, _ := subject.DeleteChildValue(data, []string{"a", "5"})
|
||||
test.AssertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
|
||||
})
|
||||
|
||||
t.Run("TestDelete_list_index_out_of_bounds_by_1", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
a: [3, 4]
|
||||
`)
|
||||
result, _ := subject.DeleteChildValue(data, []string{"a", "2"})
|
||||
test.AssertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
|
||||
})
|
||||
|
||||
t.Run("TestDelete_no_paths", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
a: [3, 4]
|
||||
b:
|
||||
- name: test
|
||||
`)
|
||||
result, _ := subject.DeleteChildValue(data, []string{})
|
||||
test.AssertResult(t, fmt.Sprintf("%v", data), fmt.Sprintf("%v", result))
|
||||
})
|
||||
|
||||
t.Run("TestDelete_array_map_item", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
b:
|
||||
- name: fred
|
||||
value: blah
|
||||
- name: john
|
||||
value: test
|
||||
`)
|
||||
var expected = test.ParseData(`
|
||||
b:
|
||||
- value: blah
|
||||
- name: john
|
||||
value: test
|
||||
`)
|
||||
result, _ := subject.DeleteChildValue(data, []string{"b", "0", "name"})
|
||||
test.AssertResult(t, fmt.Sprintf("%v", expected), fmt.Sprintf("%v", result))
|
||||
})
|
||||
}
|
||||
70
pkg/yqlib/lib.go
Normal file
70
pkg/yqlib/lib.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
mergo "gopkg.in/imdario/mergo.v0"
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
type YqLib interface {
|
||||
ReadPath(dataBucket interface{}, path string) (interface{}, error)
|
||||
WritePath(dataBucket interface{}, path string, value interface{}) interface{}
|
||||
PrefixPath(dataBucket interface{}, prefix string) interface{}
|
||||
DeletePath(dataBucket interface{}, path string) (interface{}, error)
|
||||
Merge(dst interface{}, src interface{}, overwrite bool, append bool) error
|
||||
}
|
||||
|
||||
type lib struct {
|
||||
navigator DataNavigator
|
||||
parser PathParser
|
||||
}
|
||||
|
||||
func NewYqLib(l *logging.Logger) YqLib {
|
||||
return &lib{
|
||||
navigator: NewDataNavigator(l),
|
||||
parser: NewPathParser(),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lib) ReadPath(dataBucket interface{}, path string) (interface{}, error) {
|
||||
var paths = l.parser.ParsePath(path)
|
||||
return l.navigator.ReadChildValue(dataBucket, paths)
|
||||
}
|
||||
|
||||
func (l *lib) WritePath(dataBucket interface{}, path string, value interface{}) interface{} {
|
||||
var paths = l.parser.ParsePath(path)
|
||||
return l.navigator.UpdatedChildValue(dataBucket, paths, value)
|
||||
}
|
||||
|
||||
func (l *lib) PrefixPath(dataBucket interface{}, prefix string) interface{} {
|
||||
var paths = l.parser.ParsePath(prefix)
|
||||
|
||||
// Inverse order
|
||||
for i := len(paths)/2 - 1; i >= 0; i-- {
|
||||
opp := len(paths) - 1 - i
|
||||
paths[i], paths[opp] = paths[opp], paths[i]
|
||||
}
|
||||
|
||||
var mapDataBucket = dataBucket
|
||||
for _, key := range paths {
|
||||
singlePath := []string{key}
|
||||
mapDataBucket = l.navigator.UpdatedChildValue(nil, singlePath, mapDataBucket)
|
||||
}
|
||||
|
||||
return mapDataBucket
|
||||
}
|
||||
|
||||
func (l *lib) DeletePath(dataBucket interface{}, path string) (interface{}, error) {
|
||||
var paths = l.parser.ParsePath(path)
|
||||
return l.navigator.DeleteChildValue(dataBucket, paths)
|
||||
}
|
||||
|
||||
func (l *lib) Merge(dst interface{}, src interface{}, overwriteFlag bool, appendFlag bool) error {
|
||||
opts := []func(*mergo.Config){}
|
||||
if overwriteFlag {
|
||||
opts = append(opts, mergo.WithOverride)
|
||||
}
|
||||
if appendFlag {
|
||||
opts = append(opts, mergo.WithAppendSlice)
|
||||
}
|
||||
return mergo.Merge(dst, src, opts...)
|
||||
}
|
||||
183
pkg/yqlib/lib_test.go
Normal file
183
pkg/yqlib/lib_test.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mikefarah/yq/v2/test"
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
func TestLib(t *testing.T) {
|
||||
|
||||
var log = logging.MustGetLogger("yq")
|
||||
subject := NewYqLib(log)
|
||||
|
||||
t.Run("TestReadPath", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
2: c
|
||||
`)
|
||||
|
||||
got, _ := subject.ReadPath(data, "b.2")
|
||||
test.AssertResult(t, `c`, got)
|
||||
})
|
||||
|
||||
t.Run("TestReadPath_WithError", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
- c
|
||||
`)
|
||||
|
||||
_, err := subject.ReadPath(data, "b.[a]")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error due to invalid path")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TestWritePath", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
2: c
|
||||
`)
|
||||
|
||||
got := subject.WritePath(data, "b.3", "a")
|
||||
test.AssertResult(t, `[{b [{2 c} {3 a}]}]`, fmt.Sprintf("%v", got))
|
||||
})
|
||||
|
||||
t.Run("TestPrefixPath", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
2: c
|
||||
`)
|
||||
|
||||
got := subject.PrefixPath(data, "a.d")
|
||||
test.AssertResult(t, `[{a [{d [{b [{2 c}]}]}]}]`, fmt.Sprintf("%v", got))
|
||||
})
|
||||
|
||||
t.Run("TestDeletePath", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
2: c
|
||||
3: a
|
||||
`)
|
||||
|
||||
got, _ := subject.DeletePath(data, "b.2")
|
||||
test.AssertResult(t, `[{b [{3 a}]}]`, fmt.Sprintf("%v", got))
|
||||
})
|
||||
|
||||
t.Run("TestDeletePath_WithError", func(t *testing.T) {
|
||||
var data = test.ParseData(`
|
||||
---
|
||||
b:
|
||||
- c
|
||||
`)
|
||||
|
||||
_, err := subject.DeletePath(data, "b.[a]")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error due to invalid path")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TestMerge", func(t *testing.T) {
|
||||
var dst = test.ParseData(`
|
||||
---
|
||||
a: b
|
||||
c: d
|
||||
`)
|
||||
var src = test.ParseData(`
|
||||
---
|
||||
a: 1
|
||||
b: 2
|
||||
`)
|
||||
|
||||
var mergedData = make(map[interface{}]interface{})
|
||||
mergedData["root"] = dst
|
||||
var mapDataBucket = make(map[interface{}]interface{})
|
||||
mapDataBucket["root"] = src
|
||||
|
||||
err := subject.Merge(&mergedData, mapDataBucket, false, false)
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error")
|
||||
}
|
||||
test.AssertResult(t, `[{a b} {c d}]`, fmt.Sprintf("%v", mergedData["root"]))
|
||||
})
|
||||
|
||||
t.Run("TestMerge_WithOverwrite", func(t *testing.T) {
|
||||
var dst = test.ParseData(`
|
||||
---
|
||||
a: b
|
||||
c: d
|
||||
`)
|
||||
var src = test.ParseData(`
|
||||
---
|
||||
a: 1
|
||||
b: 2
|
||||
`)
|
||||
|
||||
var mergedData = make(map[interface{}]interface{})
|
||||
mergedData["root"] = dst
|
||||
var mapDataBucket = make(map[interface{}]interface{})
|
||||
mapDataBucket["root"] = src
|
||||
|
||||
err := subject.Merge(&mergedData, mapDataBucket, true, false)
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error")
|
||||
}
|
||||
test.AssertResult(t, `[{a 1} {b 2}]`, fmt.Sprintf("%v", mergedData["root"]))
|
||||
})
|
||||
|
||||
t.Run("TestMerge_WithAppend", func(t *testing.T) {
|
||||
var dst = test.ParseData(`
|
||||
---
|
||||
a: b
|
||||
c: d
|
||||
`)
|
||||
var src = test.ParseData(`
|
||||
---
|
||||
a: 1
|
||||
b: 2
|
||||
`)
|
||||
|
||||
var mergedData = make(map[interface{}]interface{})
|
||||
mergedData["root"] = dst
|
||||
var mapDataBucket = make(map[interface{}]interface{})
|
||||
mapDataBucket["root"] = src
|
||||
|
||||
err := subject.Merge(&mergedData, mapDataBucket, false, true)
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error")
|
||||
}
|
||||
test.AssertResult(t, `[{a b} {c d} {a 1} {b 2}]`, fmt.Sprintf("%v", mergedData["root"]))
|
||||
})
|
||||
|
||||
t.Run("TestMerge_WithAppendAndOverwrite", func(t *testing.T) {
|
||||
var dst = map[interface{}]interface{}{
|
||||
"a": "initial",
|
||||
"b": []string{"old"},
|
||||
}
|
||||
var src = map[interface{}]interface{}{
|
||||
"a": "replaced",
|
||||
"b": []string{"new"},
|
||||
}
|
||||
|
||||
err := subject.Merge(&dst, src, true, true)
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error")
|
||||
}
|
||||
test.AssertResult(t, `map[a:replaced b:[old new]]`, fmt.Sprintf("%v", dst))
|
||||
})
|
||||
|
||||
t.Run("TestMerge_WithError", func(t *testing.T) {
|
||||
err := subject.Merge(nil, nil, false, false)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error due to nil")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
65
pkg/yqlib/path_parser.go
Normal file
65
pkg/yqlib/path_parser.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package yqlib
|
||||
|
||||
type PathParser interface {
|
||||
ParsePath(path string) []string
|
||||
}
|
||||
|
||||
type pathParser struct{}
|
||||
|
||||
func NewPathParser() PathParser {
|
||||
return &pathParser{}
|
||||
}
|
||||
|
||||
func (p *pathParser) ParsePath(path string) []string {
|
||||
return p.parsePathAccum([]string{}, path)
|
||||
}
|
||||
|
||||
func (p *pathParser) parsePathAccum(paths []string, remaining string) []string {
|
||||
head, tail := p.nextYamlPath(remaining)
|
||||
if tail == "" {
|
||||
return append(paths, head)
|
||||
}
|
||||
return p.parsePathAccum(append(paths, head), tail)
|
||||
}
|
||||
|
||||
func (p *pathParser) nextYamlPath(path string) (pathElement string, remaining string) {
|
||||
switch path[0] {
|
||||
case '[':
|
||||
// e.g [0].blah.cat -> we need to return "0" and "blah.cat"
|
||||
return p.search(path[1:], []uint8{']'}, true)
|
||||
case '"':
|
||||
// e.g "a.b".blah.cat -> we need to return "a.b" and "blah.cat"
|
||||
return p.search(path[1:], []uint8{'"'}, true)
|
||||
default:
|
||||
// e.g "a.blah.cat" -> return "a" and "blah.cat"
|
||||
return p.search(path[0:], []uint8{'.', '['}, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pathParser) search(path string, matchingChars []uint8, skipNext bool) (pathElement string, remaining string) {
|
||||
for i := 0; i < len(path); i++ {
|
||||
var char = path[i]
|
||||
if p.contains(matchingChars, char) {
|
||||
var remainingStart = i + 1
|
||||
if skipNext {
|
||||
remainingStart = remainingStart + 1
|
||||
} else if !skipNext && char != '.' {
|
||||
remainingStart = i
|
||||
}
|
||||
if remainingStart > len(path) {
|
||||
remainingStart = len(path)
|
||||
}
|
||||
return path[0:i], path[remainingStart:]
|
||||
}
|
||||
}
|
||||
return path, ""
|
||||
}
|
||||
|
||||
func (p *pathParser) contains(matchingChars []uint8, candidate uint8) bool {
|
||||
for _, a := range matchingChars {
|
||||
if a == candidate {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
29
pkg/yqlib/path_parser_test.go
Normal file
29
pkg/yqlib/path_parser_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mikefarah/yq/v2/test"
|
||||
)
|
||||
|
||||
var parsePathsTests = []struct {
|
||||
path string
|
||||
expectedPaths []string
|
||||
}{
|
||||
{"a.b", []string{"a", "b"}},
|
||||
{"a.b[0]", []string{"a", "b", "0"}},
|
||||
{"a.b.d[+]", []string{"a", "b", "d", "+"}},
|
||||
{"a", []string{"a"}},
|
||||
{"a.b.c", []string{"a", "b", "c"}},
|
||||
{"\"a.b\".c", []string{"a.b", "c"}},
|
||||
{"a.\"b.c\".d", []string{"a", "b.c", "d"}},
|
||||
{"[1].a.d", []string{"1", "a", "d"}},
|
||||
{"a[0].c", []string{"a", "0", "c"}},
|
||||
{"[0]", []string{"0"}},
|
||||
}
|
||||
|
||||
func TestParsePath(t *testing.T) {
|
||||
for _, tt := range parsePathsTests {
|
||||
test.AssertResultComplex(t, tt.expectedPaths, NewPathParser().ParsePath(tt.path))
|
||||
}
|
||||
}
|
||||
42
pkg/yqlib/value_parser.go
Normal file
42
pkg/yqlib/value_parser.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ValueParser interface {
|
||||
ParseValue(argument string) interface{}
|
||||
}
|
||||
|
||||
type valueParser struct{}
|
||||
|
||||
func NewValueParser() ValueParser {
|
||||
return &valueParser{}
|
||||
}
|
||||
|
||||
func (v *valueParser) ParseValue(argument string) interface{} {
|
||||
var value, err interface{}
|
||||
var inQuotes = len(argument) > 0 && argument[0] == '"'
|
||||
if !inQuotes {
|
||||
intValue, intErr := strconv.ParseInt(argument, 10, 64)
|
||||
floatValue, floatErr := strconv.ParseFloat(argument, 64)
|
||||
if intErr == nil && floatErr == nil {
|
||||
if int64(floatValue) == intValue {
|
||||
return intValue
|
||||
}
|
||||
return floatValue
|
||||
} else if floatErr == nil {
|
||||
// In case cannot parse the int due to large precision
|
||||
return floatValue
|
||||
}
|
||||
value, err = strconv.ParseBool(argument)
|
||||
if err == nil {
|
||||
return value
|
||||
}
|
||||
if argument == "[]" {
|
||||
return make([]interface{}, 0)
|
||||
}
|
||||
return argument
|
||||
}
|
||||
return argument[1 : len(argument)-1]
|
||||
}
|
||||
26
pkg/yqlib/value_parser_test.go
Normal file
26
pkg/yqlib/value_parser_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mikefarah/yq/v2/test"
|
||||
)
|
||||
|
||||
var parseValueTests = []struct {
|
||||
argument string
|
||||
expectedResult interface{}
|
||||
testDescription string
|
||||
}{
|
||||
{"true", true, "boolean"},
|
||||
{"\"true\"", "true", "boolean as string"},
|
||||
{"3.4", 3.4, "number"},
|
||||
{"\"3.4\"", "3.4", "number as string"},
|
||||
{"", "", "empty string"},
|
||||
{"1212121", int64(1212121), "big number"},
|
||||
}
|
||||
|
||||
func TestParseValue(t *testing.T) {
|
||||
for _, tt := range parseValueTests {
|
||||
test.AssertResultWithContext(t, tt.expectedResult, NewValueParser().ParseValue(tt.argument), tt.testDescription)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
then use the UI (https://snapcraft.io/yq/release)
|
||||
|
||||
- go get
|
||||
- update the readme instructions
|
||||
|
||||
- brew
|
||||
- brew bump-formula-pr --url=https://github.com/mikefarah/yq/archive/2.2.0.tar.gz yq
|
||||
|
||||
@@ -6,7 +6,7 @@ set -e
|
||||
X=$(./yq w ./examples/sample.yaml b.c 3 | ./yq r - b.c)
|
||||
|
||||
if [[ $X != 3 ]]; then
|
||||
echo "Failed acceptance test: expected 2 but was $X"
|
||||
echo "Failed acceptance test: expected 3 but was $X"
|
||||
exit 1
|
||||
fi
|
||||
echo "acceptance tests passed"
|
||||
|
||||
@@ -3,24 +3,14 @@
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
gometalinter \
|
||||
--skip=examples \
|
||||
--tests \
|
||||
--vendor \
|
||||
--disable=aligncheck \
|
||||
--disable=gotype \
|
||||
--disable=goconst \
|
||||
--cyclo-over=20 \
|
||||
--deadline=300s \
|
||||
./...
|
||||
./bin/golangci-lint run
|
||||
|
||||
gometalinter \
|
||||
--skip=examples \
|
||||
--tests \
|
||||
--vendor \
|
||||
--disable=aligncheck \
|
||||
--disable=gotype \
|
||||
--disable=goconst \
|
||||
--disable=gocyclo \
|
||||
--deadline=300s \
|
||||
./...
|
||||
# ./bin/golangci-lint \
|
||||
# --tests \
|
||||
# --vendor \
|
||||
# --disable=aligncheck \
|
||||
# --disable=gotype \
|
||||
# --disable=goconst \
|
||||
# --disable=gocyclo \
|
||||
# --deadline=300s \
|
||||
# ./...
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
set -e
|
||||
|
||||
go test -coverprofile=coverage.out
|
||||
go test -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out -o cover/coverage.html
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
go get -u github.com/alecthomas/gometalinter
|
||||
go get -u golang.org/x/tools/cmd/goimports
|
||||
go get -u github.com/mitchellh/gox
|
||||
go get -u github.com/kardianos/govendor
|
||||
go get -u github.com/aktau/github-release
|
||||
|
||||
# install all the linters
|
||||
gometalinter --install --update
|
||||
set -e
|
||||
wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.21.0
|
||||
go get golang.org/x/tools/cmd/goimports
|
||||
@@ -16,6 +16,7 @@ release() {
|
||||
}
|
||||
|
||||
upload() {
|
||||
mkdir -p ./build-done
|
||||
while IFS= read -r -d $'\0'; do
|
||||
file=$REPLY
|
||||
BINARY=$(basename "${file}")
|
||||
@@ -27,6 +28,7 @@ upload() {
|
||||
--tag "$CURRENT" \
|
||||
--name "${BINARY}" \
|
||||
--file "$file"
|
||||
mv "$file" "./build-done/${BINARY}"
|
||||
done < <(find ./build -mindepth 1 -maxdepth 1 -print0)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
go test -v $(go list ./... | grep -v -E 'vendor|examples')
|
||||
go test -v $(go list ./... | grep -v -E 'examples')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: yq
|
||||
version: '2.4.0'
|
||||
version: '2.4.1'
|
||||
summary: A lightweight and portable command-line YAML processor
|
||||
description: |
|
||||
The aim of the project is to be the jq or sed of yaml files.
|
||||
|
||||
6
test/fixture/keyonly.yaml
Normal file
6
test/fixture/keyonly.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
a:
|
||||
b:
|
||||
c: 1
|
||||
d:
|
||||
e: 2
|
||||
f:
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
yaml "gopkg.in/mikefarah/yaml.v2"
|
||||
cobra "gopkg.in/spf13/cobra.v0"
|
||||
yaml "github.com/mikefarah/yaml/v2"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type resulter struct {
|
||||
@@ -19,7 +19,7 @@ type resulter struct {
|
||||
Command *cobra.Command
|
||||
}
|
||||
|
||||
func runCmd(c *cobra.Command, input string) resulter {
|
||||
func RunCmd(c *cobra.Command, input string) resulter {
|
||||
buf := new(bytes.Buffer)
|
||||
c.SetOutput(buf)
|
||||
c.SetArgs(strings.Split(input, " "))
|
||||
@@ -30,7 +30,7 @@ func runCmd(c *cobra.Command, input string) resulter {
|
||||
return resulter{err, output, c}
|
||||
}
|
||||
|
||||
func parseData(rawData string) yaml.MapSlice {
|
||||
func ParseData(rawData string) yaml.MapSlice {
|
||||
var parsedData yaml.MapSlice
|
||||
err := yaml.Unmarshal([]byte(rawData), &parsedData)
|
||||
if err != nil {
|
||||
@@ -40,21 +40,21 @@ func parseData(rawData string) yaml.MapSlice {
|
||||
return parsedData
|
||||
}
|
||||
|
||||
func assertResult(t *testing.T, expectedValue interface{}, actualValue interface{}) {
|
||||
func AssertResult(t *testing.T, expectedValue interface{}, actualValue interface{}) {
|
||||
t.Helper()
|
||||
if expectedValue != actualValue {
|
||||
t.Error("Expected <", expectedValue, "> but got <", actualValue, ">", fmt.Sprintf("%T", actualValue))
|
||||
}
|
||||
}
|
||||
|
||||
func assertResultComplex(t *testing.T, expectedValue interface{}, actualValue interface{}) {
|
||||
func AssertResultComplex(t *testing.T, expectedValue interface{}, actualValue interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(expectedValue, actualValue) {
|
||||
t.Error("Expected <", expectedValue, "> but got <", actualValue, ">", fmt.Sprintf("%T", actualValue))
|
||||
}
|
||||
}
|
||||
|
||||
func assertResultWithContext(t *testing.T, expectedValue interface{}, actualValue interface{}, context interface{}) {
|
||||
func AssertResultWithContext(t *testing.T, expectedValue interface{}, actualValue interface{}, context interface{}) {
|
||||
t.Helper()
|
||||
if expectedValue != actualValue {
|
||||
t.Error(context)
|
||||
@@ -62,7 +62,7 @@ func assertResultWithContext(t *testing.T, expectedValue interface{}, actualValu
|
||||
}
|
||||
}
|
||||
|
||||
func writeTempYamlFile(content string) string {
|
||||
func WriteTempYamlFile(content string) string {
|
||||
tmpfile, _ := ioutil.TempFile("", "testyaml")
|
||||
defer func() {
|
||||
_ = tmpfile.Close()
|
||||
@@ -72,11 +72,11 @@ func writeTempYamlFile(content string) string {
|
||||
return tmpfile.Name()
|
||||
}
|
||||
|
||||
func readTempYamlFile(name string) string {
|
||||
func ReadTempYamlFile(name string) string {
|
||||
content, _ := ioutil.ReadFile(name)
|
||||
return string(content)
|
||||
}
|
||||
|
||||
func removeTempYamlFile(name string) {
|
||||
func RemoveTempYamlFile(name string) {
|
||||
_ = os.Remove(name)
|
||||
}
|
||||
49
vendor/vendor.json
vendored
49
vendor/vendor.json
vendored
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"comment": "",
|
||||
"ignore": "test",
|
||||
"package": [
|
||||
{
|
||||
"checksumSHA1": "40vJyUB4ezQSn/NSadsKEOrudMc=",
|
||||
"path": "github.com/inconshreveable/mousetrap",
|
||||
"revision": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75",
|
||||
"revisionTime": "2014-10-17T20:07:13Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "ljd3FhYRJ91cLZz3wsH9BQQ2JbA=",
|
||||
"path": "github.com/pkg/errors",
|
||||
"revision": "816c9085562cd7ee03e7f8188a1cfd942858cded",
|
||||
"revisionTime": "2018-03-11T21:45:15Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "OJI0OgC5V8gZtfS1e0CDYMhkDNc=",
|
||||
"path": "github.com/spf13/pflag",
|
||||
"revision": "3ebe029320b2676d667ae88da602a5f854788a8a",
|
||||
"revisionTime": "2018-06-01T13:25:42Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "RwlkCZz8VFXAE4aHQQOSC0hLu5k=",
|
||||
"path": "gopkg.in/imdario/mergo.v0",
|
||||
"revision": "9316a62528ac99aaecb4e47eadd6dc8aa6533d58",
|
||||
"revisionTime": "2018-06-08T14:01:56Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "7wtGubs4v7+RZovtlmyT9KwA/gE=",
|
||||
"path": "gopkg.in/mikefarah/yaml.v2",
|
||||
"revision": "e175af14aaa1d0eff2ee04b691e4a4827a111416",
|
||||
"revisionTime": "2018-06-13T04:05:11Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "rL5r44ASTGubGW88gqQwlvVQshw=",
|
||||
"path": "gopkg.in/op/go-logging.v1",
|
||||
"revision": "b2cb9fa56473e98db8caba80237377e83fe44db5",
|
||||
"revisionTime": "2016-02-11T21:21:56Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "xsZjAbfLrXcMtY6fyQ8QC6EvJD0=",
|
||||
"path": "gopkg.in/spf13/cobra.v0",
|
||||
"revision": "ef82de70bb3f60c65fb8eebacbb2d122ef517385",
|
||||
"revisionTime": "2018-04-27T13:45:50Z"
|
||||
}
|
||||
],
|
||||
"rootPath": "github.com/mikefarah/yq"
|
||||
}
|
||||
@@ -11,7 +11,7 @@ var (
|
||||
GitDescribe string
|
||||
|
||||
// Version is main version number that is being run at the moment.
|
||||
Version = "2.4.0"
|
||||
Version = "2.4.1"
|
||||
|
||||
// VersionPrerelease is a pre-release marker for the version. If this is "" (empty string)
|
||||
// then it means that it is a final release. Otherwise, this is a pre-release
|
||||
|
||||
139
yq.go
139
yq.go
@@ -10,11 +10,14 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mikefarah/yq/v2/pkg/marshal"
|
||||
"github.com/mikefarah/yq/v2/pkg/yqlib"
|
||||
|
||||
errors "github.com/pkg/errors"
|
||||
|
||||
yaml "gopkg.in/mikefarah/yaml.v2"
|
||||
yaml "github.com/mikefarah/yaml/v2"
|
||||
"github.com/spf13/cobra"
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
cobra "gopkg.in/spf13/cobra.v0"
|
||||
)
|
||||
|
||||
var trimOutput = true
|
||||
@@ -22,12 +25,17 @@ var writeInplace = false
|
||||
var writeScript = ""
|
||||
var outputToJSON = false
|
||||
var overwriteFlag = false
|
||||
var keyOnlyFlag = false
|
||||
var allowEmptyFlag = false
|
||||
var appendFlag = false
|
||||
var verbose = false
|
||||
var version = false
|
||||
var docIndex = "0"
|
||||
var log = logging.MustGetLogger("yq")
|
||||
var lib = yqlib.NewYqLib(log)
|
||||
var jsonConverter = marshal.NewJsonConverter()
|
||||
var yamlConverter = marshal.NewYamlConverter()
|
||||
var valueParser = yqlib.NewValueParser()
|
||||
|
||||
func main() {
|
||||
cmd := newCommandCLI()
|
||||
@@ -104,6 +112,7 @@ yq r -- things.yaml --key-starting-with-dashes
|
||||
}
|
||||
cmdRead.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
|
||||
cmdRead.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "output as json")
|
||||
cmdRead.PersistentFlags().BoolVarP(&keyOnlyFlag, "keyonly", "k", false, "output with top level keys only")
|
||||
return cmdRead
|
||||
}
|
||||
|
||||
@@ -270,12 +279,18 @@ func readProperty(cmd *cobra.Command, args []string) error {
|
||||
log.Debugf("processing %v - requested index %v", currentIndex, docIndexInt)
|
||||
if updateAll || currentIndex == docIndexInt {
|
||||
log.Debugf("reading %v in index %v", path, currentIndex)
|
||||
mappedDoc, errorParsing := readPath(dataBucket, path)
|
||||
log.Debugf("%v", mappedDoc)
|
||||
if errorParsing != nil {
|
||||
return errors.Wrapf(errorParsing, "Error reading path in document index %v", currentIndex)
|
||||
if path == "" {
|
||||
log.Debug("no path")
|
||||
log.Debugf("%v", dataBucket)
|
||||
mappedDocs = append(mappedDocs, dataBucket)
|
||||
} else {
|
||||
mappedDoc, errorParsing := lib.ReadPath(dataBucket, path)
|
||||
log.Debugf("%v", mappedDoc)
|
||||
if errorParsing != nil {
|
||||
return errors.Wrapf(errorParsing, "Error reading path in document index %v", currentIndex)
|
||||
}
|
||||
mappedDocs = append(mappedDocs, mappedDoc)
|
||||
}
|
||||
mappedDocs = append(mappedDocs, mappedDoc)
|
||||
}
|
||||
currentIndex = currentIndex + 1
|
||||
}
|
||||
@@ -291,6 +306,13 @@ func readProperty(cmd *cobra.Command, args []string) error {
|
||||
dataBucket = mappedDocs
|
||||
}
|
||||
|
||||
if keyOnlyFlag {
|
||||
for _, value := range dataBucket.(yaml.MapSlice) {
|
||||
cmd.Println(value.Key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
dataStr, err := toString(dataBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -299,15 +321,6 @@ func readProperty(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPath(dataBucket interface{}, path string) (interface{}, error) {
|
||||
if path == "" {
|
||||
log.Debug("no path")
|
||||
return dataBucket, nil
|
||||
}
|
||||
var paths = parsePath(path)
|
||||
return recurse(dataBucket, paths[0], paths[1:])
|
||||
}
|
||||
|
||||
func newProperty(cmd *cobra.Command, args []string) error {
|
||||
updatedData, err := newYaml(args)
|
||||
if err != nil {
|
||||
@@ -339,8 +352,7 @@ func newYaml(args []string) (interface{}, error) {
|
||||
path := entry.Key.(string)
|
||||
value := entry.Value
|
||||
log.Debugf("setting %v to %v", path, value)
|
||||
var paths = parsePath(path)
|
||||
dataBucket = updatedChildValue(dataBucket, paths, value)
|
||||
dataBucket = lib.WritePath(dataBucket, path, value)
|
||||
}
|
||||
|
||||
return dataBucket, nil
|
||||
@@ -416,8 +428,7 @@ func writeProperty(cmd *cobra.Command, args []string) error {
|
||||
path := entry.Key.(string)
|
||||
value := entry.Value
|
||||
log.Debugf("setting %v to %v", path, value)
|
||||
var paths = parsePath(path)
|
||||
dataBucket = updatedChildValue(dataBucket, paths, value)
|
||||
dataBucket = lib.WritePath(dataBucket, path, value)
|
||||
}
|
||||
}
|
||||
return dataBucket, nil
|
||||
@@ -429,28 +440,18 @@ func prefixProperty(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return errors.New("Must provide <filename> <prefixed_path>")
|
||||
}
|
||||
prefixPath := args[1]
|
||||
|
||||
var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
|
||||
if errorParsingDocIndex != nil {
|
||||
return errorParsingDocIndex
|
||||
}
|
||||
|
||||
var paths = parsePath(args[1])
|
||||
|
||||
// Inverse order
|
||||
for i := len(paths)/2 - 1; i >= 0; i-- {
|
||||
opp := len(paths) - 1 - i
|
||||
paths[i], paths[opp] = paths[opp], paths[i]
|
||||
}
|
||||
|
||||
var updateData = func(dataBucket interface{}, currentIndex int) (interface{}, error) {
|
||||
|
||||
if updateAll || currentIndex == docIndexInt {
|
||||
log.Debugf("Prefixing %v to doc %v", paths, currentIndex)
|
||||
var mapDataBucket = dataBucket
|
||||
for _, key := range paths {
|
||||
singlePath := []string{key}
|
||||
mapDataBucket = updatedChildValue(nil, singlePath, mapDataBucket)
|
||||
}
|
||||
log.Debugf("Prefixing %v to doc %v", prefixPath, currentIndex)
|
||||
var mapDataBucket = lib.PrefixPath(dataBucket, prefixPath)
|
||||
return mapDataBucket, nil
|
||||
}
|
||||
return dataBucket, nil
|
||||
@@ -496,7 +497,6 @@ func deleteProperty(cmd *cobra.Command, args []string) error {
|
||||
return errors.New("Must provide <filename> <path_to_delete>")
|
||||
}
|
||||
var deletePath = args[1]
|
||||
var paths = parsePath(deletePath)
|
||||
var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
|
||||
if errorParsingDocIndex != nil {
|
||||
return errorParsingDocIndex
|
||||
@@ -505,7 +505,7 @@ func deleteProperty(cmd *cobra.Command, args []string) error {
|
||||
var updateData = func(dataBucket interface{}, currentIndex int) (interface{}, error) {
|
||||
if updateAll || currentIndex == docIndexInt {
|
||||
log.Debugf("Deleting path in doc %v", currentIndex)
|
||||
return deleteChildValue(dataBucket, paths)
|
||||
return lib.DeletePath(dataBucket, deletePath)
|
||||
}
|
||||
return dataBucket, nil
|
||||
}
|
||||
@@ -532,7 +532,7 @@ func mergeProperties(cmd *cobra.Command, args []string) error {
|
||||
// map
|
||||
var mapDataBucket = make(map[interface{}]interface{})
|
||||
mapDataBucket["root"] = dataBucket
|
||||
if err := merge(&mergedData, mapDataBucket, overwriteFlag, appendFlag); err != nil {
|
||||
if err := lib.Merge(&mergedData, mapDataBucket, overwriteFlag, appendFlag); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, f := range filesToMerge {
|
||||
@@ -544,7 +544,7 @@ func mergeProperties(cmd *cobra.Command, args []string) error {
|
||||
return nil, err
|
||||
}
|
||||
mapDataBucket["root"] = fileToMerge
|
||||
if err := merge(&mergedData, mapDataBucket, overwriteFlag, appendFlag); err != nil {
|
||||
if err := lib.Merge(&mergedData, mapDataBucket, overwriteFlag, appendFlag); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -567,72 +567,33 @@ func readWriteCommands(args []string, expectedArgs int, badArgsMessage string) (
|
||||
return nil, errors.New(badArgsMessage)
|
||||
} else {
|
||||
writeCommands = make(yaml.MapSlice, 1)
|
||||
writeCommands[0] = yaml.MapItem{Key: args[expectedArgs-2], Value: parseValue(args[expectedArgs-1])}
|
||||
writeCommands[0] = yaml.MapItem{Key: args[expectedArgs-2], Value: valueParser.ParseValue(args[expectedArgs-1])}
|
||||
}
|
||||
return writeCommands, nil
|
||||
}
|
||||
|
||||
func parseValue(argument string) interface{} {
|
||||
var value, err interface{}
|
||||
var inQuotes = len(argument) > 0 && argument[0] == '"'
|
||||
if !inQuotes {
|
||||
value, err = strconv.ParseFloat(argument, 64)
|
||||
if err == nil {
|
||||
return value
|
||||
}
|
||||
value, err = strconv.ParseBool(argument)
|
||||
if err == nil {
|
||||
return value
|
||||
}
|
||||
if argument == "[]" {
|
||||
return make([]interface{}, 0)
|
||||
}
|
||||
return argument
|
||||
}
|
||||
return argument[1 : len(argument)-1]
|
||||
}
|
||||
|
||||
func toString(context interface{}) (string, error) {
|
||||
if outputToJSON {
|
||||
return jsonToString(context)
|
||||
return jsonConverter.JsonToString(context)
|
||||
}
|
||||
return yamlToString(context)
|
||||
}
|
||||
|
||||
func yamlToString(context interface{}) (string, error) {
|
||||
switch context := context.(type) {
|
||||
case string:
|
||||
return context, nil
|
||||
default:
|
||||
return marshalContext(context)
|
||||
}
|
||||
}
|
||||
|
||||
func marshalContext(context interface{}) (string, error) {
|
||||
out, err := yaml.Marshal(context)
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error printing yaml")
|
||||
}
|
||||
|
||||
outStr := string(out)
|
||||
// trim the trailing new line as it's easier for a script to add
|
||||
// it in if required than to remove it
|
||||
if trimOutput {
|
||||
return strings.Trim(outStr, "\n "), nil
|
||||
}
|
||||
return outStr, nil
|
||||
return yamlConverter.YamlToString(context, trimOutput)
|
||||
}
|
||||
|
||||
func safelyRenameFile(from string, to string) {
|
||||
if renameError := os.Rename(from, to); renameError != nil {
|
||||
log.Warningf("Error renaming from %v to %v, attemting to copy contents", from, to)
|
||||
log.Warning(renameError.Error())
|
||||
log.Debugf("Error renaming from %v to %v, attemting to copy contents", from, to)
|
||||
log.Debug(renameError.Error())
|
||||
// can't do this rename when running in docker to a file targeted in a mounted volume,
|
||||
// so gracefully degrade to copying the entire contents.
|
||||
if copyError := copyFileContents(from, to); copyError != nil {
|
||||
log.Errorf("Failed copying from %v to %v", from, to)
|
||||
log.Errorf(copyError.Error())
|
||||
log.Error(copyError.Error())
|
||||
} else {
|
||||
removeErr := os.Remove(from)
|
||||
if removeErr != nil {
|
||||
log.Errorf("failed removing original file: %s", from)
|
||||
log.Error(removeErr.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
69
yq_test.go
69
yq_test.go
@@ -1,41 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/mikefarah/yq/v2/pkg/marshal"
|
||||
"github.com/mikefarah/yq/v2/test"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var parseValueTests = []struct {
|
||||
argument string
|
||||
expectedResult interface{}
|
||||
testDescription string
|
||||
}{
|
||||
{"true", true, "boolean"},
|
||||
{"\"true\"", "true", "boolean as string"},
|
||||
{"3.4", 3.4, "number"},
|
||||
{"\"3.4\"", "3.4", "number as string"},
|
||||
{"", "", "empty string"},
|
||||
}
|
||||
|
||||
func TestParseValue(t *testing.T) {
|
||||
for _, tt := range parseValueTests {
|
||||
assertResultWithContext(t, tt.expectedResult, parseValue(tt.argument), tt.testDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultilineString(t *testing.T) {
|
||||
testString := `
|
||||
abcd
|
||||
efg`
|
||||
formattedResult, _ := yamlToString(testString)
|
||||
assertResult(t, testString, formattedResult)
|
||||
formattedResult, _ := marshal.NewYamlConverter().YamlToString(testString, false)
|
||||
test.AssertResult(t, testString, formattedResult)
|
||||
}
|
||||
|
||||
func TestNewYaml(t *testing.T) {
|
||||
result, _ := newYaml([]string{"b.c", "3"})
|
||||
formattedResult := fmt.Sprintf("%v", result)
|
||||
assertResult(t,
|
||||
test.AssertResult(t,
|
||||
"[{b [{c 3}]}]",
|
||||
formattedResult)
|
||||
}
|
||||
@@ -43,11 +30,19 @@ func TestNewYaml(t *testing.T) {
|
||||
func TestNewYamlArray(t *testing.T) {
|
||||
result, _ := newYaml([]string{"[0].cat", "meow"})
|
||||
formattedResult := fmt.Sprintf("%v", result)
|
||||
assertResult(t,
|
||||
test.AssertResult(t,
|
||||
"[[{cat meow}]]",
|
||||
formattedResult)
|
||||
}
|
||||
|
||||
func TestNewYamlBigInt(t *testing.T) {
|
||||
result, _ := newYaml([]string{"b", "1212121"})
|
||||
formattedResult := fmt.Sprintf("%v", result)
|
||||
test.AssertResult(t,
|
||||
"[{b 1212121}]",
|
||||
formattedResult)
|
||||
}
|
||||
|
||||
func TestNewYaml_WithScript(t *testing.T) {
|
||||
writeScript = "examples/instruction_sample.yaml"
|
||||
expectedResult := `b:
|
||||
@@ -55,8 +50,8 @@ func TestNewYaml_WithScript(t *testing.T) {
|
||||
e:
|
||||
- name: Mike Farah`
|
||||
result, _ := newYaml([]string{""})
|
||||
actualResult, _ := yamlToString(result)
|
||||
assertResult(t, expectedResult, actualResult)
|
||||
actualResult, _ := marshal.NewYamlConverter().YamlToString(result, true)
|
||||
test.AssertResult(t, expectedResult, actualResult)
|
||||
}
|
||||
|
||||
func TestNewYaml_WithUnknownScript(t *testing.T) {
|
||||
@@ -71,5 +66,29 @@ func TestNewYaml_WithUnknownScript(t *testing.T) {
|
||||
} else {
|
||||
expectedOutput = `open fake-unknown: no such file or directory`
|
||||
}
|
||||
assertResult(t, expectedOutput, err.Error())
|
||||
test.AssertResult(t, expectedOutput, err.Error())
|
||||
}
|
||||
|
||||
func TestReadWithKeyOnly(t *testing.T) {
|
||||
readCmd := createReadCmd()
|
||||
expectedResult := `b
|
||||
d
|
||||
f
|
||||
`
|
||||
actualResult, err := executeTestCommand(readCmd, "test/fixture/keyonly.yaml", "a", "-k")
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
test.AssertResult(t, expectedResult, actualResult)
|
||||
}
|
||||
|
||||
func executeTestCommand(command *cobra.Command, args ...string) (output string, err error) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
command.SetOutput(buf)
|
||||
command.SetArgs(args)
|
||||
|
||||
_, err = command.ExecuteC()
|
||||
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user