神刀安全网

Creating console applications in Golang

Creating a few console applications in the past I worked near to the ideal (for me at least) setup to created console applications. The following article I’ll show you how I’m implementing new console appications and the reasons behind it.

My latest console application is Elastico , an application to work with Elasticsearch. It is very much a work on progress, but I think the structure is very flexible and works quite well.

Within my applications I’m using the following libraries:

  • "github.com/codegangsta/cli"
  • "github.com/op/go-logging"
  • "github.com/fatih/color"
  • "github.com/kr/pretty"

The main function looks like this:

func main() {       app := cli.NewApp()     app.Name = "Elastico"     app.Commands = []cli.Command{}      app.Commands = append(app.Commands, searchCmds...)     app.Commands = append(app.Commands, mappingCmds...)     app.Commands = append(app.Commands, snapshotCmds...)     app.Commands = append(app.Commands, indexCmds...)     app.Commands = append(app.Commands, clusterCmds...)     app.Commands = append(app.Commands, documentCmds...)      app.Version = "0.0.1"      app.Before = func(context *cli.Context) error {         backend1 := logging.NewLogBackend(os.Stderr, "", 0)         backend1Leveled := logging.AddModuleLevel(backend1)         backend1Leveled.SetLevel(logging.ERROR, "")         logging.SetBackend(backend1Leveled)          if context.GlobalBool("debug") {             backend1Leveled.SetLevel(logging.DEBUG, "")         }          if t, err := elastico.New(context.String("host")); err != nil {             return err         } else {             e = t             return nil         }     }      app.EnableBashCompletion = true     app.BashComplete = func(c *cli.Context) {     }      app.Flags = []cli.Flag{         cli.BoolFlag{             Name:  "debug",             Usage: "Enable debug mode",         },         cli.BoolFlag{             Name:  "json",             Usage: "Return json output",         },         cli.StringFlag{             Name:   "host",             Value:  "http://127.0.0.1:9200",             EnvVar: "ELASTICO_HOST",             Usage:  "Host to operate on",         },     }      app.Flags = append(app.Flags, []cli.Flag{}...)      app.Run(os.Args) } 

Within the main function I’ll define the application, the sections and (global) flags. The before handler is being created here as well, taking care of the global setup. Where it is of any using I’m using environment variables as well. In this case the ELASTICO_HOST is being used as the host flag, for easy implementation within scripts.

All functions are being wrapped using the run function:

func run(fn func(*cli.Context) (json.M, error)) func(*cli.Context) {       return func(c *cli.Context) {         for _, flag := range c.Command.Flags {             if _, ok := flag.(RequiredFlag); !ok {                 continue             }              if c.IsSet(flag.GetName()) {                 continue             }              err := fmt.Errorf("Missing parameter %s.", flag.GetName())              ErrTemplate.Execute(os.Stderr, err)             os.Exit(1)         }          resp, err := fn(c)         if err != nil {             ErrTemplate.Execute(os.Stderr, err)             os.Exit(1)         }         if resp == nil {             return         }          buff := new(bytes.Buffer)         defer func() {             if err != nil {                 ErrTemplate.Execute(os.Stderr, err)                 os.Exit(1)             }              buff.WriteTo(os.Stdout)         }()          if c.GlobalBool("json") {             b, _ := json.MarshalIndent(resp, "", "  ")             buff.Write(b)         } else {             if t, ok := templates[c.Command.Name]; ok {                 err = t.Execute(buff, resp)             } else {                 b, _ := json.MarshalIndent(resp, "", "  ")                 buff.Write(b)             }         }         return     } } 

This function will handle required flags (which I’ll talk later about), showing the error template on errors to StdErr , writing output templated or in json format to StdOut . If functions are returning errors, the application will return with an (error) Exit code as well. This is very important for embedding in scripts.

New function templates are being registered using this function. It will register the template and will also check the local filesystem if there are existing templates in the ~/.elastico folder, these are prioritised. This allows users to customize their output templates like there preferences, just using GO templating.

