Develop command line applications in Go with flag package

Read more articles on Go
github logo
Github Repository
Code for this articles is available on GitHub
Develop command line applications in Go with flag package

Subscribe

Get updated 1 - 2 times a month when new articles are published, no spam ever.

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.

  1. 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
  1. 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
  1. Add a function printDebug that prints out logs only when value of debug 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
  1. 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.

Go Command Breakdown

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).

Golang github search term repos

You can find the GitHub Repository Search api here.

  1. 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.

  1. 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.

Golang search users breakdown

We are going to use GitHub User Search api, you can find the documentation here.

  1. 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
  1. Write a function executeSearchUsers that parses the arguments (the ones in purpse and blue) and calls the findUsers 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.

Subscribe

Get updated 1 - 2 times a month when new articles are published, no spam ever.

    TheDeveloperCafe © 2022-2024