urfave / cli

A simple, fast, and fun package for building command line apps in Go

Home Page:https://cli.urfave.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

App.Before not firing when calling --help

rdlaitila opened this issue · comments

My urfave/cli version is

github.com/urfave/cli/v2 v2.25.1

Checklist

  • Are you running the latest v2 release? The list of releases is here.
  • Did you check the manual for your release? The v2 manual is here
  • Did you perform a search about this problem? Here's the GitHub guide about searching.

Dependency Management

  • My project is using go modules.

Describe the bug

App.Before fails to fire when running --help

To reproduce

Create a simple CLI program with a App.Before callback

package main

import (
	"fmt"
	"log"
	"os"

	"github.com/urfave/cli/v2"
)

func main() {
	app := &cli.App{
		Name:   "boom",
		Usage:  "make an explosive entrance",
		Before: before,
		Action: func(*cli.Context) error {
			fmt.Println("boom! I say!")
			return nil
		},
	}
	if err := app.Run(os.Args); err != nil {
		log.Fatal(err)
	}
}

func before(c *cli.Context) error {
	panic("HALT!")
}

Observed behavior

The CLI successfully prints the help screen

Expected behavior

The CLI should panic with HALT! due to running the requested App.Before callback

Additional context

In my usage I rely heavily on the App.Before callback to setup required dependencies, fill in missing flag information, and validate state and configurations. This works well when the entrypoint is an existing or non existing flag/command. However the App.Before seems to be bypassed when printing help.

In my usage if the user were to try and print help while using the program in an invalid context, my Before callback cannot catch this to inform the user.

@rdlaitila This is by design. Can you provide an example of when you need a Before callback to be triggered when "--help" is invoked. ?

@dearchap thanks for the quick reply. I have other initialization logic that is meant to present to the user even under --help scenarios. Here is one contrived example where I would like to set a default for a flag that is not set, that is dynamic based on where the user is in the filesystem:

package main

import (
	"fmt"
	"log"
	"os"

	"github.com/urfave/cli/v2"
)

func main() {
	app := &cli.App{
		Name:   "boom",
		Usage:  "make an explosive entrance",
		Before: before,
		Action: func(*cli.Context) error {
			fmt.Println("boom! I say!")
			return nil
		},
		Flags: []cli.Flag{
			&cli.PathFlag{
				Name:  "project-dir",
				Usage: "The path to the project",
			},
		},
	}
	if err := app.Run(os.Args); err != nil {
		log.Fatal(err)
	}
}

func before(c *cli.Context) error {
	if !c.IsSet("project-dir") {
		cwd, _ := os.Getwd()
		c.Set("project-dir", cwd)
		// other behaviors such as settings the default text of the flag so the
		// user knows what 'project-dir' is currently being operated on.
	}
	return nil
}

My example works with any combination of valid or invalid commands / flags, but fails for the one case of calling --help where the user still may want to have specific defaults set and contextual information provided to them.

I did experiment with calling App.Setup prior to calling a function to do my checks/setup, but that call does not seem to do flag parsing so flag information is not filled in yet. I have an odd case where I'm hoping cli can do the heavy lifting of parsing flags for me, but I can interrupt prior to actually running any commands to do some additional setup and fiddling with the flag values and settings.

EDIT: I want to add that my cli project dynamically generates new commands and flags for the user based on a project configuration within a directory. Since --help is not injectable in the way i'm describing, i'm unable to load a settings file to know what custom user-defined commands and flags are available to show to the user when issuing --help

@rdlaitila In this case you should actually define the flag as

cwd, _ := os.Getcwd()
app := &App {
             ....
             Flags: []cli.Flag{
			&cli.PathFlag{
				Name:  "project-dir",
				Usage: "The path to the project",
                                Value: cwd,
			},
		},

This way you know that the default for project-dir will be cwd if nothing is set. If you have other flags which you want to fill in dynamically depending on contents on project-dir you should default them similarly. Of course that would sort of preclude creating flags dynamically. I'm sorry but at this point there are no other hooks available to do what you want.

@rdlaitila I am revisiting this in the hope of adding more callback hooks. What kind of hooks would you like to see ?

@dearchap thanks for the follow up. Per your original suggestion I ended up moving almost all of my dynamic initialization logic outside of the App's setup/bootstrapping hooks. This ensured that any dynamic commands and flags that are defined by a user supplied config file are present in the App's declarative structure prior to calling into App.Run (by parsing the config file and generating new flag/command structs).

The downside to this is my initialization logic becomes a imperative shell around urfave/cli's declarative behavior for which I was hoping to avoid. I really enjoy the declarative nature of urfave/cli and was hoping I could have the right pre/before/bootstrap/setup hooks on the app, flag, or command levels to dynamically generate and mutate the structure of the App and sub components prior to the app actually running, even in the --help scenario I detailed earlier.

Again the only big hiccup I had was in order for my project to parse a config file with user defined flags/commands, I need to locate said config file prior to parsing. So I need to manually parse a few flags and environment variables prior to flag/command generation ie: --project-file to know what to load. So a bit of a chicken and egg problem where I need cli structure to parse --project-file and at the same time generate new cli structures prior to the full CLI being executed.

Below is what I could imagine as a way to hook into the CLI bootstrap/setup lifecycle to eagerly get values before the full CLI is parsed ready for execution. Here we introduce a Build callback that allows injecting actions into the flag/command graph while its being parsed:

package main

var values = struct{
	ProjectDir *string
	ProjectFile *string
	Project *Project
}{}

var app = &cli.App{
	Name: "A Dynamic CLI App",
	Flags: []cli.Flag{
		&cli.PathFlag{
			Name: "project-dir",
			Env: ["PROJECT_DIR"],
			Usage: "Path to the project directory. defaults to current directory"
			Destination: values.ProjectDir,
			Build: func(c *cli.FlagBuildCtx) error {															
				// set value if the flag was not found or parsed via flag or env var
				if !c.Parsed() {
					cwd, _ := os.Cwd()
					c.SetValue(cwd)
				}
				return nil	
			}
		},
		&cli.PathFlag{
			Name: "project-file",
			Env: ["PROJECT_FILE"],
			Usage: "Path to the project file. defaults to {project-dir}/config.yaml"
			Destination: values.ProjectFile,
			Build: func(c *cli.FlagBuildCtx) error {
				// set value if the flag was not found or pasred via flag or env var
				if !c.Parsed() {
					path := strings.Join([]string{values.ProjectDir, "config.yaml"}, "/")
					c.SetValue(path)
				} else {
					c.SetValue(c.ParsedValue)
				}				
				project, _ := parseConfigFile(values.ProjectFile)				
				values.Project = project
				return nil
			}
		}
	}
	Commands: []*cli.Command{
		&cli.Command{
			Name: "init",
			Usage: "Initializes a new project",
			Action: func(c *cli.Context) error {
				...
			}
		},
	},
	Build: func(c *cli.AppBuildCtx) error {
		c.AfterFlagBuilt("project-file", func(cf *cli.FlagBuildCtx) error {
			for _, cmd := range values.Project.Commands {
				c.AddCommand(&cli.Command{
					Name: cmd.Name,
					Usage: cmd.usage,
					Build: buildProjectCommand,
				})
			}
		})
	}
}

func parseConfigFile(path string) error {
	...
}

func buildProjectCommand(c *cli.CommandBuildCtx) error {	
	...
}

Each component type has a specific build context called in whatever order urfave/cli handles the go structs:

  • cli.AppBuildCtx
  • cli.FlagBuildCtx
  • cli.CommandBuildCtx

Each build context can have lifecycle or other component specific actions:

  • AppBuildCtx.AfterFlagBuilt(flag string, func)
  • AppBuildCtx.AfterCommandBuilt(command string, func)
  • AppBuildCtx.AddFlag(cli.Flag)
  • AppBuildCtx.AddCommand(*cli.Command)
  • CommandBuildCtx.AfterFlagBuild(flag string, func)
  • CommandBuildCtx.AfterSubCommandBuilt(command string, func)
  • CommandBuildCtx.AddFlag(cli.Flag)
  • CommandBuildCtx.AddSubCommand(*cli.Command)

Just a rough take on the idea to hook into urfave/cli's bootstraping logic and move any imperative bits into urfave/cli's declarative structure. This all runs pre command invocation (even in --help scenarios).

No expectations here just what I think would solve my advanced use case. Thanks again for the discussion!

This is a really important and nice feature. I also need to have "app.Before" called before "--help" to prepare some dynamic contents for the help output.

Thank you very much.

Update: with HelpFlag=nil, I think this feature is not a must. Because developer can totally disable the bultin help action, and use their own code to handle helps.