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.