Compare commits

...

21 Commits
v0.0.5 ... main

Author SHA1 Message Date
Erik Brakkee
6162670570 Added the parse option to yamltool because it gives much better error messages than yuamllint and yq. 2025-11-30 17:11:11 +01:00
Erik Brakkee
ef0ef6e215 preserving ordering in arrays. 2025-01-10 19:38:43 +01:00
Erik Brakkee
56844a3c24 can now also merge resources and do a symmetric diff. 2025-01-07 22:07:31 +01:00
Erik Brakkee
f52507aa8f Merge branch 'main' of https://git.wamblee.org/public/gotools 2025-01-07 09:29:13 +01:00
Erik Brakkee
191c32b743 before merging in essential fixes. 2025-01-07 09:27:12 +01:00
0f8c2f7666 Merge branch 'main' of https://git.wamblee.org/public/gotools 2025-01-07 09:22:04 +01:00
677f7d57ba fixed issue where nested was lost an in general nested maps did not
work.
2025-01-07 09:20:15 +01:00
Erik Brakkee
ec7a29598d added go.sum (by go mod tidy) 2025-01-06 16:13:30 +01:00
47bb4af991 added install option to Makefile 2024-12-25 00:10:11 +01:00
1adc47377f added option for configuring the output for arrays. 2024-12-25 00:08:36 +01:00
ebd2bc41d4 yamldiff updates with better array diff 2024-12-24 23:27:56 +01:00
3a6a913ef2 yamldiff re-implementation in go 2024-12-24 22:39:09 +01:00
03a1a4e132 adding build info 2024-12-24 22:38:49 +01:00
b71e4db7db failures not counted correctly in some cases.
Summary output added.
2024-12-13 22:24:18 +01:00
0a4ed3a95f First version of a tool to list used dependencies and their licenses. 2024-11-24 23:27:55 +01:00
f269c95ce4 fixing illegal chars (just in case), copied from go-junit-report 2024-11-22 22:56:30 +01:00
3ce73b8db9 fixed problem with testcase test 2024-11-22 22:45:58 +01:00
fe733f96d6 simplification and elapsed time now available everywhere. 2024-11-22 22:42:02 +01:00
7cf5827f22 added time 2024-11-22 22:03:23 +01:00
09fd2cb2a4 simple testcase names 2024-11-22 21:50:28 +01:00
74383ddcc0 added a prefix option to separate test results 2024-11-22 21:34:52 +01:00
14 changed files with 706 additions and 107 deletions

View File

@ -14,6 +14,9 @@ build: vet
mkdir -p bin mkdir -p bin
go build -o bin ./cmd/... go build -o bin ./cmd/...
install:
go install ./...
test: build test: build
go test -count=1 -coverprofile=testout/coverage.out ${TESTFLAGS} ./... go test -count=1 -coverprofile=testout/coverage.out ${TESTFLAGS} ./...

25
cmd/go2junit/escape.go Normal file
View File

@ -0,0 +1,25 @@
package main
import "strings"
// copied from go-junit-report for fixing issues with chars that
// are out of range.
// from encoding/xml/xml.go, replace chars by unknown char
func isInCharacterRange(r rune) (inrange bool) {
return r == 0x09 ||
r == 0x0A ||
r == 0x0D ||
r >= 0x20 && r <= 0xD7FF ||
r >= 0xE000 && r <= 0xFFFD ||
r >= 0x10000 && r <= 0x10FFFF
}
func escapeIllegalChars(str string) string {
return strings.Map(func(r rune) rune {
if isInCharacterRange(r) {
return r
}
return '\uFFFD'
}, str)
}

View File

