Go cli Daily Library

Keywords: Go github git JSON Linux

Introduction to ##

cli Is a library for building command line programs.We've previously described a library for building command line programs cobra .Functionally, they are similar, and cobra has the advantage of providing a scaffold for easy development.cli is very simple, all initialization is to create oneCli.AppThe object of the structure.Add the appropriate functionality by assigning values to the field of the object.

cli is the same author as negroni in our last article urfave.

Quick use

cli needs to be used with Go Modules.Create directory and initialize:

$ mkdir cli && cd cli
$ go mod init github.com/darjun/go-daily-lib/cli

Install cli library with v1 and v2 versions.If there are no special requirements, install v2 version:

$ go get -u github.com/urfave/cli/v2

Use:

package main

import (
  "fmt"
  "log"
  "os"

  "github.com/urfave/cli/v2"
)

func main() {
  app := &cli.App{
    Name:  "hello",
    Usage: "hello world example",
    Action: func(c *cli.Context) error {
      fmt.Println("hello world")
      return nil
    },
  }

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

It's very easy to use, create one in theoryCli.AppThe object of the structure is then called its Run() method, passing in the parameters of the command line.A blank cli application is as follows:

func main() {
  (&cli.App{}).Run(os.Args)
}

But this blank program is of little use.Our hello world program, set up Name/Usage/Action.Both Name and Usage are shown in the help. Action is the function actually executed when the command line program is called. The information needed can be obtained from the parametersCli.ContextObtain.

Compile, run (environment: Win10 + Git Bash):

$ go build -o hello
$ ./hello
hello world

In addition to this, cli generates additional help information for us:

$ ./hello --help
NAME:
   hello - hello world example

USAGE:
   hello [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 (default: false)

parameter

adoptCli.ContextRelated methods allow us to get parameter information passed to the command line:

  • NArg(): Returns the number of parameters;
  • Args(): BackCli.ArgsObject that calls its Get(i) to get the parameter at position i.

Example:

func main() {
  app := &cli.App{
    Name:  "arguments",
    Usage: "arguments example",
    Action: func(c *cli.Context) error {
      for i := 0; i < c.NArg(); i++ {
        fmt.Printf("%d: %s\n", i+1, c.Args().Get(i))
      }
      return nil
    },
  }

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

Here's just a simple output:

$ go run main.go hello world
1: hello
2: world

option

How come there are fewer options for a useful command line program?cli settings and get options are simple.stayCli.AppWhen the {} structure is initialized, you can add options by setting the field Flags.The Flags field is []cli.FlagType,cli.FlagIt is actually an interface type.cli implements corresponding XxFlags for common types, such as BoolFlag/DurationFlag/StringFlag.They have some fields in common, Name/Value/Usage (name/default/definition).See an example:

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:  "lang",
        Value: "english",
        Usage: "language for the greeting",
      },
    },
    Action: func(c *cli.Context) error {
      name := "world"
      if c.NArg() > 0 {
        name = c.Args().Get(0)
      }

      if c.String("lang") == "english" {
        fmt.Println("hello", name)
      } else {
        fmt.Println("Hello", name)
      }
      return nil
    },
  }

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

The above is a greeting command line program that specifies the language with the option lang and defaults to English.Set the option to a value other than english, using Chinese.If there are parameters, use the first parameter as the person name, otherwise use the world.Note that options are acquired through c.Type(name), Type is the option type, and Name is the option name.Compile, run:

$ go build -o flags

# Default Call
$ ./flags
hello world

# Set up non-English
$ ./flags --lang chinese
//Hello world

# Incoming parameter as person name
$ ./flags --lang chinese dj
//Hello dj

We can see the options through. /flags --help:

$ ./flags --help
NAME:
   flags - A new cli application

USAGE:
   flags [global options] command [command options] [arguments...]

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

GLOBAL OPTIONS:
   --lang value  language for the greeting (default: "english")
   --help, -h    show help (default: false)

Input variable

In addition to getting the value of the option through c.Type(name), we can also save the option in a predefined variable.Simply set the address of the Destination field as a variable:

func main() {
  var language string

  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:        "lang",
        Value:       "english",
        Usage:       "language for the greeting",
        Destination: &language,
      },
    },
    Action: func(c *cli.Context) error {
      name := "world"
      if c.NArg() > 0 {
        name = c.Args().Get(0)
      }

      if language == "english" {
        fmt.Println("hello", name)
      } else {
        fmt.Println("Hello", name)
      }
      return nil
    },
  }

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

The effect is the same as that of the above program.

Placeholder value

