Docs: Jump start the cli based project - go-easygen/easygen GitHub Wiki

Jump start the cli based project

This is on how to jump-start a cli based project.

The cli package really make it easy to code your command line handling program, especially when you need to deal with sub commands like apt-get, git etc do. Now, want to make it even simpler?

Usage

Yes! Use the code auto generation. It can make things even more simple. Run the following on command line so that you can see for yourself:

go get github.com/go-easygen/easygen
ls -l $GOPATH/bin
export PATH=$PATH:$GOPATH/bin

easygen $GOPATH/src/github.com/go-easygen/easygen/test/commandlineCLI-024

Result

You should see the following Go code:

// -*- go -*-
////////////////////////////////////////////////////////////////////////////
// Program: gogo
// Purpose: Golang package manager
////////////////////////////////////////////////////////////////////////////

package main

import (
	"github.com/mkideal/cli"
)

var app = &cli.Register(&cli.Command{
	Name: "gogo",
	Desc: "Golang package manager",
	Text: "  gogo is a new golang package manager\n  very very good",
	Argv: func() interface{} { return new(gogoT) },
	Fn:   gogo,

	NumArg:      cli.ExactN(1),
})

type gogoT struct {
	cli.Helper
	Version	bool	`cli:"v,version" usage:"display version"`
	List	bool	`cli:"l,list" usage:"list all sub commands or not"`
}

func gogo(ctx *cli.Context) error {
	argv := ctx.Argv().(*gogoT)
	ctx.String("%s: %v", ctx.Path(), jsonIndent(argv))
	ctx.String("[gogo]: %v\n", ctx.Args())

	return nil
}


////////////////////////////////////////////////////////////////////////////
// Program: build
// Purpose: Build golang application
////////////////////////////////////////////////////////////////////////////

package main

import (
	"github.com/mkideal/cli"
)

var buildCmd = app.Register(&cli.Command{
	Name: "build",
	Desc: "Build golang application",
	Text: "Usage:\n  gogo build [Options] Arch(i386|amd64)",
	Argv: func() interface{} { return new(buildT) },
	Fn:   build,

	NumArg:      cli.ExactN(1),
	CanSubRoute: true,
})

type buildT struct {
	cli.Helper
	Dir	string	`cli:"dir" usage:"source code root dir" dft:"./"`
	Suffix	string	`cli:"suffix" usage:"source file suffix" dft:".go,.c,.s"`
	Out	string	`cli:"o,out" usage:"output filename"`
}

func build(ctx *cli.Context) error {
	argv := ctx.Argv().(*buildT)
	ctx.String("%s: %v", ctx.Path(), jsonIndent(argv))
	ctx.String("[build]: %v\n", ctx.Args())

	return nil
}

////////////////////////////////////////////////////////////////////////////
// Program: install
// Purpose: Install golang application
////////////////////////////////////////////////////////////////////////////

package main

import (
	"github.com/mkideal/cli"
)

var installCmd = app.Register(&cli.Command{
	Name: "install",
	Desc: "Install golang application",
	Text: "Usage:\n  gogo install [Options] package [package...]",
	Argv: func() interface{} { return new(installT) },
	Fn:   install,

	NumArg:      cli.AtLeast(1),
	CanSubRoute: true,
})

type installT struct {
	cli.Helper
	Dir	string	`cli:"dir" usage:"source code root dir" dft:"./"`
	Suffix	string	`cli:"suffix" usage:"source file suffix" dft:".go,.c,.s"`
	Out	string	`cli:"o,out" usage:"output filename"`
}

func install(ctx *cli.Context) error {
	argv := ctx.Argv().(*installT)
	ctx.String("%s: %v", ctx.Path(), jsonIndent(argv))
	ctx.String("[install]: %v\n", ctx.Args())

	return nil
}

////////////////////////////////////////////////////////////////////////////
// Program: publish
// Purpose: Publish golang application
////////////////////////////////////////////////////////////////////////////

package main

import (
	"github.com/mkideal/cli"
)

var publishCmd = app.Register(&cli.Command{
	Name: "publish",
	Desc: "Publish golang application",
	Argv: func() interface{} { return new(publishT) },
	Fn:   publish,
})

type publishT struct {
	cli.Helper
	Dir	string	`cli:"dir" usage:"source code root dir" dft:"./"`
	Suffix	string	`cli:"suffix" usage:"source file suffix" dft:".go,.c,.s"`
	Out	string	`cli:"o,out" usage:"output filename"`
	List	bool	`cli:"l,list" usage:"list all sub commands"`
}

