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

Compare commits

..

18 Commits
1.0 ... 1.2

Author SHA1 Message Date
mfarah
15f51d4bf0 Can update a yaml file from an instruction yaml file 2015-10-08 10:31:31 +11:00
mfarah
219105f999 Test result order no longer matters 2015-10-07 13:36:39 +11:00
mfarah
955ecc2547 Removed usage of log 2015-10-06 16:39:19 +11:00
mfarah
1d5fbd5ad0 Use cobra for cli for nicer cli-ness 2015-10-06 16:17:28 +11:00
mfarah
1605938c2c Readme updates 2015-10-06 11:58:51 +11:00
mfarah
2780903b60 Fixed readme formatting 2015-10-06 11:51:36 +11:00
mfarah
81672d4efe Can now splat maps 2015-10-06 10:08:31 +11:00
mfarah
6a696c81db Individual tests now have their own test data 2015-10-06 10:02:40 +11:00
mfarah
805729e4f7 DRYd code 2015-10-06 09:25:55 +11:00
mfarah
7db31006da Fixed write bug: can now update yaml with single key in path 2015-10-05 16:03:11 +11:00
mfarah
8409be1cdf Removed debug printLn 2015-10-05 16:02:07 +11:00
mfarah
c03c4813d4 Can read from STDIN 2015-10-05 15:48:34 +11:00
mfarah
5ee6a9b9ca Updated README 2015-10-05 14:48:57 +11:00
mfarah
8efc46deae Fixed bad paths causing recursing down scalar values 2015-10-05 14:47:24 +11:00
mfarah
e4d5769f29 Added array splat functionality 2015-10-05 14:41:50 +11:00
Mike Farah
fea6e0db3d Clarified README 2015-10-04 17:07:39 +11:00
Mike Farah
6823d43325 Added ability to update yaml files inplace 2015-10-03 18:28:30 +10:00
Mike Farah
da4b1a6449 Minor Readme fix 2015-10-03 18:19:13 +10:00
8 changed files with 358 additions and 94 deletions

105
README.md
View File

