Skip to content

App.Before not firing when calling --help #1719

Closed as not planned
Closed as not planned

Description

@rdlaitila

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.

Activity

  1. added
    area/v2relates to / is being considered for v2
    kind/bugdescribes or fixes a bug
    status/triagemaintainers still need to look into this
    on Apr 10, 2023
  2. dearchap commented on Apr 11, 2023

    @dearchap
    Contributor

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

  3. rdlaitila commented on Apr 11, 2023

    @rdlaitila
    Author

    @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

  4. dearchap commented on Apr 12, 2023

    @dearchap
    Contributor

    @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.

  5. dearchap commented on Jun 19, 2023

    @dearchap
    Contributor

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

  6. rdlaitila commented on Jun 19, 2023

    @rdlaitila
    Author

    @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!

  7. added this to the Release 2.x milestone on Jun 21, 2023
  8. wxiaoguang commented on Jun 28, 2023

    @wxiaoguang
    Contributor

    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.

  9. wxiaoguang commented on Jul 23, 2023

    @wxiaoguang
    Contributor

    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.

  10. Olimpian commented on Jul 29, 2025

    @Olimpian

    A new case to use beforeFun() before --help flag is to set up a language for that help-information.
    And also it is really strange, that help as a function calls befireFuns(), but help as a flag - is not, imho.

    UPDATE:
    Version: github.com/urfave/cli/v3 v3.3.8
    Also, if i add new BoolFlag with Name = "help", it adds without problems. And it is shown in help-message, but you can't call it, cause standard --help is in prior. And you can do it for every command, not only for app. And copied flag several types.

    &cli.Command{
    Name: "comand1",
    Flags: []cli.Flag{
    			&cli.BoolFlag{
    				Name: "help",
    			},
    			&cli.BoolFlag{
    				Name: "help",
    			},
    		},
    }
    

    get if you call
    app command1 --help
    you will see:

    OPTIONS:
       --help      (default: false)
       --help      (default: false)
       --help, -h  show help
    

    And you can create similarly named flags of any type or name.
    I think it's a bug...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/v2relates to / is being considered for v2kind/bugdescribes or fixes a bugstatus/triagemaintainers still need to look into this

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions