diff --git a/go.mod b/go.mod index 1082ea8..b925b1e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( dario.cat/mergo v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.1.5 // indirect + github.com/alecthomas/kong v1.12.1 // indirect github.com/cloudflare/circl v1.6.0 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 4e0affc..e09d4cf 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/alecthomas/kong v1.12.1 h1:iq6aMJDcFYP9uFrLdsiZQ2ZMmcshduyGv4Pek0MQPW0= +github.com/alecthomas/kong v1.12.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= diff --git a/internal/homesick/cli/cli.go b/internal/homesick/cli/cli.go index f7c2452..9afef8e 100644 --- a/internal/homesick/cli/cli.go +++ b/internal/homesick/cli/cli.go @@ -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() {