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

A use-case clarification needed #6

Open
Shpota opened this issue Feb 26, 2020 · 3 comments
Open

A use-case clarification needed #6

Shpota opened this issue Feb 26, 2020 · 3 comments

Comments

@Shpota
Copy link

Shpota commented Feb 26, 2020

Hi guys,

We are currently evaluating a CLI utility to use with goxygen (a generator of web projects). I like the idea of a light-weight CLI utility that is straight forward and doesn't bring unneeded dependencies. That's why I thought we could try Clir. But I went through the Clirs documentation/examples and I can't figure out if it is capable of covering our use-case.

Soon we'll have to implement something like this:
goxygen init --frontend angular --db mysql my-new-app
or
goxygen init --frontend=angular --db=mysql my-new-app

The options for the flags would have to be validated against a predefined set of parameters. Say for the --frontend flag the options could only be angular,react and vue, etc.

Q: Can you please tell me if we can achieve this with Clir? Or how much of it can already be handled by the lib?

Thank you.

Best regards,
Sasha.

@leaanthony
Copy link
Owner

Hi there Sasha! Thanks for your interest.

The goal of Clir was really to take the Go approach to creating CLIs: clear, concise, no frills. It doesn't cover your use case 100% but gets very close. For instance, there is no built-in validation, that's up to the user to decide how to do that. I did look at your use case and came up with an approximation:

package main

import (
	"fmt"

	"github.com/leaanthony/clir"
	"github.com/leaanthony/slicer"
)

func main() {

	app := clir.NewCli("goxygen", "A generator of web projects", "0.0.1")

	var frontend = "vue"
	var db = "mysql"
	var name string

	initCmd := app.NewSubCommand("init", "Initialize a new project")

	initCmd.
		StringFlag("frontend", "The frontend to use", &frontend).
		StringFlag("db", "database to use", &db).
		StringFlag("name", "Name of project", &name)

	initCmd.Action(func() error {

		// Setup frontend flag
		frontendOptions := slicer.String([]string{"react", "vue", "angular"})

		// Validate frontend
		if !frontendOptions.Contains(frontend) {
			return fmt.Errorf("frontend '%s' is invalid. Valid options: %s", frontend, frontendOptions.Join(", "))
		}

		// Validate db....

		// Validate name....
		if len(name) == 0 {
			return fmt.Errorf("name required")
		}

		setupProject(frontend, db, name)

		return nil
	})

	err := app.Run()
	if err != nil {
		println("Error:", err.Error())
	}
}

func setupProject(frontend, db, name string) {
	println("Setting up project: ", frontend, db, name)
}

I've been thinking about whether to add required/validation to the package. The goal has always to let it be simple though. If you're happy to work with me, we could add in the missing features.

@moqmar
Copy link

moqmar commented Mar 5, 2020

I haven't used clir a lot yet, but maybe it's possible to add hook interfaces that don't infer with the current usage of the library?

I'm thinking of an implementation somewhat like this:

type StringContext struct {
  Flag string
  Exists bool
  Value string
  Error error
}
type StringHook interface{
  StringHook(context *StringContext)
}
func (c *Cli) StringFlag(name, description string, variable *string, hooks ...StringHook) *Cli

Then clir could provide e.g. defaults & validations very easily:

app := clir.NewCli("goxygen", "A generator of web projects", "0.0.1")
initCmd := app.NewSubCommand("init", "Initialize a new project")
initCmd.
	StringFlag("frontend", "The frontend to use", &frontend, clir.WithPossibleValues("react", "vue", "angular")).
	StringFlag("db", "database to use", &db, clir.WithDefault("mysql"), clir.WithPossibleValues("mysql", "postgres")).
	StringFlag("name", "Name of project", &name, clir.WithRequirement()).
	IntFlag("n", "A positive number", &n, clir.WithIntHook(func(context *clir.IntContext) {
		if context.Value <= 0 {
			context.Error = errors.New(context.Flag+ " must be positive")
		}
	}))

It would also very flexibly allow stuff like this:

var duration time.Duration
initCmd.StringFlag("timeout", "Maximum duration for this", nil,
  WithEnvironmentFallback("TIMEOUT"),
  ToInterval(&duration),
)

What would you think about something like that? Or would it be too complex for clir's simplicity?

@leaanthony
Copy link
Owner

leaanthony commented Mar 6, 2020

I actually like this, and thanks for taking time to write about it! I originally was toying with the idea of making flags their own type and then having chained methods like this:

initCmd.AddFlag( clir.StringFlag("db", "database to use", &db).Default("mysql").OneOf("mysql", "postgres"))

but potentially just having them as context types in the actual call may be tidier?

Alternatively, We could just return a StringFlag type from StringFlag and define all flags in a non-chained way:

initCmd.StringFlag("db", "database to use", &db).Default("mysql").OneOf("mysql", "postgres")
initCmd.IntFlag("n", "A positive number", &n).Required()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants