package main import ( "bytes" "fmt" yaml "github.com/goccy/go-yaml" "github.com/spf13/cobra" "os" "reflect" ) var VERBOSITY = 2 func read(file string) []byte { data, err := os.ReadFile(file) if err != nil { panic(err) } return data } func parse(data []byte) yaml.MapSlice { var result yaml.MapSlice decoder := yaml.NewDecoder(bytes.NewReader(data), yaml.UseOrderedMap()) err := decoder.Decode(&result) if err != nil { panic(err) } return result } type TypeId int const ( Map TypeId = iota Slice Scalar ) func Type(elem any) TypeId { switch elem.(type) { case yaml.MapSlice: return Map case []any: return Slice default: return Scalar } } // hack to be able to compare slices and dictionires that cannot be put into a map. func strval(v any) string { return fmt.Sprintf("%v", v) } func subtract(yaml2 yaml.MapSlice, yaml1 yaml.MapSlice) yaml.MapSlice { res := make(yaml.MapSlice, 0) for _, item := range yaml2 { k := item.Key v2 := item.Value v1 := yaml1.ToMap()[k] switch { case reflect.DeepEqual(v1, v2): // delete is implicit by not copying to the output case Type(v1) == Map && Type(v2) == Map: diff := subtract(v2.(yaml.MapSlice), v1.(yaml.MapSlice)) if len(diff) > 0 { res = append(res, item) } case Type(v1) == Slice && Type(v2) == Slice: v2set := make(map[string]bool) v1set := make(map[string]bool) stringToValue := make(map[any]any) for _, v := range v2.([]any) { sval := strval(v) stringToValue[sval] = v v2set[sval] = true } for _, v := range v1.([]any) { v1set[strval(v)] = true } s := make([]any, 0) for k2, _ := range v2set { if v1set[k2] { if VERBOSITY == 2 { s = append(s, "") } else if VERBOSITY == 3 { s = append(s, stringToValue[k2]) } } else { s = append(s, stringToValue[k2]) } } res = append(res, yaml.MapItem{ Key: k, Value: s, }) default: res = append(res, item) } } return res } func execute(cmd *cobra.Command, args []string) error { if len(args) != 2 { return fmt.Errorf("Parameters expected") } if VERBOSITY < 1 || VERBOSITY > 3 { return fmt.Errorf("Array verbosity out of range") } file1 := os.Args[1] file2 := os.Args[2] yaml1 := parse(read(file1)) yaml2 := parse(read(file2)) yaml2 = subtract(yaml2, yaml1) enc := yaml.NewEncoder(os.Stdout, yaml.UseLiteralStyleIfMultiline(true), yaml.Indent(2), // Set indentation //yaml.UseOrderedMap(), // Preserve map order ) err := enc.Encode(yaml2) return err } func main() { cmd := &cobra.Command{ Use: "yamldiff ", Short: "Shows one-way difference between yaml files", Long: ` Shows the changes in compared to `, RunE: func(cmd *cobra.Command, args []string) error { return execute(cmd, args) }, } cmd.PersistentFlags().IntVarP(&VERBOSITY, "array-output-level", "v", 1, "Array output level: , 1: only show changed/added values, 2 (default) show identical as , 3: show all values") cmd.Execute() }