Clean up disparity between Context and Kong.

Previously, there was a confusing mix of functionality shared between
the two wherein you would need to use the Kong type for printing errors,
etc. but it did not have access to the context in order to print
context-sensitive usage information. This has been fixed.

Additionally, there are now fuzzy correction suggestions for flags and
commands

Also added a server example which shows how Kong can be used for parsing
in interactive shells. Run with:

    $ go run ./_examples/server/*.go

Then interact with:

    $ ssh -p 6740 127.0.0.1
This commit is contained in:
Alec Thomas
2018-06-25 14:45:20 +10:00
parent f7acb2b389
commit 6408010083
20 changed files with 489 additions and 130 deletions
+49
View File
@@ -0,0 +1,49 @@
// nolint: govet
package main
import (
"fmt"
"github.com/alecthomas/kong"
)
// Ensure the grammar compiles.
var _ = kong.Must(&grammar{})
// Server interface.
type grammar struct {
Help helpCmd `cmd help:"Show help."`
Question helpCmd `cmd hidden name:"?" help:"Show help."`
Status statusCmd `cmd help:"Show server status."`
}
type statusCmd struct {
Verbose bool `short:"v" help:"Show verbose status information."`
}
func (s *statusCmd) Run(ctx *kong.Context) error {
ctx.Printf("OK")
return nil
}
type helpCmd struct {
Command []string `arg optional help:"Show help on command."`
}
// Run shows help.
func (h *helpCmd) Run(realCtx *kong.Context) error {
ctx, err := kong.Trace(realCtx.Kong, h.Command)
if err != nil {
return err
}
if ctx.Error != nil {
return ctx.Error
}
err = ctx.PrintUsage(false)
if err != nil {
return err
}
fmt.Fprintln(realCtx.Stdout)
return nil
}
+154
View File
@@ -0,0 +1,154 @@
package main
import (
"errors"
"fmt"
"io"
"log"
"os"
"strings"
"golang.org/x/crypto/ssh/terminal"
"github.com/chzyer/readline"
"github.com/gliderlabs/ssh"
"github.com/google/shlex"
"github.com/kr/pty"
"github.com/alecthomas/colour"
"github.com/alecthomas/kong"
)
type context struct {
kong *kong.Context
rl *readline.Instance
}
func handle(log *log.Logger, s ssh.Session) error {
log.Printf("New SSH")
sshPty, _, isPty := s.Pty()
if !isPty {
return errors.New("No PTY requested")
}
log.Printf("Using TERM=%s width=%d height=%d", sshPty.Term, sshPty.Window.Width, sshPty.Window.Height)
cpty, tty, err := pty.Open()
if err != nil {
return err
}
defer tty.Close()
state, err := terminal.GetState(int(cpty.Fd()))
if err != nil {
return err
}
defer terminal.Restore(int(cpty.Fd()), state)
colour.Fprintln(tty, "^BWelcome!^R")
go io.Copy(cpty, s)
go io.Copy(s, cpty)
parser, err := buildShellParser(tty)
if err != nil {
return err
}
rl, err := readline.NewEx(&readline.Config{
Prompt: "> ",
Stderr: tty,
Stdout: tty,
Stdin: tty,
FuncOnWidthChanged: func(f func()) {},
FuncMakeRaw: func() error {
_, err := terminal.MakeRaw(int(cpty.Fd())) // nolint: govet
return err
},
FuncExitRaw: func() error { return nil },
})
if err != nil {
return err
}
log.Printf("Loop")
for {
tty.Sync()
var line string
line, err = rl.Readline()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
var args []string
args, err := shlex.Split(string(line))
if err != nil {
parser.Errorf("%s", err)
continue
}
var ctx *kong.Context
ctx, err = parser.Parse(args)
if err != nil {
parser.Errorf("%s", err)
if err, ok := err.(*kong.ParseError); ok {
log.Println(err.Error())
err.Context.PrintUsage(false)
}
continue
}
err = ctx.Run(ctx)
if err != nil {
parser.Errorf("%s", err)
continue
}
}
}
func buildShellParser(tty *os.File) (*kong.Kong, error) {
parser, err := kong.New(&grammar{},
kong.Name(""),
kong.Description("Example using Kong for interactive command parsing."),
kong.Writers(tty, tty),
kong.Exit(func(int) {}),
kong.ConfigureHelp(kong.HelpOptions{
NoAppSummary: true,
}),
kong.NoDefaultHelp(),
)
return parser, err
}
func handlerWithError(handle func(log *log.Logger, s ssh.Session) error) ssh.Handler {
return func(s ssh.Session) {
prefix := fmt.Sprintf("%s->%s ", s.LocalAddr(), s.RemoteAddr())
l := log.New(os.Stdout, prefix, log.LstdFlags)
err := handle(l, s)
if err != nil {
log.Printf("error: %s", err)
s.Exit(1)
} else {
log.Printf("Bye")
s.Exit(0)
}
}
}
var cli struct {
HostKey string `type:"existingfile" help:"SSH host key to use." default:"./_examples/server/server_rsa_key"`
Bind string `help:"Bind address for server." default:"127.0.0.1:6740"`
}
func main() {
ctx := kong.Parse(&cli,
kong.Name("server"),
kong.Description("A network server using Kong for interacting with clients."))
ssh.Handle(handlerWithError(handle))
log.Printf("SSH listening on: %s", cli.Bind)
log.Printf("Using host key: %s", cli.HostKey)
log.Println()
parts := strings.Split(cli.Bind, ":")
log.Printf("Connect with: ssh -p %s %s", parts[1], parts[0])
log.Println()
err := ssh.ListenAndServe(cli.Bind, nil, ssh.HostKeyFile(cli.HostKey))
ctx.FatalIfErrorf(err)
}
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAxK1QbPQYibc0VFZdc6GHPka5oTeGnXsMZiyX4JbHUJl1oNDB
Xg9NATbft+q/6ZDjyVEQhq8xgLvYFkL8qBLt/6UAaOub0RtmPqQwmxoNLWuXFFwn
YlBKApQ4gf58/jOcGPpdEwfjkjwLb536Bni25XMU4cYrdIvQIhtMaK+Zqja/3MAC
V6ZRZZCd8hABJqaZu+3mRElnF1d7gfMvA/hhaq7Y5VYr8rUrBHHimrT/GEP6aCbf
Npo43SfRnUDu2+EAK7vA9cM8fg/O/mvNR1/9jzOWyr8ZDLD6R6iaZCQ2anEPcqim
MCOtibSeVOms07Zcn/TSsgmfGwa8rQkpXVJA5wIDAQABAoIBAQCTUccGdajTrzkx
WyfQ71NgoJV3XyIkYAEfn5N8FTTi+LAVb4kILanemP3mw55RE8isCV65pA0OgqYP
tsmOE+/WKAAwlxs1/LIPhekqpM7uEMMv6v9NMxrc562UIc36kyn/w7loAebCqNtg
FhMsOcu1/wfLPidau0eB5LTNTYtq5RuSKxoindvatk+Zmk0KjoA+f25MlwCEHQNr
ygpopclyTHVln2t3t0o97/a7dHa9+HlmVO4GxWvTTiqtcFErTGWtTUW8aeZFS83r
p+JZNxReSJ2MlM9bm15wJ0L86GTeYZQiaNuC1XETbFvX+9Ffkl+7EtsdYDLV1N6r
/eOP2f0hAoGBAOKVDHmnru7SVmbH5BI8HW5sd6IVztZM3+BKzy6AaPc4/FgG6MOr
bJyFbmN8S/gVi4OYOJXgfaeKcycYJFTjXUSnNRQ9eT0MseD9SxzEXV7RGtnvudiu
pbRmtBRtf3e4beaN9X4SfWk4+Frw7B8UsPXwV/09s7AW279cES565IkfAoGBAN42
TQSC/jQmJBpGSnqWfqQtKPTSKFoZ/JQbxoy9QckAMqVSFwBBgwQYr4MbI7WyjPRE
s43kpf+Sq/++fc3hyk5XAWBKscK0KLs0HBRZyLybQYI+f4/x2giVzKeRRNVa9nQa
VdIU8i+eO2AUzG690q89HGkRBsfXekjq5kXC9Cc5AoGAUY0b5F16FPMXrf6cFAQX
A7t+g5Qd0fvxSCUk1LPbE8Aq8vPpqyN0ABH2XVBLd4spn7+V/jvCfh7Su2txCCyd
USxtak+F53c+PqBr/HqgsJPKek5SMa8KbRfaENAoZMq4o5bMmQfGo6yhlvnHwpgL
6TkMMlWW6vYPOZzFglkxEDkCgYApT78Rz6ii2VRs7hR6pe/1Zc/vdAK8fYhPoLpQ
//5y9+5yfch467UH1e8LWMhSx1cdMoiPIKsb0JDZgviwhgGufs5qsHhL0mKgKxft
UKPZLKQJKsVcZYI7hl396Sv63mZjP2IlJG/CGpC/VB6NmAzLN3lIrzmrfYvmcoVN
AumRQQKBgB4Uznek3nq5ZQf93+ucvXpf0bLRqq1va7jYmRosyiCN52grbclj5uMq
vxr1uoqmgtCfqdgUbm0s+kVK6D4bPkz4HQOSXImXhLs8/KdixYfPLSarxYvTxZKg
mMF1XqcdRwSv3RZYtUbbF7dYQYsC1/ZKXvtPldeoDmTZ+U7b2qbE
-----END RSA PRIVATE KEY-----
+1
View File
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErVBs9BiJtzRUVl1zoYc+RrmhN4adewxmLJfglsdQmXWg0MFeD00BNt+36r/pkOPJURCGrzGAu9gWQvyoEu3/pQBo65vRG2Y+pDCbGg0ta5cUXCdiUEoClDiB/nz+M5wY+l0TB+OSPAtvnfoGeLblcxThxit0i9AiG0xor5mqNr/cwAJXplFlkJ3yEAEmppm77eZESWcXV3uB8y8D+GFqrtjlVivytSsEceKatP8YQ/poJt82mjjdJ9GdQO7b4QAru8D1wzx+D87+a81HX/2PM5bKvxkMsPpHqJpkJDZqcQ9yqKYwI62JtJ5U6azTtlyf9NKyCZ8bBrytCSldUkDn