@ -5,16 +5,26 @@ import (
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"log"
"os" "os"
"path/filepath" "path/filepath"
"runtime/debug"
"strings"
) )
func getVersion() string {
if info, ok := debug.ReadBuildInfo(); ok {
return info.Main.Version
}
return "unknown"
}
func (t *Test) MarshalXML(e *xml.Encoder, start xml.StartElement) error { func (t *Test) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if len(t.Tests) == 0 { if len(t.Tests) == 0 {
start.Name = xml.Name{Local: "testcase"} start.Name = xml.Name{Local: "testcase"}
classname := "" classname := ""
if t.Parent != nil { if t.parent != nil {
classname = t.Parent.Name classname = t.parent.Name
} }
var skipped *Result var skipped *Result
if t.Skipped > 0 { if t.Skipped > 0 {
@ -28,8 +38,10 @@ func (t *Test) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
Message: "failed", Message: "failed",
} }
} }
log.Printf("TIME %f", t.Time)
parts := strings.Split(t.Name, "/")
tc := Testcase{ tc := Testcase{
Name: t.Name, Name: parts[len(parts)-1],
// parent class name // parent class name
Classname: classname, Classname: classname,
Time: t.Time, Time: t.Time,
@ -46,61 +58,13 @@ func (t *Test) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
} }
func main() { func main() {
/*
testsuites := Testsuites{
Time: 1.23,
TImestamp: time.Now(),
Suites: []*Testsuite{
{
Name: "hello",
Tests: 0,
Failures: 0,
Errors: 0,
Disabled: 0,
Package: "",
Skipped: 0,
Time: "",
Timestamp: time.Now(),
Testsuites: []*Testsuite{
{
Name: "abc",
Tests: 0,
Failures: 0,
Errors: 0,
Disabled: 0,
Package: "",
Skipped: 0,
Time: "",
Timestamp: time.Time{},
Testsuites: nil,
Testcases: []*Testcase{
{
Name: "test",
Classname: "",
Time: "",
Skipped: nil,
Error: &Result{
Message: "error",
},
Failure: nil,
SystemOut: "",
},
},
SystemOut: "ddd",
},
},
Testcases: nil,
SystemOut: "hello",
},
},
}
*/ fmt.Fprintf(os.Stderr, "go2junit version %s\n", getVersion())
testsuites := Testsuites{} testsuites := Testsuites{}
if len(os.Args) != 2 { if len(os.Args) != 2 && len(os.Args) != 3 {
fmt.Fprintf(os.Stderr, "Usage: go2junit <outputdir> \n") fmt.Fprintf(os.Stderr, "Usage: go2junit <outputdir> [<prefix>]\n")
os.Exit(1) os.Exit(1)
} }
path := os.Args[1] path := os.Args[1]
@ -109,6 +73,10 @@ func main() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
prefix := ""
if len(os.Args) == 3 {
prefix = os.Args[2]
}
var file = os.Stdin var file = os.Stdin
@ -127,26 +95,28 @@ func main() {
//.fmt.Printf("Parsed %d:\n%v\n\n", lineno, item) //.fmt.Printf("Parsed %d:\n%v\n\n", lineno, item)
pkg := prefix + item.Package
switch item.Action { switch item.Action {
case "start": case "start":
testsuites.Suite(item.Time, item.Package) testsuites.Start(item.Time, pkg)
case "run": case "run":
testsuites.Test(item.Time, item.Package, item.Test) fmt.Println()
testsuites.Test(item.Time, pkg, item.Test)
case "output": case "output":
testsuites.Output(item.Time, item.Package, item.Test, item.Output) testsuites.Output(item.Time, pkg, item.Test, escapeIllegalChars(item.Output))
fmt.Printf("%s", item.Output) fmt.Printf("%s", item.Output)
case "pause": case "pause":
testsuites.Output(item.Time, item.Package, item.Test, "PAUSED") testsuites.Output(item.Time, pkg, item.Test, "PAUSED")
case "cont": case "cont":
testsuites.Output(item.Time, item.Package, item.Test, "CONTINUED") testsuites.Output(item.Time, pkg, item.Test, "CONTINUED")
case "pass": case "pass":
testsuites.Pass(item.Time, item.Package, item.Test, item.Elapsed) testsuites.Pass(item.Time, pkg, item.Test)
case "bench": case "bench":
testsuites.Bench(item.Time, item.Package, item.Test, item.Output, item.Elapsed) testsuites.Bench(item.Time, pkg, item.Test, escapeIllegalChars(item.Output))
case "fail": case "fail":
testsuites.Fail(item.Time, item.Package, item.Test, item.Elapsed) testsuites.Fail(item.Time, pkg, item.Test)
case "skip": case "skip":
testsuites.Skip(item.Time, item.Package, item.Test) testsuites.Skip(item.Time, pkg, item.Test)
} }
} }
testsuites.Complete() testsuites.Complete()
@ -169,4 +139,35 @@ func main() {
} }
} }
fmt.Printf("\n\nSUMMARY\n\n")
fmt.Printf("%-60s %-10s %-10s %-10s %-10s %-10s %-10s\n\n", "SUITE", "COUNT", "PASSED", "FAILURES", "ERRORS", "DISABLED", "SKIPPED")
for _, suite := range testsuites.Suites {
fmt.Printf("%-60s %-10d %-10d %-10d %-10d %-10d %-10d\n",
suite.Name,
suite.TestCount,
suite.TestCount-suite.Failures-suite.Errors-suite.Skipped-suite.Disabled,
suite.Failures, suite.Errors, suite.Disabled, suite.Skipped)
}
fmt.Printf("\n%-60s %-10d %-10d %-10d %-10d %-10d %-10d\n",
"TOTAL",
testsuites.Tests,
testsuites.Tests-testsuites.Failures-testsuites.Errors-testsuites.Skipped-testsuites.Disabled,
testsuites.Failures, testsuites.Errors, testsuites.Disabled, testsuites.Skipped)
if testsuites.Failures+testsuites.Errors+testsuites.Skipped+testsuites.Disabled > 0 {
fmt.Printf("\nFAILED TESTS\n\n")
printFailedTests("", "", testsuites.Suites)
os.Exit(1)
}
os.Exit(0)
}
func printFailedTests(indent string, parentTest string, tests []*Test) {
for _, test := range tests {
if test.Failures > 0 {
testName := strings.TrimPrefix(test.Name, parentTest+"/")
fmt.Printf("%s%s\n", indent, testName)
printFailedTests(indent+" ", test.Name, test.Tests)
}
}
} }