cli can set placeholders for options in the Usage field, surrounded by inverted quotes `.Only the first one will take effect and the other will remain unchanged.Placeholder values help generate easy-to-understand help information:

func main() {
  app := & cli.App{
    Flags : []cli.Flag {
      &cli.StringFlag{
        Name:"config",
        Usage: "Load configuration from `FILE`",
      },
    },
  }

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

After setting a placeholder value, the placeholder value is displayed in the help message after the corresponding option, which is also valid for short options:

$ go build -o placeholder
$ ./placeholder --help
NAME:
   placeholder - A new cli application

USAGE:
   placeholder [global options] command [command options] [arguments...]

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

GLOBAL OPTIONS:
   --config FILE  Load configuration from FILE
   --help, -h     show help (default: false)

alias

Options can have multiple Aliases, and the Alliases field of the corresponding option can be set:

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:    "lang",
        Aliases: []string{"language", "l"},
        Value:   "english",
        Usage:   "language for the greeting",
      },
    },
    Action: func(c *cli.Context) error {
      name := "world"
      if c.NArg() > 0 {
        name = c.Args().Get(0)
      }

      if c.String("lang") == "english" {
        fmt.Println("hello", name)
      } else {
        fmt.Println("Hello", name)
      }
      return nil
    },
  }

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

The effect is the same with -lang chinese, --language chinese, and -l chinese.If you specify the same option with a different name, you will get an error:

$ go build -o aliase
$ ./aliase --lang chinese
//Hello world
$ ./aliase --language chinese
//Hello world
$ ./aliase -l chinese
//Hello world
$ ./aliase -l chinese --lang chinese
Cannot use two forms of the same flag: l lang

environment variable

In addition to manually specifying command line options when executing the program, we can also read the specified environment variables as the values for the options.Simply set the name of the environment variable to the EnvVars field of the option object.You can specify multiple environment variable names, cli looks for them in turn, and the first value environment variable is used.

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:    "lang",
        Value:   "english",
        Usage:   "language for the greeting",
        EnvVars: []string{"APP_LANG", "SYSTEM_LANG"},
      },
    },
    Action: func(c *cli.Context) error {
      if c.String("lang") == "english" {
        fmt.Println("hello")
      } else {
        fmt.Println("Hello")
      }
      return nil
    },
  }

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

Compile, run:

$ go build -o env
$ APP_LANG=chinese ./env
//Hello

file

cli also supports reading the value of an option from a file, setting the FilePath field of the option object to the file path:

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:     "lang",
        Value:    "english",
        Usage:    "language for the greeting",
        FilePath: "./lang.txt",
      },
    },
    Action: func(c *cli.Context) error {
      if c.String("lang") == "english" {
        fmt.Println("hello")
      } else {
        fmt.Println("Hello")
      }
      return nil
    },
  }

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

stayMain.goCreate a sibling directoryLang.txt, enter the content chinese.Then compile and run the program:

$ go build -o file
$ ./file
//Hello

cli also supports reading option values from configuration files such as YAML/JSON/TOML, which are not covered here.

Option Priority

We have described several ways to set option values above. If more than one way works at the same time, set them from high to low with the following priority:

  • User-specified command line option values;
  • Environment variables;
  • Configuration files;
  • Default value of option.

Combine Short Options

We often encounter situations where there are several short options.For example, the linux command ls-a-l can be abbreviated as ls-a L.cli also supports short option writing, only settingCli.AppThe UseShortOptionHandling field of true is sufficient:

func main() {
  app := &cli.App{
    UseShortOptionHandling: true,
    Commands: []*cli.Command{
      {
        Name:  "short",
        Usage: "complete a task on the list",
        Flags: []cli.Flag{
          &cli.BoolFlag{Name: "serve", Aliases: []string{"s"}},
          &cli.BoolFlag{Name: "option", Aliases: []string{"o"}},
          &cli.BoolFlag{Name: "message", Aliases: []string{"m"}},
        },
        Action: func(c *cli.Context) error {
          fmt.Println("serve:", c.Bool("serve"))
          fmt.Println("option:", c.Bool("option"))
          fmt.Println("message:", c.Bool("message"))
          return nil
        },
      },
    },
  }

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

Compile and run:

$ go build -o short
$ ./short short -som "some message"
serve: true
option: true
message: true

It is important to note that once UseShortOptionHandling is set to true, we can no longer specify the option through - which can create ambiguity.For example, -l a n g, cli does not know if it should be interpreted as l/a/n/g with four options or l a n g with one.--still valid.

Required Options

If the equired field of the option is set to true, then that option is necessary.Necessary options must be specified or error will occur:

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:     "lang",
        Value:    "english",
        Usage:    "language for the greeting",
        Required: true,
      },
    },
    Action: func(c *cli.Context) error {
      if c.String("lang") == "english" {
        fmt.Println("hello")
      } else {
        fmt.Println("Hello")
      }
      return nil
    },
  }

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

Run without specifying the option lang:

$ ./required
2020/06/23 22:11:32 Required flag "lang" not set

Default value in help text

By default, the default value of the option in the help text is displayed as the Value field value.Sometimes Value is not the actual default value.At this point, we can set it up with DefaultText:

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.IntFlag{
        Name:     "port",
        Value:    0,
        Usage:    "Use a randomized port",
        DefaultText :"random",
      },
    },
  }

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

In the code logic above, if the Value is set to 0 and a port is randomly assigned, default: 0 in the help message is easily misinterpreted.This can be avoided with DefaultText:

$ go build -o default-text
$ ./default-text --help
NAME:
   default-text - A new cli application

USAGE:
   default-text [global options] command [command options] [arguments...]

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

GLOBAL OPTIONS:
   --port value  Use a randomized port (default: random)
   --help, -h    show help (default: false)

Subcommand

Subcommand missions make line programs more organized.Git has a large number of commands, many of which exist as subcommands under a command.For example, git remote command has add/rename/remove subcommands, git submodule has add/status/init/update subcommands.

cli by settingCli.AppSubcommands can be added by adding commands to the Commands field and setting the SubCommands field for each command.Very convenient!

func main() {
  app := &cli.App{
    Commands: []*cli.Command{
      {
        Name:    "add",
        Aliases: []string{"a"},
        Usage:   "add a task to the list",
        Action: func(c *cli.Context) error {
          fmt.Println("added task: ", c.Args().First())
          return nil
        },
      },
      {
        Name:    "complete",
        Aliases: []string{"c"},
        Usage:   "complete a task on the list",
        Action: func(c *cli.Context) error {
          fmt.Println("completed task: ", c.Args().First())
          return nil
        },
      },
      {
        Name:    "template",
        Aliases: []string{"t"},
        Usage:   "options for task templates",
        Subcommands: []*cli.Command{
          {
            Name:  "add",
            Usage: "add a new template",
            Action: func(c *cli.Context) error {
              fmt.Println("new task template: ", c.Args().First())
              return nil
            },
          },
          {
            Name:  "remove",
            Usage: "remove an existing template",
            Action: func(c *cli.Context) error {
              fmt.Println("removed task template: ", c.Args().First())
              return nil
            },
          },
        },
      },
    },
  }

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

The template command defines two subcommands, add/remove.Compile, run:

$ go build -o subcommand
$ ./subcommand add dating
added task:  dating
$ ./subcommand complete dating
completed task:  dating
$ ./subcommand template add alarm
new task template:  alarm
$ ./subcommand template remove alarm
removed task template:  alarm

Note that subcommands do not appear in the help information by default and require help to explicitly invoke the command to which the subcommand belongs (. /subcommand template --help):

$ ./subcommand --help
NAME:
   subcommand - A new cli application

USAGE:
   subcommand [global options] command [command options] [arguments...]

COMMANDS:
   add, a       add a task to the list
   complete, c  complete a task on the list
   template, t  options for task templates
   help, h      Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h  show help (default: false)

$ ./subcommand template --help
NAME:
   subcommand template - options for task templates

USAGE:
   subcommand template command [command options] [arguments...]

COMMANDS:
   add      add a new template
   remove   remove an existing template
   help, h  Shows a list of commands or help for one command

OPTIONS:
   --help, -h  show help (default: false)

classification

When there are a large number of subcommands, you can set the Category field to categorize them and display the same categorized commands together in the help information:

func main() {
  app := &cli.App{
    Commands: []*cli.Command{
      {
        Name:  "noop",
        Usage: "Usage for noop",
      },
      {
        Name:     "add",
        Category: "template",
        Usage:    "Usage for add",
      },
      {
        Name:     "remove",
        Category: "template",
        Usage:    "Usage for remove",
      },
    },
  }

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

Compile, run:

$ go build -o categories
$ ./categories --help
NAME:
   categories - A new cli application

USAGE:
   categories [global options] command [command options] [arguments...]

COMMANDS:
   noop     Usage for noop
   help, h  Shows a list of commands or help for one command
   template:
     add     Usage for add
     remove  Usage for remove

GLOBAL OPTIONS:
   --help, -h  show help (default: false)

Look at the COMMANDS section above.

Custom Help Information

All help information text in cli can be customized, and the help information template for the entire application is specified by AppHelpTemplate.Help information templates for commands are set through CommandHelpTemplate, and help information templates for subcommands are set through SubcommandHelpTemplate.It can even be overwrittenCli.HelpPrinterThis function outputs help information on its own.The following program adds personal website and WeChat information after the default help information:

func main() {
  cli.AppHelpTemplate = fmt.Sprintf(`%s

