Goal of this article #
The goal of this article is to teach you how to create cli applications using Go's flag package. In next article you will learn how to write the exact same application using a 3rd party cli package. The complete code for this article is available in this GitHub repository.
What are you building? #
You are going to build a CLI uses the GitHub API to provide 2 commands, one for searching repos and other for searching users.
$ go run main.go -debug search-repos <name>
$ go run main.go -debug search-users -sort <sort_name> <name>
These commands will accept flags (debug
and sort
in above example) that are optional. So we can have multiple variations as follows.
$ go run main.go search-repos <name>
$ go run main.go -debug search-repos <name>
$ go run main.go -debug search-users -sort <sort_name> <name>
$ go run main.go search-users <name>
$ go run main.go -debug search-users <name>
Project Setup #
Create a new go project.
mkdir go-cli-flag
cd go-cli-flag
go mod init github.com/gurleensethi/go-cli-flag
Create a main.go
file.
package main import "fmt" func main() { fmt.Println("Hello World") }
main.go
Basic commands #
Set up the program to accept 2 commands: search-repos
and search-users
.
The following program will print any command that you pass to the cli program. For example go run main.go hello-world
will print hello-world
.
package main import ( "flag" "fmt" "os" ) func main() { flag.Parse() if len(flag.Args()) < 1 { os.Exit(1) } command := flag.Args()[0] fmt.Println(command) }
main.go
You are using flag.Args()
here instead of os.Args()
because later you will be adding a top level flag to the program.
Now allow only search-repos
and search-users
commands to be excuted and print a help message if command is invalid.
package main import ( "flag" "fmt" "os" ) var ( usage = `Specify a command to execute: - search-repos: Search for github repos - search-users: Serach for users on github.` ) func main() { flag.Parse() if len(flag.Args()) < 1 { fmt.Println(usage) os.Exit(1) } command := flag.Args()[0] err := executeCommand(command, flag.Args()[1:]) if err != nil { fmt.Println(err) os.Exit(1) } } func executeCommand(command string, args []string) error { switch command { case "search-repos": fmt.Println("search repos command") return nil case "search-users": fmt.Println("search users command") return nil default: return fmt.Errorf("invalid command: '%s'\n\n%s\n", command, usage) } }
main.go
Now if you run with an invalid command such as go run main.go hello-world
, you will get a help message:
❯ go run main.go hello-world
invalid command: 'hello-world'
Specify a command to execute:
- search-repos: Search for github repos
- search-users: Serach for users on github.
exit status 1
Note: From here on I will not be showing the complete code example as it will get too long to read so use your brain 🧠 while reading. I will focus only on parts we add or change. The complete code is available in this GitHub repo.
Debug logs under a flag #
Add your first flag to the cli command, debug
. This will be a top level flag and whenever passed will print out debug logs.
- Declare the boolean flag.
package main import ( "flag" "fmt" "os" ) var ( debug = flag.Bool("debug", false, "log out all the debug information") // 👈 usage = `Specify a command to execute: - search-repos: Search for github repos - search-users: Serach for users on github.` ) ...
main.go
- The call to
flag.Parse()
will parse this flag.
... func main() { flag.Parse() // 👈 if len(flag.Args()) < 1 { fmt.Println(usage) os.Exit(1) } command := flag.Args()[0] err := executeCommand(command, flag.Args()[1:]) if err != nil { fmt.Println(err) os.Exit(1) } } ...
main.go
- Add a function
printDebug
that prints out logs only when value ofdebug
flag is true.
... var ( debug = flag.Bool("debug", false, "log out all the debug information") ) func main() { ... } // 👇 func printDebug(msg string) { if *debug { fmt.Printf("[DEBUG]: %s\n", msg) } } ...
main.go
- Update
executeCommand
function, add a debug log printing the command.
... func executeCommand(command string, args []string) error { printDebug("Execute Command: " + command) // 👈 switch command { case "search-repos": fmt.Println("search repos command") return nil case "search-users": fmt.Println("search users command") return nil default: return fmt.Errorf("invalid command: '%s'\n\n%s\n", command, usage) } } ...
main.go
Run the program with the debug
flag, for example go run main.go -debug search-repos
will have the following output:
❯ go run main.go -debug search-repos
[DEBUG]: Execute Command: search-repos
search repos command
Go will try to parse anything that is a flag until it finds a non-flag input, once it does everything else will become part of the Args
.
Search repos command #
Now you are going to implement the search-repos
command that searches for repositories by name on github. We are focusing on the green box below (search term).
You can find the GitHub Repository Search api here.
- Let's write a function that given a search term searches the GitHub repositories. Brace yourself for a large (but simple) function 📄.
... func findRepos(term string) ([]string, error) { type repo struct { FullName string `json:"full_Name"` } type searchResult struct { Items []repo `json:"items"` } // Prepare github repository search url. req, err := http.NewRequest(http.MethodGet, "https://api.github.com/search/repositories", nil) if err != nil { printDebug(fmt.Sprintf("%v", err)) return nil, errors.New("failed to connect to github") } query := req.URL.Query() query.Set("q", term) req.URL.RawQuery = query.Encode() // Make http request. res, err := http.DefaultClient.Do(req) if err != nil { printDebug(fmt.Sprintf("%v", err)) return nil, errors.New("failed to connect to github") } if res.StatusCode < 200 || res.StatusCode >= 300 { return nil, errors.New("failed to connect to github") } // Parse the json response. results := searchResult{} err = json.NewDecoder(res.Body).Decode(&results) if err != nil { printDebug(fmt.Sprintf("%v", err)) return nil, errors.New("failed to connect to github") } // Extract out the repo names. repos := make([]string, 0) for _, r := range results.Items { repos = append(repos, r.FullName) } return repos, nil } ...
main.go
findRepos
will return either an error or the search results.
- Write a function
executeSearchRepos
that parses the arguments and executes the above function.
... func executeCommand(command string, args []string) error { printDebug("Execute Command: " + command) switch command { case "search-repos": return executeSearchRepos(args) // 👈 call to function case "search-users": fmt.Println("search users command") return nil default: return fmt.Errorf("invalid command: '%s'\n\n%s\n", command, usage) } } // 👇 accepts the arguments and calls `findRepos` func executeSearchRepos(args []string) error { if len(args) == 0 { return errors.New("provide a search term for searching repos: search-repos <search_term>") } searchTerm := args[0] printDebug(fmt.Sprintf("[search-repos] Search Term: %s", searchTerm)) repos, err := findRepos(searchTerm) if err != nil { return err } fmt.Println(strings.Join(repos, ", ")) return nil } ...
main.go
🎉 Awesome, your cli program is now ready to search github repositories.
❯ go run main.go search-repos golang
GoesToEleven/GolangTraining, docker-library/golang, aceld/golang, astaxie/build-web-application-with-golang, xiaobaiTech/golangFamily, golang101/golang101, Alikhll/golang-developer-roadmap, golangci/golangci-lint, cncamp/golang, hackstoic/golang-open-source-projects, geektutu/7days-golang, GoogleCloudPlatform/golang-samples, senghoo/golang-design-pattern, prometheus/client_golang, rubyhan1314/Golang-100-Days, yangwenmai/learning-golang, GoesToEleven/golang-web-dev, 0voice/Introduction-to-Golang, overnote/over-golang, a8m/golang-cheat-sheet, dariubs/GoBooks, cch123/golang-notes, hyper0x/Golang_Puzzlers, Leslie1sMe/golang, polaris1119/The-Golang-Standard-Library-by-Example, shirou/gopsutil, iswbm/GolangCodingTime, Quorafind/golang-developer-roadmap-cn, hashicorp/golang-lru, gizak/termui
Using the -debug
flag.
❯ go run main.go -debug search-repos golang
[DEBUG]: Execute Command: search-repos
[DEBUG]: [search-repos] Search Term: golang
GoesToEleven/GolangTraining, docker-library/golang, aceld/golang, astaxie/build-web-application-with-golang, xiaobaiTech/golangFamily, golang101/golang101, Alikhll/golang-developer-roadmap, golangci/golangci-lint, cncamp/golang, hackstoic/golang-open-source-projects, geektutu/7days-golang, GoogleCloudPlatform/golang-samples, senghoo/golang-design-pattern, prometheus/client_golang, rubyhan1314/Golang-100-Days, yangwenmai/learning-golang, GoesToEleven/golang-web-dev, 0voice/Introduction-to-Golang, overnote/over-golang, a8m/golang-cheat-sheet, dariubs/GoBooks, cch123/golang-notes, hyper0x/Golang_Puzzlers, Leslie1sMe/golang, polaris1119/The-Golang-Standard-Library-by-Example, shirou/gopsutil, iswbm/GolangCodingTime, Quorafind/golang-developer-roadmap-cn, hashicorp/golang-lru, gizak/termui
Search users command #
Now we are going to develop a command to search github users based on a search term. This one will be a bit more involved than than preivous one.
Here is the breakdown of the command.
We are going to use GitHub User Search api, you can find the documentation here.
- Write a function
findUsers
that makes the http call to the user search api.
Again, brace yourself for a large (but easy to understand) functon.
... func findUsers(term, sort string) ([]string, error) { type user struct { Login string `json:"login"` } type searchResult struct { Items []user `json:"items"` } // Prepare github repository search url. req, err := http.NewRequest(http.MethodGet, "https://api.github.com/search/users", nil) if err != nil { printDebug(fmt.Sprintf("%v", err)) return nil, errors.New("failed to connect to github") } query := req.URL.Query() query.Set("q", term) query.Set("sort", sort) req.URL.RawQuery = query.Encode() // Make http request. res, err := http.DefaultClient.Do(req) if err != nil { printDebug(fmt.Sprintf("%v", err)) return nil, errors.New("failed to connect to github") } if res.StatusCode < 200 || res.StatusCode >= 300 { return nil, errors.New("failed to connect to github") } // Parse the json response. results := searchResult{} err = json.NewDecoder(res.Body).Decode(&results) if err != nil { printDebug(fmt.Sprintf("%v", err)) return nil, errors.New("failed to connect to github") } // Extract out the repo names. repos := make([]string, 0) for _, r := range results.Items { repos = append(repos, r.Login) } return repos, nil } ...
main.go
- Write a function
executeSearchUsers
that parses the arguments (the ones in purpse and blue) and calls thefindUsers
method.
The important thing to notice here is the use of flag.FlagSet
, you can use flag.FlatSet
to manually parse flags from a stirng (args in our case). By default flag.Parse()
parses flags from os.Args()
, but you can create a new flag.FlagSet
using flag.NewFlagSet()
to parse them from custom slice of strings.
... func executeCommand(command string, args []string) error { printDebug(fmt.Sprintf("Command: %s", command)) printDebug(fmt.Sprintf("Args: %v", args)) switch command { case "search-repos": return executeSearchRepos(args) case "search-users": return executeSearchUsers(args) // 👈 default: return fmt.Errorf("invalid command: '%s'", command) } } func executeSearchUsers(args []string) error { flagSet := flag.NewFlagSet("search-repos", flag.ExitOnError) sort := flagSet.String("sort", "", "sort results by") flagSet.Parse(args) printDebug(fmt.Sprintf("[search-repos] Args: %s", flagSet.Args())) if len(flagSet.Args()) == 0 { return errors.New("provide a search term for searching repos: search-repos <search_term>") } searchTerm := flagSet.Args()[0] printDebug(fmt.Sprintf("[search-repos] Search Term: %s", searchTerm)) users, err := findUsers(searchTerm, *sort) if err != nil { return err } fmt.Println(strings.Join(users, ", ")) return nil } ...
main.go
Use the search-users
command to search for users.
❯ go run main.go search-users john
john, john-smilga, johnpapa, johngrib, JohnHammond, johndpope, JohnSundell, johnmyleswhite, johnlindquist, johno, JohnTitor, johnafish, johnlui, jmurowaniecki, fjh658, johnBuffer, johnno1962, johnkil, jdegoes, JohnCoene, jhnwr, jeresig, jhawthorn, Lin20, johndbritton, johnpolacek, johnoseni1, johnbillion, johnymontana, jwasham
You can also use the -sort
flag to sort the result.
❯ go run main.go search-users -sort followers john
jwasham, jeresig, johnpapa, john-smilga, JohnHammond, springframeworkguru, JohnSundell, topjohnwu, jdalton, johnmyleswhite, johnafish, johnlui, jwiegley, jgm, jnunemaker, johnkil, johnlindquist, flatplanet, jdegoes, johnthebrit, joschu, johno, johnBuffer, johnno1962, johndbritton, joreilly, jaewonhimnae, johnleider, john-preston, JohnTitor
Additionally you can also add the -debug
flag.
❯ go run main.go -debug search-users -sort followers john
[DEBUG]: Command: search-users
[DEBUG]: Args: [-sort followers john]
[DEBUG]: [search-repos] Args: [john]
[DEBUG]: [search-repos] Search Term: john
jwasham, jeresig, johnpapa, john-smilga, JohnHammond, springframeworkguru, JohnSundell, topjohnwu, jdalton, johnmyleswhite, johnafish, johnlui, jwiegley, jgm, jnunemaker, johnkil, johnlindquist, flatplanet, jdegoes, johnthebrit, joschu, johno, johnBuffer, johnno1962, johndbritton, joreilly, jaewonhimnae, johnleider, john-preston, JohnTitor
The complete code is available in this GitHub repository.
Amazing! You did it, if you made it to the end of the article you are part of the small group of people who know how to read properly.