Technology

CLI: Command Line Programming with Go

21 Nov 2014 11:34am, by

CLI, or “command line interface,” is a program that users interact with on the command line. Go has become a very popular choice for CLI development due to its lack of deployment dependencies by compiling to a static binary. If you have written a CLI that has dependencies for installation, you know how important this can be.

In this post, we will cover the basics of creating a CLI with Go and the standard library packages required to do so.

Arguments

Most CLI programs expect some input in the form of CLI arguments. Program arguments are handled in Go as a slice of strings:

var Args []string

 

Retrieving the name of the currently running program

package main

import (
    "fmt"
    "os"
)

func main() {
    // Program Name is always the first (implicit) argument
    cmd := os.Args[0]

    fmt.Printf("Program Name: %s\n", cmd)
}

This program is in the code/example1 directory. You can build and run it with these commands:

go build
./example1

You should see the following output:

Program Name: ./example1

 

Determine the Number of Arguments Passed into Your Program

To find out how many arguments were passed in, you can take the length of arguments and subtract 1 (remember that the first argument is always your program name). Or, you can augment the slice by using os.Args[1:] which simply says: “give me a new subslice starting with index 1 (not 0) to the end of the slice.”

package main

import (
    "fmt"
    "os"
)

func main() {
    argCount := len(os.Args[1:])
    fmt.Printf("Total Arguments (excluding program name): %d\n", argCount)
}

 

Running this with ./example2 will result in Total Arguments (excluding program name): 0

Running with this ./example2 –foo=bar will result in Total Arguments (excluding program name): 1

Enumerating arguments

Here is a quick way to run through the current arguments

package main

import (
    "fmt"
    "os"
)

func main() {
    for i, a := range os.Args[1:] {
        fmt.Printf("Argument %d is %s\n", i+1, a)
    }

}
Running the program with ./example3 -local u=admin --help results in:
Argument 1 is -local
Argument 2 is u=admin
Argument 3 is --help

The Flag package

So far we have examined how to look at the arguments of a program at a very basic level. Interrogating them at this level and assigning them to variables for use in our program would be very cumbersome. This is where the Flag Package comes in.

package main

import (
    "flag"
    "fmt"
)

func main() {
    var port int
    flag.IntVar(&port, "p", 8000, "specify port to use.  defaults to 8000.")
    flag.Parse()

    fmt.Printf("port = %d", port)
}

The first thing we did is state that we are going to have a flag that is of type int with a default value of 8000, and usage text to help users understand our intent for that flag.

To have the flags package populate your defined flags, you need to call flag.Parse().

Running this program, with no arguments, will result in port = 8000. This is because we told the flag package that if nothing is passed in for port, the default of 8000 should be used.

Running with ./example4 –p=9000 results in port = 9000.

As a bonus, the flag package provides us with “program usage” output for free. If we run ./example4 –help we now get:

Usage of ./example4:
-p=8000: specify port to use.  defaults to 8000.

flag.Args()

Many CLI programs take both flags and “non-flag” arguments. flag.Args() will return the arguments that it does not consider flags.

package main

import (
    "flag"
    "fmt"
)

func main() {
    var port int
    flag.IntVar(&port, "p", 8000, "specify port to use.  defaults to 8000.")
    flag.Parse()

    fmt.Printf("port = %d\n", port)
    fmt.Printf("other args: %+v\n", flag.Args())
}

Running this program with ./example5 -p=9000 foo=10 -bar will result in:

port = 9000
other args: [foo=10 -bar]

The flag package will stop looking for flags as soon as it encounters any parameter that doesn’t begin with a hyphen. Because foo=10 isn’t a hyphen-prefixed flag, it, and everything after it (including -bar) will be considered a non-option flag.

This distinction allows command [flags] subcommand [subflags] style applications to be built with ease.

Invalid flag arguments

Go is a strongly typed language, so if we try to pass an arbitrary string into an int flag, it will let us know.

