How to Build Cli in Go

May 11, 2022
hackajob Staff

Building Command Line Interface In Go

How exactly do you build a Command Line Interface in Go? What's the easiest way and how can this improve your work? We're getting into all of this in our latest tech tutorial. Check it all out below.

What you'll need

It's easier than you think to make a simple CLI application so we're going to show you how. While making this application, we'll use the urfave/cli library, which will make our work much easier. You can find it here: https://github.com/urfave/cli

Our application will implement two commands in Linux. One of them is the kill command and the other is the volumes command. By coding the operation of these two commands, we'll be able to kill processes and list mounted volumes.

Since we will use the urfave/cli package while developing the application, let's download it first:

go get github.com/urfave/cli

Fantastic, you should have all the components needed so let's get started!

How to build our CLI application

Now, let's start coding our CLI application, then let's go into the details of these two commands:

package  main

import  (
	"log"
	"os"
	"github.com/urfave/cli"
)

func  main()  {
	err := cli.NewApp().Run(os.Args)
	if err !=  nil  {
		log.Fatal(err)
	}
}

First, create a new CLI application using NewApp and start the application by calling Run.

os.Args, on the other hand, takes the arguments we give when starting the application. Let's run it as it is and see the output:

-> go run main.go
NAME:
   main.exe - A new cli application
USAGE:
   main.exe [global options] command [command options] [arguments...]

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h  show help

Now we've got an idea about usage and output so we'll gradually implement our commands and fill in the names and descriptions of those commands. Firstly, let's look at the kill command:

package  main

  
import  (
    "log"
    "os"

    "github.com/urfave/cli"
)

func  main()  {

   	app := cli.NewApp()
	app.Name = "Basic Kill and Delete Command İmplementation CLI"
	app.Usage = "Let's you kill processes by name or id and delete files or folders"

	app.Commands = []cli.Command{
		{
			Name:        "kill",
			HelpName:    "kill",
			Action:      KillAction,
			ArgsUsage:   ` `,
			Usage:       `kills processes by process id or process name.`,
			Description: `Terminate a process.`,
			Flags: []cli.Flag{
				&cli.UintFlag{
					Name:  "id",
					Usage: "kill process by process ID.",
				},
				&cli.StringFlag{
					Name:  "name",
					Usage: "kill process by process name. ",
				},
			},
		},
    }
  


    err := app.Run(os.Args)
    if err !=  nil  {
        log.Fatal(err)
    }
}

Start by filling in the fields we saw in the previous output, then set the functions and properties of the commands. As you can imagine, usage and description are used when the help command is printed on the screen. In the Flags section, we set the flags that the command will receive. The kill command will get the id and name flags.

Other features can also be set. For example, if it needs to get the flag by setting Before and if it doesn't, you can return the board that you want directly.  The main part here is the place we give as Action. Now let's look at the KillAction function:


func  KillAction(c *cli.Context)  error  {
	if  len(c.Args())  >  0  {
		return errors.New("no arguments is expected, use flags")
	}

	if c.IsSet("id")  && c.IsSet("name")  {
		return errors.New("either pid or name flag must be provided")
	}

	if  !c.IsSet("id")  && c.String("name")  ==  ""  {
		return errors.New("name flag cannot be empty")
	}

	if err :=  killProcess(c);err !=  nil  {
		return err
	}
	fmt.Println("Process killed successfully.")
	return  nil
}

func killProcess(c *cli.Context) error {
	if c.IsSet("id") {
		proc, err := process.NewProcess(int32(c.Uint("id")))
		if err != nil {
			return err
		}

		return proc.Kill()
	}

	processes, err := process.Processes()
	if err != nil {
		return err
	}

	var (
		errs  []string
		found bool
	)

	target := c.String("name")
	for _, p := range processes {
		name, _ := p.Name()
		if name == "" {
			continue
		}

		if isEqualProcessName(name, target) {
			found = true
			if err := p.Kill(); err != nil {
				e := err.Error()
				errs = append(errs, e)
			}
		}
	}

	if !found {
		return errors.New("process not found")
	}
	if len(errs) == 0 {
		return nil
	}
	return errors.New(strings.Join(errs, "\n"))
}

func isEqualProcessName(proc1 string, proc2 string) bool {
	if runtime.GOOS == "linux" {
		return proc1 == proc2
	}
	return strings.EqualFold(proc1, proc2)
}

After doing some checks first in the KillAction function, it's clear that if there's no argument, id or name, it'll return an error. Next, we'll call killProcess and if it returns success it'll print a success message. In killProcess, we use the github.com/shirou/gopsutil/v3/process library.

As we saw above, you can pull this library with go get. Let's take a quick look at killAction. If the id is set, we'll take the processes and kill them if the ids match. Then we do the same for name. But here we are comparing with strings.EqualFold to eliminate the capitalisation issue in windows.

Let's continue with the volumes command:

{
    Name:  "volumes",
    HelpName:  "volumes",
    Action: ActionVolumes,
    ArgsUsage:  `  `,
    Usage:  `lists mounted file system volumes.`,
    Description:  `List the mounted volumes.`,
},

As we mentioned in the kill command above, we also set the necessary variables in the volumes command. Unlike the above, we call the ActionVolumes function in the Action section. Let's take a closer look at this function:

type Volume struct {
	Name       string
	Total      uint64
	Used       uint64
	Available  uint64
	UsePercent float64
	Mount      string
}

func ActionVolumes(c *cli.Context) error {
	stats, err := disk.Partitions(true)
	if err != nil {
		return err
	}

	var vols []*Volume

	for _, stat := range stats {
		usage, err := disk.Usage(stat.Mountpoint)
		if err != nil {
			continue
		}

		vol := &Volume{
			Name:       stat.Device,
			Total:      usage.Total,
			Used:       usage.Used,
			Available:  usage.Free,
			UsePercent: usage.UsedPercent,
		}

		vols = append(vols, vol)
	}

	volsByteArr, err := json.MarshalIndent(vols, "", "\t")
	if err != nil {
		return err
	}

	fmt.Println(string(volsByteArr))
	return nil
}

We've finished the code but let's try running both commands separately and seeing their output:

-> ping localhost -n 100

All good? Now, let's run the above command and then try to kill it with the command:

->go run main.go kill --name PING.EXE
Process killed successfully.

It should have killed successfully. Finally, let's use the volumes command:

-> go run main.go volumes     
[
    {
            "Name": "C:",
            "Total": 147187560448,   
            "Used": 96198782976,     
            "Available": 50988777472,
            "UsePercent": 65.3579573458493,
            "Mount": ""
    },
    {
            "Name": "X:",
            "Total": 127133544448,
            "Used": 463794176,
            "Available": 126669750272,
            "UsePercent": 0.3648086569235081,
            "Mount": ""
    }
]

And that's it! You should have successfully written a simple CLI application using urfave/cli. Let us know how you use it in your project.

Like what you've read or want more like this? Let us know! Email us here or DM us: Twitter, LinkedIn, Facebook, we'd love to hear from you.