View File

@ -2,7 +2,6 @@ package main
import ( import (
"encoding/xml" "encoding/xml"
"log"
"strings" "strings"
"time" "time"
) )
@ -40,7 +39,9 @@ type Test struct {
Tests []*Test `xml:"testsuite,omitempty"` Tests []*Test `xml:"testsuite,omitempty"`
SystemOut string `xml:"system-out,omitempty"` SystemOut string `xml:"system-out,omitempty"`
Parent *Test `xml:"-"` hasTests bool
parent *Test
t0 time.Time
} }
type Result struct { type Result struct {
@ -75,8 +76,8 @@ func (testsuites *Testsuites) getRootSuite(t time.Time, pkg string, create bool)
Name: pkg, Name: pkg,
Skipped: 0, Skipped: 0,
Timestamp: t, Timestamp: t,
t0: t,
} }
log.Printf("Adding suite %s", pkg)
testsuites.Suites = append(testsuites.Suites, &suite) testsuites.Suites = append(testsuites.Suites, &suite)
return &suite return &suite
} }
@ -90,12 +91,16 @@ func (suite *Test) getSuite(t time.Time, name string) *Test {
s := Test{ s := Test{
Name: suite.Name + "/" + name, Name: suite.Name + "/" + name,
Timestamp: t, Timestamp: t,
t0: t,
} }
suite.Tests = append(suite.Tests, &s) suite.Tests = append(suite.Tests, &s)
return &s return &s
} }
func (suite *Test) getTest(t time.Time, testname string) *Test { func (suite *Test) getTest(t time.Time, testname string) *Test {
if testname == "" {
return suite
}
suitename := suite.Name suitename := suite.Name
path := strings.Split(testname, "/") path := strings.Split(testname, "/")
for i := 0; i < len(path); i++ { for i := 0; i < len(path); i++ {
@ -111,75 +116,53 @@ func (testsuites *Testsuites) getTest(t time.Time, pkg string, testname string)
return test return test
} }
func (testsuites *Testsuites) Suite(t time.Time, pkg string) { func (testsuites *Testsuites) Start(t time.Time, pkg string) {
testsuites.getRootSuite(t, pkg, true) testsuites.getRootSuite(t, pkg, true)
} }
func (testsuites *Testsuites) Test(t time.Time, pkg string, test string) { func (testsuites *Testsuites) Test(t time.Time, pkg string, test string) {
// This can be a test suite as well // This can be a test suite as well
testsuites.getTest(t, pkg, test) testobj := testsuites.getTest(t, pkg, test)
testobj.hasTests = true
} }
func (testsuites *Testsuites) Output(t time.Time, pkg string, test string, output string) { func (testsuites *Testsuites) Output(t time.Time, pkg string, test string, output string) {
if test == "" {
ts := testsuites.getRootSuite(t, pkg, true)
ts.SystemOut = ts.SystemOut + output
return
}
ts := testsuites.getRootSuite(t, pkg+"/"+test, false)
if ts != nil {
ts.SystemOut = ts.SystemOut + output
return
}
tc := testsuites.getTest(t, pkg, test) tc := testsuites.getTest(t, pkg, test)
tc.SystemOut = tc.SystemOut + output tc.SystemOut = tc.SystemOut + output
} }
func (testsuites *Testsuites) Pass(t time.Time, pkg string, test string, elapsed float64) { func (testsuites *Testsuites) Pass(t time.Time, pkg string, test string) {
if test == "" { tc := testsuites.getTest(t, pkg, test)
return tc.Time = float64(t.Sub(tc.t0).Nanoseconds()) / 1000_000_000.0
}
if testsuites.getRootSuite(t, pkg+"/"+test, false) != nil {
return
}
} }
func (testsuites *Testsuites) Bench(t time.Time, pkg string, test string, output string, elapsed float64) { func (testsuites *Testsuites) Bench(t time.Time, pkg string, test string, output string) {
tc := testsuites.getTest(t, pkg, test) tc := testsuites.getTest(t, pkg, test)
tc.SystemOut = tc.SystemOut + output + "\n" tc.SystemOut = tc.SystemOut + output + "\n"
} }
func (testsuites *Testsuites) Fail(t time.Time, pkg string, test string, elapsed float64) { func (testsuites *Testsuites) Fail(t time.Time, pkg string, test string) {
if test == "" {
return
}
if testsuites.getRootSuite(t, pkg+"/"+test, false) != nil {
return
}
tc := testsuites.getTest(t, pkg, test) tc := testsuites.getTest(t, pkg, test)
tc.Time = elapsed tc.Time = float64(t.Sub(tc.t0).Nanoseconds()) / 1000_000_000.0
tc.Failures = 1 tc.Failures = 1
} }
func (testsuites *Testsuites) Skip(t time.Time, pkg string, test string) { func (testsuites *Testsuites) Skip(t time.Time, pkg string, test string) {
if test == "" {
return
}
if testsuites.getRootSuite(t, pkg+"/"+test, false) != nil {
return
}
tc := testsuites.getTest(t, pkg, test) tc := testsuites.getTest(t, pkg, test)
tc.Time = float64(t.Sub(tc.t0).Nanoseconds()) / 1000_000_000.0
tc.Skipped = 1 tc.Skipped = 1
} }
func (suite *Test) Complete() { func (suite *Test) Complete() {
suite.TestCount = 0 suite.TestCount = 0
suite.Failures = 0 if len(suite.Tests) > 0 {
suite.Errors = 0 suite.Failures = 0
suite.Disabled = 0 suite.Errors = 0
suite.Skipped = 0 suite.Disabled = 0
suite.Skipped = 0
}
if len(suite.Tests) == 0 { if len(suite.Tests) == 0 && suite.hasTests {
suite.TestCount = 1 suite.TestCount = 1
} }
@ -190,7 +173,7 @@ func (suite *Test) Complete() {
suite.Errors += ts.Errors suite.Errors += ts.Errors
suite.Disabled += ts.Disabled suite.Disabled += ts.Disabled
suite.Skipped += ts.Skipped suite.Skipped += ts.Skipped
ts.Parent = suite ts.parent = suite
} }
} }

40
cmd/golicenses/input.go Normal file
View File

@ -0,0 +1,40 @@
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
)
type ModuleDependency struct {
Path string
Version string
Dir string
Indirect bool
License string
}
func pareseGoMod() []ModuleDependency {
// Get list of all modules
cmd := exec.Command("go", "list", "-m", "-json", "all")
output, err := cmd.Output()
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting module list: %v\n", err)
os.Exit(1)
}
// Split the JSON objects (one per line)
modules := []ModuleDependency{}
decoder := json.NewDecoder(strings.NewReader(string(output)))
for decoder.More() {
var mod ModuleDependency
if err := decoder.Decode(&mod); err != nil {
fmt.Fprintf(os.Stderr, "Error decoding module: %v\n", err)
continue
}
modules = append(modules, mod)
}
return modules
}

