diff --git a/data_navigator.go b/data_navigator.go index c98983b6..1961aca5 100644 --- a/data_navigator.go +++ b/data_navigator.go @@ -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) +} diff --git a/data_navigator_test.go b/data_navigator_test.go index 540c737f..daae8798 100644 --- a/data_navigator_test.go +++ b/data_navigator_test.go @@ -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)) +} diff --git a/yq.go b/yq.go index 64720102..6538fb25 100644 --- a/yq.go +++ b/yq.go @@ -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 ") + } + + 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") diff --git a/yq_test.go b/yq_test.go index b00cc4b4..9df68df3 100644 --- a/yq_test.go +++ b/yq_test.go @@ -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) +}