GO Approximation of Typer
Typer is one of my favourite Python packages. It allows building CLI apps with next to zero boilerplate. See the Typer docs for an example.
Working in Go, I want something similar. So I am creating Goat ๐, trying to get the best experience possible.
package main
import (
"fmt"
"github.com/tmr232/goat" // One explicit dependency
)
// Generate all the necessary magic
//go:generate go run github.com/tmr232/goat/cmd/goater
func app(name string, goodbye bool) {
if goodbye {
fmt.Printf("Goodbye, %s.\n", name)
} else {
fmt.Printf("Hello, %s!\n", name)
}
}
func main() {
// Let goat know what to run
goat.Run(app)
}
Slowly moving forward, but not yet stable.
Experimentation, bug reports, and feature requests are very welcome.
The Goat API is built of 4 main parts, numbered in the code below.
package main
import (
"fmt"
"github.com/tmr232/goat"
)
// (1) The goater command
//go:generate go run github.com/tmr232/goat/cmd/goater
// (3) The app function signature
func app(name string, goodbye bool, question *string, times int) {
// (4) Flag Descriptors
goat.Flag(name).
Usage("The name to greet")
goat.Flag(goodbye).
Name("bye").
Usage("Enable to say Goodbye")
goat.Flag(question).
Usage("Instead of a greeting, ask a question.")
goat.Flag(times).
Usage("Number of repetitions").
Default(1)
for i := 0; i < times; i++ {
if question != nil {
fmt.Printf("%s, %s?", *question, name)
} else {
if goodbye {
fmt.Printf("Goodbye, %s.\n", name)
} else {
fmt.Printf("Hello, %s!\n", name)
}
}
}
}
func main() {
// (2) The Run function
goat.Run(app)
}
To get the functionality we aim for, we use code generation.
We read the user's code, infer the relevant information,
and generate wrapper code that later calls into it.
This generation is done using the goater
command.
You can either run it manually in the relevant package directory,
or use go:generate
to do it for you.
To let the goater
command know which functions to wrap,
we call goat.Run
with those function.
In this case - goat.Run(app)
means that we'll wrap the app
function.
Later, during execution, goat.Run
calls into the wrapper for app
.
This is where things get interesting.
The app function (any function passed into goat.Run
) is parsed during code
generation, and a wrapper is generated for it.
The signature of the function determines the types of flags that will be generated.
An int
argument will result in an int
flag, a bool
argument in a bool
flag,
and a string
argument in a string
flag.
If an argument is a pointer (*string
, for example) the flag will be optional.
If an argument is not a pointer, it'll be a required flag.
bool
is an exception as it is never required.
A name and a type for a flag are nice, but hardly enough. We may want to define aliases, usage strings, or default values. To do this - we describe our flags as follows:
goat.Flag(name).
Usage("The name to greet")
You can use the following to add data to your flags:
Usage(string)
- add a usage stringName(string)
- set the name of the flagDefault(any)
- set the flag's default value. Works only with non-pointer flags.
Goat also allows defining subcommands
package main
import (
"fmt"
"github.com/tmr232/goat" // One explicit dependency
)
//go:generate go run github.com/tmr232/goat/cmd/goater
func server(name string) {
}
func app(name string, goodbye bool) {
if goodbye {
fmt.Printf("Goodbye, %s.\n", name)
} else {
fmt.Printf("Hello, %s!\n", name)
}
}
func main() {
// Let goat know what to run
goat.Run(app)
}
Goat currently uses urfave/cli for parsing flags. Other than that, the generated code currently only depends on the standard library.
In the future, I plan to write backends for other popular flag-parsing libraries (namely Cobra and flag) so that users can choose what they depend on.
See this blog post for more implementation details.