Command-Line Tools

0
919
8 min read

In this article by Aaron Torres, author of the book, Go Cookbook, we will cover the following recipes:

  • Using command-line arguments
  • Working with Unix pipes
  • An ANSI coloring application

(For more resources related to this topic, see here.)

Using command-line arguments

This article will expand on other uses for these arguments by constructing a command that supports nested subcommands. This will demonstrate Flagsets and also using positional arguments passed into your application.

This recipe requires a main function to run. There are a number of third-party packages for dealing with complex nested arguments and flags, but we’ll again investigate doing so using only the standard library.


Getting ready

You need to perform the following steps for the installation:

  1. Download and install Go on your operating system at https://golang.org/doc/install and configure your GOPATH.
  2. Open a terminal/console application.
  3. Navigate to your GOPATH/src and create a project directory, for example, $GOPATH/src/github.com/yourusername/customrepo.

    All code will be run and modified from this directory.

  4. Optionally, install the latest tested version of the code using the go get github.com/agtorre/go-cookbook/ command.

How to do it…

  1. From your terminal/console application, create and navigate to the chapter2/cmdargs directory.
  2. Copy tests from https://github.com/agtorre/go-cookbook/tree/master/chapter2/cmdargs or use this as an exercise to write some of your own.
  3. Create a file called cmdargs.go with the following content:
    package main 
    import (
     "flag"
     "fmt"
     "os"
    )
    
    const version = "1.0.0"
    const usage = `Usage:
    
    %s [command]
    
    Commands:
     Greet
     Version
    `
    
    const greetUsage = `Usage:
    
    %s greet name [flag]
    
    Positional Arguments:
     name
     the name to greet
    
    Flags:
    `
    
    // MenuConf holds all the levels
    // for a nested cmd line argument
    type MenuConf struct {
     Goodbye bool
    }
    
    // SetupMenu initializes the base flags
    func (m *MenuConf) SetupMenu() *flag.FlagSet {
     menu := flag.NewFlagSet("menu", flag.ExitOnError)
     menu.Usage = func() {
      fmt.Printf(usage, os.Args[0])
      menu.PrintDefaults()
     }
     return menu
    }
    
    // GetSubMenu return a flag set for a submenu
    func (m *MenuConf) GetSubMenu() *flag.FlagSet {
     submenu := flag.NewFlagSet("submenu", flag.ExitOnError)
     submenu.BoolVar(&m.Goodbye, "goodbye", false, "Say goodbye instead of hello")
    
     submenu.Usage = func() {
      fmt.Printf(greetUsage, os.Args[0])
      submenu.PrintDefaults()
     }
     return submenu
    }
    
    // Greet will be invoked by the greet command
    func (m *MenuConf) Greet(name string) {
     if m.Goodbye {
      fmt.Println("Goodbye " + name + "!")
     } else {
      fmt.Println("Hello " + name + "!")
     }
    }
    
    // Version prints the current version that is
    // stored as a const
    func (m *MenuConf) Version() {
     fmt.Println("Version: " + version)
    }
  4. Create a file called main.go with the following content:
    package main
    
    import (
     "fmt"
     "os"
     "strings"
    )
    
    func main() {
     c := MenuConf{}
     menu := c.SetupMenu()
     menu.Parse(os.Args[1:])
    
     // we use arguments to switch between commands
     // flags are also an argument
     if len(os.Args) > 1 {
      // we don't care about case
      switch strings.ToLower(os.Args[1]) {
       case "version":
        c.Version()
       case "greet":
        f := c.GetSubMenu()
        if len(os.Args) < 3 {
         f.Usage()
         return
        }
        if len(os.Args) > 3 {
         if.Parse(os.Args[3:])
        }
        c.Greet(os.Args[2])
       default:
        fmt.Println("Invalid command")
        menu.Usage()
        return
       }
      } else {
       menu.Usage()
       return
      }
    }
  5. Run the go build command.
  6. Run the following commands and try a few other combinations of arguments:
    $./cmdargs -h 
    Usage:
    
    ./cmdargs [command]
    
    Commands:
     Greet
     Version
    
    $./cmdargs version
    Version: 1.0.0
    
    $./cmdargs greet
    Usage:
    
    ./cmdargs greet name [flag]
    
    Positional Arguments:
     name
     the name to greet
    
    Flags:
     -goodbye
     Say goodbye instead of hello
    
    $./cmdargs greet reader
    Hello reader!
    
    $./cmdargs greet reader -goodbye
  7. Goodbye reader!

    If you copied or wrote your own tests go up one directory and run go test, and ensure all tests pass.

How it works…

Flagsets can be used to set up independent lists of expected arguments, usage strings, and more. The developer is required to do validation on a number of arguments, parsing in the right subset of arguments to commands, and defining usage strings. This can be error prone and requires a lot of iteration to get it completely correct.

The flag package makes parsing arguments much easier and includes convenience methods to get the number of flags, arguments, and more. This recipe demonstrates basic ways to construct a complex command-line application using arguments, including a package-level config, required positional arguments, multi-leveled command usage, and how to split these things into multiple files or packages if needed.

Working with Unix pipes

Unix pipes are useful when passing the output of one program to the input of another. Consider the following example:

$ echo "test case" | wc -l
   1

