mkideal / cli

CLI - A package for building command line app with go
MIT License
732 stars 43 forks source link

Preserve orders for map types #29

Closed suntong closed 7 years ago

suntong commented 7 years ago

The go map is known to not preserver map orders. I.e., if we run the following sample code a couple of times,

$ go run 006-map.go -Dx=1 -D y=2
{"Macros":{"x":1,"y":2}}

The output might not always be the same. Is it possible to obtain/preserver output orders to that given on command line? I know the current data-structure Macros map[string]int need to be replaced by some cli specific types, i.e., API changes. So,

Just asking. Thanks.

suntong commented 7 years ago

Hmm, in fact, I do hope that at least I can obtain orders that given on command line. Thx.

mkideal commented 7 years ago

You can replace map with slice.Here is an example:

// main.go
package main

import "github.com/mkideal/cli"
import "strings"
import "strconv"

type KV struct {
    Key   string
    Value int
}

type KVs []KV

// Decode implements cli.Decoder interface
func (kvs *KVs) Decode(s string) error {
    i := strings.Index(s, "=")
    if i >= 0 {
        key := s[:i]
        val, err := strconv.ParseInt(s[i+1:], 10, 64)
        if err != nil {
            return err
        }
        *kvs = append(*kvs, KV{key, int(val)})
    }
    return nil
}

type argT struct {
    cli.Helper
    Values KVs `cli:"D" usage:"define macro values"`
}

func main() {
    cli.Run(new(argT), func(ctx *cli.Context) error {
        ctx.JSONIndentln(ctx.Argv(), "", "    ")
        return nil
    })
}

Then run it:

go run -Dx=1 -D y=2
suntong commented 7 years ago

perfect! Thanks a lot!

suntong commented 7 years ago

If I implement cli.Decoder interface on my own, is it possible to call the default behavior from it somehow?

I.e., can I make the following code any more simpler:

package main

import (
    "strconv"
    "strings"

    "github.com/mkideal/cli"
)

type mapT map[string]int64

type argT struct {
    Macros mapT `cli:"D" usage:"define macros"`
}

var keys = []string{}

// Decode implements cli.Decoder interface
func (maps *mapT) Decode(s string) error {
    *maps = make(map[string]int64)
    keys = []string{}
    i := strings.Index(s, "=")
    if i >= 0 {
        key := s[:i]
        val, err := strconv.ParseInt(s[i+1:], 10, 64)
        if err != nil {
            return err
        }
        keys = append(keys, key)
        (*maps)[key] = val
    }
    return nil
}

func main() {
    cli.Run(new(argT), func(ctx *cli.Context) error {
        ctx.JSONln(ctx.Argv())
        return nil
    })
}

Also, when I run it, I miss "x":1 output. Why is that?

$ go run 106-2map.go -Dx=1 -D y=2
{"Macros":{"y":2}}
suntong commented 7 years ago

Oh, maybe I can collect my keys in a validator. Hmm..., guess not. validator is the last step. :-(

mkideal commented 7 years ago

Also, when I run it, I miss "x":1 output. Why is that?

Replace this line

*maps = make(map[string]int64)

with

if (*maps) == nil {
    *maps = make(map[string]int64)
}

is it possible to call the default behavior from it somehow?

You can implement mapT in a your own pkg, and export mapT(i.e. rename MapT). Then you reused it as following:

package main

import "yourpkg"

type argT struct {
    Macros yourpkg.MapT `cli:"D" usage:"define macros"`
}
...

Another, MapT maybe implemented as following:

type MapStringToInt64 struct {
    Keys []string
    Values map[string]int64
}

// DecodeSlice implements cli.SliceDecoder
// NOTE: if SliceDecoder not implemented, the Decode method would be only invoked once
func (MapStringToInt64) DecodeSlice() {}

// Decode implements cli.Decoder interface
func (m *MapStringToInt64) Decode(s string) error {
    if (m.Values) == nil {
        m.Values = make(map[string]int64)
    }
    i := strings.Index(s, "=")
    if i >= 0 {
        key := s[:i]
        val, err := strconv.ParseInt(s[i+1:], 10, 64)
        if err != nil {
            return err
        }
        m.Keys = append(m.Keys, key)
        m.Values[key] = val
    }
    return nil
}

NOTE: you shoud pull latest cli, SliceDecoder is a new feature.

suntong commented 7 years ago

I realize my mistake after sleeping on it. :-)

Thanks a lot for your reply and solution. Two thumbs up :+1:

suntong commented 7 years ago

NOTE: if SliceDecoder not implemented, the Decode method would be only invoked once

You meant it would be invoked only once per parameter, right? I.e., if I have 3 parameters, it would be invoked 3 times, right?