mirror of
https://github.com/taigrr/yq
synced 2025-01-18 04:53:17 -08:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a90629822 | ||
|
|
8aa69fc9ba | ||
|
|
690442d02e | ||
|
|
d1c545cca0 | ||
|
|
c4af37ed68 | ||
|
|
01845ea923 | ||
|
|
58bdb3ed21 | ||
|
|
0cb8a47ccb | ||
|
|
35ceb01222 | ||
|
|
c1b803364a | ||
|
|
364c1a8af3 | ||
|
|
2a0290aeac | ||
|
|
4ea8d5c4ee | ||
|
|
eeb16443d4 | ||
|
|
eebd319246 | ||
|
|
5a3c3a7152 |
38
README.md
38
README.md
@@ -1,9 +1,11 @@
|
|||||||
# yaml
|
# yaml
|
||||||
yaml command line tool written in go
|
yaml command line tool written in go
|
||||||
|
|
||||||
Allows you to read (and soon update) yaml files given a yaml path.
|
Allows you to read (and soon update) yaml files given a yaml path. All in a lovely dependency free binary!
|
||||||
|
|
||||||
## Install
|
[Download latest release](https://github.com/mikefarah/yaml/releases/latest)
|
||||||
|
|
||||||
|
or alternatively install using go get:
|
||||||
```
|
```
|
||||||
go get github.com/mikefarah/yaml
|
go get github.com/mikefarah/yaml
|
||||||
```
|
```
|
||||||
@@ -25,6 +27,18 @@ yaml sample.yaml b.c
|
|||||||
```
|
```
|
||||||
will output the value of '2'.
|
will output the value of '2'.
|
||||||
|
|
||||||
|
### Handling '.' in the yaml key
|
||||||
|
Given a sample.yaml file of:
|
||||||
|
```yaml
|
||||||
|
b.x:
|
||||||
|
c: 2
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yaml sample.yaml \"b.x\".c
|
||||||
|
```
|
||||||
|
will output the value of '2'.
|
||||||
|
|
||||||
### Arrays
|
### Arrays
|
||||||
You can give an index to access a specific element:
|
You can give an index to access a specific element:
|
||||||
e.g.: given a sample file of
|
e.g.: given a sample file of
|
||||||
@@ -38,10 +52,22 @@ b:
|
|||||||
```
|
```
|
||||||
then
|
then
|
||||||
```
|
```
|
||||||
yaml sample.yaml b.e.1.name
|
yaml sample.yaml b.e[1].name
|
||||||
```
|
```
|
||||||
will output 'sam'
|
will output 'sam'
|
||||||
|
|
||||||
## TODO
|
### Updating yaml
|
||||||
* Updating yaml files
|
Given a sample.yaml file of:
|
||||||
* Handling '.' in path names
|
```yaml
|
||||||
|
b:
|
||||||
|
c: 2
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yaml w sample.yaml b.c 'cat'
|
||||||
|
```
|
||||||
|
will output:
|
||||||
|
```yaml
|
||||||
|
b:
|
||||||
|
c: cat
|
||||||
|
```
|
||||||
|
|||||||
50
data_navigator.go
Normal file
50
data_navigator.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// "fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func write(context map[interface{}]interface{}, head string, tail []string, value interface{}) {
|
||||||
|
// e.g. if updating a.b.c, we need to get the 'b' map...
|
||||||
|
toUpdate := readMap(context, head, tail[0:len(tail)-1]).(map[interface{}]interface{})
|
||||||
|
// and then set the 'c' key.
|
||||||
|
key := (tail[len(tail)-1])
|
||||||
|
toUpdate[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func readMap(context map[interface{}]interface{}, head string, tail []string) interface{} {
|
||||||
|
value := context[head]
|
||||||
|
if len(tail) > 0 {
|
||||||
|
return recurse(value, tail[0], tail[1:len(tail)])
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func recurse(value interface{}, head string, tail []string) interface{} {
|
||||||
|
switch value.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
index, err := strconv.ParseInt(head, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error accessing array: %v", err)
|
||||||
|
}
|
||||||
|
return readArray(value.([]interface{}), index, tail)
|
||||||
|
case nil:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return readMap(value.(map[interface{}]interface{}), head, tail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readArray(array []interface{}, head int64, tail []string) interface{} {
|
||||||
|
if head > int64(len(array)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
value := array[head]
|
||||||
|
if len(tail) > 0 {
|
||||||
|
return recurse(value, tail[0], tail[1:len(tail)])
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
64
data_navigator_test.go
Normal file
64
data_navigator_test.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rawData = `
|
||||||
|
a: Easy!
|
||||||
|
b:
|
||||||
|
c: 2
|
||||||
|
d: [3, 4]
|
||||||
|
`
|
||||||
|
|
||||||
|
var parsedData map[interface{}]interface{}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
err := yaml.Unmarshal([]byte(rawData), &parsedData)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error parsing yaml: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadMap_simple(t *testing.T) {
|
||||||
|
assertResult(t, 2, readMap(parsedData, "b", []string{"c"}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadMap_key_doesnt_exist(t *testing.T) {
|
||||||
|
assertResult(t, nil, readMap(parsedData, "b.x.f", []string{"c"}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadMap_with_array(t *testing.T) {
|
||||||
|
assertResult(t, 4, readMap(parsedData, "b", []string{"d", "1"}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadMap_with_array_out_of_bounds(t *testing.T) {
|
||||||
|
assertResult(t, nil, readMap(parsedData, "b", []string{"d", "3"}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrite_simple(t *testing.T) {
|
||||||
|
|
||||||
|
write(parsedData, "b", []string{"c"}, "4")
|
||||||
|
|
||||||
|
b := parsedData["b"].(map[interface{}]interface{})
|
||||||
|
assertResult(t, "4", b["c"].(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertResult(t *testing.T, expectedValue interface{}, actualValue interface{}) {
|
||||||
|
if expectedValue != actualValue {
|
||||||
|
t.Error("Expected <", expectedValue, "> but got <", actualValue, ">")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertResultWithContext(t *testing.T, expectedValue interface{}, actualValue interface{}, context interface{}) {
|
||||||
|
if expectedValue != actualValue {
|
||||||
|
t.Error(context)
|
||||||
|
t.Error(": expected <", expectedValue, "> but got <", actualValue, ">")
|
||||||
|
}
|
||||||
|
}
|
||||||
55
path_parser.go
Normal file
55
path_parser.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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:len(path)], []uint8{']'}, true)
|
||||||
|
case '"':
|
||||||
|
// e.g "a.b".blah.cat -> we need to return "a.b" and "blah.cat"
|
||||||
|
return search(path[1:len(path)], []uint8{'"'}, true)
|
||||||
|
default:
|
||||||
|
// e.g "a.blah.cat" -> return "a" and "blah.cat"
|
||||||
|
return search(path[0:len(path)], []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:len(path)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(matchingChars []uint8, candidate uint8) bool {
|
||||||
|
for _, a := range matchingChars {
|
||||||
|
if a == candidate {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
42
path_parser_test.go
Normal file
42
path_parser_test.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var parsePathsTests = []struct {
|
||||||
|
path string
|
||||||
|
expectedPaths []string
|
||||||
|
}{
|
||||||
|
{"a.b", []string{"a", "b"}},
|
||||||
|
{"a.b[0]", []string{"a", "b", "0"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
func testParsePath(t *testing.T) {
|
||||||
|
for _, tt := range parsePathsTests {
|
||||||
|
assertResultWithContext(t, tt.expectedPaths, parsePath(tt.path), tt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,13 @@ golint
|
|||||||
go test
|
go test
|
||||||
|
|
||||||
# acceptance test
|
# acceptance test
|
||||||
X=$(go run yaml.go sample.yaml b.c)
|
go build
|
||||||
|
X=$(./yaml sample.yaml b.c)
|
||||||
|
|
||||||
if [ $X != 2 ]
|
if [ $X != 2 ]
|
||||||
then
|
then
|
||||||
echo "Failed acceptance test: expected 2 but was $X"
|
echo "Failed acceptance test: expected 2 but was $X"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
go install
|
||||||
|
|||||||
97
yaml.go
97
yaml.go
@@ -19,22 +19,77 @@ func main() {
|
|||||||
{
|
{
|
||||||
Name: "read",
|
Name: "read",
|
||||||
Aliases: []string{"r"},
|
Aliases: []string{"r"},
|
||||||
Usage: "read <filename> <path>\n\te.g.: yaml read sample.json a.b.c\n\t(default) reads a property from a given yaml file",
|
Usage: "read <filename> <path>\n\te.g.: yaml read sample.json a.b.c\n\t(default) reads a property from a given yaml file\n",
|
||||||
Action: readProperty,
|
Action: readProperty,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "write",
|
||||||
|
Aliases: []string{"w"},
|
||||||
|
Usage: "write <filename> <path> <value>\n\te.g.: yaml write sample.json a.b.c 5\n\tupdates a property from a given yaml file, outputs to stdout\n",
|
||||||
|
Action: writeProperty,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
app.Action = readProperty
|
app.Action = readProperty
|
||||||
app.Run(os.Args)
|
app.Run(os.Args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func readProperty(c *cli.Context) {
|
func readProperty(c *cli.Context) {
|
||||||
|
|
||||||
|
var parsedData map[interface{}]interface{}
|
||||||
|
|
||||||
|
readYaml(c, &parsedData)
|
||||||
|
|
||||||
|
if len(c.Args()) == 1 {
|
||||||
|
printYaml(parsedData)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var paths = parsePath(c.Args()[1])
|
||||||
|
|
||||||
|
printYaml(readMap(parsedData, paths[0], paths[1:len(paths)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeProperty(c *cli.Context) {
|
||||||
var parsedData map[interface{}]interface{}
|
var parsedData map[interface{}]interface{}
|
||||||
readYaml(c, &parsedData)
|
readYaml(c, &parsedData)
|
||||||
|
|
||||||
var path = c.Args()[1]
|
if len(c.Args()) < 3 {
|
||||||
var paths = strings.Split(path, ".")
|
log.Fatalf("Must provide <filename> <path_to_update> <value>")
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println(readMap(parsedData, paths[0], paths[1:len(paths)]))
|
var paths = parsePath(c.Args()[1])
|
||||||
|
|
||||||
|
write(parsedData, paths[0], paths[1:len(paths)], getValue(c.Args()[2]))
|
||||||
|
|
||||||
|
printYaml(parsedData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getValue(argument string) interface{} {
|
||||||
|
var value, err interface{}
|
||||||
|
var inQuotes = 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
|
||||||
|
}
|
||||||
|
return argument
|
||||||
|
}
|
||||||
|
return argument[1 : len(argument)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func printYaml(context interface{}) {
|
||||||
|
out, err := yaml.Marshal(context)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error printing yaml: %v", err)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
fmt.Println(strings.Trim(outStr, "\n "))
|
||||||
}
|
}
|
||||||
|
|
||||||
func readYaml(c *cli.Context, parsedData *map[interface{}]interface{}) {
|
func readYaml(c *cli.Context, parsedData *map[interface{}]interface{}) {
|
||||||
@@ -43,11 +98,6 @@ func readYaml(c *cli.Context, parsedData *map[interface{}]interface{}) {
|
|||||||
}
|
}
|
||||||
var rawData = readFile(c.Args()[0])
|
var rawData = readFile(c.Args()[0])
|
||||||
|
|
||||||
if len(c.Args()) == 1 {
|
|
||||||
fmt.Println(string(rawData[:]))
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := yaml.Unmarshal([]byte(rawData), &parsedData)
|
err := yaml.Unmarshal([]byte(rawData), &parsedData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("error: %v", err)
|
log.Fatalf("error: %v", err)
|
||||||
@@ -61,32 +111,3 @@ func readFile(filename string) []byte {
|
|||||||
}
|
}
|
||||||
return rawData
|
return rawData
|
||||||
}
|
}
|
||||||
|
|
||||||
func readMap(context map[interface{}]interface{}, head string, tail []string) interface{} {
|
|
||||||
value := context[head]
|
|
||||||
if len(tail) > 0 {
|
|
||||||
return recurse(value, tail[0], tail[1:len(tail)])
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func recurse(value interface{}, head string, tail []string) interface{} {
|
|
||||||
switch value.(type) {
|
|
||||||
case []interface{}:
|
|
||||||
index, err := strconv.ParseInt(head, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error accessing array: %v", err)
|
|
||||||
}
|
|
||||||
return readArray(value.([]interface{}), index, tail)
|
|
||||||
default:
|
|
||||||
return readMap(value.(map[interface{}]interface{}), head, tail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readArray(array []interface{}, head int64, tail []string) interface{} {
|
|
||||||
value := array[head]
|
|
||||||
if len(tail) > 0 {
|
|
||||||
return recurse(value, tail[0], tail[1:len(tail)])
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|||||||
43
yaml_test.go
43
yaml_test.go
@@ -1,41 +1,22 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rawData = `
|
var getValueTests = []struct {
|
||||||
a: Easy!
|
argument string
|
||||||
b:
|
expectedResult interface{}
|
||||||
c: 2
|
testDescription string
|
||||||
d: [3, 4]
|
}{
|
||||||
`
|
{"true", true, "boolean"},
|
||||||
|
{"\"true\"", "true", "boolean as string"},
|
||||||
var parsedData map[interface{}]interface{}
|
{"3.4", 3.4, "number"},
|
||||||
|
{"\"3.4\"", "3.4", "number as string"},
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
err := yaml.Unmarshal([]byte(rawData), &parsedData)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error parsing yaml: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(m.Run())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadMap_simple(t *testing.T) {
|
func TestGetValue(t *testing.T) {
|
||||||
result := readMap(parsedData, "b", []string{"c"})
|
for _, tt := range getValueTests {
|
||||||
if result != 2 {
|
assertResultWithContext(t, tt.expectedResult, getValue(tt.argument), tt.testDescription)
|
||||||
t.Error("Excpted 2 but got ", result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReadMap_array(t *testing.T) {
|
|
||||||
result := readMap(parsedData, "b", []string{"d", "1"})
|
|
||||||
if result != 4 {
|
|
||||||
t.Error("Excpted 4 but got ", result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user