WEBSITE: http://darjun.github.io

WECHAT: GoUpUp`, cli.AppHelpTemplate)

  (&cli.App{}).Run(os.Args)
}

Compile and run:

$ go build -o help
$ ./help --help
NAME:
   help - A new cli application

USAGE:
   help [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 (default: false)


WEBSITE: http://darjun.github.io

WECHAT: GoUpUp

We can also rewrite the entire template:

func main() {
  cli.AppHelpTemplate = `NAME:
  {{.Name}} - {{.Usage}}
USAGE:
  {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}
  {{if len .Authors}}
AUTHOR:
  {{range .Authors}}{{ . }}{{end}}
  {{end}}{{if .Commands}}
COMMANDS:
 {{range .Commands}}{{if not .HideHelp}}   {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}}
GLOBAL OPTIONS:
  {{range .VisibleFlags}}{{.}}
  {{end}}{{end}}{{if .Copyright }}
COPYRIGHT:
  {{.Copyright}}
  {{end}}{{if .Version}}
VERSION:
  {{.Version}}
{{end}}
 `

  app := &cli.App{
    Authors: []*cli.Author{
      {
        Name:  "dj",
        Email: "darjun@126.com",
      },
    },
  }
  app.Run(os.Args)
}

Where XXX correspondsCli.AppFields set in the {} structure, such as Authors above:

$ ./help --help
NAME:
  help - A new cli application
USAGE:
  help [global options] command [command options] [arguments...]

AUTHOR:
  dj <darjun@126.com>

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

GLOBAL OPTIONS:
  --help, -h  show help (default: false)

Note the AUTHOR section.

By overwriting HelpPrinter, we can output help information ourselves:

func main() {
  cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) {
    fmt.Println("Simple help!")
  }

  (&cli.App{}).Run(os.Args)
}

Compile, run:

$ ./help --help
Simple help!

Built-in Options

Help Options

By default, the help option is--help/-h.We can passCli.HelpFlagField settings:

func main() {
  cli.HelpFlag = &cli.BoolFlag{
    Name:    "haaaaalp",
    Aliases: []string{"halp"},
    Usage:   "HALP",
  }

  (&cli.App{}).Run(os.Args)
}

View help:

$ go run main.go --halp
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:
   --haaaaalp, --halp  HALP (default: false)

Version Options

The default version option - v/--version outputs the version information of the application.We can passCli.VersionFlagSet version options:

func main() {
  cli.VersionFlag = &cli.BoolFlag{
    Name:    "print-version",
    Aliases: []string{"V"},
    Usage:   "print only the version",
  }

  app := &cli.App{
    Name:    "version",
    Version: "v1.0.0",
  }
  app.Run(os.Args)
}

This allows you to output version information by specifying--print-version/-V.Function:

$ go run main.go --print-version
version version v1.0.0

$ go run main.go -V
version version v1.0.0

We can also set it upCli.VersionPrinterField controls the output of version information:

const (
  Revision = "0cebd6e32a4e7094bbdbf150a1c2ffa56c34e91b"
)

func main() {
  cli.VersionPrinter = func(c *cli.Context) {
    fmt.Printf("version=%s revision=%s\n", c.App.Version, Revision)
  }

  app := &cli.App{
    Name:    "version",
    Version: "v1.0.0",
  }
  app.Run(os.Args)
}

The above program outputs both version number and SHA value submitted by git:

$ go run main.go -v
version=v1.0.0 revision=0cebd6e32a4e7094bbdbf150a1c2ffa56c34e91b

summary

Cli is very flexible and only needs setupCli.AppThe field value of can achieve the corresponding function without additional memory function and method.In addition, cli supports Bash auto-completion, and zsh is better supported, so you can explore it yourself if you are interested.

If you find a fun and useful GoLanguage Library, you are welcome to submit your issue on GitHub, the GoDaily Library_

Reference resources

  1. cli GitHub: https://github.com/urfave/cli
  2. Go Daily Library of cobra: https://darjun.github.io/2020/01/17/godailylib/cobra/
  3. Go Daily Library of GitHub: https://github.com/darjun/go-daily-lib

I

My blog: https://darjun.github.io

Welcome to my WeChat Public Number GoUpUp to learn and progress together.

Posted by printf on Tue, 23 Jun 2020 17:14:20 -0700