@@ -1,7 +1,7 @@
# yaml # yaml
yaml command line tool written in go yaml is portable command line tool written in go
Allows you to read (and soon update) yaml files given a yaml path. All in a lovely dependency free binary! Allows you to read and update yaml files from bash (or whatever). All in a lovely dependency free binary!
[Download latest release](https://github.com/mikefarah/yaml/releases/latest) [Download latest release](https://github.com/mikefarah/yaml/releases/latest)
@@ -12,7 +12,7 @@ go get github.com/mikefarah/yaml
## Read examples ## Read examples
``` ```
yaml <yaml file> <path> yaml r <yaml file> <path>
``` ```
### Basic ### Basic
@@ -23,10 +23,37 @@ b:
``` ```
then then
```bash ```bash
yaml sample.yaml b.c yaml r sample.yaml b.c
``` ```
will output the value of '2'. will output the value of '2'.
### Reading from STDIN
Given a sample.yaml file of:
```bash
cat sample.yaml | yaml r - b.c
```
will output the value of '2'.
### Splat
Given a sample.yaml file of:
```yaml
---
bob:
item1:
cats: bananas
item2:
cats: apples
```
then
```bash
yaml r sample.yaml bob.*.cats
```
will output
```yaml
- bananas
- apples
```
### Handling '.' in the yaml key ### Handling '.' in the yaml key
Given a sample.yaml file of: Given a sample.yaml file of:
```yaml ```yaml
@@ -35,7 +62,7 @@ b.x:
``` ```
then then
```bash ```bash
yaml sample.yaml \"b.x\".c yaml r sample.yaml \"b.x\".c
``` ```
will output the value of '2'. will output the value of '2'.
@@ -52,11 +79,33 @@ b:
``` ```
then then
``` ```
yaml sample.yaml b.e[1].name yaml r sample.yaml b.e[1].name
``` ```
will output 'sam' will output 'sam'
### Updating yaml ### Array Splat
e.g.: given a sample file of
```yaml
b:
e:
- name: fred
value: 3
- name: sam
value: 4
```
then
```
yaml r sample.yaml b.e[*].name
```
will output:
```
- fred
- sam
```
## Update examples
### Update to stdout
Given a sample.yaml file of: Given a sample.yaml file of:
```yaml ```yaml
b: b:
@@ -64,10 +113,50 @@ b:
``` ```
then then
```bash ```bash
yaml w sample.yaml b.c 'cat' yaml w sample.yaml b.c cat
``` ```
will output: will output:
```yaml ```yaml
b: b:
c: cat c: cat
``` ```
### Updating yaml in-place
Given a sample.yaml file of:
```yaml
b:
c: 2
```
then
```bash
yaml w -i sample.yaml b.c cat
```
will update the sample.yaml file so that the value of 'c' is cat.
### Updating multiple values with a script
Given a sample.yaml file of:
```yaml
b:
c: 2
e:
- name: Billy Bob
```
and a script update_instructions.yaml of:
```yaml
b.c: 3
b.e[0].name: Howdy Partner
```
then
```bash
yaml -w -s update_instructions.yaml sample.yaml
```
will output:
```yaml
b:
c: 3
e:
- name: Howdy Partner
```

View File

@@ -2,38 +2,58 @@ package main
import ( import (
// "fmt" // "fmt"
"log"
"strconv" "strconv"
) )
func write(context map[interface{}]interface{}, head string, tail []string, value interface{}) { 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... if len(tail) == 0 {
toUpdate := readMap(context, head, tail[0:len(tail)-1]).(map[interface{}]interface{}) context[head] = value
// and then set the 'c' key. } else {
key := (tail[len(tail)-1]) // e.g. if updating a.b.c, we need to get the 'b' map...
toUpdate[key] = value 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{} { func readMap(context map[interface{}]interface{}, head string, tail []string) interface{} {
value := context[head] if head == "*" {
if len(tail) > 0 { return readMapSplat(context, tail)
return recurse(value, tail[0], tail[1:len(tail)])
} }
return value value := context[head]
return calculateValue(value, tail)
}
func readMapSplat(context map[interface{}]interface{}, tail []string) interface{} {
var newArray = make([]interface{}, len(context))
var i = 0
for _, value := range context {
if len(tail) > 0 {
newArray[i] = recurse(value, tail[0], tail[1:len(tail)])
} else {
newArray[i] = value
}
i++
}
return newArray
} }
func recurse(value interface{}, head string, tail []string) interface{} { func recurse(value interface{}, head string, tail []string) interface{} {
switch value.(type) { switch value.(type) {
case []interface{}: case []interface{}:
if head == "*" {
return readArraySplat(value.([]interface{}), tail)
}
index, err := strconv.ParseInt(head, 10, 64) index, err := strconv.ParseInt(head, 10, 64)
if err != nil { if err != nil {
log.Fatalf("Error accessing array: %v", err) die("Error accessing array: %v", err)
} }
return readArray(value.([]interface{}), index, tail) return readArray(value.([]interface{}), index, tail)
case nil: case map[interface{}]interface{}:
return nil
default:
return readMap(value.(map[interface{}]interface{}), head, tail) return readMap(value.(map[interface{}]interface{}), head, tail)
default:
return nil
} }
} }
@@ -43,6 +63,19 @@ func readArray(array []interface{}, head int64, tail []string) interface{} {
} }
value := array[head] value := array[head]
return calculateValue(value, tail)
}
func readArraySplat(array []interface{}, tail []string) interface{} {
var newArray = make([]interface{}, len(array))
for index, value := range array {
newArray[index] = calculateValue(value, tail)
}
return newArray
}
func calculateValue(value interface{}, tail []string) interface{} {
if len(tail) > 0 { if len(tail) > 0 {
return recurse(value, tail[0], tail[1:len(tail)]) return recurse(value, tail[0], tail[1:len(tail)])
} }

View File

@@ -4,59 +4,141 @@ import (
"fmt" "fmt"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"os" "os"
"sort"
"testing" "testing"
) )
var rawData = ` func parseData(rawData string) map[interface{}]interface{} {
a: Easy! var parsedData map[interface{}]interface{}
b:
c: 2
d: [3, 4]
`
var parsedData map[interface{}]interface{}
func TestMain(m *testing.M) {
err := yaml.Unmarshal([]byte(rawData), &parsedData) err := yaml.Unmarshal([]byte(rawData), &parsedData)
if err != nil { if err != nil {
fmt.Println("Error parsing yaml: %v", err) fmt.Println("Error parsing yaml: %v", err)
os.Exit(1) os.Exit(1)
} }
return parsedData
os.Exit(m.Run())
} }
func TestReadMap_simple(t *testing.T) { func TestReadMap_simple(t *testing.T) {
assertResult(t, 2, readMap(parsedData, "b", []string{"c"})) var data = parseData(`
---
b:
c: 2
`)
assertResult(t, 2, readMap(data, "b", []string{"c"}))
}
func TestReadMap_splat(t *testing.T) {
var data = parseData(`
---
mapSplat:
item1: things
item2: whatever
`)
var result = readMap(data, "mapSplat", []string{"*"}).([]interface{})
var actual = []string{result[0].(string), result[1].(string)}
sort.Strings(actual)
assertResult(t, "[things whatever]", fmt.Sprintf("%v", actual))
}
func TestReadMap_deep_splat(t *testing.T) {
var data = parseData(`
---
mapSplatDeep:
item1:
cats: bananas
item2:
cats: apples
`)
var result = readMap(data, "mapSplatDeep", []string{"*", "cats"}).([]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) { func TestReadMap_key_doesnt_exist(t *testing.T) {
assertResult(t, nil, readMap(parsedData, "b.x.f", []string{"c"})) var data = parseData(`
---
b:
c: 2
`)
assertResult(t, nil, readMap(data, "b.x.f", []string{"c"}))
}
func TestReadMap_recurse_against_string(t *testing.T) {
var data = parseData(`
---
a: cat
`)
assertResult(t, nil, readMap(data, "a", []string{"b"}))
} }
func TestReadMap_with_array(t *testing.T) { func TestReadMap_with_array(t *testing.T) {
assertResult(t, 4, readMap(parsedData, "b", []string{"d", "1"})) var data = parseData(`
---
b:
d:
- 3
- 4
`)
assertResult(t, 4, readMap(data, "b", []string{"d", "1"}))
} }
func TestReadMap_with_array_out_of_bounds(t *testing.T) { func TestReadMap_with_array_out_of_bounds(t *testing.T) {
assertResult(t, nil, readMap(parsedData, "b", []string{"d", "3"})) var data = parseData(`
---
b:
d:
- 3
- 4
`)
assertResult(t, nil, readMap(data, "b", []string{"d", "3"}))
}
func TestReadMap_with_array_splat(t *testing.T) {
var data = parseData(`
e:
-
name: Fred
thing: cat
-
name: Sam
thing: dog
`)
assertResult(t, "[Fred Sam]", fmt.Sprintf("%v", readMap(data, "e", []string{"*", "name"})))
} }
func TestWrite_simple(t *testing.T) { func TestWrite_simple(t *testing.T) {
var data = parseData(`
b:
c: 2
`)
write(parsedData, "b", []string{"c"}, "4") write(data, "b", []string{"c"}, "4")
b := parsedData["b"].(map[interface{}]interface{}) b := data["b"].(map[interface{}]interface{})
assertResult(t, "4", b["c"].(string)) assertResult(t, "4", b["c"].(string))
} }
func TestWrite_with_no_tail(t *testing.T) {
var data = parseData(`
b:
c: 2
`)
write(data, "b", []string{}, "4")
b := data["b"]
assertResult(t, "4", fmt.Sprintf("%v", b))
}
func assertResult(t *testing.T, expectedValue interface{}, actualValue interface{}) { func assertResult(t *testing.T, expectedValue interface{}, actualValue interface{}) {
if expectedValue != actualValue { if expectedValue != actualValue {
t.Error("Expected <", expectedValue, "> but got <", 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{}) {
if expectedValue != actualValue { if expectedValue != actualValue {
t.Error(context) t.Error(context)
t.Error(": expected <", expectedValue, "> but got <", actualValue, ">") t.Error(": expected <", expectedValue, "> but got <", actualValue, ">")

2
instruction_sample.yaml Normal file
View File

@@ -0,0 +1,2 @@
b.c: cat
b.e[0].name: Mike Farah

View File

@@ -6,7 +6,7 @@ go test
# acceptance test # acceptance test
go build go build
X=$(./yaml sample.yaml b.c) X=$(./yaml r sample.yaml b.c)
if [ $X != 2 ] if [ $X != 2 ]
then then

View File

@@ -1,4 +1,9 @@
a: Easy! as one two three a: Easy! as one two three
b: b:
c: 3 c: things
d: [3, 4] d: whatever
things:
thing1:
cat: 'fred'
thing2:
cat: 'sam'

145
yaml.go
View File

@@ -2,69 +2,96 @@ package main
import ( import (
"fmt" "fmt"
"github.com/codegangsta/cli" "github.com/spf13/cobra"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"strconv" "strconv"
"strings" "strings"
) )
var trimOutput = true
var writeInplace = false
var writeScript = ""
func main() { func main() {
app := cli.NewApp() var cmdRead = &cobra.Command{
app.Name = "yaml" Use: "read [yaml_file] [path]",
app.Usage = "command line tool for reading and writing yaml" Aliases: []string{"r"},
app.Commands = []cli.Command{ Short: "yaml r sample.yaml a.b.c",
{ Example: `
Name: "read", yaml read things.yaml a.b.c
Aliases: []string{"r"}, yaml r - a.b.c (reads from stdin)
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", yaml r things.yaml a.*.c
Action: readProperty, yaml r things.yaml a.array[0].blah
}, yaml r things.yaml a.array[*].blah
{ `,
Name: "write", Long: "Outputs the value of the given path in the yaml file to STDOUT",
Aliases: []string{"w"}, Run: readProperty,
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) var cmdWrite = &cobra.Command{
Use: "write [yaml_file] [path] [value]",
Aliases: []string{"w"},
Short: "yaml w [--inplace/-i] sample.yaml a.b.c newValueForC",
Example: `
yaml write things.yaml a.b.c cat
yaml write --inplace things.yaml a.b.c cat
yaml w -i things.yaml a.b.c cat
`,
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.`,
Run: writeProperty,
}
cmdWrite.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
cmdWrite.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for updating yaml")
var rootCmd = &cobra.Command{Use: "yaml"}
rootCmd.PersistentFlags().BoolVarP(&trimOutput, "trim", "t", true, "trim yaml output")
rootCmd.AddCommand(cmdRead, cmdWrite)
rootCmd.Execute()
} }
func readProperty(c *cli.Context) { func readProperty(cmd *cobra.Command, args []string) {
var parsedData map[interface{}]interface{} var parsedData map[interface{}]interface{}
readYaml(c, &parsedData) readYaml(args[0], &parsedData)
if len(c.Args()) == 1 { if len(args) == 1 {
printYaml(parsedData) printYaml(parsedData)
os.Exit(0) os.Exit(0)
} }
var paths = parsePath(c.Args()[1]) var paths = parsePath(args[1])
printYaml(readMap(parsedData, paths[0], paths[1:len(paths)])) printYaml(readMap(parsedData, paths[0], paths[1:len(paths)]))
} }
func writeProperty(c *cli.Context) { func writeProperty(cmd *cobra.Command, args []string) {
var parsedData map[interface{}]interface{} var writeCommands map[string]interface{}
readYaml(c, &parsedData) if writeScript != "" {
readYaml(writeScript, &writeCommands)
if len(c.Args()) < 3 { } else if len(args) < 3 {
log.Fatalf("Must provide <filename> <path_to_update> <value>") die("Must provide <filename> <path_to_update> <value>")
} else {
writeCommands[args[1]] = parseValue(args[2])
} }
var paths = parsePath(c.Args()[1]) var parsedData map[interface{}]interface{}
readYaml(args[0], &parsedData)
write(parsedData, paths[0], paths[1:len(paths)], getValue(c.Args()[2])) for path, value := range writeCommands {
var paths = parsePath(path)
write(parsedData, paths[0], paths[1:len(paths)], value)
}
printYaml(parsedData) if writeInplace {
ioutil.WriteFile(args[0], []byte(yamlToString(parsedData)), 0644)
} else {
printYaml(parsedData)
}
} }
func parseValue(argument string) interface{} {
func getValue(argument string) interface{} {
var value, err interface{} var value, err interface{}
var inQuotes = argument[0] == '"' var inQuotes = argument[0] == '"'
if !inQuotes { if !inQuotes {
@@ -82,32 +109,58 @@ func getValue(argument string) interface{} {
} }
func printYaml(context interface{}) { func printYaml(context interface{}) {
fmt.Println(yamlToString(context))
}
func yamlToString(context interface{}) string {
out, err := yaml.Marshal(context) out, err := yaml.Marshal(context)
if err != nil { if err != nil {
log.Fatalf("error printing yaml: %v", err) die("error printing yaml: %v", err)
} }
outStr := string(out) outStr := string(out)
// trim the trailing new line as it's easier for a script to add // trim the trailing new line as it's easier for a script to add
// it in if required than to remove it // it in if required than to remove it
fmt.Println(strings.Trim(outStr, "\n ")) if trimOutput {
return strings.Trim(outStr, "\n ")
}
return outStr
} }
func readYaml(c *cli.Context, parsedData *map[interface{}]interface{}) { func readYaml(filename string, parsedData interface{}) {
if len(c.Args()) == 0 { if filename == "" {
log.Fatalf("Must provide filename") die("Must provide filename")
} }
var rawData = readFile(c.Args()[0])
err := yaml.Unmarshal([]byte(rawData), &parsedData) var rawData []byte
if err != nil { if filename == "-" {
log.Fatalf("error: %v", err) rawData = readStdin()
} else {
rawData = readFile(filename)
} }
err := yaml.Unmarshal([]byte(rawData), parsedData)
if err != nil {
die("error: %v", err)
}
}
func readStdin() []byte {
bytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
die("error reading stdin", err)
}
return bytes
} }
func readFile(filename string) []byte { func readFile(filename string) []byte {
var rawData, readError = ioutil.ReadFile(filename) var rawData, readError = ioutil.ReadFile(filename)
if readError != nil { if readError != nil {
log.Fatalf("error: %v", readError) die("error: %v", readError)
} }
return rawData return rawData
} }
func die(message ...interface{}) {
fmt.Println(message)
os.Exit(1)
}

View File

@@ -4,7 +4,7 @@ import (
"testing" "testing"
) )
var getValueTests = []struct { var parseValueTests = []struct {
argument string argument string
expectedResult interface{} expectedResult interface{}
testDescription string testDescription string
@@ -15,8 +15,8 @@ var getValueTests = []struct {
{"\"3.4\"", "3.4", "number as string"}, {"\"3.4\"", "3.4", "number as string"},
} }
func TestGetValue(t *testing.T) { func TestParseValue(t *testing.T) {
for _, tt := range getValueTests { for _, tt := range parseValueTests {
assertResultWithContext(t, tt.expectedResult, getValue(tt.argument), tt.testDescription) assertResultWithContext(t, tt.expectedResult, parseValue(tt.argument), tt.testDescription)
} }
} }