17
cmd/golicenses/main.go Normal file
View File

@ -0,0 +1,17 @@
package main
func main() {
modules := pareseGoMod()
module := NewModule(modules)
module.GenerateLicenseNames()
module.DumpOverview()
module.DumpText(true)
}
func truncateString(s string, length int) string {
if len(s) <= length {
return s
}
return s[:length-3] + "..."
}

193
cmd/golicenses/output.go Normal file
View File

@ -0,0 +1,193 @@
package main
import (
"crypto/sha512"
"encoding/base64"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
)
type LicenseHash string
type License struct {
LicenseType string
Text string
// unique name of a license.
LicenseName string
}
func hash(data string) string {
hasher := sha512.New()
hasher.Write([]byte(data))
hash := hasher.Sum(nil)
return base64.StdEncoding.EncodeToString(hash)
}
func detectLicenseType(content string) string {
content = strings.ToLower(content)
licenses := map[string]string{
"mit": "MIT License",
"apache license": "Apache License",
"bsd": "BSD License",
"gnu general public": "GPL",
"mozilla public": "Mozilla Public License",
"isc": "ISC License",
"creative commons": "Creative Commons",
"unlicense": "Unlicense",
}
for key, license := range licenses {
if strings.Contains(content, key) {
return license
}
}
return ""
}
func NewLicense(text string) *License {
licenseType := detectLicenseType(text)
return &License{
LicenseType: licenseType,
Text: text,
}
}
type Licenses map[string]*License
type Dependency struct {
Module string
Version string
Direct bool
License *License
}
func NewDependency(module string, version string, direct bool, license *License) *Dependency {
return &Dependency{
Module: module,
Version: version,
Direct: direct,
License: license,
}
}
type Dependencies []*Dependency
type Module struct {
Licenses Licenses
Dependencies Dependencies
}
func NewModule(modules []ModuleDependency) *Module {
module := &Module{
Licenses: Licenses{},
Dependencies: Dependencies{},
}
for _, mod := range modules {
if mod.Dir == "" {
continue // Skip modules without local copies
}
license, err := module.findLicense(mod.Dir)
if err != nil {
log.Printf("ERROR: %v", err)
continue
}
module.Licenses[hash(license.Text)] = license
dependency := NewDependency(mod.Path, mod.Version, !mod.Indirect, license)
module.Dependencies = append(module.Dependencies, dependency)
}
sort.Slice(module.Dependencies, func(i, j int) bool {
direct1 := module.Dependencies[i].Direct
direct2 := module.Dependencies[j].Direct
if direct1 != direct2 {
if direct1 {
return true
}
return false
}
return module.Dependencies[i].Module < module.Dependencies[j].Module
})
return module
}
func (module Module) GenerateLicenseNames() {
usedNames := make(map[string]bool)
for _, license := range module.Licenses {
basename := strings.ReplaceAll(license.LicenseType, " ", "_")
i := 0
name := fmt.Sprintf("%s-%d", basename, i)
for usedNames[name] {
i++
name = fmt.Sprintf("%s-%d", basename, i)
}
license.LicenseName = name
usedNames[name] = true
}
}
func (module Module) DumpOverview() {
// Find and print license information for each module
fmt.Printf("%-50s %-20s %-10s %-20s %-20s\n", "MODULE", "VERSION", "DIRECT", "LICENSE", "HASH")
fmt.Println()
for _, dependency := range module.Dependencies {
fmt.Printf("%-50s %-20s %-10v %-20s %-20s\n",
truncateString(dependency.Module, 49),
truncateString(dependency.Version, 19),
dependency.Direct,
dependency.License.LicenseType,
dependency.License.LicenseName)
}
}
func (module Module) DumpText(directOnly bool) {
for _, dependency := range module.Dependencies {
if directOnly && !dependency.Direct {
continue
}
fmt.Println(strings.Repeat("=", 80))
if dependency.Direct {
fmt.Printf("Direct dependency")
} else {
fmt.Printf("Indirect dependency")
}
fmt.Printf(" %s %s\n", dependency.Module, dependency.Version)
fmt.Printf("LICENSE\n\n %s\n\n",
dependency.License.Text)
}
}
func (module *Module) findLicense(dir string) (*License, error) {
licenseFiles := []string{
"LICENSE",
"LICENSE.txt",
"LICENSE.md",
"license",
"license.txt",
"license.md",
}
for _, file := range licenseFiles {
path := filepath.Join(dir, file)
if _, err := os.Stat(path); err == nil {
// Read first line of license file
contentBytes, err := os.ReadFile(path)
if err == nil {
content := string(contentBytes)
content = strings.TrimSpace(content)
if len(content) > 0 {
hashcode := hash(content)
license := module.Licenses[hashcode]
if license != nil {
return license, nil
}
return NewLicense(content), nil
}
}
}
}
return nil, fmt.Errorf("No license found in '%s'", dir)
}