func publish(ctx *cli.Context) error {
	argv := ctx.Argv().(*publishT)
	ctx.String("%s: %v", ctx.Path(), jsonIndent(argv))
	ctx.String("[publish]: %v\n", ctx.Args())

	return nil
}

As you can see the skeleton of the program has been automatically generated.

Source

So what's behind the scene of the "magic"? Take a look at the driving Yaml file, that's everything you specify to make the magic happen. I'm republishing it below for your reference:

# program name, name for the executable
ProgramName: gogo

# package name
# - For standalone program that does not belong to any package, e.g., 
#   https://github.com/suntong/easygen/blob/7791e4f0e5605543d27da1671a21376cdb9dcf2a/easygen/easygen.go
#   just ignore the first line, the `package` output, and copy the rest
# - If you don't mind using a separated file to handle commandline paramters,
#   then name the package as "main". see the spin-out "TF-minus1.go" file under
#   https://github.com/suntong/easygen/tree/d1ab0b5fe80ddac57fe9ef51f6ccb3ab998cd5ee
# - If you are using it in a pacakge, look no further than
#   https://github.com/suntong/easygen/blob/master/easygenapi/config.go
#   which was a direct dump: easygen test/commandlineFlag | gofmt > easygenapi/config.go
#
PackageName: main

Name: gogo
Var: app
Desc: "Golang package manager"
Text: '  gogo is a new golang package manager\n  very very good'
NumArg: cli.ExactN(1)

Options:
  - Name: Version
    Type: bool
    Flag: v,version
    Usage: display version

  - Name: List
    Type: bool
    Flag: l,list
    Usage: list all sub commands or not


Command:

  - Name: build
    Desc: "Build golang application"
    Text: 'Usage:\n  gogo build [Options] Arch(i386|amd64)'
    NumArg: cli.ExactN(1)
    
    Options:
      - Name: Dir
        Type: string
        Flag: dir
        Value: '"./"'
        Usage: source code root dir

      - Name: Suffix
        Type: string
        Flag: suffix
        Value: '".go,.c,.s"'
        Usage: "source file suffix"

      - Name: Out
        Type: string
        Flag: o,out
        Usage: "output filename"

  - Name: install
    Desc: "Install golang application"
    Text: 'Usage:\n  gogo install [Options] package [package...]'
    NumArg: cli.AtLeast(1)
    
    Options:
      - Name: Dir
        Type: string
        Flag: dir
        Value: '"./"'
        Usage: source code root dir

      - Name: Suffix
        Type: string
        Flag: suffix
        Value: '".go,.c,.s"'
        Usage: "source file suffix"

      - Name: Out
        Type: string
        Flag: o,out
        Usage: "output filename"

  - Name: publish
    Desc: "Publish golang application"
    
    Options:
      - Name: Dir
        Type: string
        Flag: dir
        Value: '"./"'
        Usage: source code root dir

      - Name: Suffix
        Type: string
        Flag: suffix
        Value: '".go,.c,.s"'
        Usage: "source file suffix"

      - Name: Out
        Type: string
        Flag: o,out
        Usage: "output filename"

      - Name: List
        Type: bool
        Flag: l,list
        Usage: "list all sub commands"

Yes, you are right -- that's all you need to have the above code skeleton generated.

Takeaway

Understanding how to use easygen for cli based project

Now, the question is, how it can help your own project. Assuming you have never code a cli based project before, what you need to do are,

  • Take a look at the above Yaml file and see the generated Go code, and see their relationship.
  • Take a look at the orginal finished Go code that such code generation is for, and see what else you need to do to make it a working code.
  • The generated Go code was meant to be cut into each corresponding Go code files, e.g., the main.go, build.go, install.go and publish.go as in the above finished code
  • At this point, you have a good starting point already, because the code skeletons are already in place.

Round-trip generation

This is for illustration only. However, for round-trip generation, I.e., you generate and use the generated code, and you find yourself need to add more options later, then such manual cut & paste (into each corresponding Go code) method will be flimsy. So what shall we do?

  • Take a look at the template file that is being used to generate the Go code. This is where the true magic happens.
  • I recommend removing all the repeating part, and the sample implementation part out. I.e., your own build.go, install.go and publish.go contains the true implementation, and use the code-gen to automatically generate all the declaration part. This way the automatically-generated code can always been overwritten (with the latest definition) without affecting your existing implementation.
  • For details on code automatic generation, refer to easygen for all other information.

Practical Example

