diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..6c18ec1 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,219 @@ +/* +Copyright © 2016 Tom Hudson +Copyright © 2023 Joseph LaFreniere +*/ +package cmd + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "log" + "os" + + internal "github.com/lafrenierejm/gron/internal/gron" + "github.com/mattn/go-colorable" + "github.com/spf13/cobra" +) + +var version = "0.0.1" + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "gron", + Version: version, + Short: "Transform JSON or YAML into discrete assignments to make it greppable", + Long: `gron transforms JSON or YAML (from a file, URL, or stdin) into discrete assignments to make it easier to grep for what you want and see the absolute "path" to it. + +Examples: + gron /tmp/apiresponse.json + gron http://jsonplaceholder.typicode.com/users/1 + curl -s http://jsonplaceholder.typicode.com/users/1 | gron + gron http://jsonplaceholder.typicode.com/users/1 | grep company | gron --ungron +`, + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + colorizeFlag, err := cmd.Flags().GetBool("colorize") + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + insecureFlag, err := cmd.Flags().GetBool("insecure") + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + jsonFlag, err := cmd.Flags().GetBool("json") + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + monochromeFlag, err := cmd.Flags().GetBool("monochrome") + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + sortFlag, err := cmd.Flags().GetBool("sort") + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + streamFlag, err := cmd.Flags().GetBool("stream") + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + ungronFlag, err := cmd.Flags().GetBool("ungron") + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + valuesFlag, err := cmd.Flags().GetBool("values") + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + yamlFlag, err := cmd.Flags().GetBool("yaml") + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + + var rawInput io.Reader + if len(args) == 0 || args[0] == "" || args[0] == "-" { + rawInput = os.Stdin + } else { + filename := args[0] + if validURL(filename) { + rawInput, err = getURL(filename, insecureFlag) + if err != nil { + log.Println(err) + os.Exit(1) + } + } else { + rawInput, err = os.Open(filename) + if err != nil { + log.Println(err) + os.Exit(1) + } + } + } + + var conv internal.StatementConv = internal.StatementToString + var colorize bool = false + if colorizeFlag { + colorize = true + } else if !monochromeFlag { + nocolorEnv, nocolorEnvPresent := os.LookupEnv("NO_COLOR") + if nocolorEnvPresent && nocolorEnv != "" { + colorize = false + } else { + colorize = true + } + } + if colorize { + conv = internal.StatementToColorString + } + + var actionExit int + var actionErr error + if ungronFlag { + actionExit, actionErr = internal.Ungron( + rawInput, + colorable.NewColorableStdout(), + jsonFlag, + colorize, + ) + } else if valuesFlag { + actionExit, actionErr = gronValues(rawInput, colorable.NewColorableStdout()) + } else if streamFlag { + actionExit, actionErr = internal.GronStream( + rawInput, + colorable.NewColorableStdout(), + conv, + yamlFlag, + sortFlag, + jsonFlag, + ) + } else { + actionExit, actionErr = internal.Gron( + rawInput, + colorable.NewColorableStdout(), + conv, + yamlFlag, + sortFlag, + jsonFlag, + ) + } + + if actionExit != 0 || actionErr != nil { + log.Println(err) + } + os.Exit(actionExit) + }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.Flags().BoolP("colorize", "c", false, "Colorize output (default on TTY)") + rootCmd.Flags().BoolP("insecure", "k", false, "Disable certificate validation when reading from a URL") + rootCmd.Flags().BoolP("json", "j", false, "Represent gron data as JSON stream") + rootCmd.Flags().BoolP("monochrome", "m", false, "Do not colorize output") + rootCmd.Flags().BoolP("sort", "", true, "Sort output") + rootCmd.Flags().BoolP("stream", "s", false, "Treat each line of input as a separate JSON object") + rootCmd.Flags().BoolP("ungron", "u", false, "Reverse the operation (turn assignments back into JSON)") + rootCmd.Flags().BoolP("values", "v", false, "Print just the values of provided assignments") + rootCmd.Flags().BoolP("version", "", false, "Print version information") + rootCmd.Flags().BoolP("yaml", "y", false, "Treat input as YAML instead of JSON") +} + +// gronValues prints just the scalar values from some input gron statements +// without any quotes or anything of that sort; a bit like jq -r +// e.g. json[0].user.name = "Sam"; -> Sam +func gronValues(r io.Reader, w io.Writer) (int, error) { + scanner := bufio.NewScanner(os.Stdin) + + for scanner.Scan() { + s := internal.StatementFromString(scanner.Text()) + + // strip off the leading 'json' bare key + if s[0].Typ == internal.TypBare && s[0].Text == "json" { + s = s[1:] + } + + // strip off the leading dots + if s[0].Typ == internal.TypDot || s[0].Typ == internal.TypLBrace { + s = s[1:] + } + + for _, t := range s { + switch t.Typ { + case internal.TypString: + var text string + err := json.Unmarshal([]byte(t.Text), &text) + if err != nil { + // just swallow errors and try to continue + continue + } + fmt.Println(text) + + case internal.TypNumber, internal.TypTrue, internal.TypFalse, internal.TypNull: + fmt.Println(t.Text) + + default: + // Nothing + } + } + } + + return 0, nil +} diff --git a/url.go b/cmd/url.go similarity index 88% rename from url.go rename to cmd/url.go index a2e10f9..e2456cd 100644 --- a/url.go +++ b/cmd/url.go @@ -1,4 +1,4 @@ -package main +package cmd import ( "bufio" @@ -28,11 +28,10 @@ func getURL(url string, insecure bool) (io.Reader, error) { if err != nil { return nil, err } - req.Header.Set("User-Agent", fmt.Sprintf("gron/%s", gronVersion)) + req.Header.Set("User-Agent", fmt.Sprintf("gron/%s", version)) req.Header.Set("Accept", "application/json") resp, err := client.Do(req) - if err != nil { return nil, err } diff --git a/url_test.go b/cmd/url_test.go similarity index 96% rename from url_test.go rename to cmd/url_test.go index fe46b69..452159f 100644 --- a/url_test.go +++ b/cmd/url_test.go @@ -1,4 +1,4 @@ -package main +package cmd import ( "testing" diff --git a/flake.nix b/flake.nix index 44d73ed..b29b48f 100644 --- a/flake.nix +++ b/flake.nix @@ -31,29 +31,32 @@ overlays = [ inputs.gomod2nix.overlays.default (final: prev: { }) ]; config = { }; }; + gron = pkgs.buildGoApplication { + pname = "gron"; + version = self'.shortRev or "dirty"; + # In 'nix develop', we don't need a copy of the source tree + # in the Nix store. + src = ./.; + modules = ./gomod2nix.toml; + meta = with pkgs.lib; { + description = + "Transform JSON or YAML into discrete assignments to make it easier to `grep` for what you want and see the absolute 'path' to it"; + homepage = "https://github.com/lafrenierejm/gron"; + license = licenses.mit; + maintainers = with maintainers; [ lafrenierejm ]; + }; + }; in { # Per-system attributes can be defined here. The self' and inputs' # module parameters provide easy access to attributes of the same # system. packages = rec { - gron = pkgs.buildGoApplication { - pname = "gron"; - version = self'.shortRev or "dirty"; - # In 'nix develop', we don't need a copy of the source tree - # in the Nix store. - src = ./.; - modules = ./gomod2nix.toml; - meta = with pkgs.lib; { - description = - "Transform JSON or YAML into discrete assignments to make it easier to `grep` for what you want and see the absolute 'path' to it"; - homepage = "https://github.com/lafrenierejm/gron"; - license = licenses.mit; - maintainers = with maintainers; [ lafrenierejm ]; - }; - }; + inherit gron; default = gron; }; + apps.default = gron; + # Auto formatters. This also adds a flake check to ensure that the # source tree was auto formatted. treefmt.config = { @@ -61,7 +64,7 @@ package = pkgs.treefmt; flakeCheck = false; # use pre-commit's check instead programs = { - gofmt.enable = true; + gofumpt.enable = true; prettier.enable = true; }; settings.formatter = { @@ -97,9 +100,11 @@ # Inherit all of the pre-commit hooks. inputsFrom = [ config.pre-commit.devShell ]; buildInputs = with pkgs; [ + cobra-cli go go-tools godef + gofumpt gomod2nix gopls gotools diff --git a/go.mod b/go.mod index 41e05fc..d2e308d 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,14 @@ require ( github.com/mattn/go-colorable v0.1.13 github.com/nwidger/jsoncolor v0.3.2 github.com/pkg/errors v0.9.1 + github.com/spf13/cobra v1.7.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-isatty v0.0.19 // indirect + github.com/spf13/pflag v1.0.5 // indirect golang.org/x/sys v0.10.0 // indirect ) diff --git a/go.sum b/go.sum index 5615de9..12cf517 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,9 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -13,6 +16,11 @@ github.com/nwidger/jsoncolor v0.3.2 h1:rVJJlwAWDJShnbTYOQ5RM7yTA20INyKXlJ/fg4JMh github.com/nwidger/jsoncolor v0.3.2/go.mod h1:Cs34umxLbJvgBMnVNVqhji9BhoT/N/KinHqZptQ7cf4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/gomod2nix.toml b/gomod2nix.toml index 380eca8..7bb77c4 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -4,6 +4,9 @@ schema = 3 [mod."github.com/fatih/color"] version = "v1.15.0" hash = "sha256-7b+scFVQeEUoXfeCDd8X2gS8GMoWA+HxjK8wfbypa5s=" + [mod."github.com/inconshreveable/mousetrap"] + version = "v1.1.0" + hash = "sha256-XWlYH0c8IcxAwQTnIi6WYqq44nOKUylSWxWO/vi+8pE=" [mod."github.com/mattn/go-colorable"] version = "v0.1.13" hash = "sha256-qb3Qbo0CELGRIzvw7NVM1g/aayaz4Tguppk9MD2/OI8=" @@ -16,6 +19,12 @@ schema = 3 [mod."github.com/pkg/errors"] version = "v0.9.1" hash = "sha256-mNfQtcrQmu3sNg/7IwiieKWOgFQOVVe2yXgKBpe/wZw=" + [mod."github.com/spf13/cobra"] + version = "v1.7.0" + hash = "sha256-bom9Zpnz8XPwx9IVF+GAodd3NVQ1dM1Uwxn8sy4Gmzs=" + [mod."github.com/spf13/pflag"] + version = "v1.0.5" + hash = "sha256-w9LLYzxxP74WHT4ouBspH/iQZXjuAh2WQCHsuvyEjAw=" [mod."golang.org/x/sys"] version = "v0.10.0" hash = "sha256-eeifyHj8IcTAruekJAGMCcjFyU2GAIAgvxS36hPZM1U=" diff --git a/internal/gron/decoder.go b/internal/gron/decoder.go index ec51383..184ac5b 100644 --- a/internal/gron/decoder.go +++ b/internal/gron/decoder.go @@ -2,8 +2,9 @@ package gron import ( "encoding/json" - "gopkg.in/yaml.v3" "io" + + "gopkg.in/yaml.v3" ) // an ActionFn represents a main action of the program, it accepts @@ -15,8 +16,8 @@ type Decoder interface { Decode(interface{}) error } -func MakeDecoder(r io.Reader, optYAML int) Decoder { - if optYAML > 0 { +func MakeDecoder(r io.Reader, asYaml bool) Decoder { + if asYaml { return yaml.NewDecoder(r) } else { d := json.NewDecoder(r) diff --git a/internal/gron/gron.go b/internal/gron/gron.go index c9c205c..ba1f64d 100644 --- a/internal/gron/gron.go +++ b/internal/gron/gron.go @@ -6,8 +6,6 @@ import ( "fmt" "io" "sort" - - "github.com/fatih/color" ) // Exit codes @@ -21,48 +19,24 @@ const ( exitJSONEncode ) -// Option bitfields -const ( - optMonochrome = 1 << iota - optNoSort - optJSON - optYAML -) - -// Output colors -var ( - strColor = color.New(color.FgYellow) - braceColor = color.New(color.FgMagenta) - bareColor = color.New(color.FgBlue, color.Bold) - numColor = color.New(color.FgRed) - boolColor = color.New(color.FgCyan) -) - // Gron is the default action. Given JSON as the input it returns a list // of assignment statements. Possible options are optNoSort and optMonochrome -func Gron(r io.Reader, w io.Writer, opts int) (int, error) { +func Gron(r io.Reader, w io.Writer, conv StatementConv, inYaml bool, sortOutput bool, outJson bool) (int, error) { var err error - var conv StatementConv - if opts&optMonochrome > 0 { - conv = StatementToString - } else { - conv = StatementToColorString - } - - ss, err := StatementsFromJSON(MakeDecoder(r, opts&optYAML), Statement{{"json", TypBare}}) + ss, err := StatementsFromJSON(MakeDecoder(r, inYaml), Statement{{"json", TypBare}}) if err != nil { goto out } // Go's maps do not have well-defined ordering, but we want a consistent // output for a given input, so we must sort the statements - if opts&optNoSort == 0 { + if sortOutput { sort.Sort(ss) } for _, s := range ss { - if opts&optJSON > 0 { + if outJson { s, err = s.Jsonify() if err != nil { goto out @@ -81,20 +55,20 @@ out: // GronStream is like the gron action, but it treats the input as one // JSON object per line. There's a bit of code duplication from the // gron action, but it'd be fairly messy to combine the two actions -func GronStream(r io.Reader, w io.Writer, opts int) (int, error) { +func GronStream( + r io.Reader, + w io.Writer, + conv StatementConv, + inYaml bool, + outSort bool, + outJson bool, +) (int, error) { var err error errstr := "failed to form statements" var i int var sc *bufio.Scanner var buf []byte - var conv func(s Statement) string - if opts&optMonochrome > 0 { - conv = StatementToString - } else { - conv = StatementToColorString - } - // Helper function to make the prefix statements for each line makePrefix := func(index int) Statement { return Statement{ @@ -114,7 +88,7 @@ func GronStream(r io.Reader, w io.Writer, opts int) (int, error) { {";", TypSemi}, } - if opts&optJSON > 0 { + if outJson { top, err = top.Jsonify() if err != nil { goto out @@ -133,7 +107,7 @@ func GronStream(r io.Reader, w io.Writer, opts int) (int, error) { line := bytes.NewBuffer(sc.Bytes()) var ss Statements - ss, err = StatementsFromJSON(MakeDecoder(line, opts&optYAML), makePrefix(i)) + ss, err = StatementsFromJSON(MakeDecoder(line, inYaml), makePrefix(i)) i++ if err != nil { goto out @@ -141,12 +115,12 @@ func GronStream(r io.Reader, w io.Writer, opts int) (int, error) { // Go's maps do not have well-defined ordering, but we want a consistent // output for a given input, so we must sort the statements - if opts&optNoSort == 0 { + if outSort { sort.Sort(ss) } for _, s := range ss { - if opts&optJSON > 0 { + if outJson { s, err = s.Jsonify() if err != nil { goto out @@ -165,5 +139,4 @@ out: return exitFormStatements, fmt.Errorf(errstr+": %s", err) } return exitOK, nil - } diff --git a/internal/gron/gron_test.go b/internal/gron/gron_test.go index a5c829e..d6fbd56 100644 --- a/internal/gron/gron_test.go +++ b/internal/gron/gron_test.go @@ -32,7 +32,7 @@ func TestGron(t *testing.T) { } out := &bytes.Buffer{} - code, err := Gron(in, out, optMonochrome) + code, err := Gron(in, out, StatementToString, false, true, false) if code != exitOK { t.Errorf("want exitOK; have %d", code) @@ -47,7 +47,6 @@ func TestGron(t *testing.T) { t.Errorf("gronned %s does not match %s", c.inFile, c.outFile) } } - } func TestGronStream(t *testing.T) { @@ -71,7 +70,7 @@ func TestGronStream(t *testing.T) { } out := &bytes.Buffer{} - code, err := GronStream(in, out, optMonochrome) + code, err := GronStream(in, out, StatementToString, false, true, false) if code != exitOK { t.Errorf("want exitOK; have %d", code) @@ -86,7 +85,6 @@ func TestGronStream(t *testing.T) { t.Errorf("gronned %s does not match %s", c.inFile, c.outFile) } } - } func TestLargeGronStream(t *testing.T) { @@ -109,7 +107,7 @@ func TestLargeGronStream(t *testing.T) { } out := &bytes.Buffer{} - code, err := GronStream(in, out, optMonochrome) + code, err := GronStream(in, out, StatementToString, false, true, false) if code != exitOK { t.Errorf("want exitOK; have %d", code) @@ -124,7 +122,6 @@ func TestLargeGronStream(t *testing.T) { t.Errorf("gronned %s does not match %s", c.inFile, c.outFile) } } - } func TestUngron(t *testing.T) { @@ -159,7 +156,7 @@ func TestUngron(t *testing.T) { } out := &bytes.Buffer{} - code, err := Ungron(in, out, optMonochrome) + code, err := Ungron(in, out, false, false) if code != exitOK { t.Errorf("want exitOK; have %d", code) @@ -206,7 +203,7 @@ func TestGronJ(t *testing.T) { } out := &bytes.Buffer{} - code, err := Gron(in, out, optMonochrome|optJSON) + code, err := Gron(in, out, StatementToString, false, true, true) if code != exitOK { t.Errorf("want exitOK; have %d", code) @@ -221,7 +218,6 @@ func TestGronJ(t *testing.T) { t.Errorf("gronned %s does not match %s", c.inFile, c.outFile) } } - } func TestGronStreamJ(t *testing.T) { @@ -245,7 +241,7 @@ func TestGronStreamJ(t *testing.T) { } out := &bytes.Buffer{} - code, err := GronStream(in, out, optMonochrome|optJSON) + code, err := GronStream(in, out, StatementToString, false, true, true) if code != exitOK { t.Errorf("want exitOK; have %d", code) @@ -260,7 +256,6 @@ func TestGronStreamJ(t *testing.T) { t.Errorf("gronned %s does not match %s", c.inFile, c.outFile) } } - } func TestUngronJ(t *testing.T) { @@ -292,7 +287,7 @@ func TestUngronJ(t *testing.T) { } out := &bytes.Buffer{} - code, err := Ungron(in, out, optMonochrome|optJSON) + code, err := Ungron(in, out, true, false) if code != exitOK { t.Errorf("want exitOK; have %d", code) @@ -329,7 +324,7 @@ func BenchmarkBigJSON(b *testing.B) { b.Fatalf("failed to rewind input: %s", err) } - _, err := Gron(in, out, optMonochrome|optNoSort) + _, err := Gron(in, out, StatementToString, false, true, false) if err != nil { b.Fatalf("failed to gron: %s", err) } diff --git a/internal/gron/statements.go b/internal/gron/statements.go index 74a11d1..6280c7d 100644 --- a/internal/gron/statements.go +++ b/internal/gron/statements.go @@ -272,7 +272,6 @@ out: // ungron turns statements into a proper datastructure func (ss Statements) ToInterface() (interface{}, error) { - // Get all the individually parsed statements var parsed []interface{} for _, s := range ss { @@ -303,13 +302,11 @@ func (ss Statements) ToInterface() (interface{}, error) { merged = m } return merged, nil - } // Less compares two statements for sort.Sort // Implements a natural sort to keep array indexes in order func (ss Statements) Less(a, b int) bool { - // ss[a] and ss[b] are both slices of tokens. The first // thing we need to do is find the first token (if any) // that differs, then we can use that token to decide @@ -368,7 +365,6 @@ func (ss Statements) Less(a, b int) bool { na, _ := json.Number(ta.Text).Float64() nb, _ := json.Number(tb.Text).Float64() return na < nb - } // Contains searches the statements for a given statement @@ -398,7 +394,6 @@ func StatementsFromJSON(r Decoder, prefix Statement) (Statements, error) { // fill takes a prefix statement and some value and recursively fills // the statement list using that value func (ss *Statements) fill(prefix Statement, v interface{}) { - // Add a statement for the current prefix and value ss.AddWithValue(prefix, valueTokenFromInterface(v)) @@ -431,5 +426,4 @@ func (ss *Statements) fill(prefix Statement, v interface{}) { ss.fill(prefix.withNumericKey(k), sub) } } - } diff --git a/internal/gron/statements_test.go b/internal/gron/statements_test.go index 8cf8c3d..cddbd9b 100644 --- a/internal/gron/statements_test.go +++ b/internal/gron/statements_test.go @@ -17,7 +17,6 @@ func statementsFromStringSlice(strs []string) Statements { } func TestStatementsSimple(t *testing.T) { - j := []byte(`{ "dotted": "A dotted value", "a quoted": "value", @@ -33,8 +32,7 @@ func TestStatementsSimple(t *testing.T) { "": 2 }`) - ss, err := StatementsFromJSON(MakeDecoder(bytes.NewReader(j), 0), Statement{{"json", TypBare}}) - + ss, err := StatementsFromJSON(MakeDecoder(bytes.NewReader(j), false), Statement{{"json", TypBare}}) if err != nil { t.Errorf("Want nil error from makeStatementsFromJSON() but got %s", err) } @@ -62,11 +60,9 @@ func TestStatementsSimple(t *testing.T) { t.Errorf("Statement group should contain `%s` but doesn't", want) } } - } func TestStatementsSimpleYaml(t *testing.T) { - j := []byte(`'': 2 a quoted: value anarr: @@ -83,8 +79,7 @@ x: | y: "z" id: 66912849`) - ss, err := StatementsFromJSON(MakeDecoder(bytes.NewReader(j), optYAML), Statement{{"yaml", TypBare}}) - + ss, err := StatementsFromJSON(MakeDecoder(bytes.NewReader(j), true), Statement{{"yaml", TypBare}}) if err != nil { t.Errorf("Want nil error from makeStatementsFromJSON() but got %s", err) } @@ -113,8 +108,8 @@ id: 66912849`) t.Errorf("Statement group should contain `%s` but doesn't", want) } } - } + func TestStatementsSorting(t *testing.T) { want := statementsFromStringSlice([]string{ `json.a = true;`, @@ -207,7 +202,6 @@ func TestUngronStatementsSimple(t *testing.T) { } have, err := in.ToInterface() - if err != nil { t.Fatalf("want nil error but have: %s", err) } diff --git a/internal/gron/token.go b/internal/gron/token.go index d6be2a7..d40eda1 100644 --- a/internal/gron/token.go +++ b/internal/gron/token.go @@ -50,25 +50,6 @@ const ( TypError ) -// a sprintFn adds color to its input -type sprintFn func(...interface{}) string - -// mapping of token types to the appropriate color sprintFn -var sprintFns = map[TokenTyp]sprintFn{ - TypBare: bareColor.SprintFunc(), - TypNumericKey: numColor.SprintFunc(), - TypQuotedKey: strColor.SprintFunc(), - TypLBrace: braceColor.SprintFunc(), - TypRBrace: braceColor.SprintFunc(), - TypString: strColor.SprintFunc(), - TypNumber: numColor.SprintFunc(), - TypTrue: boolColor.SprintFunc(), - TypFalse: boolColor.SprintFunc(), - TypNull: boolColor.SprintFunc(), - TypEmptyArray: braceColor.SprintFunc(), - TypEmptyObject: braceColor.SprintFunc(), -} - // isValue returns true if the token is a valid value type func (t Token) isValue() bool { switch t.Typ { @@ -108,7 +89,6 @@ func (t Token) formatColor() string { return fn(text) } return text - } // valueTokenFromInterface takes any valid value and @@ -143,7 +123,6 @@ func valueTokenFromInterface(v interface{}) Token { // quoteString takes a string and returns a quoted and // escaped string valid for use in gron output func quoteString(s string) string { - out := &bytes.Buffer{} // bytes.Buffer never returns errors on these methods. // errors are explicitly ignored to keep the linter @@ -187,5 +166,4 @@ func quoteString(s string) string { _ = out.WriteByte('"') return out.String() - } diff --git a/internal/gron/token_test.go b/internal/gron/token_test.go index 41240b5..991861a 100644 --- a/internal/gron/token_test.go +++ b/internal/gron/token_test.go @@ -29,7 +29,6 @@ var cases = []struct { } func TestValueTokenFromInterface(t *testing.T) { - for _, c := range cases { have := valueTokenFromInterface(c.in) @@ -43,7 +42,6 @@ func TestValueTokenFromInterface(t *testing.T) { } func BenchmarkValueTokenFromInterface(b *testing.B) { - for i := 0; i < b.N; i++ { for _, c := range cases { _ = valueTokenFromInterface(c.in) diff --git a/internal/gron/ungron.go b/internal/gron/ungron.go index 5a9f76a..a77220b 100644 --- a/internal/gron/ungron.go +++ b/internal/gron/ungron.go @@ -17,27 +17,26 @@ import ( "bytes" "encoding/json" "fmt" + "io" "reflect" "strconv" "strings" "unicode" "unicode/utf8" - "github.com/nwidger/jsoncolor" "github.com/pkg/errors" - "io" ) // Ungron is the reverse of gron. Given assignment statements as input, // it returns JSON. The only option is optMonochrome -func Ungron(r io.Reader, w io.Writer, opts int) (int, error) { +func Ungron(r io.Reader, w io.Writer, outJson bool, colorize bool) (int, error) { scanner := bufio.NewScanner(r) var maker StatementMaker // Allow larger internal buffer of the scanner (min: 64KiB ~ max: 1MiB) scanner.Buffer(make([]byte, 64*1024), 1024*1024) - if opts&optJSON > 0 { + if outJson { maker = StatementFromJSONSpec } else { maker = StatementFromStringMaker @@ -84,7 +83,7 @@ func Ungron(r io.Reader, w io.Writer, opts int) (int, error) { j := out.Bytes() // If the output isn't monochrome, add color to the JSON - if opts&optMonochrome == 0 { + if colorize { c, err := colorizeJSON(j) // If we failed to colorize the JSON for whatever reason, @@ -107,26 +106,6 @@ func Ungron(r io.Reader, w io.Writer, opts int) (int, error) { return exitOK, nil } -func colorizeJSON(src []byte) ([]byte, error) { - out := &bytes.Buffer{} - f := jsoncolor.NewFormatter() - - f.StringColor = strColor - f.ObjectColor = braceColor - f.ArrayColor = braceColor - f.FieldColor = bareColor - f.NumberColor = numColor - f.TrueColor = boolColor - f.FalseColor = boolColor - f.NullColor = boolColor - - err := f.Format(out, src) - if err != nil { - return out.Bytes(), err - } - return out.Bytes(), nil -} - // errRecoverable is an error type to represent errors that // can be recovered from; e.g. an empty line in the input type errRecoverable struct { @@ -160,7 +139,6 @@ func newLexer(text string) *lexer { // lex runs the lexer and returns the lexed statement func (l *lexer) lex() Statement { - for lexfn := lexStatement; lexfn != nil; { lexfn = lexfn(l) } @@ -266,7 +244,6 @@ func (l *lexer) acceptUntil(delims string) { // rune contained in the provided string, unless that rune was // escaped with a backslash func (l *lexer) acceptUntilUnescaped(delims string) { - // Read until we hit an unescaped rune or the end of the input inEscape := false for { @@ -313,7 +290,6 @@ func lexStatement(l *lexer) lexFn { l.emit(TypError) return nil } - } // lexBareWord lexes for bare identifiers. diff --git a/internal/gron/ungron_test.go b/internal/gron/ungron_test.go index 79247cb..1a96059 100644 --- a/internal/gron/ungron_test.go +++ b/internal/gron/ungron_test.go @@ -206,7 +206,6 @@ func TestTokensSimple(t *testing.T) { l := newLexer(in) tokens := l.lex() have, err := ungronTokens(tokens) - if err != nil { t.Fatalf("failed to ungron statement: %s", err) } @@ -288,5 +287,4 @@ func TestMerge(t *testing.T) { if !eq { t.Errorf("Have and want datastructures are unequal") } - } diff --git a/main.go b/main.go index 2b148bc..e181917 100644 --- a/main.go +++ b/main.go @@ -1,476 +1,10 @@ +/* +Copyright © 2023 NAME HERE +*/ package main -import ( - "flag" - "fmt" - "io" - "os" - "sort" - "strings" - - "github.com/fatih/color" - "github.com/mattn/go-colorable" - "github.com/nwidger/jsoncolor" - "github.com/pkg/errors" - - "bufio" - "bytes" - "encoding/json" - internal "github.com/lafrenierejm/gron/internal/gron" -) - -// Exit codes -const ( - exitOK = iota - exitOpenFile - exitReadInput - exitFormStatements - exitFetchURL - exitParseStatements - exitJSONEncode -) - -// Option bitfields -const ( - optMonochrome = 1 << iota - optNoSort - optJSON - optYAML -) - -// Output colors -var ( - strColor = color.New(color.FgYellow) - braceColor = color.New(color.FgMagenta) - bareColor = color.New(color.FgBlue, color.Bold) - numColor = color.New(color.FgRed) - boolColor = color.New(color.FgCyan) -) - -// gronVersion stores the current gron version, set at build -// time with the ldflags -X option -var gronVersion = "dev" - -func init() { - flag.Usage = func() { - h := "Transform JSON or YAML (from a file, URL, or stdin) into discrete assignments to make it greppable\n\n" - - h += "Usage:\n" - h += " gron [OPTIONS] [FILE|URL|-]\n\n" - - h += "Options:\n" - h += " -u, --ungron Reverse the operation (turn assignments back into JSON)\n" - h += " -v, --values Print just the values of provided assignments\n" - h += " -c, --colorize Colorize output (default on tty)\n" - h += " -m, --monochrome Monochrome (don't colorize output)\n" - h += " -s, --stream Treat each line of input as a separate JSON object\n" - h += " -k, --insecure Disable certificate validation\n" - h += " -j, --json Represent gron data as JSON stream\n" - h += " -y, --yaml Treat input as YAML instead of JSON\n" - h += " --no-sort Don't sort output (faster)\n" - h += " --version Print version information\n\n" - - h += "Exit Codes:\n" - h += fmt.Sprintf(" %d\t%s\n", exitOK, "OK") - h += fmt.Sprintf(" %d\t%s\n", exitOpenFile, "Failed to open file") - h += fmt.Sprintf(" %d\t%s\n", exitReadInput, "Failed to read input") - h += fmt.Sprintf(" %d\t%s\n", exitFormStatements, "Failed to form statements") - h += fmt.Sprintf(" %d\t%s\n", exitFetchURL, "Failed to fetch URL") - h += fmt.Sprintf(" %d\t%s\n", exitParseStatements, "Failed to parse statements") - h += fmt.Sprintf(" %d\t%s\n", exitJSONEncode, "Failed to encode JSON") - h += "\n" - - h += "Examples:\n" - h += " gron /tmp/apiresponse.json\n" - h += " gron http://jsonplaceholder.typicode.com/users/1 \n" - h += " curl -s http://jsonplaceholder.typicode.com/users/1 | gron\n" - h += " gron http://jsonplaceholder.typicode.com/users/1 | grep company | gron --ungron\n" - - fmt.Fprintf(os.Stderr, h) - } -} +import "github.com/lafrenierejm/gron/cmd" func main() { - var ( - ungronFlag bool - colorizeFlag bool - monochromeFlag bool - streamFlag bool - noSortFlag bool - versionFlag bool - insecureFlag bool - jsonFlag bool - yamlFlag bool - valuesFlag bool - ) - - flag.BoolVar(&ungronFlag, "ungron", false, "") - flag.BoolVar(&ungronFlag, "u", false, "") - flag.BoolVar(&colorizeFlag, "colorize", false, "") - flag.BoolVar(&colorizeFlag, "c", false, "") - flag.BoolVar(&monochromeFlag, "monochrome", false, "") - flag.BoolVar(&monochromeFlag, "m", false, "") - flag.BoolVar(&streamFlag, "s", false, "") - flag.BoolVar(&streamFlag, "stream", false, "") - flag.BoolVar(&noSortFlag, "no-sort", false, "") - flag.BoolVar(&versionFlag, "version", false, "") - flag.BoolVar(&insecureFlag, "k", false, "") - flag.BoolVar(&insecureFlag, "insecure", false, "") - flag.BoolVar(&jsonFlag, "j", false, "") - flag.BoolVar(&jsonFlag, "json", false, "") - flag.BoolVar(&yamlFlag, "y", false, "") - flag.BoolVar(&yamlFlag, "yaml", false, "") - flag.BoolVar(&valuesFlag, "values", false, "") - flag.BoolVar(&valuesFlag, "value", false, "") - flag.BoolVar(&valuesFlag, "v", false, "") - - flag.Parse() - - // Print version information - if versionFlag { - fmt.Printf("gron version %s\n", gronVersion) - os.Exit(exitOK) - } - - // If executed as 'ungron' set the --ungron flag - if strings.HasSuffix(os.Args[0], "ungron") { - ungronFlag = true - } - - // Determine what the program's input should be: - // file, HTTP URL or stdin - var rawInput io.Reader - filename := flag.Arg(0) - if filename == "" || filename == "-" { - rawInput = os.Stdin - } else if validURL(filename) { - r, err := getURL(filename, insecureFlag) - if err != nil { - fatal(exitFetchURL, err) - } - rawInput = r - } else { - r, err := os.Open(filename) - if err != nil { - fatal(exitOpenFile, err) - } - rawInput = r - } - - var opts int - // The monochrome option should be forced if the output isn't a terminal - // to avoid doing unnecessary work calling the color functions - switch { - case colorizeFlag: - color.NoColor = false - case monochromeFlag || color.NoColor: - opts = opts | optMonochrome - } - if noSortFlag { - opts = opts | optNoSort - } - if jsonFlag { - opts = opts | optJSON - } - if yamlFlag { - opts = opts | optYAML - } - - // Pick the appropriate action: gron, ungron, gronValues, or gronStream - var a internal.ActionFn = internal.Gron - if ungronFlag { - a = internal.Ungron - } else if valuesFlag { - a = gronValues - } else if streamFlag { - a = internal.GronStream - } - exitCode, err := a(rawInput, colorable.NewColorableStdout(), opts) - - if exitCode != exitOK { - fatal(exitCode, err) - } - - os.Exit(exitOK) -} - -// gron is the default action. Given JSON as the input it returns a list -// of assignment statements. Possible options are optNoSort and optMonochrome -func gron(r io.Reader, w io.Writer, opts int) (int, error) { - var err error - - var conv internal.StatementConv - if opts&optMonochrome > 0 { - conv = internal.StatementToString - } else { - conv = internal.StatementToColorString - } - - top := "json" - if opts&optYAML > 0 { - top = "yaml" - } - - ss, err := internal.StatementsFromJSON(internal.MakeDecoder(r, opts&optYAML), internal.Statement{{top, internal.TypBare}}) - if err != nil { - goto out - } - - // Go's maps do not have well-defined ordering, but we want a consistent - // output for a given input, so we must sort the statements - if opts&optNoSort == 0 { - sort.Sort(ss) - } - - for _, s := range ss { - if opts&optJSON > 0 { - s, err = s.Jsonify() - if err != nil { - goto out - } - } - fmt.Fprintln(w, conv(s)) - } - -out: - if err != nil { - return exitFormStatements, fmt.Errorf("failed to form statements: %s", err) - } - return exitOK, nil -} - -// gronStream is like the gron action, but it treats the input as one -// JSON object per line. There's a bit of code duplication from the -// gron action, but it'd be fairly messy to combine the two actions -func gronStream(r io.Reader, w io.Writer, opts int) (int, error) { - var err error - errstr := "failed to form statements" - var i int - var sc *bufio.Scanner - var buf []byte - - var conv func(s internal.Statement) string - if opts&optMonochrome > 0 { - conv = internal.StatementToString - } else { - conv = internal.StatementToColorString - } - - // Helper function to make the prefix statements for each line - makePrefix := func(index int) internal.Statement { - return internal.Statement{ - {"json", internal.TypBare}, - {"[", internal.TypLBrace}, - {fmt.Sprintf("%d", index), internal.TypNumericKey}, - {"]", internal.TypRBrace}, - } - } - - // The first line of output needs to establish that the top-level - // thing is actually an array... - top := internal.Statement{ - {"json", internal.TypBare}, - {"=", internal.TypEquals}, - {"[]", internal.TypEmptyArray}, - {";", internal.TypSemi}, - } - - if opts&optJSON > 0 { - top, err = top.Jsonify() - if err != nil { - goto out - } - } - - fmt.Fprintln(w, conv(top)) - - // Read the input line by line - sc = bufio.NewScanner(r) - buf = make([]byte, 0, 64*1024) - sc.Buffer(buf, 1024*1024) - i = 0 - for sc.Scan() { - - d := internal.MakeDecoder(bytes.NewBuffer(sc.Bytes()), opts) - - var ss internal.Statements - ss, err = internal.StatementsFromJSON(d, makePrefix(i)) - i++ - if err != nil { - goto out - } - - // Go's maps do not have well-defined ordering, but we want a consistent - // output for a given input, so we must sort the statements - if opts&optNoSort == 0 { - sort.Sort(ss) - } - - for _, s := range ss { - if opts&optJSON > 0 { - s, err = s.Jsonify() - if err != nil { - goto out - } - - } - fmt.Fprintln(w, conv(s)) - } - } - if err = sc.Err(); err != nil { - errstr = "error reading multiline input: %s" - } - -out: - if err != nil { - return exitFormStatements, fmt.Errorf(errstr+": %s", err) - } - return exitOK, nil - -} - -// ungron is the reverse of gron. Given assignment statements as input, -// it returns JSON. The only option is optMonochrome -func ungron(r io.Reader, w io.Writer, opts int) (int, error) { - scanner := bufio.NewScanner(r) - var maker internal.StatementMaker - - // Allow larger internal buffer of the scanner (min: 64KiB ~ max: 1MiB) - scanner.Buffer(make([]byte, 64*1024), 1024*1024) - - if opts&optJSON > 0 { - maker = internal.StatementFromJSONSpec - } else { - maker = internal.StatementFromStringMaker - } - - // Make a list of statements from the input - var ss internal.Statements - for scanner.Scan() { - s, err := maker(scanner.Text()) - if err != nil { - return exitParseStatements, err - } - ss.Add(s) - } - if err := scanner.Err(); err != nil { - return exitReadInput, fmt.Errorf("failed to read input statements") - } - - // turn the statements into a single merged interface{} type - merged, err := ss.ToInterface() - if err != nil { - return exitParseStatements, err - } - - // If there's only one top level key and it's "json", make that the top level thing - mergedMap, ok := merged.(map[string]interface{}) - if ok { - if len(mergedMap) == 1 { - if _, exists := mergedMap["json"]; exists { - merged = mergedMap["json"] - } - } - } - - // Marshal the output into JSON to display to the user - out := &bytes.Buffer{} - enc := json.NewEncoder(out) - enc.SetIndent("", " ") - enc.SetEscapeHTML(false) - err = enc.Encode(merged) - if err != nil { - return exitJSONEncode, errors.Wrap(err, "failed to convert statements to JSON") - } - j := out.Bytes() - - // If the output isn't monochrome, add color to the JSON - if opts&optMonochrome == 0 { - c, err := colorizeJSON(j) - - // If we failed to colorize the JSON for whatever reason, - // we'll just fall back to monochrome output, otherwise - // replace the monochrome JSON with glorious technicolor - if err == nil { - j = c - } - } - - // For whatever reason, the monochrome version of the JSON - // has a trailing newline character, but the colorized version - // does not. Strip the whitespace so that neither has the newline - // character on the end, and then we'll add a newline in the - // Fprintf below - j = bytes.TrimSpace(j) - - fmt.Fprintf(w, "%s\n", j) - - return exitOK, nil -} - -// gronValues prints just the scalar values from some input gron statements -// without any quotes or anything of that sort; a bit like jq -r -// e.g. json[0].user.name = "Sam"; -> Sam -func gronValues(r io.Reader, w io.Writer, opts int) (int, error) { - scanner := bufio.NewScanner(os.Stdin) - - for scanner.Scan() { - s := internal.StatementFromString(scanner.Text()) - - // strip off the leading 'json' bare key - if s[0].Typ == internal.TypBare && s[0].Text == "json" { - s = s[1:] - } - - // strip off the leading dots - if s[0].Typ == internal.TypDot || s[0].Typ == internal.TypLBrace { - s = s[1:] - } - - for _, t := range s { - switch t.Typ { - case internal.TypString: - var text string - err := json.Unmarshal([]byte(t.Text), &text) - if err != nil { - // just swallow errors and try to continue - continue - } - fmt.Println(text) - - case internal.TypNumber, internal.TypTrue, internal.TypFalse, internal.TypNull: - fmt.Println(t.Text) - - default: - // Nothing - } - } - } - - return exitOK, nil -} - -func colorizeJSON(src []byte) ([]byte, error) { - out := &bytes.Buffer{} - f := jsoncolor.NewFormatter() - - f.StringColor = strColor - f.ObjectColor = braceColor - f.ArrayColor = braceColor - f.FieldColor = bareColor - f.NumberColor = numColor - f.TrueColor = boolColor - f.FalseColor = boolColor - f.NullColor = boolColor - - err := f.Format(out, src) - if err != nil { - return out.Bytes(), err - } - return out.Bytes(), nil -} - -func fatal(code int, err error) { - fmt.Fprintf(os.Stderr, "%s\n", err) - os.Exit(code) + cmd.Execute() }