Add delete command

The delete (short option "d") will delete the YAML subtree at the
provided path in the specified file (or STDIN), if it the node exists.

More complex support is currently omitted, for example:

  - specify nodes to delete using an external script
  - deleting common elements from all elements of an array
This commit is contained in:
Matthew Huxtable 2018-05-04 16:46:58 +01:00 committed by Mike Farah
parent 26153b3eb5
commit 8072e66d46
4 changed files with 255 additions and 11 deletions

View File

@ -18,9 +18,7 @@ func entryInSlice(context yaml.MapSlice, key interface{}) *yaml.MapItem {
return nil
}
func writeMap(context interface{}, paths []string, value interface{}) yaml.MapSlice {
log.Debugf("writeMap for %v for %v with value %v\n", paths, context, value)
func getMapSlice(context interface{}) yaml.MapSlice {
var mapSlice yaml.MapSlice
switch context.(type) {
case yaml.MapSlice:
@ -28,6 +26,25 @@ func writeMap(context interface{}, paths []string, value interface{}) yaml.MapSl
default:
mapSlice = make(yaml.MapSlice, 0)
}
return mapSlice
}
func getArray(context interface{}) (array []interface{}, ok bool) {
switch context.(type) {
case []interface{}:
array = context.([]interface{})
ok = true
default:
array = make([]interface{}, 0)
ok = false
}
return
}
func writeMap(context interface{}, paths []string, value interface{}) yaml.MapSlice {
log.Debugf("writeMap for %v for %v with value %v\n", paths, context, value)
mapSlice := getMapSlice(context)
if len(paths) == 0 {
return mapSlice
@ -66,13 +83,7 @@ func updatedChildValue(child interface{}, remainingPaths []string, value interfa
func writeArray(context interface{}, paths []string, value interface{}) []interface{} {
log.Debugf("writeArray for %v for %v with value %v\n", paths, context, value)
var array []interface{}
switch context.(type) {
case []interface{}:
array = context.([]interface{})
default:
array = make([]interface{}, 0)
}
array, _ := getArray(context)
if len(paths) == 0 {
return array
@ -199,3 +210,93 @@ func mapToMapSlice(data map[interface{}]interface{}) yaml.MapSlice {
sort.SliceStable(mapSlice, func(i, j int) bool { return mapSlice[i].Key.(string) < mapSlice[j].Key.(string) })
return mapSlice
}
func deleteMap(context interface{}, paths []string) yaml.MapSlice {
log.Debugf("deleteMap for %v for %v\n", paths, context)
mapSlice := getMapSlice(context)
if len(paths) == 0 {
return mapSlice
}
var found bool
var index int
var child yaml.MapItem
for index, child = range mapSlice {
if child.Key == paths[0] {
found = true
break
}
}
if !found {
return mapSlice
}
remainingPaths := paths[1:]
var newSlice yaml.MapSlice
if len(remainingPaths) > 0 {
newChild := yaml.MapItem{Key: child.Key}
newChild.Value = deleteChildValue(child.Value, remainingPaths)
newSlice = make(yaml.MapSlice, len(mapSlice))
for i := range mapSlice {
item := mapSlice[i]
if i == index {
item = newChild
}
newSlice[i] = item
}
} else {
// Delete item from slice at index
newSlice = append(mapSlice[:index], mapSlice[index+1:]...)
log.Debugf("\tDeleted item index %d from mapSlice", index)
}
log.Debugf("\t\tlen: %d\tcap: %d\tslice: %v", len(mapSlice), cap(mapSlice), mapSlice)
log.Debugf("\tReturning mapSlice %v\n", mapSlice)
return newSlice
}
func deleteArray(context interface{}, paths []string, index int64) interface{} {
log.Debugf("deleteArray for %v for %v\n", paths, context)
array, ok := getArray(context)
if !ok {
// did not get an array
return context
}
if index >= int64(len(array)) {
return array
}
remainingPaths := paths[1:]
if len(remainingPaths) > 0 {
// Recurse into the array element at index
array[index] = deleteMap(array[index], remainingPaths)
} 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
}
func deleteChildValue(child interface{}, remainingPaths []string) interface{} {
log.Debugf("deleteChildValue for %v for %v\n", remainingPaths, child)
idx, nextIndexErr := strconv.ParseInt(remainingPaths[0], 10, 64)
if nextIndexErr != nil {
// must be a map
log.Debugf("\tdetected a map, invoking deleteMap\n")
return deleteMap(child, remainingPaths)
}
log.Debugf("\tdetected an array, so traversing element with index %d\n", idx)
return deleteArray(child, remainingPaths, idx)
}

View File

@ -308,3 +308,80 @@ func TestWriteArray_no_paths(t *testing.T) {
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))
}

60
yq.go
View File

@ -64,7 +64,13 @@ func newCommandCLI() *cobra.Command {
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(), createNewCmd(), createMergeCmd())
rootCmd.AddCommand(
createReadCmd(),
createWriteCmd(),
createDeleteCmd(),
createNewCmd(),
createMergeCmd(),
)
rootCmd.SetOutput(os.Stdout)
return rootCmd
@ -121,6 +127,26 @@ a.b.e:
return cmdWrite
}
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]",
@ -316,6 +342,38 @@ func write(cmd *cobra.Command, filename string, updatedData interface{}) error {
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")

View File

@ -122,3 +122,11 @@ func TestNewYaml_WithUnknownScript(t *testing.T) {
expectedOutput := `open fake-unknown: no such file or directory`
assertResult(t, expectedOutput, err.Error())
}
func TestDeleteYaml(t *testing.T) {
result, _ := deleteYaml([]string{"examples/sample.yaml", "b.c"})
formattedResult := fmt.Sprintf("%v", result)
assertResult(t,
"[{a Easy! as one two three} {b [{d [3 4]} {e [[{name fred} {value 3}] [{name sam} {value 4}]]}]}]",
formattedResult)
}