114
cmd/yamltool/diff.go Normal file
View File

@ -0,0 +1,114 @@
package main
import (
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"os"
"reflect"
)
// 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 v2 != nil && v1 == nil:
res = append(res, item)
case reflect.DeepEqual(v1, v2):
// delete is implicit by not copying to the output
case reflect.TypeOf(v1) == reflect.TypeOf(yaml.MapSlice{}) &&
reflect.TypeOf(v2) == reflect.TypeOf(yaml.MapSlice{}):
diff := subtract(v2.(yaml.MapSlice), v1.(yaml.MapSlice))
if len(diff) > 0 {
mi := yaml.MapItem{Key: k, Value: diff}
res = append(res, mi)
}
case Type(v1) == Slice && Type(v2) == Slice:
// To be improved can be really confusing.
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 _, v2value := range v2.([]any) {
k2 := strval(v2value)
if v1set[k2] {
if VERBOSITY == 2 {
s = append(s, "<UNMODIFIED>")
} 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 diff(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
return fmt.Errorf("Expected 2 files")
}
if VERBOSITY < 0 || VERBOSITY > 3 {
return fmt.Errorf("Array verbosity out of range")
}
file1 := args[0]
file2 := args[1]
yaml1, err := parse(read(file1))
if err != nil {
panic(fmt.Errorf("%s: %w", file1, err))
}
yaml2, err := parse(read(file2))
if err != nil {
panic(fmt.Errorf("%s: %w", file2, err))
}
diff1 := subtract(yaml2, yaml1)
diff2 := make(yaml.MapSlice, 0)
if SYMMETRIC_DIFF {
diff2 = subtract(yaml1, yaml2)
}
diff := diff1
if SYMMETRIC_DIFF {
diff = make(yaml.MapSlice, 0)
diff = append(diff,
yaml.MapItem{Key: "forward", Value: diff1},
yaml.MapItem{Key: "backward", Value: diff2},
)
}
if VERBOSITY > 0 {
if err := encode(os.Stdout, diff); err != nil {
return err
}
}
if len(diff1) > 0 || len(diff2) > 0 {
os.Exit(1)
}
return nil
}

16
cmd/yamltool/encode.go Normal file
View File

@ -0,0 +1,16 @@
package main
import (
"github.com/goccy/go-yaml"
"io"
"os"
)
func encode(writer io.Writer, data any) error {
enc := yaml.NewEncoder(os.Stdout,
yaml.UseLiteralStyleIfMultiline(true),
yaml.Indent(2), // Set indentation
//yaml.UseOrderedMap(), // Preserve map order
)
return enc.Encode(data)
}

65
cmd/yamltool/merge.go Normal file
View File

@ -0,0 +1,65 @@
package main
import (
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"os"
"reflect"
)
type MyMap yaml.MapSlice
func (m *MyMap) Set(key any, value any) {
for i := range len(*m) {
if (*m)[i].Key == key {
(*m)[i].Value = value
return
}
}
*m = append(*m, yaml.MapItem{Key: key, Value: value})
}
func (m MyMap) Get(key any) any {
for _, item := range m {
if item.Key == key {
return item.Value
}
}
return nil
}
func mergeMap(yaml1 yaml.MapSlice, yaml2 yaml.MapSlice) yaml.MapSlice {
res := MyMap(yaml1)
for _, item := range yaml2 {
initialValue := res.Get(item.Key)
value := item.Value
switch {
case initialValue != nil:
if reflect.TypeOf(initialValue) == reflect.TypeOf(yaml.MapSlice{}) &&
reflect.TypeOf(value) == reflect.TypeOf(yaml.MapSlice{}) {
mergedMap := mergeMap(initialValue.(yaml.MapSlice), value.(yaml.MapSlice))
res.Set(item.Key, mergedMap)
} else {
res.Set(item.Key, item.Value)
}
default:
res.Set(item.Key, item.Value)
}
}
return yaml.MapSlice(res)
}
func merge(cmd *cobra.Command, args []string) error {
res := make(yaml.MapSlice, 0)
for _, arg := range args {
config, err := parse(read(arg))
if err != nil {
return fmt.Errorf("%s: %w", arg, err)
}
res = mergeMap(res, config)
}
encode(os.Stdout, res)
return nil
}

40
cmd/yamltool/parse.go Normal file
View File

@ -0,0 +1,40 @@
package main
import (
"bytes"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"os"
)
func read(file string) []byte {
data, err := os.ReadFile(file)
if err != nil {
panic(err)
}
return data
}
func parse(data []byte) (yaml.MapSlice, error) {
var result yaml.MapSlice
decoder := yaml.NewDecoder(bytes.NewReader(data),
yaml.UseOrderedMap())
err := decoder.Decode(&result)
if err != nil {
return nil, err
}
return result, nil
}
func parseFiles(cmd *cobra.Command, args []string) error {
for _, arg := range args {
_, err := parse(read(arg))
if err != nil {
fmt.Printf("%s: %v\n", arg, err.Error())
}
}
return nil
}

80
cmd/yamltool/yamltool.go Normal file
View File

@ -0,0 +1,80 @@
package main
import (
yaml "github.com/goccy/go-yaml"
"github.com/spf13/cobra"
)
var VERBOSITY = 3
var SYMMETRIC_DIFF = false
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
}
}
func main() {
cmd := &cobra.Command{
Use: "yamltool",
Short: "Shows one-way difference between yaml files",
Long: `
Shows the changes in <file2> compared to <file1>`,
}
diff := &cobra.Command{
Use: "diff [file1] [file2]",
Short: "Shows one-way difference between yaml files",
Long: `
Shows the additions and modifications in <file2> compared to <file1>`,
RunE: func(cmd *cobra.Command, args []string) error {
return diff(cmd, args)
},
}
cmd.AddCommand(diff)
merge := &cobra.Command{
Use: "merge [file1] ... [fileN]",
Short: "Merge yaml files.",
Long: `Changes will be merged into the first file, so later files override earlier ones`,
RunE: func(cmd *cobra.Command, args []string) error {
return merge(cmd, args)
},
}
cmd.AddCommand(merge)
parse := &cobra.Command{
Use: "parse [file1] ... [fileN]",
Short: "Parse yaml files.",
Long: `Parse yaml files, usually gives better error messages than yamllint or yq`,
RunE: func(cmd *cobra.Command, args []string) error {
return parseFiles(cmd, args)
},
}
cmd.AddCommand(parse)
diff.PersistentFlags().IntVarP(&VERBOSITY, "array-output-level",
"v", 3, `Array output level: ,
0: no output, only exit status,
1: only show changed/added values,
2: show identical as <UNMODIFIED>,
3: show all values`)
diff.Flags().BoolVarP(&SYMMETRIC_DIFF, "symmetric-diff",
"s", false, `Symmetric difference, compare in both directions`)
cmd.Execute()
}

12
go.mod
View File

@ -1,3 +1,13 @@
module git.wamblee.org/public/gotools module git.wamblee.org/public/gotools
go 1.23.3 go 1.24.7
require (
github.com/goccy/go-yaml v1.15.13
github.com/spf13/cobra v1.8.1
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)

12
go.sum Normal file
View File

@ -0,0 +1,12 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/goccy/go-yaml v1.15.13 h1:Xd87Yddmr2rC1SLLTm2MNDcTjeO/GYo0JGiww6gSTDg=
github.com/goccy/go-yaml v1.15.13/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=