In this lab you will become familar with Go dependencies using them to create a better command-line.
- Go dependencies
- Cobra CLI
Starting with lab 2, this lab we will add a dependency to the project using a tool known for better commandline development called Cobra.
Note: This lab assumes you have a solution for lab 2 as a starting point.
- CLI tools: https://awesome-go.com/#command-line
From the root of the project:
go get -u github.com/spf13/cobra
note: need help understanding go get
? checkout out go help get
Take a look at go.mod
cat go.mod
module github.com/codementor/wman
go 1.14
require (
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/mitchellh/mapstructure v1.3.1 // indirect
github.com/pelletier/go-toml v1.8.0 // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/cobra v1.0.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.7.0 // indirect
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 // indirect
gopkg.in/ini.v1 v1.57.0 // indirect
)
From project root, mkdir -p pkg/cmd
Create a file under cmd
named root.go
package cmd
import (
"github.com/spf13/cobra"
)
// NewWmanCmd creates a new root command for wman
func NewWmanCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "wman",
Short: "Weather Man CLI",
Long: `Weather Man (wman) CLI for capturing weather information.`,
SilenceUsage: true,
Example: ` # Run reverse function
wman reverse nofluff
`,
}
return cmd
}
Here the func NewWmanCmd
returns a pointer to corba.Command. Inside the func, we configure the Command structure.
Lets change main.go to call and run this command.
import (
"os"
"github.com/codementor/wman/pkg/cmd"
)
func main() {
if err := cmd.NewWmanCmd().Execute(); err != nil {
os.Exit(-1)
}
}
Try running: make run
or with some flags: go run cmd/wman/main.go --help
Create a file under cmd
named print.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var (
printExample = ` # Prints a hello message to the passed in name
wman print nofluff`
)
// newPrintCmd returns a new initialized instance of the print sub command
func newPrintCmd() *cobra.Command {
printCmd := &cobra.Command{
Use: "print",
Short: "Print hello to the passed in argument",
Example: printExample,
RunE: PrintCmd,
}
return printCmd
}
// PrintCmd performs the print sub command
func PrintCmd(cmd *cobra.Command, args []string) error {
// might be worth err checking args
fmt.Printf("Hello %q\n", args[0])
return nil
}
Add before the return cmd
the following (which includes the return for reference):
cmd.AddCommand(newPrintCmd())
return cmd
Lets run it: go run cmd/wman/main.go print nofluff
try: go run cmd/wman/main.go
try: go run cmd/wman/main.go p
how great is:
go run cmd/wman/main.go p
Error: unknown command "p" for "wman"
Did you mean this?
print
Run 'wman --help' for usage.
exit status 255
Run go run cmd/wman/main.go print nofluff
go run cmd/wman/main.go print nofluff
Hello "nofluff"
In the print.go
file, do the following:
Add a structure for print options:
type printOptions struct {
Reverse bool
}
note: a structure is not required but is common.
First line after the newPrintCmd
func, create an instance of the options.
func newPrintCmd() *cobra.Command {
opts := &printOptions{}
After the printCmd
is created and before it is returned write the following:
printCmd.Flags().BoolVar(&opts.Reverse, "reverse", false, "If set to true, it reverses the argument passed in (default \"false\")")
Test the command: go run cmd/wman/main.go print -h
Change the method signature to PrintCmd
func and implement the logic:
func PrintCmd(options *printOptions, args []string) error {
Now we need to change the RunE
of construction of cobra.Command
for printCmd
RunE: func(cmd *cobra.Command, args []string) error {
return PrintCmd(opts, args)
},
In order to implement the logic in PrintCmd
it is necessary to import our string package: "github.com/codementor/wman/pkg/string"
, if you try to import that package and run you get:
go run cmd/wman/main.go print tests
# github.com/codementor/wman/pkg/cmd
pkg/cmd/print.go:27:39: use of package string without selector
pkg/cmd/print.go:38:43: use of package string without selector
the issue if you search through code is there are other uses of the word string
in fact string is a keyword. To resolve this, try importing:
import wstring "github.com/codementor/wman/pkg/string"
which means that a call to Reverse
func is wstring.Reverse()
Implement the rest of it such that print nofluff --reverse
produces:
go run cmd/wman/main.go print nofluff --reverse
Hello "ffulfon"
Before we are done...
make lint
make test
go mod tidy
# see the change in go.mod
In pkg/string/string.go
add the following:
// Reversable is a string that is reversable
type Reversable string
// Reverse reverse a reversable
func (s Reversable) Reverse() Reversable {
return Reversable(Reverse(string(s)))
}
Here we define a new type called Reversable
which is of type string. Followed by a method which is specific to "Reversable" types. The function is complicated by a series of casting, the s
Reversable is casted to string, which is passed to the Reverse
function that returns a string, so it must be casted back to Reversable
as a return type.
The logic in print.go
changes to:
name := wstring.Reversable(args[0])
if options.Reverse {
name = name.Reverse()
}
fmt.Printf("Hello %q\n", name)
return nil
https://github.com/codementor/wman/tree/lab3-solution
Clone: git clone -b lab3-solution https://github.com/codementor/wman.git
and
https://github.com/codementor/wman/tree/lab3-with-bonus-solution
Clone: git clone -b lab3-with-bonus-solution https://github.com/codementor/wman.git