urfave / cli

A simple, fast, and fun package for building command line apps in Go
https://cli.urfave.org
MIT License
21.91k stars 1.69k forks source link

Issue with object being nil in action function #1817

Closed rhugga closed 6 months ago

rhugga commented 9 months ago

I'm not sure what has changed or why this isn't working but I was doing something similar in code about 2 years back.

I have the following sample struct with a Load() method that is a cli.BeforeFunc() (gonna simplify this greatly)

// Options encapsulates run time options coming from the cli and env
type Options struct {
    Host string
}

// Load is a cli before func used to inject cli options into the app context
func (o *Options) Load(app *cli.Context) error {
        o.Host = app.String("host")
    return nil
}

Then I have a top level command:

func Cmd(ctx context.Context) *cli.Command {
    return &cli.Command{
        Name:  "mycommand",
        Subcommands: []*cli.Command{
            MyCmd(ctx), 
        },
    }
}

Then I have a subcommand that will be using the options.


func MyCmd(ctx context.Context) *cli.Command {
        // here I instantiate my options struct
    options := &common.Options{}
    return &cli.Command{
        Name:   "foo",
        Before: options.Load,   // cli beforeFunc that loads cli options into my struct 
        Action: MyCmdAction(ctx, options),   // pass options to action function
        Flags:  myCommandFlags,
    }
}

func MyCmdAction(ctx context.Context, o *common.Options) func(app *cli.Context) error {
    return func(app *cli.Context) error {
        fmt.Printf("OPTIONS=%+v\n", o) // nil here
        return nil
    }
}

I have no idea why options is nil when passed to MyCmdAction(). Any ideas? I was doing stuff similar to this before.

dearchap commented 8 months ago

@rhugga Your sample code looks ok. I believe it should work. We've done some optimizations in urfave/cli since 2 years however all unit tests before those changes still pass. My guess is its the way the App.Run is called. Can you provide a complete sample with App initialization and when App.Run() is called ?

dearchap commented 7 months ago

@rhugga The following code based on your sample works as intended

package main

import (
    "context"
    "fmt"
    "os"

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

// Options encapsulates run time options coming from the cli and env
type Options struct {
    Host string
}

// Load is a cli before func used to inject cli options into the app context
func (o *Options) Load(app *cli.Context) error {
    o.Host = app.String("host")
    return nil
}

func Cmd(ctx context.Context) *cli.Command {
    return &cli.Command{
        Name: "mycommand",
        Subcommands: []*cli.Command{
            MyCmd(ctx),
        },
    }
}

func MyCmd(ctx context.Context) *cli.Command {
    // here I instantiate my options struct
    options := &Options{}
    return &cli.Command{
        Name:   "foo",
        Before: options.Load,              // cli beforeFunc that loads cli options into my struct
        Action: MyCmdAction(ctx, options), // pass options to action function
    }
}

func MyCmdAction(ctx context.Context, o *Options) func(app *cli.Context) error {
    return func(app *cli.Context) error {
        fmt.Printf("OPTIONS=%+v\n", o) // nil here
        return nil
    }
}

func main() {
    a := &cli.App{
        Commands: []*cli.Command{
            Cmd(context.TODO()),
        },
        Flags: []cli.Flag{
            &cli.StringFlag{
                Name: "host",
            },
        },
    }

    a.Run(os.Args)
}
$ go run main.go --host hello mycommand foo 
OPTIONS=&{Host:hello}

I moved the flags to within the subcommand and that worked as well

$ go run main.go mycommand foo --host hello
OPTIONS=&{Host:hello}

So when you say it doesnt work is it possible you didnt specify the host flag on cmdline or maybe it was defined to pickup from the env ?