From 01883dff4b6bc32a0394613af46624725937e322 Mon Sep 17 00:00:00 2001 From: Paul Lhussiez Date: Mon, 15 Apr 2019 16:34:37 +0200 Subject: [PATCH] 'new' command and set flags --- README.md | 112 +++++++++++++++++++++++++++++++++++++++----- cmd/flags.go | 1 + cmd/new.go | 52 ++++++-------------- cmd/qk/main.go | 1 + conf/input.go | 35 ++++++++++++++ renderer/analyze.go | 16 ++++++- renderer/render.go | 4 +- utils/colors.go | 11 +++++ utils/survey.go | 58 +++++++++++++++++++++++ 9 files changed, 237 insertions(+), 53 deletions(-) create mode 100644 utils/survey.go diff --git a/README.md b/README.md index 18ff58f..9832649 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Quokka supports various providers to download the templates. It supports or using a local directory. ``` -Quokka (qk) is a template engine that enables to render local or distant +Quokka (qk) is a template engine that enables to render local or distant templates/boilerplates in a user friendly way. When given a URL/Git repository or a path to a local Quokka template, quokka will ask for the required values in an interactive way except if an inpute file is given to the CLI. @@ -128,31 +128,82 @@ Available Commands: version Show build and version Flags: - -d, --debug debug mode + --debug Enable or disable debug mode --git.depth int depth of git clone in case of git provider (default 1) -h, --help help for qk -i, --input string specify an input values file to automate template rendering -k, --keep do not delete the template when operation is complete -o, --output string specify the directory where the template should be downloaded or cloned -p, --path string specify if the template is actually stored in a sub-directory of the downloaded file + -e, --set strings specify values on the command line + -y, --yes Automatically accept Use "qk [command] --help" for more information about a command. ``` - - ## Keeping the template -When downloading or cloning a template, `quokka` will create a temporary +When downloading or cloning a template, Quokka will create a temporary directory and delete it once the operation completes. If you want to keep the template (to play with it, or simply to keep a copy), make sure you pass -the `--keep` option. This option pairs well with the `--output` option which -defines where the template should be downloaded/cloned. +the `-k/--keep` option. This option pairs well with the `-o/--output` option +which defines where the template should be downloaded/cloned. + +## Input file + +The rendering of a Quokka template can be automated if the template was designed +with this in mind and if an input file is provided on the command line. + +Since there is no clear way for specifying overriding values (for example a +variable that applies to a single file and overrides an already existing +variable in the root config), the input values will also fill the overriding +variables. + +The format of the input file is also yaml. The following example demonstrates +how an input file could be used: + +`.quokka.yml` +```yaml +name: "Quokka Template" +description: "New Quokka Template" +version: "0.1.0" +variables: + slack: + confirm: true + prompt: "Add Slack integration?" + variables: + channel: + required: true + webhook: + required: true +``` + +`input.yml` +```yaml +slack: true +slack_channel: "#mychan" +slack_webhook: "complexurl +``` + +If this input file is given to Quokka, it won't prompt for these three +variables, thus requiring no input from the user to render the template. + +## Set + +Additionally, you can provide Quokka with the `-e/--set` flag (multiple time if +you wish). This works the same way as the input file but has a higher priority, +meaning that if you pass both an input file and a `-e` flag that defines a +variable, the one passed on the command line will have a higher priority. + +The `--set` flags work by providing it with a `key=value` style kind of string. +If we take the example above using the input file, we can effectively replace +the `slack_channel` variable by doing so: + +```sh +$ qk template/ output -i input.yml --set "slack_channel=#anotherchan" +$ # Or +$ qk template/ output -i input.yml -e "slack_channel=#anotherchan" +``` ## Examples @@ -162,11 +213,46 @@ $ qk git@github.com:Depado/quokka.git output --path _example/license $ # Clone the template in a specific directory, render it in a specific directory and keep the template $ qk git@github.com:Depado/quokka.git myamazingproject --path _example/cleanarch --keep --output "template" $ # Reuse the downloaded template -$ quokka template/ myotherproject +$ qk template/ myotherproject +$ # Pass an input file to Quokka +$ qk template/ output -i in.yml ``` # Template Creation +## New command + +If `quokka` is installed, simply run `quokka new `. This will ask for +basic information such as the template name, description and version with some +sane defaults (version number for example is set to `0.1.0` by default). +You can also pass these values as flags on the command line. + +This command will check if the output directory and a `.quokka.yml` file already +exist. This command is in charge of creating a new directory and creating the +initial `.quokka.yml` file with those basic information, helping you getting +started with Quokka template development. + +
Command Line Help + +``` +$ qk new --help +Create a new quokka template + +Usage: + qk new [output] [flags] + +Flags: + -d, --description string description of the new template + -h, --help help for new + -n, --name string name of the new template + -v, --version string version of the new template + +Global Flags: + --debug Enable or disable debug mode + -y, --yes Automatically accept +``` +
+ ## The root `.quokka.yml` file To configure your template, place a `.quokka.yml` at the root of your template. diff --git a/cmd/flags.go b/cmd/flags.go index 9c40660..b6841b3 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -17,6 +17,7 @@ func AddRendererFlags(c *cobra.Command) { c.Flags().BoolP("keep", "k", false, "do not delete the template when operation is complete") c.Flags().StringP("path", "p", "", "specify if the template is actually stored in a sub-directory of the downloaded file") c.Flags().StringP("output", "o", "", "specify the directory where the template should be downloaded or cloned") + c.Flags().StringSliceP("set", "e", []string{}, "specify values on the command line") // Git options c.Flags().Int("git.depth", 1, "depth of git clone in case of git provider") diff --git a/cmd/new.go b/cmd/new.go index 01b6ef7..44d6be4 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -3,55 +3,33 @@ package cmd import ( "fmt" "os" + "path/filepath" - "gopkg.in/AlecAivazis/survey.v1" - - _ "github.com/Depado/quokka/conf" // Import conf to ensure package init for survey "github.com/Depado/quokka/utils" ) -func askIfNotString(in *string, name, message, def string, debug bool) { - var err error - if *in == "" { - if err = survey.AskOne(&survey.Input{ - Message: message, - Default: def, - }, in, nil); err != nil { - utils.ErrPrintln("Canceled operation") - os.Exit(0) - } - } else if debug { - utils.OkPrintln(utils.Green.Sprint(name), "already filled:", *in) - } -} - // NewQuokkaTemplate will create a new Quokka template with default params func NewQuokkaTemplate(path, name, description, version string, yes, debug bool) { var err error + var fd *os.File - if _, err = os.Stat(path); !os.IsNotExist(err) { - if yes { - utils.OkPrintln("Output destination already exists but 'yes' option was used") - } else { - var confirmed bool - prompt := &survey.Confirm{ - Message: "The output destination already exists. Continue ?", - } - survey.AskOne(prompt, &confirmed, nil) // nolint: errcheck - if !confirmed { - utils.ErrPrintln("Canceled operation") - os.Exit(0) - } - } - } else { + if !utils.ConfirmFileExists(path, true, yes, debug) { if err = os.MkdirAll(path, os.ModePerm); err != nil { utils.FatalPrintln("Unable to create directory") } } + qf := filepath.Join(path, ".quokka.yml") + utils.ConfirmFileExists(qf, false, yes, debug) - askIfNotString(&name, "name", "Template name?", "Quokka Template", debug) - askIfNotString(&description, "description", "Template description?", "New Quokka Template", debug) - askIfNotString(&version, "version", "Template version?", "0.1.0", debug) + utils.AskIfEmptyString(&name, "name", "Template name?", "Quokka Template", debug) + utils.AskIfEmptyString(&description, "description", "Template description?", "New Quokka Template", debug) + utils.AskIfEmptyString(&version, "version", "Template version?", "0.1.0", debug) - fmt.Printf("Name: %s\nDescription: %s\nVersion: %s\n", name, description, version) + if fd, err = os.Create(qf); err != nil { + utils.FatalPrintln("Unable to create file") + } + defer fd.Close() + if _, err = fd.WriteString(fmt.Sprintf("name: \"%s\"\ndescription: \"%s\"\nversion: \"%s\"\n", name, description, version)); err != nil { + utils.FatalPrintln("Unable to write in file") + } } diff --git a/cmd/qk/main.go b/cmd/qk/main.go index cc8aa7d..c0c53d2 100644 --- a/cmd/qk/main.go +++ b/cmd/qk/main.go @@ -37,6 +37,7 @@ var rootc = &cobra.Command{ viper.GetString("output"), viper.GetString("path"), viper.GetString("input"), + viper.GetStringSlice("set"), viper.GetBool("keep"), viper.GetInt("git.depth"), viper.GetBool("yes"), diff --git a/conf/input.go b/conf/input.go index a64dd5c..dc58d8f 100644 --- a/conf/input.go +++ b/conf/input.go @@ -1,7 +1,9 @@ package conf import ( + "fmt" "io/ioutil" + "strings" "gopkg.in/yaml.v2" ) @@ -9,6 +11,25 @@ import ( // InputCtx is the input context type InputCtx yaml.MapSlice +// MergeCtx will merge two InputCtx into a single one +func MergeCtx(a, b InputCtx) InputCtx { + out := a + for _, v := range b { + var found bool + for i, base := range out { + if base.Key == v.Key { + out[i].Value = v.Value + found = true + } + } + if !found { + out = append(out, v) + } + } + + return out +} + // GetInputContext will return a map of string to interface{} that will then // be used to determine whether or not a value from the root config file // has already been filled @@ -20,3 +41,17 @@ func GetInputContext(path string) (InputCtx, error) { } return out, yaml.Unmarshal(input, &out) } + +// GetSetContext will return the map of string to interface{} that contains the +// set flags passed on the command line parsed +func GetSetContext(set []string) (InputCtx, error) { + out := InputCtx{} + for _, s := range set { + tmp := strings.SplitN(s, "=", 2) + if len(tmp) != 2 { + return out, fmt.Errorf("invalid set option: %s", s) + } + out = append(out, yaml.MapItem{Key: tmp[0], Value: tmp[1]}) + } + return out, nil +} diff --git a/renderer/analyze.go b/renderer/analyze.go index 64bf1e8..8508aa6 100644 --- a/renderer/analyze.go +++ b/renderer/analyze.go @@ -6,6 +6,7 @@ import ( "github.com/Depado/quokka/conf" "github.com/Depado/quokka/utils" + "github.com/davecgh/go-spew/spew" "github.com/fatih/color" ) @@ -46,15 +47,28 @@ func HandleRootConfig(dir string, ctx conf.InputCtx) *conf.Root { // Analyze is a work in progress function to analyze the template directory // and gather information about where the configuration files are stored and to // which templates they should apply. -func Analyze(dir, output, input string) { +func Analyze(dir, output, input string, set []string) { var err error var ctx conf.InputCtx + if input != "" { if ctx, err = conf.GetInputContext(input); err != nil { utils.FatalPrintln("Could not parse input file:", err) } utils.OkPrintln("Input file", utils.Green.Sprint(input), "found") } + if set != nil { + setCtx, err := conf.GetSetContext(set) + if err != nil { + utils.FatalPrintln("Could not parse set flags:", err) + } + ctx = conf.MergeCtx(ctx, setCtx) + spew.Dump(ctx) + utils.OkPrintln("Command line set merged in context") + } + + spew.Dump(ctx) + root := HandleRootConfig(dir, ctx) var candidates []*conf.File diff --git a/renderer/render.go b/renderer/render.go index 1bc410f..3fbe3c5 100644 --- a/renderer/render.go +++ b/renderer/render.go @@ -9,7 +9,7 @@ import ( ) // Render is the main render function -func Render(template, output, toutput, path, input string, keep bool, depth int, yes bool) { +func Render(template, output, toutput, path, input string, set []string, keep bool, depth int, yes bool) { var err error var tpath string @@ -44,5 +44,5 @@ func Render(template, output, toutput, path, input string, keep bool, depth int, utils.OkPrintln("Removed template", utils.Green.Sprint(path)) }(path) } - Analyze(tpath, output, input) + Analyze(tpath, output, input, set) } diff --git a/utils/colors.go b/utils/colors.go index fc0aa0b..0e5e14c 100644 --- a/utils/colors.go +++ b/utils/colors.go @@ -11,6 +11,9 @@ import ( // Green is a simple green foreground color var Green = color.New(color.FgGreen) +// Yellow is a simple yellow foreground color +var Yellow = color.New(color.FgYellow) + // OkPrefix is the prefix that should prefix output when everything is ok var OkPrefix = Green.Sprint("ยป") @@ -38,7 +41,15 @@ func ErrSprintln(opts ...interface{}) string { } // FatalPrintln prints out information with a red prefix and exits the program +// with an error status code func FatalPrintln(opts ...interface{}) { log.Println(append([]interface{}{ErrPrefix}, opts...)...) os.Exit(1) } + +// ExitPrintln prints out information with a red prefix and exits the program +// with an acceptable status code +func ExitPrintln(opts ...interface{}) { + log.Println(append([]interface{}{ErrPrefix}, opts...)...) + os.Exit(0) +} diff --git a/utils/survey.go b/utils/survey.go new file mode 100644 index 0000000..eba9aae --- /dev/null +++ b/utils/survey.go @@ -0,0 +1,58 @@ +package utils + +import ( + "fmt" + "os" + + "gopkg.in/AlecAivazis/survey.v1" +) + +// AskIfEmptyString will prompt the user if needed +func AskIfEmptyString(in *string, name, message, def string, debug bool) { + var err error + if *in == "" { + if err = survey.AskOne(&survey.Input{Message: message, Default: def}, in, nil); err != nil { + ExitPrintln("Canceled operation") + } + } else if debug { + OkPrintln(Green.Sprint(name), "already filled:", *in) + } +} + +// ConfirmFileExists will prompt the user with a confirmation if the +// file/directory already exists +// If the user answers "no", the program exits properly, otherwise the function +// returns true if the file existed and false if it doesn't +func ConfirmFileExists(path string, dir, yes, debug bool) bool { + if _, err := os.Stat(path); !os.IsNotExist(err) { + t := "file" + p := path + if dir { + p = path + "/" + t = "directory" + } + if yes { + OkPrintln(fmt.Sprintf( + "The destination %s %s already exists but %s option was used.", + t, + Green.Sprint(p), + Yellow.Sprint("yes"), + )) + return true + } + var confirmed bool + prompt := &survey.Confirm{ + Message: fmt.Sprintf( + "The destination %s %s already exists. Continue ?", + t, + Green.Sprint(p), + ), + } + survey.AskOne(prompt, &confirmed, nil) // nolint: errcheck + if !confirmed { + ExitPrintln("Canceled operation") + } + return true + } + return false +}