refactor(cli): use kong for command parsing
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
|
||||
"git.hrafn.xyz/aether/gosick/internal/homesick/core"
|
||||
"git.hrafn.xyz/aether/gosick/internal/homesick/version"
|
||||
"github.com/alecthomas/kong"
|
||||
)
|
||||
|
||||
func Run(args []string, stdout io.Writer, stderr io.Writer) int {
|
||||
@@ -17,128 +19,219 @@ func Run(args []string, stdout io.Writer, stderr io.Writer) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
printHelp(stdout)
|
||||
return 0
|
||||
}
|
||||
|
||||
command := args[0]
|
||||
switch command {
|
||||
case "-v", "--version", "version":
|
||||
if err := app.Version(version.String); err != nil {
|
||||
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case "help", "--help", "-h":
|
||||
printHelp(stdout)
|
||||
return 0
|
||||
case "clone":
|
||||
if len(args) < 2 {
|
||||
_, _ = fmt.Fprintln(stderr, "error: clone requires URI")
|
||||
return 1
|
||||
}
|
||||
destination := ""
|
||||
if len(args) > 2 {
|
||||
destination = args[2]
|
||||
}
|
||||
if err := app.Clone(args[1], destination); err != nil {
|
||||
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case "list":
|
||||
if err := app.List(); err != nil {
|
||||
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case "show_path":
|
||||
castle := defaultCastle(args)
|
||||
if err := app.ShowPath(castle); err != nil {
|
||||
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case "status":
|
||||
castle := defaultCastle(args)
|
||||
if err := app.Status(castle); err != nil {
|
||||
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case "diff":
|
||||
castle := defaultCastle(args)
|
||||
if err := app.Diff(castle); err != nil {
|
||||
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case "link":
|
||||
castle := defaultCastle(args)
|
||||
if err := app.LinkCastle(castle); err != nil {
|
||||
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case "unlink":
|
||||
castle := defaultCastle(args)
|
||||
if err := app.Unlink(castle); err != nil {
|
||||
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case "track":
|
||||
if len(args) < 2 || strings.TrimSpace(args[1]) == "" {
|
||||
_, _ = fmt.Fprintln(stderr, "error: track requires FILE")
|
||||
return 1
|
||||
}
|
||||
|
||||
castle := "dotfiles"
|
||||
if len(args) > 2 && strings.TrimSpace(args[2]) != "" {
|
||||
castle = args[2]
|
||||
}
|
||||
|
||||
if err := app.Track(args[1], castle); err != nil {
|
||||
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case "pull", "push", "commit", "destroy", "cd", "open", "exec", "exec_all", "rc", "generate":
|
||||
_, _ = fmt.Fprintf(stderr, "error: %s is not implemented in Go yet\n", command)
|
||||
return 2
|
||||
default:
|
||||
_, _ = fmt.Fprintf(stderr, "error: unknown command %q\n\n", command)
|
||||
printHelp(stderr)
|
||||
parser, err := kong.New(
|
||||
&cliModel{},
|
||||
kong.Name("homesick"),
|
||||
kong.Description("Go scaffold"),
|
||||
kong.Writers(stdout, stderr),
|
||||
kong.Exit(func(int) {}),
|
||||
kong.ConfigureHelp(kong.HelpOptions{Compact: true}),
|
||||
)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func defaultCastle(args []string) string {
|
||||
if len(args) > 1 && strings.TrimSpace(args[1]) != "" {
|
||||
return args[1]
|
||||
normalizedArgs := normalizeArgs(args)
|
||||
ctx, err := parser.Parse(normalizedArgs)
|
||||
if err != nil {
|
||||
var parseErr *kong.ParseError
|
||||
if errors.As(err, &parseErr) {
|
||||
if parseErr.ExitCode() == 0 || isHelpRequest(normalizedArgs) {
|
||||
return 0
|
||||
}
|
||||
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||
if parseErr.Context != nil {
|
||||
_ = parseErr.Context.PrintUsage(false)
|
||||
}
|
||||
return parseErr.ExitCode()
|
||||
}
|
||||
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return "dotfiles"
|
||||
|
||||
if err := ctx.Run(app); err != nil {
|
||||
var exitErr *cliExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
_, _ = fmt.Fprintf(stderr, "error: %v\n", exitErr.err)
|
||||
return exitErr.code
|
||||
}
|
||||
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func printHelp(w io.Writer) {
|
||||
_, _ = fmt.Fprintln(w, "homesick (Go scaffold)")
|
||||
_, _ = fmt.Fprintln(w, "")
|
||||
_, _ = fmt.Fprintln(w, "Implemented commands:")
|
||||
_, _ = fmt.Fprintln(w, " clone URI [CASTLE_NAME]")
|
||||
_, _ = fmt.Fprintln(w, " list")
|
||||
_, _ = fmt.Fprintln(w, " show_path [CASTLE]")
|
||||
_, _ = fmt.Fprintln(w, " status [CASTLE]")
|
||||
_, _ = fmt.Fprintln(w, " diff [CASTLE]")
|
||||
_, _ = fmt.Fprintln(w, " link [CASTLE]")
|
||||
_, _ = fmt.Fprintln(w, " unlink [CASTLE]")
|
||||
_, _ = fmt.Fprintln(w, " track FILE [CASTLE]")
|
||||
_, _ = fmt.Fprintln(w, " version | -v | --version")
|
||||
_, _ = fmt.Fprintln(w, "")
|
||||
_, _ = fmt.Fprintln(w, "Not implemented yet:")
|
||||
_, _ = fmt.Fprintln(w, " pull, push, commit, destroy, cd, open, exec, exec_all, rc, generate")
|
||||
_, _ = fmt.Fprintln(w, "")
|
||||
_, _ = fmt.Fprintln(w, "Build: go build -o dist/homesick-go ./cmd/homesick")
|
||||
type cliModel struct {
|
||||
Clone cloneCmd `cmd:"" help:"Clone a castle."`
|
||||
List listCmd `cmd:"" help:"List castles."`
|
||||
ShowPath showPathCmd `cmd:"" name:"show_path" help:"Show the path of a castle."`
|
||||
Status statusCmd `cmd:"" help:"Show git status for a castle."`
|
||||
Diff diffCmd `cmd:"" help:"Show git diff for a castle."`
|
||||
Link linkCmd `cmd:"" help:"Symlink all dotfiles from a castle."`
|
||||
Unlink unlinkCmd `cmd:"" help:"Unsymlink all dotfiles from a castle."`
|
||||
Track trackCmd `cmd:"" help:"Track a file in a castle."`
|
||||
Version versionCmd `cmd:"" help:"Display the current version."`
|
||||
Pull pullCmd `cmd:"" help:"Pull the specified castle."`
|
||||
Push pushCmd `cmd:"" help:"Push the specified castle."`
|
||||
Commit commitCmd `cmd:"" help:"Commit the specified castle's changes."`
|
||||
Destroy destroyCmd `cmd:"" help:"Destroy a castle."`
|
||||
Cd cdCmd `cmd:"" help:"Print the path to a castle."`
|
||||
Open openCmd `cmd:"" help:"Open a castle."`
|
||||
Exec execCmd `cmd:"" help:"Execute a command in a castle."`
|
||||
ExecAll execAllCmd `cmd:"" name:"exec_all" help:"Execute a command in every castle."`
|
||||
Rc rcCmd `cmd:"" help:"Run rc hooks for a castle."`
|
||||
Generate generateCmd `cmd:"" help:"Generate a castle."`
|
||||
}
|
||||
|
||||
type cloneCmd struct {
|
||||
URI string `arg:"" name:"URI" help:"Castle URI to clone."`
|
||||
Destination string `arg:"" optional:"" name:"CASTLE_NAME" help:"Optional local castle name."`
|
||||
}
|
||||
|
||||
func (c *cloneCmd) Run(app *core.App) error {
|
||||
return app.Clone(c.URI, c.Destination)
|
||||
}
|
||||
|
||||
type listCmd struct{}
|
||||
|
||||
func (c *listCmd) Run(app *core.App) error {
|
||||
return app.List()
|
||||
}
|
||||
|
||||
type showPathCmd struct {
|
||||
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
|
||||
}
|
||||
|
||||
func (c *showPathCmd) Run(app *core.App) error {
|
||||
return app.ShowPath(defaultCastle(c.Castle))
|
||||
}
|
||||
|
||||
type statusCmd struct {
|
||||
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
|
||||
}
|
||||
|
||||
func (c *statusCmd) Run(app *core.App) error {
|
||||
return app.Status(defaultCastle(c.Castle))
|
||||
}
|
||||
|
||||
type diffCmd struct {
|
||||
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
|
||||
}
|
||||
|
||||
func (c *diffCmd) Run(app *core.App) error {
|
||||
return app.Diff(defaultCastle(c.Castle))
|
||||
}
|
||||
|
||||
type linkCmd struct {
|
||||
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
|
||||
}
|
||||
|
||||
func (c *linkCmd) Run(app *core.App) error {
|
||||
return app.LinkCastle(defaultCastle(c.Castle))
|
||||
}
|
||||
|
||||
type unlinkCmd struct {
|
||||
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
|
||||
}
|
||||
|
||||
func (c *unlinkCmd) Run(app *core.App) error {
|
||||
return app.Unlink(defaultCastle(c.Castle))
|
||||
}
|
||||
|
||||
type trackCmd struct {
|
||||
File string `arg:"" name:"FILE" help:"File to track."`
|
||||
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
|
||||
}
|
||||
|
||||
func (c *trackCmd) Run(app *core.App) error {
|
||||
return app.Track(c.File, defaultCastle(c.Castle))
|
||||
}
|
||||
|
||||
type versionCmd struct{}
|
||||
|
||||
func (c *versionCmd) Run(app *core.App) error {
|
||||
return app.Version(version.String)
|
||||
}
|
||||
|
||||
type pullCmd struct{}
|
||||
|
||||
type pushCmd struct{}
|
||||
|
||||
type commitCmd struct{}
|
||||
|
||||
type destroyCmd struct{}
|
||||
|
||||
type cdCmd struct{}
|
||||
|
||||
type openCmd struct{}
|
||||
|
||||
type execCmd struct{}
|
||||
|
||||
type execAllCmd struct{}
|
||||
|
||||
type rcCmd struct{}
|
||||
|
||||
type generateCmd struct{}
|
||||
|
||||
func (c *pullCmd) Run() error { return notImplemented("pull") }
|
||||
func (c *pushCmd) Run() error { return notImplemented("push") }
|
||||
func (c *commitCmd) Run() error { return notImplemented("commit") }
|
||||
func (c *destroyCmd) Run() error { return notImplemented("destroy") }
|
||||
func (c *cdCmd) Run() error { return notImplemented("cd") }
|
||||
func (c *openCmd) Run() error { return notImplemented("open") }
|
||||
func (c *execCmd) Run() error { return notImplemented("exec") }
|
||||
func (c *execAllCmd) Run() error { return notImplemented("exec_all") }
|
||||
func (c *rcCmd) Run() error { return notImplemented("rc") }
|
||||
func (c *generateCmd) Run() error { return notImplemented("generate") }
|
||||
|
||||
func defaultCastle(castle string) string {
|
||||
if strings.TrimSpace(castle) == "" {
|
||||
return "dotfiles"
|
||||
}
|
||||
return castle
|
||||
}
|
||||
|
||||
func normalizeArgs(args []string) []string {
|
||||
if len(args) == 0 {
|
||||
return []string{"--help"}
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "-h", "--help":
|
||||
return []string{"--help"}
|
||||
case "help":
|
||||
if len(args) == 1 {
|
||||
return []string{"--help"}
|
||||
}
|
||||
return append(args[1:], "--help")
|
||||
case "-v", "--version":
|
||||
return []string{"version"}
|
||||
default:
|
||||
return args
|
||||
}
|
||||
}
|
||||
|
||||
func isHelpRequest(args []string) bool {
|
||||
for _, arg := range args {
|
||||
if arg == "-h" || arg == "--help" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type cliExitError struct {
|
||||
code int
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *cliExitError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func notImplemented(command string) error {
|
||||
return &cliExitError{code: 2, err: fmt.Errorf("%s is not implemented in Go yet", command)}
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
Reference in New Issue
Block a user