diff --git a/README.md b/README.md index 88d9f4d..8aec0a8 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,10 @@ scripts/test ```bash scripts/run-centry ``` + + + + +https://github.com/kristofferahl/go-centry +https://github.com/urfave/cli/blob/master/docs/v2/manual.md#subcommands +https://github.com/spf13/cobra diff --git a/cmd/centry/help.go b/cmd/centry/help.go index 1bc5c5f..bc4d6fb 100644 --- a/cmd/centry/help.go +++ b/cmd/centry/help.go @@ -12,6 +12,7 @@ import ( "github.com/mitchellh/cli" ) +// TODO: Remove if unused??? func cliHelpFunc(manifest *config.Manifest, globalOptions *cmd.OptionsSet) cli.HelpFunc { return func(commands map[string]cli.CommandFactory) string { var buf bytes.Buffer diff --git a/cmd/centry/options.go b/cmd/centry/options.go index 7ceb6be..022e9dd 100644 --- a/cmd/centry/options.go +++ b/cmd/centry/options.go @@ -2,6 +2,7 @@ package main import ( "github.com/kristofferahl/go-centry/internal/pkg/cmd" + "github.com/urfave/cli/v2" ) // OptionSetGlobal is the name of the global OptionsSet @@ -75,3 +76,63 @@ func createGlobalOptions(context *Context) *cmd.OptionsSet { return options } + +func createGlobalFlags(context *Context) []cli.Flag { + options := make([]cli.Flag, 0) + manifest := context.manifest + + options = append(options, &cli.StringFlag{ + Name: "config.log.level", + Usage: "Overrides the log level", + Value: manifest.Config.Log.Level, + }) + + options = append(options, &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Disables logging", + Value: false, + }) + + // Adding global options specified by the manifest + for _, o := range manifest.Options { + o := o + + if context.optionEnabledFunc != nil && context.optionEnabledFunc(o) == false { + continue + } + + short := []string{o.Short} + if o.Short == "" { + short = nil + } + + //TODO: Handle EnvName?? + switch o.Type { + case cmd.SelectOption: + + options = append(options, &cli.BoolFlag{ + Name: o.Name, + Aliases: short, + Usage: o.Description, + Value: false, + }) + case cmd.BoolOption: + options = append(options, &cli.BoolFlag{ + Name: o.Name, + Aliases: short, + Usage: o.Description, + Value: false, + }) + case cmd.StringOption: + options = append(options, &cli.StringFlag{ + Name: o.Name, + Aliases: short, + Usage: o.Description, + Value: o.Default, + }) + } + } + + return options +} diff --git a/cmd/centry/runtime.go b/cmd/centry/runtime.go index 052295d..3716740 100644 --- a/cmd/centry/runtime.go +++ b/cmd/centry/runtime.go @@ -6,14 +6,15 @@ import ( "github.com/kristofferahl/go-centry/internal/pkg/config" "github.com/kristofferahl/go-centry/internal/pkg/log" "github.com/kristofferahl/go-centry/internal/pkg/shell" - "github.com/mitchellh/cli" "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" ) // Runtime defines the runtime type Runtime struct { context *Context - cli *cli.CLI + cli *cli.App + args []string } // NewRuntime builds a runtime based on the given arguments @@ -23,10 +24,10 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { // Args file := "" - args := []string{} + runtime.args = []string{} if len(inputArgs) >= 1 { file = inputArgs[0] - args = inputArgs[1:] + runtime.args = inputArgs[1:] } // Load manifest @@ -42,47 +43,47 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { // Create global options options := createGlobalOptions(context) + flags := createGlobalFlags(context) // Parse global options to get cli args - args, err = options.Parse(args, context.io) - if err != nil { - return nil, err - } + // args, err = options.Parse(args, context.io) + // if err != nil { + // return nil, err + // } // Initialize cli - c := &cli.CLI{ - Name: context.manifest.Config.Name, - Version: context.manifest.Config.Version, - - Commands: map[string]cli.CommandFactory{}, - Args: args, - HelpFunc: cliHelpFunc(context.manifest, options), - HelpWriter: context.io.Stderr, - - // Autocomplete: true, - // AutocompleteInstall: "install-autocomplete", - // AutocompleteUninstall: "uninstall-autocomplete", + app := &cli.App{ + Name: context.manifest.Config.Name, + HelpName: context.manifest.Config.Name, + Usage: "A tool for building declarative CLI's over bash scripts, written in go.", // TODO: Set from manifest config + UsageText: "", + Version: context.manifest.Config.Version, + HideHelpCommand: true, + + Commands: make([]*cli.Command, 0), + Flags: flags, } + // TODO: Fix log level from options // Override the current log level from options - logLevel := options.GetString("config.log.level") - if options.GetBool("quiet") { - logLevel = "panic" - } - context.log.TrySetLogLevel(logLevel) + // logLevel := options.GetString("config.log.level") + // if options.GetBool("quiet") { + // logLevel = "panic" + // } + //context.log.TrySetLogLevel(logLevel) + context.log.TrySetLogLevel("debug") logger := context.log.GetLogger() // Register builtin commands if context.executor == CLI { - c.Commands["serve"] = func() (cli.Command, error) { - return &ServeCommand{ - Manifest: context.manifest, - Log: logger.WithFields(logrus.Fields{ - "command": "serve", - }), - }, nil + serveCmd := &ServeCommand{ + Manifest: context.manifest, + Log: logger.WithFields(logrus.Fields{ + "command": "serve", + }), } + app.Commands = append(app.Commands, serveCmd.ToCLICommand()) } // Build commands @@ -100,7 +101,6 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { logger.WithFields(logrus.Fields{ "command": cmd.Name, }).Errorf("Failed to parse script functions. %v", err) - } else { for _, fn := range funcs { fn := fn @@ -111,24 +111,68 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { continue } + cmdDescription := cmd.Description if fn.Description != "" { cmd.Description = fn.Description } + cmdHelp := cmd.Help if fn.Help != "" { cmd.Help = fn.Help } cmdKey := strings.Replace(fn.Name, script.FunctionNameSplitChar(), " ", -1) - c.Commands[cmdKey] = func() (cli.Command, error) { - return &ScriptCommand{ - Context: context, - Log: logger.WithFields(logrus.Fields{}), - GlobalOptions: options, - Command: cmd, - Script: script, - Function: fn.Name, - }, nil + cmdKeyParts := strings.Split(cmdKey, " ") + + scriptCmd := &ScriptCommand{ + Context: context, + Log: logger.WithFields(logrus.Fields{}), + GlobalOptions: options, + Command: cmd, + Script: script, + Function: fn.Name, + } + + cliCmd := scriptCmd.ToCLICommand() + + var root *cli.Command + + for depth, cmdKeyPart := range cmdKeyParts { + if depth == 0 { + if getCommand(app.Commands, cmdKeyPart) == nil { + if depth == len(cmdKeyParts)-1 { + // add destination command + app.Commands = append(app.Commands, cliCmd) + } else { + // add placeholder + app.Commands = append(app.Commands, &cli.Command{ + Name: cmdKeyPart, + Usage: cmdDescription, + UsageText: cmdHelp, + HideHelpCommand: true, + Action: nil, + }) + } + } + root = getCommand(app.Commands, cmdKeyPart) + } else { + if getCommand(root.Subcommands, cmdKeyPart) == nil { + if depth == len(cmdKeyParts)-1 { + // add destination command + root.Subcommands = append(root.Subcommands, cliCmd) + } else { + // add placeholder + root.Subcommands = append(root.Subcommands, &cli.Command{ + Name: cmdKeyPart, + Usage: "...", + UsageText: "", + HideHelpCommand: true, + Action: nil, + }) + } + } + root = getCommand(root.Subcommands, cmdKeyPart) + } } logger.Debugf("Registered command \"%s\"", cmdKey) @@ -137,21 +181,23 @@ func NewRuntime(inputArgs []string, context *Context) (*Runtime, error) { } runtime.context = context - runtime.cli = c + runtime.cli = app return runtime, nil } // Execute runs the CLI and exits with a code func (runtime *Runtime) Execute() int { + args := append([]string{""}, runtime.args...) + // Run cli - exitCode, err := runtime.cli.Run() + err := runtime.cli.Run(args) if err != nil { logger := runtime.context.log.GetLogger() logger.Error(err) } - return exitCode + return 0 } func createScript(cmd config.Command, context *Context) shell.Script { @@ -163,3 +209,13 @@ func createScript(cmd config.Command, context *Context) shell.Script { }), } } + +func getCommand(commands []*cli.Command, name string) *cli.Command { + for _, c := range commands { + if c.HasName(name) { + return c + } + } + + return nil +} diff --git a/cmd/centry/script.go b/cmd/centry/script.go index 6999cfb..083514f 100644 --- a/cmd/centry/script.go +++ b/cmd/centry/script.go @@ -10,6 +10,7 @@ import ( "github.com/kristofferahl/go-centry/internal/pkg/config" "github.com/kristofferahl/go-centry/internal/pkg/shell" "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" ) // ScriptCommand is a Command implementation that applies stuff @@ -22,21 +23,39 @@ type ScriptCommand struct { Function string } +// ToCLICommand returns a CLI command +func (sc *ScriptCommand) ToCLICommand() *cli.Command { + cmdKeys := strings.Split(strings.Replace(sc.Function, sc.Script.FunctionNameSplitChar(), " ", -1), " ") + return &cli.Command{ + Name: cmdKeys[len(cmdKeys)-1], + Usage: sc.Synopsis(), + UsageText: sc.Help(), + HideHelpCommand: true, + Action: func(c *cli.Context) error { + ec := sc.Run(c.Args().Slice()) + if ec > 0 { + return cli.Exit("command exited with non zero exit code", ec) + } + return nil + }, + } +} + // Run builds the source and executes it -func (c *ScriptCommand) Run(args []string) int { - c.Log.Debugf("Executing command \"%v\"", c.Function) +func (sc *ScriptCommand) Run(args []string) int { + sc.Log.Debugf("Executing command \"%v\"", sc.Function) var source string - switch c.Script.Language() { + switch sc.Script.Language() { case "bash": - source = generateBashSource(c, args) - c.Log.Debugf("Generated bash source:\n%s\n", source) + source = generateBashSource(sc, args) + sc.Log.Debugf("Generated bash source:\n%s\n", source) default: - c.Log.Errorf("Unsupported script language %s", c.Script.Language()) + sc.Log.Errorf("Unsupported script language %s", sc.Script.Language()) return 1 } - err := c.Script.Executable().Run(c.Context.io, []string{"-c", source}) + err := sc.Script.Executable().Run(sc.Context.io, []string{"-c", source}) if err != nil { exitCode := 1 @@ -46,36 +65,36 @@ func (c *ScriptCommand) Run(args []string) int { } } - c.Log.Errorf("Command %v exited with error! %v", c.Function, err) + sc.Log.Errorf("Command %v exited with error! %v", sc.Function, err) return exitCode } - c.Log.Debugf("Finished executing command %v...", c.Function) + sc.Log.Debugf("Finished executing command %v...", sc.Function) return 0 } // Help returns the help text of the ScriptCommand -func (c *ScriptCommand) Help() string { - return c.Command.Help +func (sc *ScriptCommand) Help() string { + return sc.Command.Help } // Synopsis returns the synopsis of the ScriptCommand -func (c *ScriptCommand) Synopsis() string { - return c.Command.Description +func (sc *ScriptCommand) Synopsis() string { + return sc.Command.Description } -func generateBashSource(c *ScriptCommand, args []string) string { +func generateBashSource(sc *ScriptCommand, args []string) string { source := []string{} source = append(source, "#!/usr/bin/env bash") source = append(source, "") source = append(source, "# Set working directory") - source = append(source, fmt.Sprintf("cd %s || exit 1", c.Context.manifest.BasePath)) + source = append(source, fmt.Sprintf("cd %s || exit 1", sc.Context.manifest.BasePath)) source = append(source, "") source = append(source, "# Set exports from flags") - for _, v := range optionsSetToEnvVars(c.GlobalOptions) { + for _, v := range optionsSetToEnvVars(sc.GlobalOptions) { if v.Value != "" { value := v.Value if v.IsString() { @@ -87,17 +106,17 @@ func generateBashSource(c *ScriptCommand, args []string) string { source = append(source, "") source = append(source, "# Sourcing scripts") - for _, s := range c.Context.manifest.Scripts { + for _, s := range sc.Context.manifest.Scripts { source = append(source, fmt.Sprintf("source %s", s)) } source = append(source, "") source = append(source, "# Sourcing command") - source = append(source, fmt.Sprintf("source %s", c.Script.FullPath())) + source = append(source, fmt.Sprintf("source %s", sc.Script.FullPath())) source = append(source, "") source = append(source, "# Executing command") - source = append(source, fmt.Sprintf("%s %s", c.Function, strings.Join(args, " "))) + source = append(source, fmt.Sprintf("%s %s", sc.Function, strings.Join(args, " "))) return strings.Join(source, "\n") } diff --git a/cmd/centry/serve.go b/cmd/centry/serve.go index b99efee..f92e220 100644 --- a/cmd/centry/serve.go +++ b/cmd/centry/serve.go @@ -11,6 +11,7 @@ import ( "github.com/kristofferahl/go-centry/internal/pkg/config" "github.com/kristofferahl/go-centry/internal/pkg/io" "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" ) // ServeCommand is a Command implementation that applies stuff @@ -19,6 +20,21 @@ type ServeCommand struct { Log *logrus.Entry } +// ToCLICommand returns a CLI command +func (sc *ServeCommand) ToCLICommand() *cli.Command { + return &cli.Command{ + Name: "serve", + Usage: sc.Synopsis(), + Action: func(c *cli.Context) error { + ec := sc.Run(c.Args().Slice()) + if ec > 0 { + return cli.Exit("failed to start the server", ec) + } + return nil + }, + } +} + // Run starts an HTTP server and blocks func (sc *ServeCommand) Run(args []string) int { sc.Log.Debugf("Serving HTTP api") diff --git a/go.mod b/go.mod index c5666e4..1336cf1 100644 --- a/go.mod +++ b/go.mod @@ -9,5 +9,6 @@ require ( github.com/mitchellh/cli v1.0.0 github.com/santhosh-tekuri/jsonschema/v2 v2.1.0 github.com/sirupsen/logrus v1.4.2 + github.com/urfave/cli/v2 v2.2.0 gopkg.in/yaml.v2 v2.2.7 ) diff --git a/go.sum b/go.sum index 02a8fa0..ae3f2a8 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,10 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= @@ -28,17 +31,25 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v2 v2.1.0 h1:7KOtBzox6l1PbyZCuQfo923yIBpoMtGCDOD78P9lv9g= github.com/santhosh-tekuri/jsonschema/v2 v2.1.0/go.mod h1:yzJzKUGV4RbWqWIBBP4wSOBqavX5saE02yirLS0OTyg= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU= +github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=