Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: option -k to preserve the order of keys #239

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 36 additions & 14 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ type cli struct {
argnames []string
argvalues []any

keys map[uintptr][]string
cliOpts []cliOption

outputYAMLSeparator bool
exitCodeError error
}
Expand Down Expand Up @@ -76,11 +79,24 @@ type flagopts struct {
RawFile map[string]string `long:"rawfile" description:"set the contents of a file to a variable"`
Args []any `long:"args" positional:"" description:"consume remaining arguments as positional string values"`
JSONArgs []any `long:"jsonargs" positional:"" description:"consume remaining arguments as positional JSON values"`
KeyOrder bool `short:"k" long:"key-order" description:"preserve order of keys"`
ExitStatus bool `short:"e" long:"exit-status" description:"exit 1 when the last value is false or null"`
Version bool `short:"v" long:"version" description:"display version information"`
Help bool `short:"h" long:"help" description:"display this help information"`
}

type cliConfig struct {
keys map[uintptr][]string
}

type cliOption func(*cliConfig)

func withKeys(keys map[uintptr][]string) cliOption {
return func(c *cliConfig) {
c.keys = keys
}
}

var addDefaultModulePaths = true

func (cli *cli) run(args []string) int {
Expand Down Expand Up @@ -124,6 +140,12 @@ Usage:
cli.outputCompact, cli.outputIndent, cli.outputTab, cli.outputYAML =
opts.OutputRaw, opts.OutputRaw0, opts.OutputJoin,
opts.OutputCompact, opts.OutputIndent, opts.OutputTab, opts.OutputYAML

if opts.KeyOrder {
cli.keys = map[uintptr][]string{}
cli.cliOpts = append(cli.cliOpts, withKeys(cli.keys))
}

defer func(x bool) { noColor = x }(noColor)
if opts.OutputColor || opts.OutputMono {
noColor = opts.OutputMono
Expand Down Expand Up @@ -157,15 +179,15 @@ Usage:
cli.argvalues = append(cli.argvalues, v)
}
for k, v := range opts.ArgJSON {
val, _ := newJSONInputIter(strings.NewReader(v), "$"+k).Next()
val, _ := newJSONInputIter(strings.NewReader(v), "$"+k, cli.cliOpts...).Next()
if err, ok := val.(error); ok {
return err
}
cli.argnames = append(cli.argnames, "$"+k)
cli.argvalues = append(cli.argvalues, val)
}
for k, v := range opts.SlurpFile {
val, err := slurpFile(v)
val, err := slurpFile(v, cli.cliOpts...)
if err != nil {
return err
}
Expand All @@ -187,7 +209,7 @@ Usage:
positional := opts.Args
for i, v := range opts.JSONArgs {
if v != nil {
val, _ := newJSONInputIter(strings.NewReader(v.(string)), "--jsonargs").Next()
val, _ := newJSONInputIter(strings.NewReader(v.(string)), "--jsonargs", cli.cliOpts...).Next()
if err, ok := val.(error); ok {
return err
}
Expand Down Expand Up @@ -234,7 +256,7 @@ Usage:
if len(modulePaths) == 0 && addDefaultModulePaths {
modulePaths = []string{"~/.jq", "$ORIGIN/../lib/gojq", "$ORIGIN/../lib"}
}
iter := cli.createInputIter(args)
iter := cli.createInputIter(args, cli.cliOpts...)
defer iter.Close()
code, err := gojq.Compile(query,
gojq.WithModuleLoader(gojq.NewModuleLoader(modulePaths)),
Expand Down Expand Up @@ -275,9 +297,9 @@ Usage:
return cli.process(iter, code)
}

func slurpFile(name string) (any, error) {
func slurpFile(name string, opts ...cliOption) (any, error) {
iter := newSlurpInputIter(
newFilesInputIter(newJSONInputIter, []string{name}, nil),
newFilesInputIter(newJSONInputIter, []string{name}, nil, opts...),
)
defer iter.Close()
val, _ := iter.Next()
Expand All @@ -287,8 +309,8 @@ func slurpFile(name string) (any, error) {
return val, nil
}

func (cli *cli) createInputIter(args []string) (iter inputIter) {
var newIter func(io.Reader, string) inputIter
func (cli *cli) createInputIter(args []string, opts ...cliOption) (iter inputIter) {
var newIter func(io.Reader, string, ...cliOption) inputIter
switch {
case cli.inputRaw:
if cli.inputSlurp {
Expand All @@ -313,9 +335,9 @@ func (cli *cli) createInputIter(args []string) (iter inputIter) {
}()
}
if len(args) == 0 {
return newIter(cli.inStream, "<stdin>")
return newIter(cli.inStream, "<stdin>", cli.cliOpts...)
}
return newFilesInputIter(newIter, args, cli.inStream)
return newFilesInputIter(newIter, args, cli.inStream, opts...)
}

func (cli *cli) process(iter inputIter, code *gojq.Code) error {
Expand Down Expand Up @@ -375,7 +397,7 @@ func (cli *cli) printValues(iter gojq.Iter) error {

func (cli *cli) createMarshaler() marshaler {
if cli.outputYAML {
return yamlFormatter(cli.outputIndent)
return yamlFormatter(cli.outputIndent, cli.cliOpts...)
}
indent := 2
if cli.outputCompact {
Expand All @@ -385,15 +407,15 @@ func (cli *cli) createMarshaler() marshaler {
} else if i := cli.outputIndent; i != nil {
indent = *i
}
f := newEncoder(cli.outputTab, indent)
f := newEncoder(cli.outputTab, indent, cli.cliOpts...)
if cli.outputRaw || cli.outputRaw0 || cli.outputJoin {
return &rawMarshaler{f, cli.outputRaw0}
}
return f
}

func (cli *cli) funcDebug(v any, _ []any) any {
if err := newEncoder(false, 0).
if err := newEncoder(false, 0, cli.cliOpts...).
marshal([]any{"DEBUG:", v}, cli.errStream); err != nil {
return err
}
Expand All @@ -404,7 +426,7 @@ func (cli *cli) funcDebug(v any, _ []any) any {
}

func (cli *cli) funcStderr(v any, _ []any) any {
if err := (&rawMarshaler{m: newEncoder(false, 0)}).
if err := (&rawMarshaler{m: newEncoder(false, 0, cli.cliOpts...)}).
marshal(v, cli.errStream); err != nil {
return err
}
Expand Down
57 changes: 43 additions & 14 deletions cli/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"math"
"math/big"
"reflect"
"sort"
"strconv"
"unicode/utf8"
Expand All @@ -18,11 +19,16 @@ type encoder struct {
indent int
depth int
buf [64]byte
keys map[uintptr][]string
}

func newEncoder(tab bool, indent int) *encoder {
func newEncoder(tab bool, indent int, opts ...cliOption) *encoder {
var c cliConfig
for _, opt := range opts {
opt(&c)
}
// reuse the buffer in multiple calls of marshal
return &encoder{w: new(bytes.Buffer), tab: tab, indent: indent}
return &encoder{w: new(bytes.Buffer), tab: tab, indent: indent, keys: c.keys}
}

func (e *encoder) flush() error {
Expand Down Expand Up @@ -185,22 +191,45 @@ func (e *encoder) encodeArray(vs []any) error {
return nil
}

func (e *encoder) encodeObject(vs map[string]any) error {
e.writeByte('{', objectColor)
e.depth += e.indent
type keyVal struct {
key string
val any
type keyVal struct {
key string
val any
}

func orderKvs(vs map[string]any, keys map[uintptr][]string) ([]keyVal, bool) {
ptr := uintptr(reflect.ValueOf(vs).UnsafePointer())
keyList := keys[ptr]
if len(keyList) != len(vs) {
return nil, false
}
kvs := make([]keyVal, len(vs))
var i int
for k, v := range vs {
for i, k := range keyList {
v, has := vs[k]
if !has {
return nil, false
}
kvs[i] = keyVal{k, v}
i++
}
sort.Slice(kvs, func(i, j int) bool {
return kvs[i].key < kvs[j].key
})
return kvs, true
}

func (e *encoder) encodeObject(vs map[string]any) error {
e.writeByte('{', objectColor)
e.depth += e.indent

kvs, ok := orderKvs(vs, e.keys)
if !ok {
kvs = make([]keyVal, len(vs))
var i int
for k, v := range vs {
kvs[i] = keyVal{k, v}
i++
}
sort.Slice(kvs, func(i, j int) bool {
return kvs[i].key < kvs[j].key
})
}

for i, kv := range kvs {
if i > 0 {
e.writeByte(',', objectColor)
Expand Down