func registerTemplate(cmd string, t string) *template.Template {       var err error      var usr *user.User     if usr, err = user.Current(); err != nil {         panic(err)     }      p := path.Join(usr.HomeDir, ".elastico")     templatePath := filepath.Join(p, fmt.Sprintf("%s.template", cmd))      if _, err := os.Stat(templatePath); err == nil {         b, _ := ioutil.ReadFile(templatePath)         templates[cmd] = template.Must(template.New("").Funcs(funcMap).Parse(string(b)))     } else {          templates[cmd] = template.Must(template.New("").Funcs(funcMap).Parse(t))     }     return templates[cmd] } 

Every section in the console application have their own file and configuration looks like this:

var (       _ = registerTemplate("document:put", ` {{- if .created }}Document created.  {{- else -}} Document updated (version {{ ._version }}).   {{- end -}}`)     _ = registerTemplate("document:delete", ` Document deleted. `)       _ = registerTemplate("document:get", `{{._source | json}}`) )  var documentCmds = []cli.Command{       cli.Command{         Name:        "document:get",         Usage:       "Retrieve document from index",         ArgsUsage:   "(-index {index}) (-type {type}) documentid",         UsageText:   "elastico document:get ",         Description: `Retrieve document from the index.`,         Action:      run(runDocumentGet),         BashComplete: func(context *cli.Context) {             fmt.Println("BashCompl")         },         Flags: []cli.Flag{             IndexFlag,             TypeFlag,         },     },     cli.Command{         Name:        "document:delete",         Usage:       "Delete document from index",         Description: ``,         Action:      run(runDocumentDelete),         Flags: []cli.Flag{             IndexFlag,             TypeFlag,         },     },     cli.Command{         Name:        "document:put",         Usage:       "Store document into index",         Description: ``,         Action:      run(runDocumentPut),         Flags: []cli.Flag{             IndexFlag,             TypeFlag,         },     }, } 

Templates are being defined for each command, together with the flags and required flags. The templates are using a common funcmap, for colorizing or formatting objects. When flags are being re-used multiple times, I’m creating a separate variable for them. This allows unity and consistency.

var IndexFlag = cli.StringFlag{       Name:  "index",     Value: "_all", } 

Required flags are being handled by using the following pattern:

type RequiredFlag struct {       cli.Flag }  func (rsf RequiredFlag) Required() bool {       return true }  func Required(flag cli.Flag) cli.Flag {       return RequiredFlag{         flag,     } } 

These flags are being checked for required values in the Before function.

New functionality can be added by adding the template to the registerTemplate , the flag to flags and the function itself:

func runIndexOpen(c *cli.Context) (json.M, error) {       path := c.String("index")     path = filepath.Join(path, "_open")      req, err := e.NewRequest("POST", path, nil)     if err != nil {         return nil, err     }      var resp json.M     if err := e.Do(req, &resp); err != nil {         return nil, err     }      return resp, nil } 

Which will execute the operation, returns a json, object or error. New tests can be added as follows:

func TestSearch(t *testing.T) {       //  setup("test-index", "test-type")     app := cli.NewApp()     if val, err := elastico.New("http://127.0.0.1:9200/"); err != nil {         t.Fatal(err)     } else {         e = val     }      globalSet := flag.NewFlagSet("test", 0)     globalSet.Set("index", "remco2")     globalSet.Set("type", "remco2")     globalCtx := cli.NewContext(app, globalSet, nil)      set := flag.NewFlagSet("test", 0)     set.Parse([]string{"-size", "10"})      c := cli.NewContext(nil, set, globalCtx)      if err := run(runSearch)(c); err != nil {                t.Fatal(err)         } } 

Summary:

  • flag variables to reuse flags for consistency
  • before handler for initialization and setup
  • add per function tests
  • use the run wrapper to handle output and errors
  • allow users the customize the output templates as they prefer
  • RequiredFlags for required parameters
  • an error template for handling errors

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Creating console applications in Golang

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