In a Go application, the left-hand side of the pipe can be read in using os.Stdin and acts like a file descriptor. To demonstrate this, this recipe will take an input on the left-hand side of a pipe and return a list of words and their number of occurrences. These words will be tokenized on white space.

Getting ready

Refer to the Getting Ready section of the Using command-line arguments recipe.

How to do it…

  1. From your terminal/console application, create a new directory, chapter2/pipes.
  2. Navigate to that directory and copy tests from https://github.com/agtorre/go-cookbook/tree/master/chapter2/pipes or use this as an exercise to write some of your own.
  3. Create a file called pipes.go with the following content:
    package main
    
    import (
     "bufio"
     "fmt"
     "os"
    )
    
    // WordCount takes a file and returns a map
    // with each word as a key and it's number of
    // appearances as a value
    func WordCount(f *os.File) map[string]int {
     result := make(map[string]int)
    
     // make a scanner to work on the file
     // io.Reader interface
     scanner := bufio.NewScanner(f)
     scanner.Split(bufio.ScanWords)
    
     for scanner.Scan() {
      result[scanner.Text()]++
     }
    
     if err := scanner.Err(); err != nil {
      fmt.Fprintln(os.Stderr, "reading input:", err)
     }
    
     return result
    }
    
    func main() {
     fmt.Printf("string: number_of_occurrencesnn")
     for key, value := range WordCount(os.Stdin) {
      fmt.Printf("%s: %dn", key, value)
     }
    }

     

  4. Run echo “some string” | go run pipes.go.
  5. You may also run:
    go build
  6. echo “some string” | ./pipes

    You should see the following output:

    $ echo "test case" | go run pipes.go
    string: number_of_occurrences
    
    test: 1
    case: 1
    
    $ echo "test case test" | go run pipes.go
    string: number_of_occurrences
    
    test: 2
  7. case: 1

    If you copied or wrote your own tests, go up one directory and run go test, and ensure that all tests pass.

How it works…

Working with pipes in go is pretty simple, especially if you’re familiar with working with files.

This recipe uses a scanner to tokenize the io.Reader interface of the os.Stdin file object. You can see how you must check for errors after completing all of the reads.

An ANSI coloring application

Coloring an ANSI terminal application is handled by a variety of code before and after a section of text that you want colored. This recipe will explore a basic coloring mechanism to color the text red or keep it plain. For a more complete application, take a look at https://github.com/agtorre/gocolorize, which supports many more colors and text types implements the fmt.Formatter interface for ease of printing.

Getting ready

Refer to the Getting Ready section of the Using command line arguments recipe.

How to do it…

  1. From your terminal/console application, create and navigate to the chapter2/ansicolor directory.
  2. Copy tests from https://github.com/agtorre/go-cookbook/tree/master/chapter2/ansicolor or use this as an exercise to write some of your own.
  3. Create a file called color.go with the following content:
    package ansicolor
    
    import "fmt"
    
    //Color of text
    type Color int
    
    const (
     // ColorNone is default
     ColorNone = iota
     // Red colored text
     Red
     // Green colored text
     Green
     // Yellow colored text
     Yellow
     // Blue colored text
     Blue
     // Magenta colored text
     Magenta
     // Cyan colored text
     Cyan
     // White colored text
     White
     // Black colored text
     Black Color = -1
    )
    
    // ColorText holds a string and its color
    type ColorText struct {
     TextColor Color
     Text string
    }
    
    func (r *ColorText) String() string {
     if r.TextColor == ColorNone {
      return r.Text
     }
    
     value := 30
     if r.TextColor != Black {
      value += int(r.TextColor)
     }
     return fmt.Sprintf("33[0;%dm%s33[0m", value, r.Text)
    }
  4. Create a new directory named example.
  5. Navigate to example and then create a file named main.go with the following content. Ensure that you modify the ansicolor import to use the path you set up in step 1:
    package main
    
    import (
     "fmt"
    
     "github.com/agtorre/go-cookbook/chapter2/ansicolor"
    )
    
    func main() {
     r := ansicolor.ColorText{ansicolor.Red, "I'm red!"}
    
     fmt.Println(r.String())
    
     r.TextColor = ansicolor.Green
     r.Text = "Now I'm green!"
    
     fmt.Println(r.String())
    
     r.TextColor = ansicolor.ColorNone
     r.Text = "Back to normal..."
    
     fmt.Println(r.String())
    }
  6. Run go run main.go. Alternatively, you may also run the following:
    go build
  7. ./example

    You should see the following with the text colored if your terminal supports the ANSI coloring format:

    $ go run main.go
    I'm red!
    Now I'm green!
  8. Back to normal…

    If you copied or wrote your own tests, go up one directory and run go test, and ensure that all the tests pass.

How it works…

This application makes use of a struct keyword to maintain state of the colored text. In this case, it stores the color of the text and the value of the text. The final string is rendered when you call the String() method, which will either return colored text or plain text depending on the values stored in the struct. By default, the text will be plain.

Summary

In this article, we demonstrated basic ways to construct a complex command-line application using arguments, including a package-level config, required positional arguments, multi-leveled command usage, and how to split these things into multiple files or packages if needed. We saw how to work with Unix pipes and explored a basic coloring mechanism to color text red or keep it plain.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here