-
Notifications
You must be signed in to change notification settings - Fork 44
Jump start the cli based project
This wiki is on how to jump-start a cli
based project.
NOTE, the latest version of this document can now be found here -- a new cookbook section has been added.
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. Now, want to make it even simpler?
Yes! Use the code auto generation. It can make things even more simple. Run the following on command line and 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
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.
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.
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
andpublish.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.
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
andpublish.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. Note that the code has been recently moved, and the document hasn't been updated yet.
UPDATE:
Note that, after the above writeup, the commandlineCLI
has been renamed as commandlineCLI-024
, meaning it is for code sample 024. However, I'm not changing the above code, as it explains the concept more clearly. Moreover, there is also a new commandlineCLI-027
set of data and template, that is built after code sample 027. Replace the above commandlineCLI
with either commandlineCLI-024
or 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.
For ideas, please discuss them here. For easygen
code issue, please comment in the commit (e.g., here).
The above, either commandlineCLI
or commandlineCLI-024
or commandlineCLI-027
, are just simple examples.
For a true real-life example, take a look at
https://github.com/suntong/dnstools/blob/master/cmd_tcping.go
which ports the tcp ping functionality over from https://github.com/cloverstd/tcping/blob/master/main.go.
Its initial scaffolding code version, generated automatically by easygen
's code-gen, is here, and you can see that porting the tcp ping functionality over is much simpler than coding https://github.com/cloverstd/tcping/blob/master/main.go from start manually.
The cmd_tcping.go also demonstrates the sample usages of the github.com/mkideal/cli/clis
package. E.g.:
clis.Verbose
clis.AbortOn
etc.