There is a new commandlineCLI-027 set of data and template, that is built after code sample 027. Replace the above commandlineCLI-024 with commandlineCLI-027, you will get code for the corresponding samples. Further more, the commandlineCLI-027 is built with the above "round-trip generation" principle in mind. Take a look at redoCmds.go in the 027-global-redo project, it is completely generated by easygen, and can be regenerated as many times as you want, and whenever you want, even after several releases.

Cookbook

Since I've written the above how-to, I've followed it to generate lots of my code wire-frames, polishing and fine-tuning my approaches along the way. Now, the procedure has mostly been stabilized, so I think it is a good idea to share how I am doing it, generating my cli-based code wire-frames. All the following code-gen are based on my cli-t0.tmpl template.

Simple case, no sub-commands

Let's start with the simpler case, generating cli handling code that has no sub-commands. This is the way tradition Unix tools use, and there are many tradition *nix tools that are doing this way, e.g., cp, rm, find, rsync etc, to name a few.

Here is how to generate a cli handling code-base for this case, using my picv v0.1.0 as the example:

  1. Provide a .yaml file -- picv.yaml
  2. Provide a code-gen shell script -- picvCLIGen.sh and run it to generate picvCLIDef.go
  3. Extract the commented out code to picvMain.go and picvCLICmd.go
  4. Now the code wire-frame is done, and ready for compilation and execution.
$ picv -h
picture vault
built on 2017-06-03

Tool to deal with camera pictures and put them in vault

Usage:
  picv [Options] dir [dirs...]

Options:

  -h, --help       display help information
  -g, --gap[=5]    Gap in days to be considered as different group/vault
  -p, --pod[=15]   Minimum number of picture to have before splitting to a different group/vault
  -v, --verbose    Verbose mode (Multiple -v options increase the verbosity.)

$ picv 
{"Help":false,"Gap":5,"Pod":15,"Verbose":{}}{"Help":false,"Gap":5,"Pod":15,"Verbose":{}}

If you need to tweak the .yaml file further, just

  1. Update the .yaml file
  2. Run go generate -x to update the picvCLIDef.go file
  3. And then do more editing to picvCLICmd.go to incorporate the new changes

With sub-commands case

The sub commands are modern way of providing cli interfaces, like apt-get, git etc. As a comparison, the tradition rcs (Revision Control System) tool uses different commands for version repo management, check-in, check-out, and view logs (rcs, ci, co, rlog respectively), but git provides these functionalities all under the same git command, but different sub commands instead (clone/init etc, commit, checkout, log).

How to generate cli handling code that has sub-commands? The procedure is exactly the same as above. I'm using my picv v0.2.0 this time as the example:

  1. Provide a .yaml file -- picv.yaml. You can see that the .yaml file only changes in its organization, while the content are reused nearly 100%, only that new sub-commands are added and options have been shifted around.
  2. Provide a code-gen shell script -- picvCLIGen.sh and run it to generate picvCLIDef.go. This step is exactly as above.
  3. Extract the commented out code to picvMain.go, cmdCut.go and the new cmdArch.go. Note that I leave the Main dispatcher in picvMain.go in this case, because it is clearer, as each sub commands just need to deal with their own handling.
  4. Now the code wire-frame is done, and ready for compilation and execution.
$ picv -h
picture vault
built on 2017-06-03

Tool to deal with camera pictures and put them in vault

Options:

  -h, --help      display help information
  -v, --verbose   Verbose mode (Multiple -v options increase the verbosity.)

Commands:

  cut    Separate picture into groups
  arch   Archive groups of picture into vaults


$ picv cut -h 
Separate picture into groups

Options:

  -h, --help       display help information
  -v, --verbose    Verbose mode (Multiple -v options increase the verbosity.)
  -g, --gap[=5]    Gap in days to be considered as different group/vault
  -p, --pod[=15]   Minimum number of picture to have before splitting to a different group/vault

$ picv cut 
{"Gap":5,"Pod":15,"Verbose":0}

$ picv cut -vv 
{"Gap":5,"Pod":15,"Verbose":2}

The rest are the same as above as well.

Further abstraction

Note the output of my picv v0.1.0 and v0.2.0 are different. This is because I've done a further abstraction step (manually, but could also be somewhat automated if you want) in v0.1.1 , which defined an option structure that is independent of the mkideal/cli package.

I view this further abstraction a logical representation of the options the tool can use, while the option structure defined in picvCLIDef.go is a "physical" representation of the options, because its foreign-package-depending nature.

This step is not absolutely necessary, but I found such practice more organized. Note how it is masking/hiding the implementation detail that -v is from global option, and how easy it is to make the corresponding changes when the -v option changed its definition location.