magefile / mage

a Make/rake-like dev tool using Go

Home Page:https://magefile.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Enhancement: RunF

perj opened this issue · comments

Describe the feature
Currently in my magefile.go:

func runf(cmd string, args ...string) mg.Fn {
	return newRunfn(func() error {
		return sh.Run(cmd, args...)
	}, "Run", args...)
}

func runfv(cmd string, args ...string) mg.Fn {
	return newRunfn(func() error {
		return sh.Run(cmd, args...)
	}, "RunV", args...)
}

type runfn struct {
	name string
	id   string
	f    func() error
}

func newRunfn(f func() error, name string, args ...string) runfn {
	id, err := json.Marshal(args)
	if err != nil {
		panic(err)
	}
	return runfn{
		name: name,
		id:   string(id),
		f:    f,
	}
}

func (fn runfn) Name() string {
	return fn.name
}

func (fn runfn) ID() string {
	return fn.id
}

func (fn runfn) Run(ctx context.Context) error {
	return fn.f()
}

Used as such:

func Test() {
	mg.Deps(
		runf("go", "test", "-tags", "test", "./..."),
		runfv("docker-compose", "pull"),
	)
}

I think it would be useful to have this in the sh or mg package, probably as RunF and RunFV and perhaps more.

What problem does this feature address?

It lets you put shell commands directly in mg.Deps calls.
I think it improves readability to avoid the single line functions just calling sh.Run or sh.RunV.

My personal opinion is that this is not needed, as there are already facilities for this use case.
As an example, using all 4 acceptable function signatures:

func SomeTarget() {
	mg.Deps(
		func() { sh.RunV("echo", "Hello, World!") },
		func() error { return sh.RunV("echo", "Season's Greetings!") },
		func(ctx context.Context) { sh.RunV("go", "version") },
		func(ctx context.Context) error { return sh.RunV("go", "doc", "fmt.Print") },
	)

	fmt.Println("running my target!")
}

Which in my case will output:

Hello, World!
Season's Greetings!
go version go1.17.5 linux/amd64
package fmt // import "fmt"

func Print(a ...interface{}) (n int, err error)
    Print formats using the default formats for its operands and writes to
    standard output. Spaces are added between operands when neither is a string.
    It returns the number of bytes written and any write error encountered.

running my target!

Therefore, unless I've misunderstood your argument, I don't think there's a strong case for adding the extra complexity within Mage.

I suppose you have a point, that does work. It's not exactly the same though, since these all have unique IDs, not based on the actual function and arguments, so they might be run multiple times instead of only once. In that way it still makes a difference.

It would work just as well to have mg.F support variadic functions though, I've realised. In that case sh.Run could be passed to mg.F.

Edit: Forgot to mention that it's also prettier, but that is subjective, of course. 🙂

since these all have unique IDs, not based on the actual function and arguments, so they might be run multiple times instead of only once

This part I'm not sure I understand what you mean.

As it stands, if I needed to reuse any of those functions, I could define them anywhere in my magefile, like so:

func myUsefulHelper() error {
	return sh.RunV("go", "version")
}

func SomeTarget() {
	mg.Deps(myUsefulHelper)
	// some interesting code...
}

func SomeOtherTarget() {
	mg.Deps(myUsefulHelper)
	// some interesting code...
}

Is this different to the usecase you had in mind?


As for it being prettier? I totally agree.
Your way looks a lot neater at the call site. I do question whether a pretty call site is worth the added complexity in this case, but as you said, subjective.

I can share the concern that this might lead to needing to duplicate all future Run versions as well as the current one. With that and the fact that mg.F could be made to support this I think that's a better way forward. I'll create an issue for that instead.