mirror of
https://github.com/taigrr/yq
synced 2025-01-18 04:53:17 -08:00
Compare commits
58 Commits
v2.4.1
...
3.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9361b8b3e9 | ||
|
|
24dcb56466 | ||
|
|
728cbe991a | ||
|
|
854f5f0fc9 | ||
|
|
feba7b04fa | ||
|
|
0621307391 | ||
|
|
924eb6c462 | ||
|
|
52eef67e37 | ||
|
|
38d35185bc | ||
|
|
d8c29b26c1 | ||
|
|
e3f4eedd51 | ||
|
|
690da9ee74 | ||
|
|
1f7f1b0def | ||
|
|
1aa5ec1d40 | ||
|
|
a065a47b37 | ||
|
|
625cfdac75 | ||
|
|
4dbdd4a805 | ||
|
|
8a6af1720d | ||
|
|
0652f67a91 | ||
|
|
df52383ffb | ||
|
|
707ad09ba5 | ||
|
|
cf389bed4a | ||
|
|
ff5b23251b | ||
|
|
9925b26b9d | ||
|
|
93dbe80a77 | ||
|
|
1e541cd65f | ||
|
|
5204a13685 | ||
|
|
3d3eaf3034 | ||
|
|
4fb44dbc47 | ||
|
|
784513dd18 | ||
|
|
865a55645c | ||
|
|
949bf1c1d7 | ||
|
|
19fe718cfb | ||
|
|
290579ac7f | ||
|
|
d7392f7b58 | ||
|
|
a3cebec2fd | ||
|
|
b81fd638d7 | ||
|
|
2344638da4 | ||
|
|
8be006fba4 | ||
|
|
53a4a47ce3 | ||
|
|
5988d0cffa | ||
|
|
b7640946ac | ||
|
|
d061b2f9f9 | ||
|
|
8c0046a622 | ||
|
|
586ffb833b | ||
|
|
9771e7001c | ||
|
|
8da9a81702 | ||
|
|
d97f1d8be2 | ||
|
|
dad61ec615 | ||
|
|
676fc63219 | ||
|
|
972e2b9575 | ||
|
|
aad15ccc6e | ||
|
|
5fc13bdccd | ||
|
|
95fec2984e | ||
|
|
64d1e58f97 | ||
|
|
4b3fbb878f | ||
|
|
26a09e6ec0 | ||
|
|
ceafed30f9 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,6 +23,7 @@ _cgo_export.*
|
|||||||
|
|
||||||
_testmain.go
|
_testmain.go
|
||||||
coverage.out
|
coverage.out
|
||||||
|
coverage.html
|
||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
*.prof
|
*.prof
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ sudo apt install yq -y
|
|||||||
```
|
```
|
||||||
### or, [Download latest binary](https://github.com/mikefarah/yq/releases/latest) or alternatively:
|
### or, [Download latest binary](https://github.com/mikefarah/yq/releases/latest) or alternatively:
|
||||||
```
|
```
|
||||||
GO111MODULE=on go get github.com/mikefarah/yq@2.4.1
|
GO111MODULE=on go get github.com/mikefarah/yq/v2
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run with Docker
|
## Run with Docker
|
||||||
|
|||||||
56
Upgrade Notes
Normal file
56
Upgrade Notes
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
|
||||||
|
# New Features
|
||||||
|
- Keeps yaml comments and formatting (string blocks are saved, number formatting is preserved, so it won't drop off trailing 0s for values like 0.10, which is important when that's a version entry )
|
||||||
|
|
||||||
|
- Handles anchors! (doc link)
|
||||||
|
- Can specify yaml tags (e.g. !!int), quoting values no longer sufficient, need to specify the tag value instead.
|
||||||
|
- Can print out matching paths and values when splatting (doc link)
|
||||||
|
- JSON output works for all commands! Yaml files with multiple documents are printed out as one JSON document per line.
|
||||||
|
- Deep splat (**) to match arbitrary paths, (doc link)
|
||||||
|
|
||||||
|
|
||||||
|
# Breaking changes
|
||||||
|
|
||||||
|
## Update scripts file format has changed to be more powerful.
|
||||||
|
Comments can be added, and delete commands have been introduced.
|
||||||
|
|
||||||
|
## Reading and splatting, matching results are printed once per line.
|
||||||
|
e.g:
|
||||||
|
|
||||||
|
```json
|
||||||
|
parent:
|
||||||
|
childA:
|
||||||
|
no: matches here
|
||||||
|
childB:
|
||||||
|
there: matches
|
||||||
|
hi: no match
|
||||||
|
there2: also matches
|
||||||
|
```
|
||||||
|
|
||||||
|
yq r sample.yaml 'parent.*.there*'
|
||||||
|
|
||||||
|
old
|
||||||
|
```yaml
|
||||||
|
- null
|
||||||
|
- - matches
|
||||||
|
- also matches
|
||||||
|
```
|
||||||
|
|
||||||
|
new
|
||||||
|
```yaml
|
||||||
|
matches
|
||||||
|
also matches
|
||||||
|
```
|
||||||
|
|
||||||
|
and you can print the matching paths:
|
||||||
|
|
||||||
|
yq r --printMode pv sample.yaml 'parent.*.there*'
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
parent.childB.there: matches
|
||||||
|
parent.childB.there2: also matches
|
||||||
|
```
|
||||||
|
|
||||||
|
# Merge command
|
||||||
|
- New flag 'autocreates' missing entries in target by default, new flag to turn that off.
|
||||||
|
|
||||||
1000
commands_test.go
1000
commands_test.go
File diff suppressed because it is too large
Load Diff
13
compare.sh
Executable file
13
compare.sh
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
GREEN='\033[0;32m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
echo "${GREEN}---Old---${NC}"
|
||||||
|
yq $@ > /tmp/yq-old-output
|
||||||
|
cat /tmp/yq-old-output
|
||||||
|
|
||||||
|
echo "${GREEN}---New---${NC}"
|
||||||
|
./yq $@ > /tmp/yq-new-output
|
||||||
|
cat /tmp/yq-new-output
|
||||||
|
|
||||||
|
echo "${GREEN}---Diff---${NC}"
|
||||||
|
colordiff /tmp/yq-old-output /tmp/yq-new-output
|
||||||
@@ -1,349 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
yaml "github.com/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))
|
|
||||||
}
|
|
||||||
@@ -7,3 +7,5 @@
|
|||||||
- lala
|
- lala
|
||||||
- land
|
- land
|
||||||
serial: 1
|
serial: 1
|
||||||
|
- become: false
|
||||||
|
gather_facts: true
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
a: simple
|
a: simple # just the best
|
||||||
b: [1, 2]
|
b: [1, 2]
|
||||||
|
c:
|
||||||
|
test: 1
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
a: other
|
a: other # better than the original
|
||||||
b: [3, 4]
|
b: [3, 4]
|
||||||
c:
|
c:
|
||||||
|
toast: leave
|
||||||
test: 1
|
test: 1
|
||||||
|
tell: 1
|
||||||
|
taco: cool
|
||||||
|
|||||||
@@ -1,2 +1,10 @@
|
|||||||
b.c: cat
|
- command: update
|
||||||
b.e[+].name: Mike Farah
|
path: b.c
|
||||||
|
value:
|
||||||
|
#great
|
||||||
|
things: frog # wow!
|
||||||
|
- command: update
|
||||||
|
path: b.e[+].name
|
||||||
|
value: Mike Farah
|
||||||
|
- command: delete
|
||||||
|
path: b.d
|
||||||
19
examples/merge-anchor.yaml
Normal file
19
examples/merge-anchor.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
foo: &foo
|
||||||
|
a: original
|
||||||
|
thing: coolasdf
|
||||||
|
thirsty: yep
|
||||||
|
|
||||||
|
bar: &bar
|
||||||
|
b: 2
|
||||||
|
thing: coconut
|
||||||
|
c: oldbar
|
||||||
|
|
||||||
|
foobarList:
|
||||||
|
<<: [*foo,*bar]
|
||||||
|
c: newbar
|
||||||
|
|
||||||
|
foobar:
|
||||||
|
<<: *foo
|
||||||
|
thirty: well beyond
|
||||||
|
thing: ice
|
||||||
|
c: 3
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
a: Easy! as one two three
|
a: true
|
||||||
b:
|
b:
|
||||||
c: 2
|
c: 2
|
||||||
d: [3, 4]
|
d: [3, 4, 5]
|
||||||
e:
|
e:
|
||||||
- name: fred
|
- name: fred
|
||||||
value: 3
|
value: 3
|
||||||
|
|||||||
@@ -1,9 +1,2 @@
|
|||||||
a: Easy! as one two three
|
|
||||||
b:
|
b:
|
||||||
c: things
|
c: things
|
||||||
d: whatever
|
|
||||||
things:
|
|
||||||
thing1:
|
|
||||||
cat: 'fred'
|
|
||||||
thing2:
|
|
||||||
cat: 'sam'
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
[4,5]
|
- 4
|
||||||
|
- 5
|
||||||
4
examples/simple-anchor.yaml
Normal file
4
examples/simple-anchor.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
foo: &foo
|
||||||
|
a: 1
|
||||||
|
|
||||||
|
foobar: *foo
|
||||||
9
go.mod
9
go.mod
@@ -1,12 +1,13 @@
|
|||||||
module github.com/mikefarah/yq/v2
|
module github.com/mikefarah/yq/v3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/mikefarah/yaml/v2 v2.4.0
|
github.com/mikefarah/yaml/v2 v2.4.0 // indirect
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.8.1
|
||||||
github.com/spf13/cobra v0.0.5
|
github.com/spf13/cobra v0.0.5
|
||||||
golang.org/x/tools v0.0.0-20191030203535-5e247c9ad0a0 // indirect
|
golang.org/x/tools v0.0.0-20191213221258-04c2e8eff935 // indirect
|
||||||
gopkg.in/imdario/mergo.v0 v0.3.7
|
gopkg.in/imdario/mergo.v0 v0.3.7 // indirect
|
||||||
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
|
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|||||||
12
go.sum
12
go.sum
@@ -10,8 +10,11 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
|
|||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mikefarah/yaml v2.1.0+incompatible h1:nu2cqmzk4WlWJNgnevY88faMcdrDzYGcsUjYFxEpB7Y=
|
||||||
github.com/mikefarah/yaml/v2 v2.4.0 h1:eYqfooY0BnvKTJxr7+ABJs13n3dg9n347GScDaU2Lww=
|
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/mikefarah/yaml/v2 v2.4.0/go.mod h1:ahVqZF4n1W4NqwvVnZzC4es67xsW9uR/RRf2RRxieJU=
|
||||||
|
github.com/mikefarah/yq v2.4.0+incompatible h1:oBxbWy8R9hI3BIUUxEf0CzikWa2AgnGrGhvGQt5jgjk=
|
||||||
|
github.com/mikefarah/yq/v2 v2.4.1 h1:tajDonaFK6WqitSZExB6fKlWQy/yCkptqxh2AXEe3N4=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
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/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
@@ -32,14 +35,21 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
|
|||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
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-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
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/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-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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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 h1:s5lp4ug7qHzUccgyFdjsX7OZDzHXRaePrF3B3vmUiuM=
|
||||||
golang.org/x/tools v0.0.0-20191030203535-5e247c9ad0a0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191030203535-5e247c9ad0a0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191213221258-04c2e8eff935 h1:kJQZhwFzSwJS2BxboKjdZzWczQOZx8VuH7Y8hhuGUtM=
|
||||||
|
golang.org/x/tools v0.0.0-20191213221258-04c2e8eff935/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:QDotlIZtaO/p+Um0ok18HRTpq5i5/SAk/qprsor+9c8=
|
||||||
@@ -48,3 +58,5 @@ gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW
|
|||||||
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog=
|
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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2 h1:XZx7nhd5GMaZpmDaEHFVafUZC7ya0fuo7cSJ3UCKYmM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
yaml "github.com/mikefarah/yaml/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func jsonToString(context interface{}) (string, error) {
|
|
||||||
out, err := json.Marshal(toJSON(context))
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error printing yaml as json: %v", err)
|
|
||||||
}
|
|
||||||
return string(out), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func 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)
|
|
||||||
}
|
|
||||||
return newArray
|
|
||||||
case yaml.MapSlice:
|
|
||||||
oldMap := context
|
|
||||||
newMap := make(map[string]interface{})
|
|
||||||
for _, entry := range oldMap {
|
|
||||||
if str, ok := entry.Key.(string); ok {
|
|
||||||
newMap[str] = toJSON(entry.Value)
|
|
||||||
} else if i, ok := entry.Key.(int); ok {
|
|
||||||
newMap[strconv.Itoa(i)] = toJSON(entry.Value)
|
|
||||||
} else if b, ok := entry.Key.(bool); ok {
|
|
||||||
newMap[strconv.FormatBool(b)] = toJSON(entry.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newMap
|
|
||||||
default:
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
245
pkg/yqlib/data_navigator.go
Normal file
245
pkg/yqlib/data_navigator.go
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
errors "github.com/pkg/errors"
|
||||||
|
yaml "gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataNavigator interface {
|
||||||
|
Traverse(value *yaml.Node, path []string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type navigator struct {
|
||||||
|
navigationStrategy NavigationStrategy
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDataNavigator(NavigationStrategy NavigationStrategy) DataNavigator {
|
||||||
|
return &navigator{
|
||||||
|
navigationStrategy: NavigationStrategy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *navigator) Traverse(value *yaml.Node, path []string) error {
|
||||||
|
realValue := value
|
||||||
|
emptyArray := make([]interface{}, 0)
|
||||||
|
if realValue.Kind == yaml.DocumentNode {
|
||||||
|
log.Debugf("its a document! returning the first child")
|
||||||
|
return n.doTraverse(value.Content[0], "", path, emptyArray)
|
||||||
|
}
|
||||||
|
return n.doTraverse(value, "", path, emptyArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *navigator) doTraverse(value *yaml.Node, head string, tail []string, pathStack []interface{}) error {
|
||||||
|
log.Debug("head %v", head)
|
||||||
|
DebugNode(value)
|
||||||
|
var errorDeepSplatting error
|
||||||
|
if head == "**" && value.Kind != yaml.ScalarNode {
|
||||||
|
errorDeepSplatting = n.recurse(value, head, tail, pathStack)
|
||||||
|
// ignore errors here, we are deep splatting so we may accidently give a string key
|
||||||
|
// to an array sequence
|
||||||
|
if len(tail) > 0 {
|
||||||
|
_ = n.recurse(value, tail[0], tail[1:], pathStack)
|
||||||
|
}
|
||||||
|
return errorDeepSplatting
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tail) > 0 {
|
||||||
|
log.Debugf("diving into %v", tail[0])
|
||||||
|
DebugNode(value)
|
||||||
|
return n.recurse(value, tail[0], tail[1:], pathStack)
|
||||||
|
}
|
||||||
|
return n.navigationStrategy.Visit(NewNodeContext(value, head, tail, pathStack))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *navigator) getOrReplace(original *yaml.Node, expectedKind yaml.Kind) *yaml.Node {
|
||||||
|
if original.Kind != expectedKind {
|
||||||
|
log.Debug("wanted %v but it was %v, overriding", expectedKind, original.Kind)
|
||||||
|
return &yaml.Node{Kind: expectedKind}
|
||||||
|
}
|
||||||
|
return original
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *navigator) recurse(value *yaml.Node, head string, tail []string, pathStack []interface{}) error {
|
||||||
|
log.Debug("recursing, processing %v", head)
|
||||||
|
switch value.Kind {
|
||||||
|
case yaml.MappingNode:
|
||||||
|
log.Debug("its a map with %v entries", len(value.Content)/2)
|
||||||
|
return n.recurseMap(value, head, tail, pathStack)
|
||||||
|
case yaml.SequenceNode:
|
||||||
|
log.Debug("its a sequence of %v things!", len(value.Content))
|
||||||
|
if head == "*" || head == "**" {
|
||||||
|
return n.splatArray(value, head, tail, pathStack)
|
||||||
|
} else if head == "+" {
|
||||||
|
return n.appendArray(value, head, tail, pathStack)
|
||||||
|
}
|
||||||
|
return n.recurseArray(value, head, tail, pathStack)
|
||||||
|
case yaml.AliasNode:
|
||||||
|
log.Debug("its an alias!")
|
||||||
|
DebugNode(value.Alias)
|
||||||
|
if n.navigationStrategy.FollowAlias(NewNodeContext(value, head, tail, pathStack)) {
|
||||||
|
log.Debug("following the alias")
|
||||||
|
return n.recurse(value.Alias, head, tail, pathStack)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *navigator) recurseMap(value *yaml.Node, head string, tail []string, pathStack []interface{}) error {
|
||||||
|
traversedEntry := false
|
||||||
|
errorVisiting := n.visitMatchingEntries(value, head, tail, pathStack, func(contents []*yaml.Node, indexInMap int) error {
|
||||||
|
log.Debug("recurseMap: visitMatchingEntries")
|
||||||
|
n.navigationStrategy.DebugVisitedNodes()
|
||||||
|
newPathStack := append(pathStack, contents[indexInMap].Value)
|
||||||
|
log.Debug("appended %v", contents[indexInMap].Value)
|
||||||
|
n.navigationStrategy.DebugVisitedNodes()
|
||||||
|
log.Debug("should I traverse? %v, %v", head, pathStackToString(newPathStack))
|
||||||
|
DebugNode(value)
|
||||||
|
if n.navigationStrategy.ShouldTraverse(NewNodeContext(contents[indexInMap+1], head, tail, newPathStack), contents[indexInMap].Value) {
|
||||||
|
log.Debug("recurseMap: Going to traverse")
|
||||||
|
traversedEntry = true
|
||||||
|
// contents[indexInMap+1] = n.getOrReplace(contents[indexInMap+1], guessKind(head, tail, contents[indexInMap+1].Kind))
|
||||||
|
errorTraversing := n.doTraverse(contents[indexInMap+1], head, tail, newPathStack)
|
||||||
|
log.Debug("recurseMap: Finished traversing")
|
||||||
|
n.navigationStrategy.DebugVisitedNodes()
|
||||||
|
return errorTraversing
|
||||||
|
} else {
|
||||||
|
log.Debug("nope not traversing")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if errorVisiting != nil {
|
||||||
|
return errorVisiting
|
||||||
|
}
|
||||||
|
|
||||||
|
if traversedEntry || head == "*" || head == "**" || !n.navigationStrategy.AutoCreateMap(NewNodeContext(value, head, tail, pathStack)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mapEntryKey := yaml.Node{Value: head, Kind: yaml.ScalarNode}
|
||||||
|
value.Content = append(value.Content, &mapEntryKey)
|
||||||
|
mapEntryValue := yaml.Node{Kind: guessKind(head, tail, 0)}
|
||||||
|
value.Content = append(value.Content, &mapEntryValue)
|
||||||
|
log.Debug("adding new node %v", head)
|
||||||
|
return n.doTraverse(&mapEntryValue, head, tail, append(pathStack, head))
|
||||||
|
}
|
||||||
|
|
||||||
|
// need to pass the node in, as it may be aliased
|
||||||
|
type mapVisitorFn func(contents []*yaml.Node, index int) error
|
||||||
|
|
||||||
|
func (n *navigator) visitDirectMatchingEntries(node *yaml.Node, head string, tail []string, pathStack []interface{}, visit mapVisitorFn) error {
|
||||||
|
var contents = node.Content
|
||||||
|
for index := 0; index < len(contents); index = index + 2 {
|
||||||
|
content := contents[index]
|
||||||
|
|
||||||
|
log.Debug("index %v, checking %v, %v", index, content.Value, content.Tag)
|
||||||
|
n.navigationStrategy.DebugVisitedNodes()
|
||||||
|
errorVisiting := visit(contents, index)
|
||||||
|
if errorVisiting != nil {
|
||||||
|
return errorVisiting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *navigator) visitMatchingEntries(node *yaml.Node, head string, tail []string, pathStack []interface{}, visit mapVisitorFn) error {
|
||||||
|
var contents = node.Content
|
||||||
|
log.Debug("visitMatchingEntries %v", head)
|
||||||
|
DebugNode(node)
|
||||||
|
// value.Content is a concatenated array of key, value,
|
||||||
|
// so keys are in the even indexes, values in odd.
|
||||||
|
// merge aliases are defined first, but we only want to traverse them
|
||||||
|
// if we don't find a match directly on this node first.
|
||||||
|
errorVisitedDirectEntries := n.visitDirectMatchingEntries(node, head, tail, pathStack, visit)
|
||||||
|
|
||||||
|
if errorVisitedDirectEntries != nil || !n.navigationStrategy.FollowAlias(NewNodeContext(node, head, tail, pathStack)) {
|
||||||
|
return errorVisitedDirectEntries
|
||||||
|
}
|
||||||
|
return n.visitAliases(contents, head, tail, pathStack, visit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *navigator) visitAliases(contents []*yaml.Node, head string, tail []string, pathStack []interface{}, visit mapVisitorFn) error {
|
||||||
|
// merge aliases are defined first, but we only want to traverse them
|
||||||
|
// if we don't find a match on this node first.
|
||||||
|
// traverse them backwards so that the last alias overrides the preceding.
|
||||||
|
// a node can either be
|
||||||
|
// an alias to one other node (e.g. <<: *blah)
|
||||||
|
// or a sequence of aliases (e.g. <<: [*blah, *foo])
|
||||||
|
log.Debug("checking for aliases")
|
||||||
|
for index := len(contents) - 2; index >= 0; index = index - 2 {
|
||||||
|
|
||||||
|
if contents[index+1].Kind == yaml.AliasNode {
|
||||||
|
valueNode := contents[index+1]
|
||||||
|
log.Debug("found an alias")
|
||||||
|
DebugNode(contents[index])
|
||||||
|
DebugNode(valueNode)
|
||||||
|
|
||||||
|
errorInAlias := n.visitMatchingEntries(valueNode.Alias, head, tail, pathStack, visit)
|
||||||
|
if errorInAlias != nil {
|
||||||
|
return errorInAlias
|
||||||
|
}
|
||||||
|
} else if contents[index+1].Kind == yaml.SequenceNode {
|
||||||
|
// could be an array of aliases...
|
||||||
|
errorVisitingAliasSeq := n.visitAliasSequence(contents[index+1].Content, head, tail, pathStack, visit)
|
||||||
|
if errorVisitingAliasSeq != nil {
|
||||||
|
return errorVisitingAliasSeq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *navigator) visitAliasSequence(possibleAliasArray []*yaml.Node, head string, tail []string, pathStack []interface{}, visit mapVisitorFn) error {
|
||||||
|
// need to search this backwards too, so that aliases defined last override the preceding.
|
||||||
|
for aliasIndex := len(possibleAliasArray) - 1; aliasIndex >= 0; aliasIndex = aliasIndex - 1 {
|
||||||
|
child := possibleAliasArray[aliasIndex]
|
||||||
|
if child.Kind == yaml.AliasNode {
|
||||||
|
log.Debug("found an alias")
|
||||||
|
DebugNode(child)
|
||||||
|
errorInAlias := n.visitMatchingEntries(child.Alias, head, tail, pathStack, visit)
|
||||||
|
if errorInAlias != nil {
|
||||||
|
return errorInAlias
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *navigator) splatArray(value *yaml.Node, head string, tail []string, pathStack []interface{}) error {
|
||||||
|
for index, childValue := range value.Content {
|
||||||
|
log.Debug("processing")
|
||||||
|
DebugNode(childValue)
|
||||||
|
childValue = n.getOrReplace(childValue, guessKind(head, tail, childValue.Kind))
|
||||||
|
var err = n.doTraverse(childValue, head, tail, append(pathStack, index))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *navigator) appendArray(value *yaml.Node, head string, tail []string, pathStack []interface{}) error {
|
||||||
|
var newNode = yaml.Node{Kind: guessKind(head, tail, 0)}
|
||||||
|
value.Content = append(value.Content, &newNode)
|
||||||
|
log.Debug("appending a new node, %v", value.Content)
|
||||||
|
return n.doTraverse(&newNode, head, tail, append(pathStack, len(value.Content)-1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *navigator) recurseArray(value *yaml.Node, head string, tail []string, pathStack []interface{}) error {
|
||||||
|
var index, err = strconv.ParseInt(head, 10, 64) // nolint
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Error parsing array index '%v' for '%v'", head, pathStackToString(pathStack))
|
||||||
|
}
|
||||||
|
|
||||||
|
for int64(len(value.Content)) <= index {
|
||||||
|
value.Content = append(value.Content, &yaml.Node{Kind: guessKind(head, tail, 0)})
|
||||||
|
}
|
||||||
|
|
||||||
|
value.Content[index] = n.getOrReplace(value.Content[index], guessKind(head, tail, value.Content[index].Kind))
|
||||||
|
|
||||||
|
return n.doTraverse(value.Content[index], head, tail, append(pathStack, index))
|
||||||
|
}
|
||||||
1
pkg/yqlib/data_navigator_test.go
Normal file
1
pkg/yqlib/data_navigator_test.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package yqlib
|
||||||
66
pkg/yqlib/delete_navigation_strategy.go
Normal file
66
pkg/yqlib/delete_navigation_strategy.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
yaml "gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteNavigationStrategy(pathElementToDelete string) NavigationStrategy {
|
||||||
|
parser := NewPathParser()
|
||||||
|
return &NavigationStrategyImpl{
|
||||||
|
visitedNodes: []*NodeContext{},
|
||||||
|
followAlias: func(nodeContext NodeContext) bool {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
autoCreateMap: func(nodeContext NodeContext) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
visit: func(nodeContext NodeContext) error {
|
||||||
|
node := nodeContext.Node
|
||||||
|
log.Debug("need to find and delete %v in here", pathElementToDelete)
|
||||||
|
DebugNode(node)
|
||||||
|
if node.Kind == yaml.SequenceNode {
|
||||||
|
newContent, errorDeleting := deleteFromArray(node.Content, pathElementToDelete)
|
||||||
|
if errorDeleting != nil {
|
||||||
|
return errorDeleting
|
||||||
|
}
|
||||||
|
node.Content = newContent
|
||||||
|
} else if node.Kind == yaml.MappingNode {
|
||||||
|
node.Content = deleteFromMap(parser, node.Content, nodeContext.PathStack, pathElementToDelete)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func deleteFromMap(pathParser PathParser, contents []*yaml.Node, pathStack []interface{}, pathElementToDelete string) []*yaml.Node {
|
||||||
|
newContents := make([]*yaml.Node, 0)
|
||||||
|
for index := 0; index < len(contents); index = index + 2 {
|
||||||
|
keyNode := contents[index]
|
||||||
|
valueNode := contents[index+1]
|
||||||
|
if !pathParser.MatchesNextPathElement(NewNodeContext(keyNode, pathElementToDelete, []string{}, pathStack), keyNode.Value) {
|
||||||
|
log.Debug("adding node %v", keyNode.Value)
|
||||||
|
newContents = append(newContents, keyNode, valueNode)
|
||||||
|
} else {
|
||||||
|
log.Debug("skipping node %v", keyNode.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newContents
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteFromArray(content []*yaml.Node, pathElementToDelete string) ([]*yaml.Node, error) {
|
||||||
|
|
||||||
|
if pathElementToDelete == "*" {
|
||||||
|
return make([]*yaml.Node, 0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var index, err = strconv.ParseInt(pathElementToDelete, 10, 64) // nolint
|
||||||
|
if err != nil {
|
||||||
|
return content, err
|
||||||
|
}
|
||||||
|
if index >= int64(len(content)) {
|
||||||
|
log.Debug("index %v is greater than content length %v", index, len(content))
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
return append(content[:index], content[index+1:]...), nil
|
||||||
|
}
|
||||||
44
pkg/yqlib/encoder.go
Normal file
44
pkg/yqlib/encoder.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
yaml "gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Encoder interface {
|
||||||
|
Encode(node *yaml.Node) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type yamlEncoder struct {
|
||||||
|
encoder *yaml.Encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYamlEncoder(destination io.Writer) Encoder {
|
||||||
|
var encoder = yaml.NewEncoder(destination)
|
||||||
|
encoder.SetIndent(2)
|
||||||
|
return &yamlEncoder{encoder}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ye *yamlEncoder) Encode(node *yaml.Node) error {
|
||||||
|
return ye.encoder.Encode(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonEncoder struct {
|
||||||
|
encoder *json.Encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJsonEncoder(destination io.Writer) Encoder {
|
||||||
|
var encoder = json.NewEncoder(destination)
|
||||||
|
return &jsonEncoder{encoder}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (je *jsonEncoder) Encode(node *yaml.Node) error {
|
||||||
|
var dataBucket interface{}
|
||||||
|
errorDecoding := node.Decode(&dataBucket)
|
||||||
|
if errorDecoding != nil {
|
||||||
|
return errorDecoding
|
||||||
|
}
|
||||||
|
return je.encoder.Encode(dataBucket)
|
||||||
|
}
|
||||||
148
pkg/yqlib/lib.go
Normal file
148
pkg/yqlib/lib.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
logging "gopkg.in/op/go-logging.v1"
|
||||||
|
yaml "gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log = logging.MustGetLogger("yq")
|
||||||
|
|
||||||
|
type UpdateCommand struct {
|
||||||
|
Command string
|
||||||
|
Path string
|
||||||
|
Value *yaml.Node
|
||||||
|
Overwrite bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func DebugNode(value *yaml.Node) {
|
||||||
|
if value == nil {
|
||||||
|
log.Debug("-- node is nil --")
|
||||||
|
} else if log.IsEnabledFor(logging.DEBUG) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
encoder := yaml.NewEncoder(buf)
|
||||||
|
errorEncoding := encoder.Encode(value)
|
||||||
|
if errorEncoding != nil {
|
||||||
|
log.Error("Error debugging node, %v", errorEncoding.Error())
|
||||||
|
}
|
||||||
|
encoder.Close()
|
||||||
|
log.Debug("Tag: %v", value.Tag)
|
||||||
|
log.Debug("%v", buf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathStackToString(pathStack []interface{}) string {
|
||||||
|
return mergePathStackToString(pathStack, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergePathStackToString(pathStack []interface{}, appendArrays bool) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for index, path := range pathStack {
|
||||||
|
switch path.(type) {
|
||||||
|
case int:
|
||||||
|
if appendArrays {
|
||||||
|
sb.WriteString("[+]")
|
||||||
|
} else {
|
||||||
|
sb.WriteString(fmt.Sprintf("[%v]", path))
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
sb.WriteString(fmt.Sprintf("%v", path))
|
||||||
|
}
|
||||||
|
|
||||||
|
if index < len(pathStack)-1 {
|
||||||
|
sb.WriteString(".")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func guessKind(head string, tail []string, guess yaml.Kind) yaml.Kind {
|
||||||
|
log.Debug("tail %v", tail)
|
||||||
|
if len(tail) == 0 && guess == 0 {
|
||||||
|
log.Debug("end of path, must be a scalar")
|
||||||
|
return yaml.ScalarNode
|
||||||
|
} else if len(tail) == 0 {
|
||||||
|
return guess
|
||||||
|
}
|
||||||
|
|
||||||
|
var _, errorParsingInt = strconv.ParseInt(tail[0], 10, 64)
|
||||||
|
if tail[0] == "+" || errorParsingInt == nil {
|
||||||
|
return yaml.SequenceNode
|
||||||
|
}
|
||||||
|
if (tail[0] == "*" || tail[0] == "**" || head == "**") && (guess == yaml.SequenceNode || guess == yaml.MappingNode) {
|
||||||
|
return guess
|
||||||
|
}
|
||||||
|
if guess == yaml.AliasNode {
|
||||||
|
log.Debug("guess was an alias, okey doke.")
|
||||||
|
return guess
|
||||||
|
}
|
||||||
|
log.Debug("forcing a mapping node")
|
||||||
|
log.Debug("yaml.SequenceNode %v", guess == yaml.SequenceNode)
|
||||||
|
log.Debug("yaml.ScalarNode %v", guess == yaml.ScalarNode)
|
||||||
|
return yaml.MappingNode
|
||||||
|
}
|
||||||
|
|
||||||
|
type YqLib interface {
|
||||||
|
Get(rootNode *yaml.Node, path string) ([]*NodeContext, error)
|
||||||
|
Update(rootNode *yaml.Node, updateCommand UpdateCommand, autoCreate bool) error
|
||||||
|
New(path string) yaml.Node
|
||||||
|
|
||||||
|
PathStackToString(pathStack []interface{}) string
|
||||||
|
MergePathStackToString(pathStack []interface{}, appendArrays bool) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type lib struct {
|
||||||
|
parser PathParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYqLib() YqLib {
|
||||||
|
return &lib{
|
||||||
|
parser: NewPathParser(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lib) Get(rootNode *yaml.Node, path string) ([]*NodeContext, error) {
|
||||||
|
var paths = l.parser.ParsePath(path)
|
||||||
|
NavigationStrategy := ReadNavigationStrategy()
|
||||||
|
navigator := NewDataNavigator(NavigationStrategy)
|
||||||
|
error := navigator.Traverse(rootNode, paths)
|
||||||
|
return NavigationStrategy.GetVisitedNodes(), error
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lib) PathStackToString(pathStack []interface{}) string {
|
||||||
|
return pathStackToString(pathStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lib) MergePathStackToString(pathStack []interface{}, appendArrays bool) string {
|
||||||
|
return mergePathStackToString(pathStack, appendArrays)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lib) New(path string) yaml.Node {
|
||||||
|
var paths = l.parser.ParsePath(path)
|
||||||
|
newNode := yaml.Node{Kind: guessKind("", paths, 0)}
|
||||||
|
return newNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lib) Update(rootNode *yaml.Node, updateCommand UpdateCommand, autoCreate bool) error {
|
||||||
|
log.Debugf("%v to %v", updateCommand.Command, updateCommand.Path)
|
||||||
|
switch updateCommand.Command {
|
||||||
|
case "update":
|
||||||
|
var paths = l.parser.ParsePath(updateCommand.Path)
|
||||||
|
navigator := NewDataNavigator(UpdateNavigationStrategy(updateCommand, autoCreate))
|
||||||
|
return navigator.Traverse(rootNode, paths)
|
||||||
|
case "delete":
|
||||||
|
var paths = l.parser.ParsePath(updateCommand.Path)
|
||||||
|
lastBit, newTail := paths[len(paths)-1], paths[:len(paths)-1]
|
||||||
|
navigator := NewDataNavigator(DeleteNavigationStrategy(lastBit))
|
||||||
|
return navigator.Traverse(rootNode, newTail)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unknown command %v", updateCommand.Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
176
pkg/yqlib/lib_test.go
Normal file
176
pkg/yqlib/lib_test.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mikefarah/yq/v3/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLib(t *testing.T) {
|
||||||
|
|
||||||
|
subject := NewYqLib()
|
||||||
|
|
||||||
|
t.Run("PathStackToString_Empty", func(t *testing.T) {
|
||||||
|
emptyArray := make([]interface{}, 0)
|
||||||
|
got := subject.PathStackToString(emptyArray)
|
||||||
|
test.AssertResult(t, ``, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PathStackToString", func(t *testing.T) {
|
||||||
|
array := make([]interface{}, 3)
|
||||||
|
array[0] = "a"
|
||||||
|
array[1] = 0
|
||||||
|
array[2] = "b"
|
||||||
|
got := subject.PathStackToString(array)
|
||||||
|
test.AssertResult(t, `a.[0].b`, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MergePathStackToString", func(t *testing.T) {
|
||||||
|
array := make([]interface{}, 3)
|
||||||
|
array[0] = "a"
|
||||||
|
array[1] = 0
|
||||||
|
array[2] = "b"
|
||||||
|
got := subject.MergePathStackToString(array, true)
|
||||||
|
test.AssertResult(t, `a.[+].b`, 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_WithError", func(t *testing.T) {
|
||||||
|
// err := subject.Merge(nil, nil, false, false)
|
||||||
|
// if err == nil {
|
||||||
|
// t.Fatal("Expected error due to nil")
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
}
|
||||||
148
pkg/yqlib/navigation_strategy.go
Normal file
148
pkg/yqlib/navigation_strategy.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
yaml "gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NodeContext struct {
|
||||||
|
Node *yaml.Node
|
||||||
|
Head string
|
||||||
|
Tail []string
|
||||||
|
PathStack []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNodeContext(node *yaml.Node, head string, tail []string, pathStack []interface{}) NodeContext {
|
||||||
|
newTail := make([]string, len(tail))
|
||||||
|
copy(newTail, tail)
|
||||||
|
|
||||||
|
newPathStack := make([]interface{}, len(pathStack))
|
||||||
|
copy(newPathStack, pathStack)
|
||||||
|
return NodeContext{
|
||||||
|
Node: node,
|
||||||
|
Head: head,
|
||||||
|
Tail: newTail,
|
||||||
|
PathStack: newPathStack,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NavigationStrategy interface {
|
||||||
|
FollowAlias(nodeContext NodeContext) bool
|
||||||
|
AutoCreateMap(nodeContext NodeContext) bool
|
||||||
|
Visit(nodeContext NodeContext) error
|
||||||
|
// node key is the string value of the last element in the path stack
|
||||||
|
// we use it to match against the pathExpression in head.
|
||||||
|
ShouldTraverse(nodeContext NodeContext, nodeKey string) bool
|
||||||
|
GetVisitedNodes() []*NodeContext
|
||||||
|
DebugVisitedNodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
type NavigationStrategyImpl struct {
|
||||||
|
followAlias func(nodeContext NodeContext) bool
|
||||||
|
autoCreateMap func(nodeContext NodeContext) bool
|
||||||
|
visit func(nodeContext NodeContext) error
|
||||||
|
visitedNodes []*NodeContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *NavigationStrategyImpl) GetVisitedNodes() []*NodeContext {
|
||||||
|
return ns.visitedNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *NavigationStrategyImpl) FollowAlias(nodeContext NodeContext) bool {
|
||||||
|
return ns.followAlias(nodeContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *NavigationStrategyImpl) AutoCreateMap(nodeContext NodeContext) bool {
|
||||||
|
return ns.autoCreateMap(nodeContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *NavigationStrategyImpl) ShouldTraverse(nodeContext NodeContext, nodeKey string) bool {
|
||||||
|
// we should traverse aliases (if enabled), but not visit them :/
|
||||||
|
if len(nodeContext.PathStack) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ns.alreadyVisited(nodeContext.PathStack) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := NewPathParser()
|
||||||
|
|
||||||
|
return (nodeKey == "<<" && ns.FollowAlias(nodeContext)) || (nodeKey != "<<" &&
|
||||||
|
parser.MatchesNextPathElement(nodeContext, nodeKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *NavigationStrategyImpl) shouldVisit(nodeContext NodeContext) bool {
|
||||||
|
pathStack := nodeContext.PathStack
|
||||||
|
if len(pathStack) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
log.Debug("tail len %v", len(nodeContext.Tail))
|
||||||
|
// SOMETHING HERE!
|
||||||
|
|
||||||
|
if ns.alreadyVisited(pathStack) || len(nodeContext.Tail) != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeKey := fmt.Sprintf("%v", pathStack[len(pathStack)-1])
|
||||||
|
log.Debug("nodeKey: %v, nodeContext.Head: %v", nodeKey, nodeContext.Head)
|
||||||
|
parser := NewPathParser()
|
||||||
|
|
||||||
|
// only visit aliases if its an exact match
|
||||||
|
return (nodeKey == "<<" && nodeContext.Head == "<<") || (nodeKey != "<<" &&
|
||||||
|
parser.MatchesNextPathElement(nodeContext, nodeKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *NavigationStrategyImpl) Visit(nodeContext NodeContext) error {
|
||||||
|
log.Debug("Visit?, %v, %v", nodeContext.Head, pathStackToString(nodeContext.PathStack))
|
||||||
|
DebugNode(nodeContext.Node)
|
||||||
|
if ns.shouldVisit(nodeContext) {
|
||||||
|
log.Debug("yep, visiting")
|
||||||
|
// pathStack array must be
|
||||||
|
// copied, as append() may sometimes reuse and modify the array
|
||||||
|
ns.visitedNodes = append(ns.visitedNodes, &nodeContext)
|
||||||
|
ns.DebugVisitedNodes()
|
||||||
|
return ns.visit(nodeContext)
|
||||||
|
}
|
||||||
|
log.Debug("nope, skip it")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *NavigationStrategyImpl) DebugVisitedNodes() {
|
||||||
|
log.Debug("Visited Nodes:")
|
||||||
|
for _, candidate := range ns.visitedNodes {
|
||||||
|
log.Debug(" - %v", pathStackToString(candidate.PathStack))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *NavigationStrategyImpl) alreadyVisited(pathStack []interface{}) bool {
|
||||||
|
log.Debug("checking already visited pathStack: %v", pathStackToString(pathStack))
|
||||||
|
for _, candidate := range ns.visitedNodes {
|
||||||
|
candidatePathStack := candidate.PathStack
|
||||||
|
if patchStacksMatch(candidatePathStack, pathStack) {
|
||||||
|
log.Debug("paths match, already seen it")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
log.Debug("never seen it before!")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func patchStacksMatch(path1 []interface{}, path2 []interface{}) bool {
|
||||||
|
log.Debug("checking against path: %v", pathStackToString(path1))
|
||||||
|
|
||||||
|
if len(path1) != len(path2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for index, p1Value := range path1 {
|
||||||
|
|
||||||
|
p2Value := path2[index]
|
||||||
|
if p1Value != p2Value {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
}
|
||||||
101
pkg/yqlib/path_parser.go
Normal file
101
pkg/yqlib/path_parser.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PathParser interface {
|
||||||
|
ParsePath(path string) []string
|
||||||
|
MatchesNextPathElement(nodeContext NodeContext, nodeKey string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type pathParser struct{}
|
||||||
|
|
||||||
|
func NewPathParser() PathParser {
|
||||||
|
return &pathParser{}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* node: node that we may traverse/visit
|
||||||
|
* head: path element expression to match against
|
||||||
|
* tail: remaining path element expressions
|
||||||
|
* pathStack: stack of actual paths we've matched to get to node
|
||||||
|
* nodeKey: actual value of this nodes 'key' or index.
|
||||||
|
*/
|
||||||
|
func (p *pathParser) MatchesNextPathElement(nodeContext NodeContext, nodeKey string) bool {
|
||||||
|
head := nodeContext.Head
|
||||||
|
if head == "**" || head == "*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if head == "+" {
|
||||||
|
log.Debug("head is +, nodeKey is %v", nodeKey)
|
||||||
|
var _, err = strconv.ParseInt(nodeKey, 10, 64) // nolint
|
||||||
|
if err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var prefixMatch = strings.TrimSuffix(head, "*")
|
||||||
|
if prefixMatch != head {
|
||||||
|
log.Debug("prefix match, %v", strings.HasPrefix(nodeKey, prefixMatch))
|
||||||
|
return strings.HasPrefix(nodeKey, prefixMatch)
|
||||||
|
}
|
||||||
|
return nodeKey == head
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pathParser) ParsePath(path string) []string {
|
||||||
|
if path == "" {
|
||||||
|
return []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
|
||||||
|
}
|
||||||
78
pkg/yqlib/path_parser_test.go
Normal file
78
pkg/yqlib/path_parser_test.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mikefarah/yq/v3/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
var parser = NewPathParser()
|
||||||
|
|
||||||
|
var parsePathsTests = []struct {
|
||||||
|
path string
|
||||||
|
expectedPaths []string
|
||||||
|
}{
|
||||||
|
{"a.b", []string{"a", "b"}},
|
||||||
|
{"a.b.**", []string{"a", "b", "**"}},
|
||||||
|
{"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 TestPathParserParsePath(t *testing.T) {
|
||||||
|
for _, tt := range parsePathsTests {
|
||||||
|
test.AssertResultComplex(t, tt.expectedPaths, parser.ParsePath(tt.path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathParserMatchesNextPathElementSplat(t *testing.T) {
|
||||||
|
var node = NodeContext{Head: "*"}
|
||||||
|
test.AssertResult(t, true, parser.MatchesNextPathElement(node, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathParserMatchesNextPathElementDeepSplat(t *testing.T) {
|
||||||
|
var node = NodeContext{Head: "**"}
|
||||||
|
test.AssertResult(t, true, parser.MatchesNextPathElement(node, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathParserMatchesNextPathElementAppendArrayValid(t *testing.T) {
|
||||||
|
var node = NodeContext{Head: "+"}
|
||||||
|
test.AssertResult(t, true, parser.MatchesNextPathElement(node, "3"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathParserMatchesNextPathElementAppendArrayInvalid(t *testing.T) {
|
||||||
|
var node = NodeContext{Head: "+"}
|
||||||
|
test.AssertResult(t, false, parser.MatchesNextPathElement(node, "cat"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathParserMatchesNextPathElementPrefixMatchesWhole(t *testing.T) {
|
||||||
|
var node = NodeContext{Head: "cat*"}
|
||||||
|
test.AssertResult(t, true, parser.MatchesNextPathElement(node, "cat"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathParserMatchesNextPathElementPrefixMatchesStart(t *testing.T) {
|
||||||
|
var node = NodeContext{Head: "cat*"}
|
||||||
|
test.AssertResult(t, true, parser.MatchesNextPathElement(node, "caterpillar"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathParserMatchesNextPathElementPrefixMismatch(t *testing.T) {
|
||||||
|
var node = NodeContext{Head: "cat*"}
|
||||||
|
test.AssertResult(t, false, parser.MatchesNextPathElement(node, "dog"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathParserMatchesNextPathElementExactMatch(t *testing.T) {
|
||||||
|
var node = NodeContext{Head: "farahtek"}
|
||||||
|
test.AssertResult(t, true, parser.MatchesNextPathElement(node, "farahtek"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathParserMatchesNextPathElementExactMismatch(t *testing.T) {
|
||||||
|
var node = NodeContext{Head: "farahtek"}
|
||||||
|
test.AssertResult(t, false, parser.MatchesNextPathElement(node, "othertek"))
|
||||||
|
}
|
||||||
16
pkg/yqlib/read_navigation_strategy.go
Normal file
16
pkg/yqlib/read_navigation_strategy.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
func ReadNavigationStrategy() NavigationStrategy {
|
||||||
|
return &NavigationStrategyImpl{
|
||||||
|
visitedNodes: []*NodeContext{},
|
||||||
|
followAlias: func(nodeContext NodeContext) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
autoCreateMap: func(nodeContext NodeContext) bool {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
visit: func(nodeContext NodeContext) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
34
pkg/yqlib/update_navigation_strategy.go
Normal file
34
pkg/yqlib/update_navigation_strategy.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
func UpdateNavigationStrategy(updateCommand UpdateCommand, autoCreate bool) NavigationStrategy {
|
||||||
|
return &NavigationStrategyImpl{
|
||||||
|
visitedNodes: []*NodeContext{},
|
||||||
|
followAlias: func(nodeContext NodeContext) bool {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
autoCreateMap: func(nodeContext NodeContext) bool {
|
||||||
|
return autoCreate
|
||||||
|
},
|
||||||
|
visit: func(nodeContext NodeContext) error {
|
||||||
|
node := nodeContext.Node
|
||||||
|
changesToApply := updateCommand.Value
|
||||||
|
if updateCommand.Overwrite || node.Value == "" {
|
||||||
|
log.Debug("going to update")
|
||||||
|
DebugNode(node)
|
||||||
|
log.Debug("with")
|
||||||
|
DebugNode(changesToApply)
|
||||||
|
node.Value = changesToApply.Value
|
||||||
|
node.Tag = changesToApply.Tag
|
||||||
|
node.Kind = changesToApply.Kind
|
||||||
|
node.Style = changesToApply.Style
|
||||||
|
node.Content = changesToApply.Content
|
||||||
|
node.HeadComment = changesToApply.HeadComment
|
||||||
|
node.LineComment = changesToApply.LineComment
|
||||||
|
node.FootComment = changesToApply.FootComment
|
||||||
|
} else {
|
||||||
|
log.Debug("skipping update as node already has value %v and overwriteFlag is ", node.Value, updateCommand.Overwrite)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
47
pkg/yqlib/value_parser.go
Normal file
47
pkg/yqlib/value_parser.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
yaml "gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ValueParser interface {
|
||||||
|
Parse(argument string, customTag string) *yaml.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
type valueParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewValueParser() ValueParser {
|
||||||
|
return &valueParser{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *valueParser) Parse(argument string, customTag string) *yaml.Node {
|
||||||
|
var err interface{}
|
||||||
|
var tag = customTag
|
||||||
|
|
||||||
|
if tag == "" {
|
||||||
|
_, err = strconv.ParseBool(argument)
|
||||||
|
if err == nil {
|
||||||
|
tag = "!!bool"
|
||||||
|
}
|
||||||
|
_, err = strconv.ParseFloat(argument, 64)
|
||||||
|
if err == nil {
|
||||||
|
tag = "!!float"
|
||||||
|
}
|
||||||
|
_, err = strconv.ParseInt(argument, 10, 64)
|
||||||
|
if err == nil {
|
||||||
|
tag = "!!int"
|
||||||
|
}
|
||||||
|
|
||||||
|
if argument == "null" {
|
||||||
|
tag = "!!null"
|
||||||
|
}
|
||||||
|
if argument == "[]" {
|
||||||
|
return &yaml.Node{Tag: "!!seq", Kind: yaml.SequenceNode}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("parsed value '%v', tag: '%v'", argument, tag)
|
||||||
|
return &yaml.Node{Value: argument, Tag: tag, Kind: yaml.ScalarNode}
|
||||||
|
}
|
||||||
38
pkg/yqlib/value_parser_test.go
Normal file
38
pkg/yqlib/value_parser_test.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mikefarah/yq/v3/test"
|
||||||
|
yaml "gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var parseValueTests = []struct {
|
||||||
|
argument string
|
||||||
|
customTag string
|
||||||
|
expectedTag string
|
||||||
|
testDescription string
|
||||||
|
}{
|
||||||
|
{"true", "", "!!bool", "boolean"},
|
||||||
|
{"true", "!!str", "!!str", "boolean forced as string"},
|
||||||
|
{"3.4", "", "!!float", "float"},
|
||||||
|
{"1212121", "", "!!int", "big number"},
|
||||||
|
{"1212121.1", "", "!!float", "big float number"},
|
||||||
|
{"3", "", "!!int", "int"},
|
||||||
|
{"null", "", "!!null", "null"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValueParserParse(t *testing.T) {
|
||||||
|
for _, tt := range parseValueTests {
|
||||||
|
actual := NewValueParser().Parse(tt.argument, tt.customTag)
|
||||||
|
test.AssertResultWithContext(t, tt.argument, actual.Value, tt.testDescription)
|
||||||
|
test.AssertResultWithContext(t, tt.expectedTag, actual.Tag, tt.testDescription)
|
||||||
|
test.AssertResult(t, yaml.ScalarNode, actual.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValueParserParseEmptyArray(t *testing.T) {
|
||||||
|
actual := NewValueParser().Parse("[]", "")
|
||||||
|
test.AssertResult(t, "!!seq", actual.Tag)
|
||||||
|
test.AssertResult(t, yaml.SequenceNode, actual.Kind)
|
||||||
|
}
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
go test -coverprofile=coverage.out
|
go test -coverprofile=coverage.out -v $(go list ./... | grep -v -E 'examples' | grep -v -E 'test')
|
||||||
go tool cover -html=coverage.out -o cover/coverage.html
|
go tool cover -html=coverage.out -o coverage.html
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
go test -v $(go list ./... | grep -v -E 'examples')
|
go test -v $(go list ./... | grep -v -E 'examples' | grep -v -E 'test')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: yq
|
name: yq
|
||||||
version: '2.4.1'
|
version: '3.0.0-beta'
|
||||||
summary: A lightweight and portable command-line YAML processor
|
summary: A lightweight and portable command-line YAML processor
|
||||||
description: |
|
description: |
|
||||||
The aim of the project is to be the jq or sed of yaml files.
|
The aim of the project is to be the jq or sed of yaml files.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
yaml "github.com/mikefarah/yaml/v2"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
yaml "gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type resulter struct {
|
type resulter struct {
|
||||||
@@ -19,7 +19,7 @@ type resulter struct {
|
|||||||
Command *cobra.Command
|
Command *cobra.Command
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCmd(c *cobra.Command, input string) resulter {
|
func RunCmd(c *cobra.Command, input string) resulter {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
c.SetOutput(buf)
|
c.SetOutput(buf)
|
||||||
c.SetArgs(strings.Split(input, " "))
|
c.SetArgs(strings.Split(input, " "))
|
||||||
@@ -30,8 +30,8 @@ func runCmd(c *cobra.Command, input string) resulter {
|
|||||||
return resulter{err, output, c}
|
return resulter{err, output, c}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseData(rawData string) yaml.MapSlice {
|
func ParseData(rawData string) yaml.Node {
|
||||||
var parsedData yaml.MapSlice
|
var parsedData yaml.Node
|
||||||
err := yaml.Unmarshal([]byte(rawData), &parsedData)
|
err := yaml.Unmarshal([]byte(rawData), &parsedData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error parsing yaml: %v\n", err)
|
fmt.Printf("Error parsing yaml: %v\n", err)
|
||||||
@@ -40,21 +40,21 @@ func parseData(rawData string) yaml.MapSlice {
|
|||||||
return parsedData
|
return parsedData
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertResult(t *testing.T, expectedValue interface{}, actualValue interface{}) {
|
func AssertResult(t *testing.T, expectedValue interface{}, actualValue interface{}) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if expectedValue != actualValue {
|
if expectedValue != actualValue {
|
||||||
t.Error("Expected <", expectedValue, "> but got <", actualValue, ">", fmt.Sprintf("%T", 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()
|
t.Helper()
|
||||||
if !reflect.DeepEqual(expectedValue, actualValue) {
|
if !reflect.DeepEqual(expectedValue, actualValue) {
|
||||||
t.Error("Expected <", expectedValue, "> but got <", actualValue, ">", fmt.Sprintf("%T", 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()
|
t.Helper()
|
||||||
if expectedValue != actualValue {
|
if expectedValue != actualValue {
|
||||||
t.Error(context)
|
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")
|
tmpfile, _ := ioutil.TempFile("", "testyaml")
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = tmpfile.Close()
|
_ = tmpfile.Close()
|
||||||
@@ -72,11 +72,11 @@ func writeTempYamlFile(content string) string {
|
|||||||
return tmpfile.Name()
|
return tmpfile.Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
func readTempYamlFile(name string) string {
|
func ReadTempYamlFile(name string) string {
|
||||||
content, _ := ioutil.ReadFile(name)
|
content, _ := ioutil.ReadFile(name)
|
||||||
return string(content)
|
return string(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeTempYamlFile(name string) {
|
func RemoveTempYamlFile(name string) {
|
||||||
_ = os.Remove(name)
|
_ = os.Remove(name)
|
||||||
}
|
}
|
||||||
@@ -11,12 +11,12 @@ var (
|
|||||||
GitDescribe string
|
GitDescribe string
|
||||||
|
|
||||||
// Version is main version number that is being run at the moment.
|
// Version is main version number that is being run at the moment.
|
||||||
Version = "2.4.1"
|
Version = "3.0.0"
|
||||||
|
|
||||||
// VersionPrerelease is a pre-release marker for the version. If this is "" (empty string)
|
// 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
|
// then it means that it is a final release. Otherwise, this is a pre-release
|
||||||
// such as "dev" (in development), "beta", "rc1", etc.
|
// such as "dev" (in development), "beta", "rc1", etc.
|
||||||
VersionPrerelease = ""
|
VersionPrerelease = "beta"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProductName is the name of the product
|
// ProductName is the name of the product
|
||||||
|
|||||||
534
yq.go
534
yq.go
@@ -6,28 +6,33 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mikefarah/yq/v3/pkg/yqlib"
|
||||||
|
|
||||||
errors "github.com/pkg/errors"
|
errors "github.com/pkg/errors"
|
||||||
|
|
||||||
yaml "github.com/mikefarah/yaml/v2"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
logging "gopkg.in/op/go-logging.v1"
|
logging "gopkg.in/op/go-logging.v1"
|
||||||
|
yaml "gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var trimOutput = true
|
var customTag = ""
|
||||||
|
var printMode = "v"
|
||||||
var writeInplace = false
|
var writeInplace = false
|
||||||
var writeScript = ""
|
var writeScript = ""
|
||||||
var outputToJSON = false
|
var outputToJSON = false
|
||||||
var overwriteFlag = false
|
var overwriteFlag = false
|
||||||
|
var autoCreateFlag = true
|
||||||
var allowEmptyFlag = false
|
var allowEmptyFlag = false
|
||||||
var appendFlag = false
|
var appendFlag = false
|
||||||
var verbose = false
|
var verbose = false
|
||||||
var version = false
|
var version = false
|
||||||
var docIndex = "0"
|
var docIndex = "0"
|
||||||
var log = logging.MustGetLogger("yq")
|
var log = logging.MustGetLogger("yq")
|
||||||
|
var lib = yqlib.NewYqLib()
|
||||||
|
var valueParser = yqlib.NewValueParser()
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cmd := newCommandCLI()
|
cmd := newCommandCLI()
|
||||||
@@ -38,7 +43,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newCommandCLI() *cobra.Command {
|
func newCommandCLI() *cobra.Command {
|
||||||
yaml.DefaultMapType = reflect.TypeOf(yaml.MapSlice{})
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "yq",
|
Use: "yq",
|
||||||
Short: "yq is a lightweight and portable command-line YAML processor.",
|
Short: "yq is a lightweight and portable command-line YAML processor.",
|
||||||
@@ -69,8 +73,8 @@ func newCommandCLI() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&trimOutput, "trim", "t", true, "trim yaml output")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode")
|
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode")
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "output as json")
|
||||||
rootCmd.Flags().BoolVarP(&version, "version", "V", false, "Print version information and quit")
|
rootCmd.Flags().BoolVarP(&version, "version", "V", false, "Print version information and quit")
|
||||||
|
|
||||||
rootCmd.AddCommand(
|
rootCmd.AddCommand(
|
||||||
@@ -95,15 +99,16 @@ func createReadCmd() *cobra.Command {
|
|||||||
yq read things.yaml a.b.c
|
yq read things.yaml a.b.c
|
||||||
yq r - a.b.c (reads from stdin)
|
yq r - a.b.c (reads from stdin)
|
||||||
yq r things.yaml a.*.c
|
yq r things.yaml a.*.c
|
||||||
yq r -d1 things.yaml a.array[0].blah
|
yq r things.yaml a.**.c
|
||||||
yq r things.yaml a.array[*].blah
|
yq r -d1 things.yaml 'a.array[0].blah'
|
||||||
yq r -- things.yaml --key-starting-with-dashes
|
yq r things.yaml 'a.array[*].blah'
|
||||||
|
yq r -- things.yaml --key-starting-with-dashes.blah
|
||||||
`,
|
`,
|
||||||
Long: "Outputs the value of the given path in the yaml file to STDOUT",
|
Long: "Outputs the value of the given path in the yaml file to STDOUT",
|
||||||
RunE: readProperty,
|
RunE: readProperty,
|
||||||
}
|
}
|
||||||
cmdRead.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
|
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().StringVarP(&printMode, "printMode", "p", "v", "print mode (v (values, default), p (paths), pv (path and value pairs)")
|
||||||
return cmdRead
|
return cmdRead
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,13 +118,17 @@ func createWriteCmd() *cobra.Command {
|
|||||||
Aliases: []string{"w"},
|
Aliases: []string{"w"},
|
||||||
Short: "yq w [--inplace/-i] [--script/-s script_file] [--doc/-d index] sample.yaml a.b.c newValue",
|
Short: "yq w [--inplace/-i] [--script/-s script_file] [--doc/-d index] sample.yaml a.b.c newValue",
|
||||||
Example: `
|
Example: `
|
||||||
yq write things.yaml a.b.c cat
|
yq write things.yaml a.b.c true
|
||||||
|
yq write things.yaml 'a.*.c' true
|
||||||
|
yq write things.yaml 'a.**' true
|
||||||
|
yq write things.yaml a.b.c --tag '!!str' true
|
||||||
|
yq write things.yaml a.b.c --tag '!!float' 3
|
||||||
yq write --inplace -- things.yaml a.b.c --cat
|
yq write --inplace -- things.yaml a.b.c --cat
|
||||||
yq w -i things.yaml a.b.c cat
|
yq w -i things.yaml a.b.c cat
|
||||||
yq w --script update_script.yaml things.yaml
|
yq w --script update_script.yaml things.yaml
|
||||||
yq w -i -s update_script.yaml things.yaml
|
yq w -i -s update_script.yaml things.yaml
|
||||||
yq w --doc 2 things.yaml a.b.d[+] foo
|
yq w --doc 2 things.yaml 'a.b.d[+]' foo
|
||||||
yq w -d2 things.yaml a.b.d[+] foo
|
yq w -d2 things.yaml 'a.b.d[+]' foo
|
||||||
`,
|
`,
|
||||||
Long: `Updates the yaml file w.r.t the given path and value.
|
Long: `Updates the yaml file w.r.t the given path and value.
|
||||||
Outputs to STDOUT unless the inplace flag is used, in which case the file is updated instead.
|
Outputs to STDOUT unless the inplace flag is used, in which case the file is updated instead.
|
||||||
@@ -127,23 +136,28 @@ Outputs to STDOUT unless the inplace flag is used, in which case the file is upd
|
|||||||
Append value to array adds the value to the end of array.
|
Append value to array adds the value to the end of array.
|
||||||
|
|
||||||
Update Scripts:
|
Update Scripts:
|
||||||
Note that you can give an update script to perform more sophisticated updated. Update script
|
Note that you can give an update script to perform more sophisticated update. Update script
|
||||||
format is a yaml map where the key is the path and the value is..well the value. e.g.:
|
format is list of update commands (update or delete) like so:
|
||||||
---
|
---
|
||||||
a.b.c: true,
|
- command: update
|
||||||
a.b.e:
|
path: b.c
|
||||||
- name: bob
|
value:
|
||||||
|
#great
|
||||||
|
things: frog # wow!
|
||||||
|
- command: delete
|
||||||
|
path: b.d
|
||||||
`,
|
`,
|
||||||
RunE: writeProperty,
|
RunE: writeProperty,
|
||||||
}
|
}
|
||||||
cmdWrite.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
|
cmdWrite.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
|
||||||
cmdWrite.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for updating yaml")
|
cmdWrite.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for updating yaml")
|
||||||
|
cmdWrite.PersistentFlags().StringVarP(&customTag, "tag", "t", "", "set yaml tag (e.g. !!int)")
|
||||||
cmdWrite.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
|
cmdWrite.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
|
||||||
return cmdWrite
|
return cmdWrite
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPrefixCmd() *cobra.Command {
|
func createPrefixCmd() *cobra.Command {
|
||||||
var cmdWrite = &cobra.Command{
|
var cmdPrefix = &cobra.Command{
|
||||||
Use: "prefix [yaml_file] [path]",
|
Use: "prefix [yaml_file] [path]",
|
||||||
Aliases: []string{"p"},
|
Aliases: []string{"p"},
|
||||||
Short: "yq p [--inplace/-i] [--doc/-d index] sample.yaml a.b.c",
|
Short: "yq p [--inplace/-i] [--doc/-d index] sample.yaml a.b.c",
|
||||||
@@ -160,9 +174,9 @@ Outputs to STDOUT unless the inplace flag is used, in which case the file is upd
|
|||||||
`,
|
`,
|
||||||
RunE: prefixProperty,
|
RunE: prefixProperty,
|
||||||
}
|
}
|
||||||
cmdWrite.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
|
cmdPrefix.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
|
||||||
cmdWrite.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
|
cmdPrefix.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
|
||||||
return cmdWrite
|
return cmdPrefix
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDeleteCmd() *cobra.Command {
|
func createDeleteCmd() *cobra.Command {
|
||||||
@@ -172,6 +186,8 @@ func createDeleteCmd() *cobra.Command {
|
|||||||
Short: "yq d [--inplace/-i] [--doc/-d index] sample.yaml a.b.c",
|
Short: "yq d [--inplace/-i] [--doc/-d index] sample.yaml a.b.c",
|
||||||
Example: `
|
Example: `
|
||||||
yq delete things.yaml a.b.c
|
yq delete things.yaml a.b.c
|
||||||
|
yq delete things.yaml a.*.c
|
||||||
|
yq delete things.yaml a.**
|
||||||
yq delete --inplace things.yaml a.b.c
|
yq delete --inplace things.yaml a.b.c
|
||||||
yq delete --inplace -- things.yaml --key-starting-with-dash
|
yq delete --inplace -- things.yaml --key-starting-with-dash
|
||||||
yq d -i things.yaml a.b.c
|
yq d -i things.yaml a.b.c
|
||||||
@@ -195,6 +211,7 @@ func createNewCmd() *cobra.Command {
|
|||||||
Example: `
|
Example: `
|
||||||
yq new a.b.c cat
|
yq new a.b.c cat
|
||||||
yq n a.b.c cat
|
yq n a.b.c cat
|
||||||
|
yq n a.b[+] --tag '!!str' true
|
||||||
yq n -- --key-starting-with-dash cat
|
yq n -- --key-starting-with-dash cat
|
||||||
yq n --script create_script.yaml
|
yq n --script create_script.yaml
|
||||||
`,
|
`,
|
||||||
@@ -206,7 +223,8 @@ Note that you can give a create script to perform more sophisticated yaml. This
|
|||||||
`,
|
`,
|
||||||
RunE: newProperty,
|
RunE: newProperty,
|
||||||
}
|
}
|
||||||
cmdNew.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for updating yaml")
|
cmdNew.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for creating yaml")
|
||||||
|
cmdNew.PersistentFlags().StringVarP(&customTag, "tag", "t", "", "set yaml tag (e.g. !!int)")
|
||||||
return cmdNew
|
return cmdNew
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,19 +240,19 @@ yq m -i things.yaml other.yaml
|
|||||||
yq m --overwrite things.yaml other.yaml
|
yq m --overwrite things.yaml other.yaml
|
||||||
yq m -i -x things.yaml other.yaml
|
yq m -i -x things.yaml other.yaml
|
||||||
yq m -i -a things.yaml other.yaml
|
yq m -i -a things.yaml other.yaml
|
||||||
|
yq m -i --autocreate=false things.yaml other.yaml
|
||||||
`,
|
`,
|
||||||
Long: `Updates the yaml file by adding/updating the path(s) and value(s) from additional yaml file(s).
|
Long: `Updates the yaml file by adding/updating the path(s) and value(s) from additional yaml file(s).
|
||||||
Outputs to STDOUT unless the inplace flag is used, in which case the file is updated instead.
|
Outputs to STDOUT unless the inplace flag is used, in which case the file is updated instead.
|
||||||
|
|
||||||
If overwrite flag is set then existing values will be overwritten using the values from each additional yaml file.
|
If overwrite flag is set then existing values will be overwritten using the values from each additional yaml file.
|
||||||
If append flag is set then existing arrays will be merged with the arrays from each additional yaml file.
|
If append flag is set then existing arrays will be merged with the arrays from each additional yaml file.
|
||||||
|
|
||||||
Note that if you set both flags only overwrite will take effect.
|
|
||||||
`,
|
`,
|
||||||
RunE: mergeProperties,
|
RunE: mergeProperties,
|
||||||
}
|
}
|
||||||
cmdMerge.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
|
cmdMerge.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
|
||||||
cmdMerge.PersistentFlags().BoolVarP(&overwriteFlag, "overwrite", "x", false, "update the yaml file by overwriting existing values")
|
cmdMerge.PersistentFlags().BoolVarP(&overwriteFlag, "overwrite", "x", false, "update the yaml file by overwriting existing values")
|
||||||
|
cmdMerge.PersistentFlags().BoolVarP(&autoCreateFlag, "autocreate", "c", true, "automatically create any missing entries")
|
||||||
cmdMerge.PersistentFlags().BoolVarP(&appendFlag, "append", "a", false, "update the yaml file by appending array values")
|
cmdMerge.PersistentFlags().BoolVarP(&appendFlag, "append", "a", false, "update the yaml file by appending array values")
|
||||||
cmdMerge.PersistentFlags().BoolVarP(&allowEmptyFlag, "allow-empty", "e", false, "allow empty yaml files")
|
cmdMerge.PersistentFlags().BoolVarP(&allowEmptyFlag, "allow-empty", "e", false, "allow empty yaml files")
|
||||||
cmdMerge.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
|
cmdMerge.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
|
||||||
@@ -254,96 +272,117 @@ func readProperty(cmd *cobra.Command, args []string) error {
|
|||||||
if errorParsingDocIndex != nil {
|
if errorParsingDocIndex != nil {
|
||||||
return errorParsingDocIndex
|
return errorParsingDocIndex
|
||||||
}
|
}
|
||||||
var mappedDocs []interface{}
|
|
||||||
var dataBucket interface{}
|
matchingNodes, errorReadingStream := readYamlFile(args[0], path, updateAll, docIndexInt)
|
||||||
var currentIndex = 0
|
|
||||||
var errorReadingStream = readStream(args[0], func(decoder *yaml.Decoder) error {
|
|
||||||
for {
|
|
||||||
errorReading := decoder.Decode(&dataBucket)
|
|
||||||
if errorReading == io.EOF {
|
|
||||||
log.Debugf("done %v / %v", currentIndex, docIndexInt)
|
|
||||||
if !updateAll && currentIndex <= docIndexInt {
|
|
||||||
return fmt.Errorf("asked to process document index %v but there are only %v document(s)", docIndex, currentIndex)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
mappedDocs = append(mappedDocs, mappedDoc)
|
|
||||||
}
|
|
||||||
currentIndex = currentIndex + 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if errorReadingStream != nil {
|
if errorReadingStream != nil {
|
||||||
return errorReadingStream
|
return errorReadingStream
|
||||||
}
|
}
|
||||||
|
|
||||||
if !updateAll {
|
return printResults(matchingNodes, cmd)
|
||||||
dataBucket = mappedDocs[0]
|
}
|
||||||
} else {
|
|
||||||
dataBucket = mappedDocs
|
|
||||||
}
|
|
||||||
|
|
||||||
dataStr, err := toString(dataBucket)
|
func readYamlFile(filename string, path string, updateAll bool, docIndexInt int) ([]*yqlib.NodeContext, error) {
|
||||||
if err != nil {
|
var matchingNodes []*yqlib.NodeContext
|
||||||
return err
|
|
||||||
|
var currentIndex = 0
|
||||||
|
var errorReadingStream = readStream(filename, func(decoder *yaml.Decoder) error {
|
||||||
|
for {
|
||||||
|
var dataBucket yaml.Node
|
||||||
|
errorReading := decoder.Decode(&dataBucket)
|
||||||
|
|
||||||
|
if errorReading == io.EOF {
|
||||||
|
return handleEOF(updateAll, docIndexInt, currentIndex)
|
||||||
|
}
|
||||||
|
var errorParsing error
|
||||||
|
matchingNodes, errorParsing = appendDocument(matchingNodes, dataBucket, path, updateAll, docIndexInt, currentIndex)
|
||||||
|
if errorParsing != nil {
|
||||||
|
return errorParsing
|
||||||
|
}
|
||||||
|
currentIndex = currentIndex + 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return matchingNodes, errorReadingStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleEOF(updateAll bool, docIndexInt int, currentIndex int) error {
|
||||||
|
log.Debugf("done %v / %v", currentIndex, docIndexInt)
|
||||||
|
if !updateAll && currentIndex <= docIndexInt {
|
||||||
|
return fmt.Errorf("Could not process document index %v as there are only %v document(s)", docIndex, currentIndex)
|
||||||
}
|
}
|
||||||
cmd.Println(dataStr)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readPath(dataBucket interface{}, path string) (interface{}, error) {
|
func appendDocument(originalMatchingNodes []*yqlib.NodeContext, dataBucket yaml.Node, path string, updateAll bool, docIndexInt int, currentIndex int) ([]*yqlib.NodeContext, error) {
|
||||||
if path == "" {
|
log.Debugf("processing document %v - requested index %v", currentIndex, docIndexInt)
|
||||||
log.Debug("no path")
|
yqlib.DebugNode(&dataBucket)
|
||||||
return dataBucket, nil
|
if !updateAll && currentIndex != docIndexInt {
|
||||||
|
return originalMatchingNodes, nil
|
||||||
}
|
}
|
||||||
var paths = parsePath(path)
|
log.Debugf("reading %v in document %v", path, currentIndex)
|
||||||
return recurse(dataBucket, paths[0], paths[1:])
|
matchingNodes, errorParsing := lib.Get(&dataBucket, path)
|
||||||
|
if errorParsing != nil {
|
||||||
|
return nil, errors.Wrapf(errorParsing, "Error reading path in document index %v", currentIndex)
|
||||||
|
}
|
||||||
|
return append(originalMatchingNodes, matchingNodes...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProperty(cmd *cobra.Command, args []string) error {
|
func printValue(node *yaml.Node, cmd *cobra.Command) error {
|
||||||
updatedData, err := newYaml(args)
|
if node.Kind == yaml.ScalarNode {
|
||||||
if err != nil {
|
cmd.Print(node.Value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferedWriter := bufio.NewWriter(cmd.OutOrStdout())
|
||||||
|
defer safelyFlush(bufferedWriter)
|
||||||
|
|
||||||
|
var encoder yqlib.Encoder
|
||||||
|
if outputToJSON {
|
||||||
|
encoder = yqlib.NewJsonEncoder(bufferedWriter)
|
||||||
|
} else {
|
||||||
|
encoder = yqlib.NewYamlEncoder(bufferedWriter)
|
||||||
|
}
|
||||||
|
if err := encoder.Encode(node); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dataStr, err := toString(updatedData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cmd.Println(dataStr)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newYaml(args []string) (interface{}, error) {
|
func printResults(matchingNodes []*yqlib.NodeContext, cmd *cobra.Command) error {
|
||||||
var writeCommands, writeCommandsError = readWriteCommands(args, 2, "Must provide <path_to_update> <value>")
|
if len(matchingNodes) == 0 {
|
||||||
if writeCommandsError != nil {
|
log.Debug("no matching results, nothing to print")
|
||||||
return nil, writeCommandsError
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var dataBucket interface{}
|
for index, mappedDoc := range matchingNodes {
|
||||||
var isArray = strings.HasPrefix(writeCommands[0].Key.(string), "[")
|
switch printMode {
|
||||||
if isArray {
|
case "p":
|
||||||
dataBucket = make([]interface{}, 0)
|
cmd.Print(lib.PathStackToString(mappedDoc.PathStack))
|
||||||
} else {
|
if index < len(matchingNodes)-1 {
|
||||||
dataBucket = make(yaml.MapSlice, 0)
|
cmd.Print("\n")
|
||||||
|
}
|
||||||
|
case "pv", "vp":
|
||||||
|
// put it into a node and print that.
|
||||||
|
var parentNode = yaml.Node{Kind: yaml.MappingNode}
|
||||||
|
parentNode.Content = make([]*yaml.Node, 2)
|
||||||
|
parentNode.Content[0] = &yaml.Node{Kind: yaml.ScalarNode, Value: lib.PathStackToString(mappedDoc.PathStack)}
|
||||||
|
parentNode.Content[1] = mappedDoc.Node
|
||||||
|
if err := printValue(&parentNode, cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if err := printValue(mappedDoc.Node, cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Printing our Scalars does not print a new line at the end
|
||||||
|
// we only want to do that if there are more values (so users can easily script extraction of values in the yaml)
|
||||||
|
if index < len(matchingNodes)-1 && mappedDoc.Node.Kind == yaml.ScalarNode {
|
||||||
|
cmd.Print("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, entry := range writeCommands {
|
return nil
|
||||||
path := entry.Key.(string)
|
|
||||||
value := entry.Value
|
|
||||||
log.Debugf("setting %v to %v", path, value)
|
|
||||||
var paths = parsePath(path)
|
|
||||||
dataBucket = updatedChildValue(dataBucket, paths, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataBucket, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDocumentIndex() (bool, int, error) {
|
func parseDocumentIndex() (bool, int, error) {
|
||||||
@@ -357,11 +396,11 @@ func parseDocumentIndex() (bool, int, error) {
|
|||||||
return false, int(docIndexInt64), nil
|
return false, int(docIndexInt64), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type updateDataFn func(dataBucket interface{}, currentIndex int) (interface{}, error)
|
type updateDataFn func(dataBucket *yaml.Node, currentIndex int) error
|
||||||
|
|
||||||
func mapYamlDecoder(updateData updateDataFn, encoder *yaml.Encoder) yamlDecoderFn {
|
func mapYamlDecoder(updateData updateDataFn, encoder yqlib.Encoder) yamlDecoderFn {
|
||||||
return func(decoder *yaml.Decoder) error {
|
return func(decoder *yaml.Decoder) error {
|
||||||
var dataBucket interface{}
|
var dataBucket yaml.Node
|
||||||
var errorReading error
|
var errorReading error
|
||||||
var errorWriting error
|
var errorWriting error
|
||||||
var errorUpdating error
|
var errorUpdating error
|
||||||
@@ -384,12 +423,12 @@ func mapYamlDecoder(updateData updateDataFn, encoder *yaml.Encoder) yamlDecoderF
|
|||||||
} else if errorReading != nil {
|
} else if errorReading != nil {
|
||||||
return errors.Wrapf(errorReading, "Error reading document at index %v, %v", currentIndex, errorReading)
|
return errors.Wrapf(errorReading, "Error reading document at index %v, %v", currentIndex, errorReading)
|
||||||
}
|
}
|
||||||
dataBucket, errorUpdating = updateData(dataBucket, currentIndex)
|
errorUpdating = updateData(&dataBucket, currentIndex)
|
||||||
if errorUpdating != nil {
|
if errorUpdating != nil {
|
||||||
return errors.Wrapf(errorUpdating, "Error updating document at index %v", currentIndex)
|
return errors.Wrapf(errorUpdating, "Error updating document at index %v", currentIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
errorWriting = encoder.Encode(dataBucket)
|
errorWriting = encoder.Encode(&dataBucket)
|
||||||
|
|
||||||
if errorWriting != nil {
|
if errorWriting != nil {
|
||||||
return errors.Wrapf(errorWriting, "Error writing document at index %v, %v", currentIndex, errorWriting)
|
return errors.Wrapf(errorWriting, "Error writing document at index %v, %v", currentIndex, errorWriting)
|
||||||
@@ -400,62 +439,125 @@ func mapYamlDecoder(updateData updateDataFn, encoder *yaml.Encoder) yamlDecoderF
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeProperty(cmd *cobra.Command, args []string) error {
|
func writeProperty(cmd *cobra.Command, args []string) error {
|
||||||
var writeCommands, writeCommandsError = readWriteCommands(args, 3, "Must provide <filename> <path_to_update> <value>")
|
var updateCommands, updateCommandsError = readUpdateCommands(args, 3, "Must provide <filename> <path_to_update> <value>")
|
||||||
if writeCommandsError != nil {
|
if updateCommandsError != nil {
|
||||||
return writeCommandsError
|
return updateCommandsError
|
||||||
}
|
}
|
||||||
|
return updateDoc(args[0], updateCommands, cmd.OutOrStdout())
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeProperties(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return errors.New("Must provide at least 2 yaml files")
|
||||||
|
}
|
||||||
|
// first generate update commands from the file
|
||||||
|
var filesToMerge = args[1:]
|
||||||
|
var updateCommands []yqlib.UpdateCommand = make([]yqlib.UpdateCommand, 0)
|
||||||
|
|
||||||
|
for _, fileToMerge := range filesToMerge {
|
||||||
|
matchingNodes, errorProcessingFile := readYamlFile(fileToMerge, "**", false, 0)
|
||||||
|
if errorProcessingFile != nil && (!allowEmptyFlag || !strings.HasPrefix(errorProcessingFile.Error(), "Could not process document index")) {
|
||||||
|
return errorProcessingFile
|
||||||
|
}
|
||||||
|
for _, matchingNode := range matchingNodes {
|
||||||
|
mergePath := lib.MergePathStackToString(matchingNode.PathStack, appendFlag)
|
||||||
|
updateCommands = append(updateCommands, yqlib.UpdateCommand{Command: "update", Path: mergePath, Value: matchingNode.Node, Overwrite: overwriteFlag})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateDoc(args[0], updateCommands, cmd.OutOrStdout())
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProperty(cmd *cobra.Command, args []string) error {
|
||||||
|
var updateCommands, updateCommandsError = readUpdateCommands(args, 2, "Must provide <path_to_update> <value>")
|
||||||
|
if updateCommandsError != nil {
|
||||||
|
return updateCommandsError
|
||||||
|
}
|
||||||
|
newNode := lib.New(updateCommands[0].Path)
|
||||||
|
|
||||||
|
for _, updateCommand := range updateCommands {
|
||||||
|
|
||||||
|
errorUpdating := lib.Update(&newNode, updateCommand, true)
|
||||||
|
|
||||||
|
if errorUpdating != nil {
|
||||||
|
return errorUpdating
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var encoder = yaml.NewEncoder(cmd.OutOrStdout())
|
||||||
|
encoder.SetIndent(2)
|
||||||
|
errorEncoding := encoder.Encode(&newNode)
|
||||||
|
encoder.Close()
|
||||||
|
return errorEncoding
|
||||||
|
}
|
||||||
|
|
||||||
|
func prefixProperty(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
|
if len(args) < 2 {
|
||||||
|
return errors.New("Must provide <filename> <prefixed_path>")
|
||||||
|
}
|
||||||
|
updateCommand := yqlib.UpdateCommand{Command: "update", Path: args[1]}
|
||||||
|
log.Debugf("args %v", args)
|
||||||
|
|
||||||
var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
|
var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
|
||||||
if errorParsingDocIndex != nil {
|
if errorParsingDocIndex != nil {
|
||||||
return errorParsingDocIndex
|
return errorParsingDocIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
var updateData = func(dataBucket interface{}, currentIndex int) (interface{}, error) {
|
var updateData = func(dataBucket *yaml.Node, currentIndex int) error {
|
||||||
if updateAll || currentIndex == docIndexInt {
|
return prefixDocument(updateAll, docIndexInt, currentIndex, dataBucket, updateCommand)
|
||||||
log.Debugf("Updating doc %v", currentIndex)
|
|
||||||
for _, entry := range writeCommands {
|
|
||||||
path := entry.Key.(string)
|
|
||||||
value := entry.Value
|
|
||||||
log.Debugf("setting %v to %v", path, value)
|
|
||||||
var paths = parsePath(path)
|
|
||||||
dataBucket = updatedChildValue(dataBucket, paths, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dataBucket, nil
|
|
||||||
}
|
}
|
||||||
return readAndUpdate(cmd.OutOrStdout(), args[0], updateData)
|
return readAndUpdate(cmd.OutOrStdout(), args[0], updateData)
|
||||||
}
|
}
|
||||||
|
|
||||||
func prefixProperty(cmd *cobra.Command, args []string) error {
|
func prefixDocument(updateAll bool, docIndexInt int, currentIndex int, dataBucket *yaml.Node, updateCommand yqlib.UpdateCommand) error {
|
||||||
if len(args) != 2 {
|
if updateAll || currentIndex == docIndexInt {
|
||||||
return errors.New("Must provide <filename> <prefixed_path>")
|
log.Debugf("Prefixing document %v", currentIndex)
|
||||||
|
yqlib.DebugNode(dataBucket)
|
||||||
|
updateCommand.Value = dataBucket.Content[0]
|
||||||
|
dataBucket.Content = make([]*yaml.Node, 1)
|
||||||
|
|
||||||
|
newNode := lib.New(updateCommand.Path)
|
||||||
|
dataBucket.Content[0] = &newNode
|
||||||
|
|
||||||
|
errorUpdating := lib.Update(dataBucket, updateCommand, true)
|
||||||
|
if errorUpdating != nil {
|
||||||
|
return errorUpdating
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteProperty(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return errors.New("Must provide <filename> <path_to_delete>")
|
||||||
|
}
|
||||||
|
var updateCommands []yqlib.UpdateCommand = make([]yqlib.UpdateCommand, 1)
|
||||||
|
updateCommands[0] = yqlib.UpdateCommand{Command: "delete", Path: args[1]}
|
||||||
|
|
||||||
|
return updateDoc(args[0], updateCommands, cmd.OutOrStdout())
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDoc(inputFile string, updateCommands []yqlib.UpdateCommand, writer io.Writer) error {
|
||||||
var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
|
var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
|
||||||
if errorParsingDocIndex != nil {
|
if errorParsingDocIndex != nil {
|
||||||
return errorParsingDocIndex
|
return errorParsingDocIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
var paths = parsePath(args[1])
|
var updateData = func(dataBucket *yaml.Node, currentIndex int) error {
|
||||||
|
|
||||||
// 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 {
|
if updateAll || currentIndex == docIndexInt {
|
||||||
log.Debugf("Prefixing %v to doc %v", paths, currentIndex)
|
log.Debugf("Updating doc %v", currentIndex)
|
||||||
var mapDataBucket = dataBucket
|
for _, updateCommand := range updateCommands {
|
||||||
for _, key := range paths {
|
log.Debugf("Processing update to Path %v", updateCommand.Path)
|
||||||
singlePath := []string{key}
|
errorUpdating := lib.Update(dataBucket, updateCommand, autoCreateFlag)
|
||||||
mapDataBucket = updatedChildValue(nil, singlePath, mapDataBucket)
|
if errorUpdating != nil {
|
||||||
|
return errorUpdating
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return mapDataBucket, nil
|
|
||||||
}
|
}
|
||||||
return dataBucket, nil
|
return nil
|
||||||
}
|
}
|
||||||
return readAndUpdate(cmd.OutOrStdout(), args[0], updateData)
|
return readAndUpdate(writer, inputFile, updateData)
|
||||||
}
|
}
|
||||||
|
|
||||||
func readAndUpdate(stdOut io.Writer, inputFile string, updateData updateDataFn) error {
|
func readAndUpdate(stdOut io.Writer, inputFile string, updateData updateDataFn) error {
|
||||||
@@ -481,152 +583,64 @@ func readAndUpdate(stdOut io.Writer, inputFile string, updateData updateDataFn)
|
|||||||
safelyRenameFile(tempFile.Name(), inputFile)
|
safelyRenameFile(tempFile.Name(), inputFile)
|
||||||
}()
|
}()
|
||||||
} else {
|
} else {
|
||||||
var writer = bufio.NewWriter(stdOut)
|
destination = stdOut
|
||||||
destination = writer
|
|
||||||
destinationName = "Stdout"
|
destinationName = "Stdout"
|
||||||
defer safelyFlush(writer)
|
|
||||||
}
|
}
|
||||||
var encoder = yaml.NewEncoder(destination)
|
|
||||||
log.Debugf("Writing to %v from %v", destinationName, inputFile)
|
log.Debugf("Writing to %v from %v", destinationName, inputFile)
|
||||||
|
|
||||||
|
bufferedWriter := bufio.NewWriter(destination)
|
||||||
|
defer safelyFlush(bufferedWriter)
|
||||||
|
|
||||||
|
var encoder yqlib.Encoder
|
||||||
|
if outputToJSON {
|
||||||
|
encoder = yqlib.NewJsonEncoder(bufferedWriter)
|
||||||
|
} else {
|
||||||
|
encoder = yqlib.NewYamlEncoder(bufferedWriter)
|
||||||
|
}
|
||||||
return readStream(inputFile, mapYamlDecoder(updateData, encoder))
|
return readStream(inputFile, mapYamlDecoder(updateData, encoder))
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteProperty(cmd *cobra.Command, args []string) error {
|
type updateCommandParsed struct {
|
||||||
if len(args) < 2 {
|
Command string
|
||||||
return errors.New("Must provide <filename> <path_to_delete>")
|
Path string
|
||||||
}
|
Value yaml.Node
|
||||||
var deletePath = args[1]
|
|
||||||
var paths = parsePath(deletePath)
|
|
||||||
var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
|
|
||||||
if errorParsingDocIndex != nil {
|
|
||||||
return errorParsingDocIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
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 dataBucket, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return readAndUpdate(cmd.OutOrStdout(), args[0], updateData)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeProperties(cmd *cobra.Command, args []string) error {
|
func readUpdateCommands(args []string, expectedArgs int, badArgsMessage string) ([]yqlib.UpdateCommand, error) {
|
||||||
if len(args) < 2 {
|
var updateCommands []yqlib.UpdateCommand = make([]yqlib.UpdateCommand, 0)
|
||||||
return errors.New("Must provide at least 2 yaml files")
|
|
||||||
}
|
|
||||||
var input = args[0]
|
|
||||||
var filesToMerge = args[1:]
|
|
||||||
var updateAll, docIndexInt, errorParsingDocIndex = parseDocumentIndex()
|
|
||||||
if errorParsingDocIndex != nil {
|
|
||||||
return errorParsingDocIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
var updateData = func(dataBucket interface{}, currentIndex int) (interface{}, error) {
|
|
||||||
if updateAll || currentIndex == docIndexInt {
|
|
||||||
log.Debugf("Merging doc %v", currentIndex)
|
|
||||||
var mergedData map[interface{}]interface{}
|
|
||||||
// merge only works for maps, so put everything in a temporary
|
|
||||||
// map
|
|
||||||
var mapDataBucket = make(map[interface{}]interface{})
|
|
||||||
mapDataBucket["root"] = dataBucket
|
|
||||||
if err := merge(&mergedData, mapDataBucket, overwriteFlag, appendFlag); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, f := range filesToMerge {
|
|
||||||
var fileToMerge interface{}
|
|
||||||
if err := readData(f, 0, &fileToMerge); err != nil {
|
|
||||||
if allowEmptyFlag && err == io.EOF {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mapDataBucket["root"] = fileToMerge
|
|
||||||
if err := merge(&mergedData, mapDataBucket, overwriteFlag, appendFlag); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mergedData["root"], nil
|
|
||||||
}
|
|
||||||
return dataBucket, nil
|
|
||||||
}
|
|
||||||
yaml.DefaultMapType = reflect.TypeOf(map[interface{}]interface{}{})
|
|
||||||
defer func() { yaml.DefaultMapType = reflect.TypeOf(yaml.MapSlice{}) }()
|
|
||||||
return readAndUpdate(cmd.OutOrStdout(), input, updateData)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readWriteCommands(args []string, expectedArgs int, badArgsMessage string) (yaml.MapSlice, error) {
|
|
||||||
var writeCommands yaml.MapSlice
|
|
||||||
if writeScript != "" {
|
if writeScript != "" {
|
||||||
if err := readData(writeScript, 0, &writeCommands); err != nil {
|
var parsedCommands = make([]updateCommandParsed, 0)
|
||||||
|
|
||||||
|
err := readData(writeScript, 0, &parsedCommands)
|
||||||
|
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debugf("Read write commands file '%v'", parsedCommands)
|
||||||
|
for index := range parsedCommands {
|
||||||
|
parsedCommand := parsedCommands[index]
|
||||||
|
updateCommand := yqlib.UpdateCommand{Command: parsedCommand.Command, Path: parsedCommand.Path, Value: &parsedCommand.Value, Overwrite: true}
|
||||||
|
updateCommands = append(updateCommands, updateCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Read write commands file '%v'", updateCommands)
|
||||||
} else if len(args) < expectedArgs {
|
} else if len(args) < expectedArgs {
|
||||||
return nil, errors.New(badArgsMessage)
|
return nil, errors.New(badArgsMessage)
|
||||||
} else {
|
} else {
|
||||||
writeCommands = make(yaml.MapSlice, 1)
|
updateCommands = make([]yqlib.UpdateCommand, 1)
|
||||||
writeCommands[0] = yaml.MapItem{Key: args[expectedArgs-2], Value: parseValue(args[expectedArgs-1])}
|
log.Debug("args %v", args)
|
||||||
|
log.Debug("path %v", args[expectedArgs-2])
|
||||||
|
log.Debug("Value %v", args[expectedArgs-1])
|
||||||
|
updateCommands[0] = yqlib.UpdateCommand{Command: "update", Path: args[expectedArgs-2], Value: valueParser.Parse(args[expectedArgs-1], customTag), Overwrite: true}
|
||||||
}
|
}
|
||||||
return writeCommands, nil
|
return updateCommands, 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 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func safelyRenameFile(from string, to string) {
|
func safelyRenameFile(from string, to string) {
|
||||||
if renameError := os.Rename(from, to); renameError != nil {
|
if renameError := os.Rename(from, to); renameError != nil {
|
||||||
log.Debugf("Error renaming from %v to %v, attemting to copy contents", from, to)
|
log.Debugf("Error renaming from %v to %v, attempting to copy contents", from, to)
|
||||||
log.Debug(renameError.Error())
|
log.Debug(renameError.Error())
|
||||||
// can't do this rename when running in docker to a file targeted in a mounted volume,
|
// 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.
|
// so gracefully degrade to copying the entire contents.
|
||||||
|
|||||||
119
yq_test.go
119
yq_test.go
@@ -1,75 +1,60 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
// import (
|
||||||
"fmt"
|
// "fmt"
|
||||||
"runtime"
|
// "runtime"
|
||||||
"testing"
|
// "testing"
|
||||||
)
|
|
||||||
|
|
||||||
var parseValueTests = []struct {
|
// "github.com/mikefarah/yq/v2/pkg/marshal"
|
||||||
argument string
|
// "github.com/mikefarah/yq/v2/test"
|
||||||
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) {
|
// func TestMultilineString(t *testing.T) {
|
||||||
for _, tt := range parseValueTests {
|
// testString := `
|
||||||
assertResultWithContext(t, tt.expectedResult, parseValue(tt.argument), tt.testDescription)
|
// abcd
|
||||||
}
|
// efg`
|
||||||
}
|
// formattedResult, _ := marshal.NewYamlConverter().YamlToString(testString, false)
|
||||||
|
// test.AssertResult(t, testString, formattedResult)
|
||||||
|
// }
|
||||||
|
|
||||||
func TestMultilineString(t *testing.T) {
|
// func TestNewYaml(t *testing.T) {
|
||||||
testString := `
|
// result, _ := newYaml([]string{"b.c", "3"})
|
||||||
abcd
|
// formattedResult := fmt.Sprintf("%v", result)
|
||||||
efg`
|
// test.AssertResult(t,
|
||||||
formattedResult, _ := yamlToString(testString)
|
// "[{b [{c 3}]}]",
|
||||||
assertResult(t, testString, formattedResult)
|
// formattedResult)
|
||||||
}
|
// }
|
||||||
|
|
||||||
func TestNewYaml(t *testing.T) {
|
// func TestNewYamlArray(t *testing.T) {
|
||||||
result, _ := newYaml([]string{"b.c", "3"})
|
// result, _ := newYaml([]string{"[0].cat", "meow"})
|
||||||
formattedResult := fmt.Sprintf("%v", result)
|
// formattedResult := fmt.Sprintf("%v", result)
|
||||||
assertResult(t,
|
// test.AssertResult(t,
|
||||||
"[{b [{c 3}]}]",
|
// "[[{cat meow}]]",
|
||||||
formattedResult)
|
// formattedResult)
|
||||||
}
|
// }
|
||||||
|
|
||||||
func TestNewYamlArray(t *testing.T) {
|
// func TestNewYaml_WithScript(t *testing.T) {
|
||||||
result, _ := newYaml([]string{"[0].cat", "meow"})
|
// writeScript = "examples/instruction_sample.yaml"
|
||||||
formattedResult := fmt.Sprintf("%v", result)
|
// expectedResult := `b:
|
||||||
assertResult(t,
|
// c: cat
|
||||||
"[[{cat meow}]]",
|
// e:
|
||||||
formattedResult)
|
// - name: Mike Farah`
|
||||||
}
|
// result, _ := newYaml([]string{""})
|
||||||
|
// actualResult, _ := marshal.NewYamlConverter().YamlToString(result, true)
|
||||||
|
// test.AssertResult(t, expectedResult, actualResult)
|
||||||
|
// }
|
||||||
|
|
||||||
func TestNewYaml_WithScript(t *testing.T) {
|
// func TestNewYaml_WithUnknownScript(t *testing.T) {
|
||||||
writeScript = "examples/instruction_sample.yaml"
|
// writeScript = "fake-unknown"
|
||||||
expectedResult := `b:
|
// _, err := newYaml([]string{""})
|
||||||
c: cat
|
// if err == nil {
|
||||||
e:
|
// t.Error("Expected error due to unknown file")
|
||||||
- name: Mike Farah`
|
// }
|
||||||
result, _ := newYaml([]string{""})
|
// var expectedOutput string
|
||||||
actualResult, _ := yamlToString(result)
|
// if runtime.GOOS == "windows" {
|
||||||
assertResult(t, expectedResult, actualResult)
|
// expectedOutput = `open fake-unknown: The system cannot find the file specified.`
|
||||||
}
|
// } else {
|
||||||
|
// expectedOutput = `open fake-unknown: no such file or directory`
|
||||||
func TestNewYaml_WithUnknownScript(t *testing.T) {
|
// }
|
||||||
writeScript = "fake-unknown"
|
// test.AssertResult(t, expectedOutput, err.Error())
|
||||||
_, err := newYaml([]string{""})
|
// }
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error due to unknown file")
|
|
||||||
}
|
|
||||||
var expectedOutput string
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
expectedOutput = `open fake-unknown: The system cannot find the file specified.`
|
|
||||||
} else {
|
|
||||||
expectedOutput = `open fake-unknown: no such file or directory`
|
|
||||||
}
|
|
||||||
assertResult(t, expectedOutput, err.Error())
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user