commit 7744206872484f02c064e4ac4549d3d5df872f7a Author: Vegard Berg Date: Thu Nov 30 12:01:20 2023 +0100 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f47c56 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# jsonsearch +Very simple tool to search by string or number value and get the JSON key from it. + +## Usage +```sh +# Search for the first occurrence of 'foo Bar'. +jsonsearch "foo Bar" myfile.json + +# Search for all occurrences of 'foo Bar'. +jsonsearch -all "foo Bar" myfile.json + +# Search using STDIN +cat myfile.json | jsonsearch "foo Bar" +# or +cat myfile.json | jsonsearch "foo Bar" - + +# Search a number +jsonsearch -type=number 3.141518 myfile.json +``` \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..113b3cc --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module git.myrkvi.com/myrkvi/jsonsearch + +go 1.21.4 + +require ( + github.com/fatih/color v1.16.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/sys v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7f13de9 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..64774b3 --- /dev/null +++ b/main.go @@ -0,0 +1,161 @@ +package main + +import ( + _ "embed" + "encoding/json" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + + "log/slog" + + "github.com/fatih/color" +) + +var findAll = flag.Bool("all", false, "Find all occurrences") +var findType = flag.String("type", "string", "The type of value to find. Must be one of 'string' or 'number'") + +type JSON = interface{} +type JSONObject = map[string]interface{} +type JSONArray = []interface{} + +func init() { + flag.Parse() +} + +func main() { + var inputData []byte + + if flag.NArg() == 1 || flag.Arg(1) == "-" { + stdin, err := io.ReadAll(os.Stdin) + if err != nil { + panic(err) + } + + inputData = stdin + } else if flag.NArg() == 2 { + file, err := os.ReadFile(flag.Arg(1)) + if err != nil { + panic(err) + } + inputData = file + } else { + fmt.Printf("Usage: %s [-all] [-type=string|number] [ | -]\n", filepath.Base(os.Args[0])) + os.Exit(0) + } + + data := make(JSONObject) + json.Unmarshal(inputData, &data) + switch *findType { + case "string": + // "EpToggle_PAYMENTINSURANCE_CAR_Description" + findValue(data, flag.Arg(0)) + case "number": + parsedNum, err := strconv.ParseFloat(flag.Arg(0), 64) + if err != nil { + panic(err) + } + findValue(data, parsedNum) + } +} + +func findValue[T comparable](haystack JSON, text T) { + switch x := haystack.(type) { + case JSONObject: + findInObject(x, text, "") + case JSONArray: + findInList(x, text, "") + default: + slog.Error("JSON is not a nested structure.") + } +} + +func findInObject[T comparable](haystack JSONObject, value T, path string) { + for k, v := range haystack { + switch x := v.(type) { + case string: + if *findType != "string" { + continue + } + if stringContains(x, value) { + printPathAndMatch(path, x) + } + + case float64: + if *findType != "number" { + continue + } + if floatEquals(x, value) { + printPathAndMatch(path, x) + } + case JSONObject: + findInObject(x, value, path+"."+k) + case []interface{}: + findInList(x, value, fmt.Sprintf("%s.%s", path, k)) + } + } +} + +func findInList[T comparable](list []interface{}, value T, path string) { + for i, v := range list { + switch x := v.(type) { + case string: + if *findType != "string" { + continue + } + if stringContains(x, value) { + printPathAndMatch(path, x) + } + + case float64: + if *findType != "number" { + continue + } + if floatEquals(x, value) { + printPathAndMatch(path, x) + } + + case JSONObject: + findInObject(x, value, fmt.Sprintf("%s[%d]", path, i)) + case []interface{}: + findInList(x, value, fmt.Sprintf("%s[%d]", path, i)) + } + } +} + +func printPathAndMatch(path string, match interface{}) { + if strings.HasPrefix(path, ".") { + path = strings.Replace(path, ".", "", 1) + } + + c := color.New(color.Bold) + c.Printf("%s", path) + fmt.Printf(": ") + c = color.New(color.FgHiBlack) + c.Printf("%v\n", match) + + if !*findAll { + os.Exit(0) + } +} + +func stringContains[T comparable](haystackString string, match T) bool { + matchString, ok := any(match).(string) + if !ok { + return false + } + return strings.Contains(haystackString, matchString) +} + +func floatEquals[T comparable](haystackFloat float64, match T) bool { + matchFloat, ok := any(match).(float64) + if !ok { + return false + } + + return haystackFloat == matchFloat +}