go-arg icon indicating copy to clipboard operation
go-arg copied to clipboard

Feature idea: configuration file alternative to CLI | env arg

Open SamuelMarks opened this issue 2 years ago • 10 comments

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:

  • Doesn't handle when replacing existing config file field explicitly with the original default value
  • Doesn't check environment variables and that env tag

SamuelMarks avatar Jul 19 '23 00:07 SamuelMarks

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"

alexflint avatar Oct 10 '23 22:10 alexflint

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.

purpleidea avatar Feb 27 '24 05:02 purpleidea

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

SamuelMarks avatar Feb 27 '24 05:02 SamuelMarks

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!

alexflint avatar Feb 27 '24 15:02 alexflint

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

SamuelMarks avatar Feb 27 '24 15:02 SamuelMarks

@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 avatar Feb 27 '24 16:02 purpleidea

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 avatar Mar 05 '24 21:03 purpleidea

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!

purpleidea avatar Mar 05 '24 21:03 purpleidea

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
}

oderwat avatar Jul 14 '24 14:07 oderwat