1
0
mirror of https://github.com/taigrr/yq synced 2025-01-18 04:53:17 -08:00

Compare commits

...

16 Commits
0.1 ... 1.0

Author SHA1 Message Date
Mike Farah
3a90629822 Remove trim flag as its not properly supported by the cli package 2015-10-03 17:48:54 +10:00
Mike Farah
8aa69fc9ba Handles quoted values when writing 2015-10-03 17:25:13 +10:00
Mike Farah
690442d02e Handles keys that dont exit 2015-10-03 17:06:49 +10:00
Mike Farah
d1c545cca0 Handle index out of range when reading arrays 2015-10-03 17:04:12 +10:00
Mike Farah
c4af37ed68 Custom path parsing 2015-10-03 17:04:12 +10:00
Mike Farah
01845ea923 Split code 2015-10-02 09:05:13 +10:00
Mike Farah
58bdb3ed21 Checking forceString parameter now less strict 2015-10-02 08:50:08 +10:00
mfarah
0cb8a47ccb Added getValue tests 2015-10-01 19:38:45 +10:00
mfarah
35ceb01222 DRY'd tests 2015-10-01 14:43:57 +10:00
mfarah
c1b803364a Now attempts to parse the write value type 2015-10-01 10:50:15 +10:00
Mike Farah
364c1a8af3 Can update yam to stdout 2015-09-29 16:29:32 +10:00
Mike Farah
2a0290aeac updated help 2015-09-29 11:05:28 +10:00
Mike Farah
4ea8d5c4ee Precheckin now installs if tests pass 2015-09-29 11:05:20 +10:00
Mike Farah
eeb16443d4 Can update simple properties (to stdout) 2015-09-29 10:56:28 +10:00
Mike Farah
eebd319246 Nicely formats output 2015-09-29 10:56:12 +10:00
Mike Farah
5a3c3a7152 Added release link in readme 2015-09-29 10:20:31 +10:00
8 changed files with 319 additions and 76 deletions

View File

@@ -1,9 +1,11 @@
# yaml
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
```
@@ -25,6 +27,18 @@ yaml sample.yaml b.c
```
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
You can give an index to access a specific element:
e.g.: given a sample file of
@@ -38,10 +52,22 @@ b:
```
then
```
yaml sample.yaml b.e.1.name
yaml sample.yaml b.e[1].name
```
will output 'sam'
## TODO
* Updating yaml files
* Handling '.' in path names
### Updating yaml
Given a sample.yaml file of:
```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
View 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
View 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
View 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
View 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)
}
}

View File

@@ -5,9 +5,13 @@ golint
go test
# acceptance test
X=$(go run yaml.go sample.yaml b.c)
go build
X=$(./yaml sample.yaml b.c)
if [ $X != 2 ]
then
echo "Failed acceptance test: expected 2 but was $X"
exit 1
fi
go install

97
yaml.go
View File

@@ -19,22 +19,77 @@ func main() {
{
Name: "read",
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,
},
{
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.Run(os.Args)
}
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{}
readYaml(c, &parsedData)
var path = c.Args()[1]
var paths = strings.Split(path, ".")
if len(c.Args()) < 3 {
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{}) {
@@ -43,11 +98,6 @@ func readYaml(c *cli.Context, parsedData *map[interface{}]interface{}) {
}
var rawData = readFile(c.Args()[0])
if len(c.Args()) == 1 {
fmt.Println(string(rawData[:]))
os.Exit(0)
}
err := yaml.Unmarshal([]byte(rawData), &parsedData)
if err != nil {
log.Fatalf("error: %v", err)
@@ -61,32 +111,3 @@ func readFile(filename string) []byte {
}
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
}

View File

@@ -1,41 +1,22 @@
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())
var getValueTests = []struct {
argument string
expectedResult interface{}
testDescription string
}{
{"true", true, "boolean"},
{"\"true\"", "true", "boolean as string"},
{"3.4", 3.4, "number"},
{"\"3.4\"", "3.4", "number as string"},
}
func TestReadMap_simple(t *testing.T) {
result := readMap(parsedData, "b", []string{"c"})
if result != 2 {
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)
func TestGetValue(t *testing.T) {
for _, tt := range getValueTests {
assertResultWithContext(t, tt.expectedResult, getValue(tt.argument), tt.testDescription)
}
}