alexflint / go-arg

Struct-based argument parsing in Go
https://pkg.go.dev/github.com/alexflint/go-arg
BSD 2-Clause "Simplified" License
2.04k stars 100 forks source link

Feature idea: configuration file alternative to CLI | env arg #225

Open SamuelMarks opened 1 year ago

SamuelMarks commented 1 year ago

I added config file support to my go-arg using project but due to #94 I could not determine when to read the config.

My project hacks support with the builtin json package. Specifically here: https://github.com/offscale/postgres-version-manager-go/blob/master/pvm/config_utils.go (with go-arg annotated struct here: config.go)

Any chance we can get support for a configuration file in your project directly?

Thanks

EDIT: Got it working in my latest commit 48cba8 but it was a major hack; here's a taste [from that commit]:

func FieldAndValueWhenNonDefaultValue(configStruct ConfigStruct) map[string]interface{} {
    val := reflect.ValueOf(configStruct)
    fieldToValue := make(map[string]interface{})

    for i := 0; i < val.NumField(); i++ {
        field := val.Type().Field(i)
        defaultTag := field.Tag.Get("default")
        fieldValue := fmt.Sprintf("%v", val.Field(i).Interface())

        if defaultTag != fieldValue && defaultTag != "" {
            fieldToValue[field.Name] = fieldValue
        }
    }

    return fieldToValue
}

Issues with this design include:

alexflint commented 1 year ago

The approach I'm hoping to take to enable this is the one from #197. Need to get that PR up to date with all recent changes to this repo, and finish a few things before publishing it as "github.com/alexflint/go-arg/v2"

purpleidea commented 8 months ago

Heya Alex!

I've been doing a little exploration of the command-line parsing libraries in golang... I'm currently using urfave/cli for https://github.com/purpleidea/mgmt/ ... I've noticed their new "v3" API is generics ridden and really clunky. I thought there must be a better way... I found this project...

...And I think the "struct populating" approach is excellent and very close to what I was looking for!

My two main concerns were:

  1. I'm also interested in pulling data from config files as a default... Hence why I'm commenting here in this issue that I found...
  2. Activity in this repo is quieter than I'd like.

I'd actually love to be more involved, but I'm fairly busy working on https://github.com/purpleidea/mgmt/ so, I don't expect too many hours.

In any case, I just figured I'd check in, say that I like what you've been doing, and in case you're still hacking at your monastery, please keep it up!

Cheers

@SamuelMarks -- if you're hacking on adding your config work into core or helping with Alex's v2 approach and need a hand, please LMK.

SamuelMarks commented 8 months ago

@purpleidea Actually after my #RewriteInRust I decided to #RewriteInC

alexflint commented 8 months ago

Thanks for the note @purpleidea. Care to jump on a call some time to say hi / chat? Any chance you're free at 3pm US Eastern time one day this week?

@SamuelMarks would be great to meet you face-to-face at some point, too, though I know you're busy!

SamuelMarks commented 8 months ago

Good idea! Send me an invite: my name at gmail.com

purpleidea commented 8 months ago

@alexflint

Thanks for the note @purpleidea. Care to jump on a call some time to say hi / chat? Any chance you're free at 3pm US Eastern time one day this week?

Sure! I'm free either today or tomorrow at that time, otherwise TBD. Send me your preferred meeting thing, I've got signal, jitsi, gmeet or anything that works in a web browser. my username at gmail. Cheers!

purpleidea commented 8 months ago

I've been thinking about how to pull values from config file into go-arg. Here's what I've come up with.

1) It's silly to directly support json, yaml, toml, etc, etc, since someone will always be missing what they want.

2) Instead, we should have a new struct tag. Let's pretend it's called file. You specify the "key" that you want to use.

3) In the parser, we have the ability for the user to pass in a function of the signature:

func(ctx context string key) (*string, error)

When the library is looking for a value to use, it calls that function with the key from the struct. If it gets a value back, it passes it through the usual parsing that we use for the default tag. If it has an issue pulling a value (io error or whatever) we error. If it doesn't find a value then we return nil, nil.

That way everyone can have their own parser. And of course if we really want to, we can add one or two common ones as a util package.

HTH

purpleidea commented 8 months ago

In the next little while, I'd really like to be able to do this for https://github.com/purpleidea/mgmt/ ... I've landed all my new CLI patches, and they look great! Mostly here: https://github.com/purpleidea/mgmt/commit/589a5f9aeb5663b3e00107f3ae8a311903c20298

But also to show how other code then simplifies, look at:

https://github.com/purpleidea/mgmt/commit/d537c3d523c202e81895d6cad0ca512f6a6f1ee5 https://github.com/purpleidea/mgmt/commit/601fcf40c4b0150f0545e453d8c149c2a854f7b1 https://github.com/purpleidea/mgmt/commit/51d21b8dab99e0b30a7fff0496064055683891e8 https://github.com/purpleidea/mgmt/commit/9527d0dcbdfc9919a0fd5db1440c95b2128dd231

It's really an improvement! Thanks Alex for this great library!

So instead of bolting config file reading onto mgmt, I'd rather bolt it onto this go-arg lib where it belongs. I would send a patch, but if that would conflict with your v2 patch, then maybe that generates more work for you which I don't want to do if you're not up for it. This patch also wouldn't need a v2 API change, so I think it's safe to add in before it.

I also don't know how receptive to this idea you are and how many cycles you have to review and merge this.

So please let me know how you'd like me to proceed.

Thanks again!

oderwat commented 4 months ago

I needed a quick way to support config through a file and just used an ".env" file approach and env variables. To make this an easy add I just hacked the ".env" variables into my program using some simple load. Here is the initial code I generated using claude.ai, because I was feeling very lazy.

package main

import (
    "bufio"
    "os"
    "strings"
)

func init() {
    loadEnv()
}

func loadEnv() {
    file, err := os.Open(".env")
    if err != nil {
        // Silently ignore if the file doesn't exist
        return
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := scanner.Text()
        if equal := strings.Index(line, "="); equal >= 0 {
            if key := strings.TrimSpace(line[:equal]); len(key) > 0 {
                value := ""
                if len(line) > equal {
                    value = strings.TrimSpace(line[equal+1:])
                }
                os.Setenv(key, value)
            }
        }
    }
}

func main() {
    var args struct {
        Server   string `arg:"env:GITEA_SERVER" default:"localhost:3000" help:"gitea server address"`
        UserName string `arg:"required,env:GITEA_USERNAME" help:"gitea user name"`
        Password string `arg:"required,env:GITEA_PASSWORD" help:"gitea password"`
    }
    arg.MustParse(&args)
    // Your program logic here
}