yq/yq.go

521 lines
13 KiB
Go
Raw Normal View History

2015-09-26 22:15:49 +00:00
package main
import (
"errors"
2015-09-28 02:00:38 +00:00
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
logging "github.com/op/go-logging"
"github.com/spf13/cobra"
yaml "gopkg.in/yaml.v2"
2015-09-26 22:15:49 +00:00
)
2015-10-06 05:07:09 +00:00
var trimOutput = true
var writeInplace = false
var writeScript = ""
2015-10-10 23:00:22 +00:00
var outputToJSON = false
var overwriteFlag = false
2017-04-11 23:16:54 +00:00
var verbose = false
var version = false
2017-12-17 22:11:08 +00:00
var log = logging.MustGetLogger("yq")
2015-10-06 05:07:09 +00:00
2015-09-26 22:15:49 +00:00
func main() {
cmd := newCommandCLI()
if err := cmd.Execute(); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
}
2017-04-11 23:16:54 +00:00
func newCommandCLI() *cobra.Command {
var rootCmd = &cobra.Command{
2017-12-17 22:11:08 +00:00
Use: "yq",
RunE: func(cmd *cobra.Command, args []string) error {
if version {
cmd.Print(GetVersionDisplay())
return nil
}
cmd.Println(cmd.UsageString())
return nil
},
PersistentPreRun: func(cmd *cobra.Command, args []string) {
var format = logging.MustStringFormatter(
`%{color}%{time:15:04:05} %{shortfunc} [%{level:.4s}]%{color:reset} %{message}`,
)
var backend = logging.AddModuleLevel(
logging.NewBackendFormatter(logging.NewLogBackend(os.Stderr, "", 0), format))
if verbose {
backend.SetLevel(logging.DEBUG, "")
} else {
backend.SetLevel(logging.ERROR, "")
}
logging.SetBackend(backend)
},
}
2015-10-13 10:42:36 +00:00
rootCmd.PersistentFlags().BoolVarP(&trimOutput, "trim", "t", true, "trim yaml output")
rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "output as json")
2017-04-11 23:16:54 +00:00
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode")
rootCmd.Flags().BoolVarP(&version, "version", "V", false, "Print version information and quit")
rootCmd.AddCommand(
createReadCmd(),
createWriteCmd(),
createDeleteCmd(),
createNewCmd(),
createMergeCmd(),
)
rootCmd.SetOutput(os.Stdout)
return rootCmd
2015-10-13 10:42:36 +00:00
}
func createReadCmd() *cobra.Command {
return &cobra.Command{
2015-10-06 05:07:09 +00:00
Use: "read [yaml_file] [path]",
Aliases: []string{"r"},
2017-12-17 22:11:08 +00:00
Short: "yq r sample.yaml a.b.c",
2015-10-06 05:07:09 +00:00
Example: `
2017-12-17 22:11:08 +00:00
yq read things.yaml a.b.c
yq r - a.b.c (reads from stdin)
yq r things.yaml a.*.c
yq r things.yaml a.array[0].blah
yq r things.yaml a.array[*].blah
2015-10-13 10:42:36 +00:00
`,
2015-10-06 05:07:09 +00:00
Long: "Outputs the value of the given path in the yaml file to STDOUT",
RunE: readProperty,
2015-09-28 02:00:38 +00:00
}
2015-10-13 10:42:36 +00:00
}
2015-09-26 23:38:44 +00:00
2015-10-13 10:42:36 +00:00
func createWriteCmd() *cobra.Command {
2015-10-06 05:07:09 +00:00
var cmdWrite = &cobra.Command{
Use: "write [yaml_file] [path] [value]",
Aliases: []string{"w"},
2017-12-17 22:11:08 +00:00
Short: "yq w [--inplace/-i] [--script/-s script_file] sample.yaml a.b.c newValueForC",
2015-10-06 05:07:09 +00:00
Example: `
2017-12-17 22:11:08 +00:00
yq write 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 --script update_script.yaml things.yaml
yq w -i -s update_script.yaml things.yaml
yq w things.yaml a.b.d[+] foo
yq w things.yaml a.b.d[+] foo
2015-10-13 10:42:36 +00:00
`,
2015-10-06 05:07:09 +00:00
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.
Append value to array adds the value to the end of array.
Update Scripts:
Note that you can give an update script to perform more sophisticated updated. Update script
format is a yaml map where the key is the path and the value is..well the value. e.g.:
---
a.b.c: true,
a.b.e:
- name: bob
`,
RunE: writeProperty,
2015-10-06 05:07:09 +00:00
}
cmdWrite.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
cmdWrite.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for updating yaml")
2015-10-13 10:42:36 +00:00
return cmdWrite
2015-10-06 05:07:09 +00:00
}
2015-09-29 06:29:32 +00:00
func createDeleteCmd() *cobra.Command {
var cmdDelete = &cobra.Command{
Use: "delete [yaml_file] [path]",
Aliases: []string{"d"},
Short: "yq d [--inplace/-i] sample.yaml a.b.c",
Example: `
yq delete things.yaml a.b.c
yq delete --inplace things.yaml a.b.c
yq d -i things.yaml a.b.c
yq d things.yaml a.b.c
`,
Long: `Deletes the given path from the YAML file.
Outputs to STDOUT unless the inplace flag is used, in which case the file is updated instead.
`,
RunE: deleteProperty,
}
cmdDelete.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
return cmdDelete
}
func createNewCmd() *cobra.Command {
var cmdNew = &cobra.Command{
Use: "new [path] [value]",
Aliases: []string{"n"},
2017-12-17 22:11:08 +00:00
Short: "yq n [--script/-s script_file] a.b.c newValueForC",
Example: `
2017-12-17 22:11:08 +00:00
yq new a.b.c cat
yq n a.b.c cat
yq n --script create_script.yaml
`,
Long: `Creates a new yaml w.r.t the given path and value.
Outputs to STDOUT
Create Scripts:
Note that you can give a create script to perform more sophisticated yaml. This follows the same format as the update script.
`,
RunE: newProperty,
}
cmdNew.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for updating yaml")
return cmdNew
}
func createMergeCmd() *cobra.Command {
var cmdMerge = &cobra.Command{
Use: "merge [initial_yaml_file] [additional_yaml_file]...",
Aliases: []string{"m"},
2017-12-17 22:11:08 +00:00
Short: "yq m [--inplace/-i] [--overwrite/-x] sample.yaml sample2.yaml",
Example: `
2017-12-17 22:11:08 +00:00
yq merge things.yaml other.yaml
yq merge --inplace things.yaml other.yaml
yq m -i things.yaml other.yaml
yq m --overwrite things.yaml other.yaml
yq m -i -x things.yaml other.yaml
`,
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.
If overwrite flag is set then existing values will be overwritten using the values from each additional yaml file.
`,
RunE: mergeProperties,
}
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")
return cmdMerge
}
func readProperty(cmd *cobra.Command, args []string) error {
data, err := read(args)
if err != nil {
return err
}
dataStr, err := toString(data)
if err != nil {
return err
2017-04-11 23:16:54 +00:00
}
cmd.Println(dataStr)
return nil
}
func read(args []string) (interface{}, error) {
2017-08-03 07:30:07 +00:00
var parsedData yaml.MapSlice
var path = ""
if len(args) < 1 {
return nil, errors.New("Must provide filename")
} else if len(args) > 1 {
2017-08-03 07:30:07 +00:00
path = args[1]
}
if err := readData(args[0], &parsedData); err != nil {
2017-08-03 07:30:07 +00:00
var generalData interface{}
if err = readData(args[0], &generalData); err != nil {
return nil, err
}
2017-08-03 07:30:07 +00:00
item := yaml.MapItem{Key: "thing", Value: generalData}
parsedData = yaml.MapSlice{item}
path = "thing." + path
}
2015-09-27 03:18:33 +00:00
if parsedData != nil && parsedData[0].Key == nil {
var interfaceData []map[interface{}]interface{}
if err := readData(args[0], &interfaceData); err == nil {
var listMap []yaml.MapSlice
for _, item := range interfaceData {
listMap = append(listMap, mapToMapSlice(item))
}
return readYamlArray(listMap, path)
}
}
2017-08-03 07:30:07 +00:00
if path == "" {
return parsedData, nil
2015-09-29 06:29:32 +00:00
}
2017-08-03 07:30:07 +00:00
var paths = parsePath(path)
2015-09-27 03:18:33 +00:00
return readMap(parsedData, paths[0], paths[1:])
2015-09-29 00:56:12 +00:00
}
func readYamlArray(listMap []yaml.MapSlice, path string) (interface{}, error) {
if path == "" {
return listMap, nil
}
var paths = parsePath(path)
if paths[0] == "*" {
if len(paths[1:]) == 0 {
return listMap, nil
}
var results []interface{}
for _, m := range listMap {
value, err := readMap(m, paths[1], paths[2:])
if err != nil {
return nil, err
}
results = append(results, value)
}
return results, nil
}
index, err := strconv.ParseInt(paths[0], 10, 64)
if err != nil {
return nil, fmt.Errorf("Error accessing array: %v", err)
}
if len(paths[1:]) == 0 {
return listMap[index], nil
}
return readMap(listMap[index], paths[1], paths[2:])
}
func newProperty(cmd *cobra.Command, args []string) error {
updatedData, err := newYaml(args)
if err != nil {
return err
}
dataStr, err := toString(updatedData)
if err != nil {
return err
}
cmd.Println(dataStr)
return nil
}
func newYaml(args []string) (interface{}, error) {
var writeCommands yaml.MapSlice
if writeScript != "" {
if err := readData(writeScript, &writeCommands); err != nil {
return nil, err
}
} else if len(args) < 2 {
return nil, errors.New("Must provide <path_to_update> <value>")
} else {
writeCommands = make(yaml.MapSlice, 1)
writeCommands[0] = yaml.MapItem{Key: args[0], Value: parseValue(args[1])}
}
2017-08-08 07:04:30 +00:00
var parsedData yaml.MapSlice
var prependCommand = ""
var isArray = strings.HasPrefix(writeCommands[0].Key.(string), "[")
if isArray {
item := yaml.MapItem{Key: "thing", Value: make(yaml.MapSlice, 0)}
parsedData = yaml.MapSlice{item}
prependCommand = "thing"
} else {
parsedData = make(yaml.MapSlice, 0)
}
2017-08-08 07:04:30 +00:00
return updateParsedData(parsedData, writeCommands, prependCommand)
}
func writeProperty(cmd *cobra.Command, args []string) error {
updatedData, err := updateYaml(args)
if err != nil {
return err
2017-04-11 23:16:54 +00:00
}
return write(cmd, args[0], updatedData)
}
func write(cmd *cobra.Command, filename string, updatedData interface{}) error {
if writeInplace {
dataStr, err := yamlToString(updatedData)
if err != nil {
return err
}
return ioutil.WriteFile(filename, []byte(dataStr), 0644)
}
dataStr, err := toString(updatedData)
if err != nil {
return err
}
cmd.Println(dataStr)
return nil
}
func deleteProperty(cmd *cobra.Command, args []string) error {
updatedData, err := deleteYaml(args)
if err != nil {
return err
}
return write(cmd, args[0], updatedData)
}
func deleteYaml(args []string) (interface{}, error) {
var parsedData yaml.MapSlice
var deletePath string
if len(args) < 2 {
return nil, errors.New("Must provide <filename> <path_to_delete>")
}
deletePath = args[1]
if err := readData(args[0], &parsedData); err != nil {
var generalData interface{}
if err = readData(args[0], &generalData); err != nil {
return nil, err
}
item := yaml.MapItem{Key: "thing", Value: generalData}
parsedData = yaml.MapSlice{item}
deletePath = "thing." + deletePath
}
path := parsePath(deletePath)
return deleteMap(parsedData, path), nil
}
func mergeProperties(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
return errors.New("Must provide at least 2 yaml files")
}
updatedData, err := mergeYaml(args)
if err != nil {
return err
}
return write(cmd, args[0], updatedData)
}
func mergeYaml(args []string) (interface{}, error) {
var updatedData map[interface{}]interface{}
for _, f := range args {
var parsedData map[interface{}]interface{}
if err := readData(f, &parsedData); err != nil {
return nil, err
}
if err := merge(&updatedData, parsedData, overwriteFlag); err != nil {
return nil, err
}
}
return mapToMapSlice(updatedData), nil
}
func updateParsedData(parsedData yaml.MapSlice, writeCommands yaml.MapSlice, prependCommand string) (interface{}, error) {
2017-08-08 06:55:57 +00:00
var prefix = ""
if prependCommand != "" {
prefix = prependCommand + "."
}
for _, entry := range writeCommands {
2017-08-08 06:55:57 +00:00
path := prefix + entry.Key.(string)
value := entry.Value
2017-08-08 06:55:57 +00:00
var paths = parsePath(path)
parsedData = writeMap(parsedData, paths, value)
}
2017-08-08 06:55:57 +00:00
if prependCommand != "" {
return readMap(parsedData, prependCommand, make([]string, 0))
}
return parsedData, nil
}
func updateYaml(args []string) (interface{}, error) {
var writeCommands yaml.MapSlice
2017-08-08 06:55:57 +00:00
var prependCommand = ""
if writeScript != "" {
if err := readData(writeScript, &writeCommands); err != nil {
return nil, err
}
} else if len(args) < 3 {
return nil, errors.New("Must provide <filename> <path_to_update> <value>")
} else {
writeCommands = make(yaml.MapSlice, 1)
writeCommands[0] = yaml.MapItem{Key: args[1], Value: parseValue(args[2])}
2015-09-29 06:29:32 +00:00
}
2017-02-26 22:01:52 +00:00
var parsedData yaml.MapSlice
if err := readData(args[0], &parsedData); err != nil {
2017-08-08 06:55:57 +00:00
var generalData interface{}
if err = readData(args[0], &generalData); err != nil {
return nil, err
}
2017-08-08 06:55:57 +00:00
item := yaml.MapItem{Key: "thing", Value: generalData}
parsedData = yaml.MapSlice{item}
prependCommand = "thing"
}
2017-08-08 06:55:57 +00:00
return updateParsedData(parsedData, writeCommands, prependCommand)
2015-10-06 05:07:09 +00:00
}
func parseValue(argument string) interface{} {
var value, err interface{}
var inQuotes = len(argument) > 0 && argument[0] == '"'
2015-10-03 07:25:13 +00:00
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)
}
2015-10-03 07:25:13 +00:00
return argument
}
2015-10-03 07:25:13 +00:00
return argument[1 : len(argument)-1]
}
func toString(context interface{}) (string, error) {
2015-10-10 23:00:22 +00:00
if outputToJSON {
return jsonToString(context)
2015-10-10 23:00:22 +00:00
}
return yamlToString(context)
}
func yamlToString(context interface{}) (string, error) {
switch context.(type) {
case string:
return context.(string), nil
default:
return marshalContext(context)
}
}
func marshalContext(context interface{}) (string, error) {
2015-09-29 01:05:28 +00:00
out, err := yaml.Marshal(context)
2015-09-29 01:05:28 +00:00
if err != nil {
return "", fmt.Errorf("error printing yaml: %v", err)
2015-09-29 01:05:28 +00:00
}
2015-09-29 06:29:32 +00:00
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
2015-10-06 05:07:09 +00:00
if trimOutput {
return strings.Trim(outStr, "\n "), nil
2017-08-03 07:30:07 +00:00
}
return outStr, nil
2017-08-03 07:30:07 +00:00
}
func readData(filename string, parsedData interface{}) error {
if filename == "" {
return errors.New("Must provide filename")
2015-09-28 02:00:38 +00:00
}
2015-10-05 04:48:34 +00:00
var rawData []byte
var err error
if filename == "-" {
rawData, err = ioutil.ReadAll(os.Stdin)
2015-10-05 04:48:34 +00:00
} else {
rawData, err = ioutil.ReadFile(filename)
2015-10-05 04:48:34 +00:00
}
2015-09-26 22:20:42 +00:00
2015-10-05 04:48:34 +00:00
if err != nil {
return err
2015-10-05 04:48:34 +00:00
}
return yaml.Unmarshal(rawData, parsedData)
2015-10-06 05:39:19 +00:00
}