package main

import (
    "flag"
    "fmt"
)

func main() {
    var port int
    flag.IntVar(&port, "p", 8000, "specify port to use.  defaults to 8000")
    flag.Parse()

    fmt.Printf("port = %d", port)
}

 

Running this program with ./example6 -p=foo results in:

invalid value “foo” for flag -p: strconv.ParseInt: parsing “foo”: invalid syntax
Usage of ./example6:
-p=8000: specify port to use.  defaults to 8000

Not only does the flag package tell us what we did wrong, but it also prints out the default usage as well.

flag.Usage

The flag package publicly declares a Usage function variable, which allows us to re-assign it to any function we want, thus allowing us to have custom usage output.

package main

import (
    "flag"
    "fmt"
    "os"
)

func main() {
    flag.Usage = func() {
        fmt.Printf("Usage of %s:\n", os.Args[0])
        fmt.Printf("    example7 file1 file2 ...\n")
        flag.PrintDefaults()
    }
    flag.Parse()
}

 

Running this program with ./example7 –help results in:

Usage of ./example7:
example7 file1 file2 …

Getting input

Up until now, we have been outputting from our CLI  but not accepting input once the program starts. We can do some basic input capturing via stdin with fmt.Scanf.

package main

import "fmt"

func main() {
    var guessColor string
    const favColor = "blue"
    for {
        fmt.Println("Guess my favorite color:")
        if _, err := fmt.Scanf("%s", &guessColor); err != nil {
            fmt.Printf("%s\n", err)
            return
        }
        if favColor == guessColor {
            fmt.Printf("%q is my favorite color!", favColor)
            return
        }
        fmt.Printf("Sorry, %q is not my favorite color.  Guess again.\n", guessColor)
    }
}

bufio.Scanner

While fmt.Scanf is great for getting some simple input, we many times require an entire line at a time.

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        line := scanner.Text()
        if line == "exit" {
            os.Exit(0)
        }
        fmt.Println(line) // Println will add back the final '\n'
    }
    if err := scanner.Err(); err != nil {
        fmt.Fprintln(os.Stderr, "reading standard input:", err)
    }
}

This is a basic echo program. To exit the program, type exit.

A basic cat program

Many of you have used cat. We are going to put together much of what we have learned in this post and build a very basic version of cat.

package main

import (
    "flag"
    "fmt"
    "io"
    "os"
)

func main() {
    flag.Usage = func() {
        fmt.Printf("Usage of %s:\n", os.Args[0])
        fmt.Printf("    cat file1 file2 ...\n")
        flag.PrintDefaults()
    }

    flag.Parse()
    if flag.NArg() == 0 {
        flag.Usage()
        os.Exit(1)
    }

    for _, fn := range flag.Args() {
        f, err := os.Open(fn); 
        if err != nil {
            panic(err)
        }
        _, err = io.Copy(os.Stdout, f)
        if err != nil {
            panic(err)
        }
    }
}

Help!

As we have already covered above, help parameters are implicitly defined if you don’t explicitly define them:

-h
–h
-help
–help

The above will all trigger usage and, depending on the how you initialize the flag parser, may cause your package to exit with an error. Also notice long and short forms of arguments are treated the same in the flag package.

Summary

As you can see, there are a lot of options when building a CLI  with Go. We have only covered the basics in this post. I would suggest a read of the following packages for a more in depth knowledge:

Additional command line libraries

There are several third party libraries that assist in making cli easier. I would recommend looking at them to see if they serve your needs as well.

Feature image via Flickr Creative Commons.

Cory LaNou is an experienced software developer with over two decades of experience, and two and a half years of production Go experience. He is currently a lead instructor at gSchool where he teaches Go and leads the local Denver Go meetup, aptly called Denver Gophers


A digest of the week’s most important stories & analyses.

View / Add Comments

Please stay on topic and be respectful of others. Review our Terms of Use.