From 7e2436b79de609152fc3bcf7fc945b7b134ea825 Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Sat, 24 Feb 2018 18:53:13 +0200 Subject: [PATCH 01/61] First try at better zsh completions: A very basic POC. Need to refactor to generate completion structure before passing to the template to avoid repeated computations. What works: * Real zsh completion (not built on bash) * Basic flags (with long flag and optional shorthand) * Basic filename completion indication (not with file extensions though) What's missing: * File extensions to filename completions * Positional args * Do we require handling only short flags? --- Gopkg.lock | 132 +++++++++++++++++++++++++ Gopkg.toml | 54 +++++++++++ zsh_completions.go | 207 ++++++++++++++++++++++------------------ zsh_completions_test.go | 171 +++++++++++++++++++++++---------- zsh_template.tmpl | 56 +++++++++++ 5 files changed, 477 insertions(+), 143 deletions(-) create mode 100644 Gopkg.lock create mode 100644 Gopkg.toml create mode 100644 zsh_template.tmpl diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 00000000..fde98ef9 --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,132 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/cpuguy83/go-md2man" + packages = ["md2man"] + revision = "20f5889cbdc3c73dbd2862796665e7c465ade7d1" + version = "v1.0.8" + +[[projects]] + name = "github.com/fsnotify/fsnotify" + packages = ["."] + revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" + version = "v1.4.7" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/hcl" + packages = [ + ".", + "hcl/ast", + "hcl/parser", + "hcl/printer", + "hcl/scanner", + "hcl/strconv", + "hcl/token", + "json/parser", + "json/scanner", + "json/token" + ] + revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8" + +[[projects]] + name = "github.com/inconshreveable/mousetrap" + packages = ["."] + revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" + version = "v1.0" + +[[projects]] + name = "github.com/magiconair/properties" + packages = ["."] + revision = "c3beff4c2358b44d0493c7dda585e7db7ff28ae6" + version = "v1.7.6" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/go-homedir" + packages = ["."] + revision = "b8bc1bf767474819792c23f32d8286a45736f1c6" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/mapstructure" + packages = ["."] + revision = "a4e142e9c047c904fa2f1e144d9a84e6133024bc" + +[[projects]] + name = "github.com/pelletier/go-toml" + packages = ["."] + revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8" + version = "v1.1.0" + +[[projects]] + name = "github.com/russross/blackfriday" + packages = ["."] + revision = "4048872b16cc0fc2c5fd9eacf0ed2c2fedaa0c8c" + version = "v1.5" + +[[projects]] + name = "github.com/spf13/afero" + packages = [ + ".", + "mem" + ] + revision = "bb8f1927f2a9d3ab41c9340aa034f6b803f4359c" + version = "v1.0.2" + +[[projects]] + name = "github.com/spf13/cast" + packages = ["."] + revision = "8965335b8c7107321228e3e3702cab9832751bac" + version = "v1.2.0" + +[[projects]] + branch = "master" + name = "github.com/spf13/jwalterweatherman" + packages = ["."] + revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394" + +[[projects]] + branch = "master" + name = "github.com/spf13/pflag" + packages = ["."] + revision = "6a877ebacf28c5fc79846f4fcd380a5d9872b997" + +[[projects]] + branch = "master" + name = "github.com/spf13/viper" + packages = ["."] + revision = "aafc9e6bc7b7bb53ddaa75a5ef49a17d6e654be5" + +[[projects]] + branch = "master" + name = "golang.org/x/sys" + packages = ["unix"] + revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd" + +[[projects]] + branch = "master" + name = "golang.org/x/text" + packages = [ + "internal/gen", + "internal/triegen", + "internal/ucd", + "transform", + "unicode/cldr", + "unicode/norm" + ] + revision = "4e4a3210bb54bb31f6ab2cdca2edcc0b50c420c1" + +[[projects]] + branch = "v2" + name = "gopkg.in/yaml.v2" + packages = ["."] + revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "d7aecc8ee6a8b343ebf8c2539348464f2566a18dfc511cd374b2cd76bebb1c6d" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 00000000..3bf16541 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,54 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + name = "github.com/cpuguy83/go-md2man" + version = "1.0.8" + +[[constraint]] + name = "github.com/inconshreveable/mousetrap" + version = "1.0.0" + +[[constraint]] + branch = "master" + name = "github.com/mitchellh/go-homedir" + +[[constraint]] + name = "github.com/spf13/pflag" + branch = "master" + +[[constraint]] + name = "github.com/spf13/viper" + branch = "master" + +[[constraint]] + branch = "v2" + name = "gopkg.in/yaml.v2" + +[prune] + go-tests = true + unused-packages = true diff --git a/zsh_completions.go b/zsh_completions.go index 889c22e2..9bdec654 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -1,11 +1,82 @@ package cobra import ( - "bytes" "fmt" "io" "os" "strings" + "text/template" + + "github.com/spf13/pflag" +) + +var ( + funcMap = template.FuncMap{ + "constructPath": constructPath, + "subCmdList": subCmdList, + "extractFlags": extractFlags, + "simpleFlag": simpleFlag, + } + zshCompletionText = ` +{{/* for pflag.Flag (specifically annotations) */}} +{{define "flagAnnotations" -}} +{{with index .Annotations "cobra_annotation_bash_completion_filename_extensions"}}:filename:_files{{end}} +{{- end}} + +{{/* for pflag.Flag with short and long options */}} +{{define "complexFlag" -}} +"(-{{.Shorthand}} --{{.Name}})"{-{{.Shorthand}},--{{.Name}}}"[{{.Usage}}]{{template "flagAnnotations" .}}" +{{- end}} + +{{/* for pflag.Flag with either short or long options */}} +{{define "simpleFlag" -}} +"{{with .Name}}--{{.}}{{else}}-{{.Shorthand}}{{end}}[{{.Usage}}]{{template "flagAnnotations" .}}" +{{- end}} + +{{/* should accept Command (that contains subcommands) as parameter */}} +{{define "argumentsC" -}} +function {{constructPath .}} { + local line + + _arguments -C \ +{{range extractFlags . -}} +{{" "}}{{if simpleFlag .}}{{template "simpleFlag" .}}{{else}}{{template "complexFlag" .}}{{end}} \ +{{end}} "1: :({{subCmdList .}})" \ + "*::arg:->args" + + case $line[1] in {{- range .Commands}} + {{.Use}}) + {{constructPath .}} + ;; +{{end}} esac +} +{{range .Commands}} +{{template "selectCmdTemplate" .}} +{{- end}} +{{- end}} + +{{/* should accept Command without subcommands as parameter */}} +{{define "arguments" -}} +function {{constructPath .}} { +{{with extractFlags . -}} +{{ " _arguments" -}} +{{range .}} \ + {{if simpleFlag .}}{{template "simpleFlag" .}}{{else}}{{template "complexFlag" .}}{{end -}} +{{end}} +{{end -}} +} +{{- end}} + +{{define "selectCmdTemplate" -}} +{{if .Commands}}{{template "argumentsC" .}}{{else}}{{template "arguments" .}}{{end}} +{{- end}} + +{{define "Main" -}} +#compdef _{{.Use}} {{.Use}} + +{{template "selectCmdTemplate" .}} +{{end}} +` ) // GenZshCompletionFile generates zsh completion file. @@ -21,106 +92,56 @@ func (c *Command) GenZshCompletionFile(filename string) error { // GenZshCompletion generates a zsh completion file and writes to the passed writer. func (c *Command) GenZshCompletion(w io.Writer) error { - buf := new(bytes.Buffer) - - writeHeader(buf, c) - maxDepth := maxDepth(c) - writeLevelMapping(buf, maxDepth) - writeLevelCases(buf, maxDepth, c) - - _, err := buf.WriteTo(w) - return err -} - -func writeHeader(w io.Writer, cmd *Command) { - fmt.Fprintf(w, "#compdef %s\n\n", cmd.Name()) -} - -func maxDepth(c *Command) int { - if len(c.Commands()) == 0 { - return 0 + tmpl, err := template.New("Main").Funcs(funcMap).Parse(zshCompletionText) + if err != nil { + return fmt.Errorf("error creating zsh completion template: %v", err) } - maxDepthSub := 0 - for _, s := range c.Commands() { - subDepth := maxDepth(s) - if subDepth > maxDepthSub { - maxDepthSub = subDepth + return tmpl.Execute(w, c) +} + +func constructPath(c *Command) string { + var path []string + tmpCmd := c + path = append(path, tmpCmd.Use) + + for { + if !tmpCmd.HasParent() { + break } + tmpCmd = tmpCmd.Parent() + path = append(path, tmpCmd.Use) } - return 1 + maxDepthSub + + // reverse path + for left, right := 0, len(path)-1; left < right; left, right = left+1, right-1 { + path[left], path[right] = path[right], path[left] + } + + return "_" + strings.Join(path, "_") } -func writeLevelMapping(w io.Writer, numLevels int) { - fmt.Fprintln(w, `_arguments \`) - for i := 1; i <= numLevels; i++ { - fmt.Fprintf(w, ` '%d: :->level%d' \`, i, i) - fmt.Fprintln(w) +// subCmdList returns a space separated list of subcommands names +func subCmdList(c *Command) string { + var subCmds []string + + for _, cmd := range c.Commands() { + subCmds = append(subCmds, cmd.Use) } - fmt.Fprintf(w, ` '%d: :%s'`, numLevels+1, "_files") - fmt.Fprintln(w) + + return strings.Join(subCmds, " ") } -func writeLevelCases(w io.Writer, maxDepth int, root *Command) { - fmt.Fprintln(w, "case $state in") - defer fmt.Fprintln(w, "esac") - - for i := 1; i <= maxDepth; i++ { - fmt.Fprintf(w, " level%d)\n", i) - writeLevel(w, root, i) - fmt.Fprintln(w, " ;;") - } - fmt.Fprintln(w, " *)") - fmt.Fprintln(w, " _arguments '*: :_files'") - fmt.Fprintln(w, " ;;") +func extractFlags(c *Command) []*pflag.Flag { + var flags []*pflag.Flag + c.LocalFlags().VisitAll(func(f *pflag.Flag) { + flags = append(flags, f) + }) + c.InheritedFlags().VisitAll(func(f *pflag.Flag) { + flags = append(flags, f) + }) + return flags } -func writeLevel(w io.Writer, root *Command, i int) { - fmt.Fprintf(w, " case $words[%d] in\n", i) - defer fmt.Fprintln(w, " esac") - - commands := filterByLevel(root, i) - byParent := groupByParent(commands) - - for p, c := range byParent { - names := names(c) - fmt.Fprintf(w, " %s)\n", p) - fmt.Fprintf(w, " _arguments '%d: :(%s)'\n", i, strings.Join(names, " ")) - fmt.Fprintln(w, " ;;") - } - fmt.Fprintln(w, " *)") - fmt.Fprintln(w, " _arguments '*: :_files'") - fmt.Fprintln(w, " ;;") - -} - -func filterByLevel(c *Command, l int) []*Command { - cs := make([]*Command, 0) - if l == 0 { - cs = append(cs, c) - return cs - } - for _, s := range c.Commands() { - cs = append(cs, filterByLevel(s, l-1)...) - } - return cs -} - -func groupByParent(commands []*Command) map[string][]*Command { - m := make(map[string][]*Command) - for _, c := range commands { - parent := c.Parent() - if parent == nil { - continue - } - m[parent.Name()] = append(m[parent.Name()], c) - } - return m -} - -func names(commands []*Command) []string { - ns := make([]string, len(commands)) - for i, c := range commands { - ns[i] = c.Name() - } - return ns +func simpleFlag(p *pflag.Flag) bool { + return p.Name == "" || p.Shorthand == "" } diff --git a/zsh_completions_test.go b/zsh_completions_test.go index 34e69496..16056297 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -2,74 +2,92 @@ package cobra import ( "bytes" - "strings" + "regexp" "testing" ) -func TestZshCompletion(t *testing.T) { +func TestGenZshCompletion(t *testing.T) { + var debug bool + var option string + tcs := []struct { name string root *Command expectedExpressions []string }{ { - name: "trivial", - root: &Command{Use: "trivialapp"}, - expectedExpressions: []string{"#compdef trivial"}, - }, - { - name: "linear", + name: "simple command", root: func() *Command { - r := &Command{Use: "linear"} - - sub1 := &Command{Use: "sub1"} - r.AddCommand(sub1) - - sub2 := &Command{Use: "sub2"} - sub1.AddCommand(sub2) - - sub3 := &Command{Use: "sub3"} - sub2.AddCommand(sub3) + r := &Command{ + Use: "mycommand", + Long: "My Command long description", + } + r.Flags().BoolVar(&debug, "debug", debug, "description") return r }(), - expectedExpressions: []string{"sub1", "sub2", "sub3"}, + expectedExpressions: []string{ + `function _mycommand {\s+_arguments \\\s+"--debug\[description\]"\s+}`, + "#compdef _mycommand mycommand", + }, }, { - name: "flat", + name: "flags with both long and short flags", root: func() *Command { - r := &Command{Use: "flat"} - r.AddCommand(&Command{Use: "c1"}) - r.AddCommand(&Command{Use: "c2"}) + r := &Command{ + Use: "testcmd", + Long: "long description", + } + r.Flags().BoolVarP(&debug, "debug", "d", debug, "debug description") return r }(), - expectedExpressions: []string{"(c1 c2)"}, + expectedExpressions: []string{ + `"\(-d --debug\)"{-d,--debug}"\[debug description\]"`, + }, }, { - name: "tree", + name: "command with subcommands", root: func() *Command { - r := &Command{Use: "tree"} - - sub1 := &Command{Use: "sub1"} - r.AddCommand(sub1) - - sub11 := &Command{Use: "sub11"} - sub12 := &Command{Use: "sub12"} - - sub1.AddCommand(sub11) - sub1.AddCommand(sub12) - - sub2 := &Command{Use: "sub2"} - r.AddCommand(sub2) - - sub21 := &Command{Use: "sub21"} - sub22 := &Command{Use: "sub22"} - - sub2.AddCommand(sub21) - sub2.AddCommand(sub22) - + r := &Command{ + Use: "rootcmd", + Long: "Long rootcmd description", + } + d := &Command{ + Use: "subcmd1", + Short: "Subcmd1 short descrition", + } + e := &Command{ + Use: "subcmd2", + Long: "Subcmd2 short description", + } + r.PersistentFlags().BoolVar(&debug, "debug", debug, "description") + d.Flags().StringVarP(&option, "option", "o", option, "option description") + r.AddCommand(d, e) return r }(), - expectedExpressions: []string{"(sub11 sub12)", "(sub21 sub22)"}, + expectedExpressions: []string{ + `\\\n\s+"1: :\(subcmd1 subcmd2\)" \\\n`, + `_arguments \\\n.*"--debug\[description]"`, + `_arguments -C \\\n.*"--debug\[description]"`, + `function _rootcmd_subcmd1 {`, + `function _rootcmd_subcmd1 {`, + `_arguments \\\n.*"\(-o --option\)"{-o,--option}"\[option description]" \\\n`, + }, + }, + { + name: "filename completion", + root: func() *Command { + var file string + r := &Command{ + Use: "mycmd", + Short: "my command short description", + } + r.Flags().StringVarP(&file, "config", "c", file, "config file") + r.MarkFlagFilename("config", "ext") + return r + }(), + expectedExpressions: []string{ + `\n +"\(-c --config\)"{-c,--config}"\[config file]:filename:_files"`, + }, }, } @@ -77,13 +95,66 @@ func TestZshCompletion(t *testing.T) { t.Run(tc.name, func(t *testing.T) { buf := new(bytes.Buffer) tc.root.GenZshCompletion(buf) - output := buf.String() + output := buf.Bytes() - for _, expectedExpression := range tc.expectedExpressions { - if !strings.Contains(output, expectedExpression) { - t.Errorf("Expected completion to contain %q somewhere; got %q", expectedExpression, output) + for _, expr := range tc.expectedExpressions { + rgx, err := regexp.Compile(expr) + if err != nil { + t.Errorf("error compiling expression (%s): %v", expr, err) + } + if !rgx.Match(output) { + t.Errorf("expeced completion (%s) to match '%s'", buf.String(), expr) } } }) } + +} + +func BenchmarkConstructPath(b *testing.B) { + c := &Command{ + Use: "main", + Long: "main long description which is very long", + Short: "main short description", + } + d := &Command{ + Use: "hello", + } + e := &Command{ + Use: "world", + } + c.AddCommand(d) + d.AddCommand(e) + for i := 0; i < b.N; i++ { + res := constructPath(e) + if res != "_main_hello_world" { + b.Errorf("expeced path to be '_main_hello_world', got %s", res) + } + } +} + +func TestExtractFlags(t *testing.T) { + var debug, cmdc, cmdd bool + c := &Command{ + Use: "cmdC", + Long: "Command C", + } + c.PersistentFlags().BoolVarP(&debug, "debug", "d", debug, "debug mode") + c.Flags().BoolVar(&cmdc, "cmd-c", cmdc, "Command C") + d := &Command{ + Use: "CmdD", + Long: "Command D", + } + d.Flags().BoolVar(&cmdd, "cmd-d", cmdd, "Command D") + c.AddCommand(d) + + resC := extractFlags(c) + resD := extractFlags(d) + + if len(resC) != 2 { + t.Errorf("expected Command C to return 2 flags, got %d", len(resC)) + } + if len(resD) != 2 { + t.Errorf("expected Command D to return 2 flags, got %d", len(resD)) + } } diff --git a/zsh_template.tmpl b/zsh_template.tmpl new file mode 100644 index 00000000..43846254 --- /dev/null +++ b/zsh_template.tmpl @@ -0,0 +1,56 @@ +{{define "complexFlag"}}{{ /* for pflag.Flag with short and long options */ -}} +"(-{{.Shorthand}} --{{.Name}})"\{-{{.Shorthand}}, --{{.Name}}\}[{{.Usage}}] +{{- end}} + +{{define "simpleFlag"}}{{ /* for pflag.Flag with either short or long options */ -}} +"{{with .Name}}-{{.}}{{else}}--{{.Shorthand}}{{end}}[{{.Usage}}]" +{{- end}} + +{{define "argumentsC"}} +{{- /* should accept Command (that contains subcommands) as parameter */ -}} +function {{constructPath .}} { + local line + + _arguments -C \ +{{range extractFlags . -}} + {{if simpleFlag .}}{{template "simpleFlag" .}}{{else}}{{template "complexFlag" .}}{{end}} \ +{{end -}} + "1: :({{subCmdList .}})" \ + "*:arg:->args" + + case $line[1] in +{{range .Commands -}} + {{.Use}}) + {{constructPath .}} + ;; +{{end -}} + esac +} +{{range .Commands -}} + +{{template "selectCmdTemplate" .}} +{{end -}} +{{end}} + +{{define "arguments"}} +function {{constructPath .}} { +{{- /* should accept Command without subcommands as parameter */ -}} +{{with extractFlags . -}} + _arguments \ +{{range .}} + {{if simpleFlag .}}{{template "simpleFlag" .}}{{else}}{{template "complexFlag" .}}{{end}} \ +{{end -}} +{{ /* leave this line empty because of the last backslash */ }} +{{end -}} +} +{{end}} + +{{define "selectCmdTemplate" -}} +{{with .Commands}}{{template "argumentsC" .}}{{else}}{{template "arguments" .}}{{end}} +{{end -}} + +{{define "Main"}} +#compdef _{{.Use}} {{.Use}} + +{{template "selectCmdTemplate" .}} +{{end}} From a15d0990180cf0d99905ad41201211f8ac3cab53 Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Sun, 25 Feb 2018 08:20:34 +0200 Subject: [PATCH 02/61] zsh-completion fixed reference to cmd name cmd.Use is not the command name :). Found it once I figured out that I need to execute the command in order to fully test the generated completion. --- zsh_completions.go | 16 +++++++++++----- zsh_completions_test.go | 10 ++++++++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/zsh_completions.go b/zsh_completions.go index 9bdec654..d1c9e07b 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -15,6 +15,7 @@ var ( "constructPath": constructPath, "subCmdList": subCmdList, "extractFlags": extractFlags, + "cmdName": cmdName, "simpleFlag": simpleFlag, } zshCompletionText = ` @@ -45,7 +46,7 @@ function {{constructPath .}} { "*::arg:->args" case $line[1] in {{- range .Commands}} - {{.Use}}) + {{cmdName .}}) {{constructPath .}} ;; {{end}} esac @@ -72,7 +73,7 @@ function {{constructPath .}} { {{- end}} {{define "Main" -}} -#compdef _{{.Use}} {{.Use}} +#compdef _{{cmdName .}} {{cmdName .}} {{template "selectCmdTemplate" .}} {{end}} @@ -102,14 +103,14 @@ func (c *Command) GenZshCompletion(w io.Writer) error { func constructPath(c *Command) string { var path []string tmpCmd := c - path = append(path, tmpCmd.Use) + path = append(path, tmpCmd.Name()) for { if !tmpCmd.HasParent() { break } tmpCmd = tmpCmd.Parent() - path = append(path, tmpCmd.Use) + path = append(path, tmpCmd.Name()) } // reverse path @@ -125,7 +126,7 @@ func subCmdList(c *Command) string { var subCmds []string for _, cmd := range c.Commands() { - subCmds = append(subCmds, cmd.Use) + subCmds = append(subCmds, cmd.Name()) } return strings.Join(subCmds, " ") @@ -142,6 +143,11 @@ func extractFlags(c *Command) []*pflag.Flag { return flags } +// cmdName returns the command's innvocation +func cmdName(c *Command) string { + return c.Name() +} + func simpleFlag(p *pflag.Flag) bool { return p.Name == "" || p.Shorthand == "" } diff --git a/zsh_completions_test.go b/zsh_completions_test.go index 16056297..4d29e542 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -21,12 +21,13 @@ func TestGenZshCompletion(t *testing.T) { r := &Command{ Use: "mycommand", Long: "My Command long description", + Run: emptyRun, } r.Flags().BoolVar(&debug, "debug", debug, "description") return r }(), expectedExpressions: []string{ - `function _mycommand {\s+_arguments \\\s+"--debug\[description\]"\s+}`, + `(?s)function _mycommand {\s+_arguments \\\s+"--debug\[description\]".*--help.*}`, "#compdef _mycommand mycommand", }, }, @@ -36,6 +37,7 @@ func TestGenZshCompletion(t *testing.T) { r := &Command{ Use: "testcmd", Long: "long description", + Run: emptyRun, } r.Flags().BoolVarP(&debug, "debug", "d", debug, "debug description") return r @@ -54,10 +56,12 @@ func TestGenZshCompletion(t *testing.T) { d := &Command{ Use: "subcmd1", Short: "Subcmd1 short descrition", + Run: emptyRun, } e := &Command{ Use: "subcmd2", Long: "Subcmd2 short description", + Run: emptyRun, } r.PersistentFlags().BoolVar(&debug, "debug", debug, "description") d.Flags().StringVarP(&option, "option", "o", option, "option description") @@ -65,7 +69,7 @@ func TestGenZshCompletion(t *testing.T) { return r }(), expectedExpressions: []string{ - `\\\n\s+"1: :\(subcmd1 subcmd2\)" \\\n`, + `\\\n\s+"1: :\(help subcmd1 subcmd2\)" \\\n`, `_arguments \\\n.*"--debug\[description]"`, `_arguments -C \\\n.*"--debug\[description]"`, `function _rootcmd_subcmd1 {`, @@ -80,6 +84,7 @@ func TestGenZshCompletion(t *testing.T) { r := &Command{ Use: "mycmd", Short: "my command short description", + Run: emptyRun, } r.Flags().StringVarP(&file, "config", "c", file, "config file") r.MarkFlagFilename("config", "ext") @@ -93,6 +98,7 @@ func TestGenZshCompletion(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { + tc.root.Execute() buf := new(bytes.Buffer) tc.root.GenZshCompletion(buf) output := buf.Bytes() From f0508c8e7665e581c7880c232466fe77b7a8964b Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Sun, 25 Feb 2018 14:12:58 +0200 Subject: [PATCH 03/61] zsh-completion ignores hidden commands and flags :) --- zsh_completions.go | 17 ++++++++--- zsh_completions_test.go | 68 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/zsh_completions.go b/zsh_completions.go index d1c9e07b..7fa0df11 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -45,11 +45,11 @@ function {{constructPath .}} { {{end}} "1: :({{subCmdList .}})" \ "*::arg:->args" - case $line[1] in {{- range .Commands}} + case $line[1] in {{- range .Commands}}{{if not .Hidden}} {{cmdName .}}) {{constructPath .}} ;; -{{end}} esac +{{end}}{{end}} esac } {{range .Commands}} {{template "selectCmdTemplate" .}} @@ -69,8 +69,10 @@ function {{constructPath .}} { {{- end}} {{define "selectCmdTemplate" -}} +{{if .Hidden}}{{/* ignore hidden*/}}{{else -}} {{if .Commands}}{{template "argumentsC" .}}{{else}}{{template "arguments" .}}{{end}} {{- end}} +{{- end}} {{define "Main" -}} #compdef _{{cmdName .}} {{cmdName .}} @@ -126,6 +128,9 @@ func subCmdList(c *Command) string { var subCmds []string for _, cmd := range c.Commands() { + if cmd.Hidden { + continue + } subCmds = append(subCmds, cmd.Name()) } @@ -135,10 +140,14 @@ func subCmdList(c *Command) string { func extractFlags(c *Command) []*pflag.Flag { var flags []*pflag.Flag c.LocalFlags().VisitAll(func(f *pflag.Flag) { - flags = append(flags, f) + if !f.Hidden { + flags = append(flags, f) + } }) c.InheritedFlags().VisitAll(func(f *pflag.Flag) { - flags = append(flags, f) + if !f.Hidden { + flags = append(flags, f) + } }) return flags } diff --git a/zsh_completions_test.go b/zsh_completions_test.go index 4d29e542..d523762f 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -3,6 +3,7 @@ package cobra import ( "bytes" "regexp" + "strings" "testing" ) @@ -114,7 +115,74 @@ func TestGenZshCompletion(t *testing.T) { } }) } +} +func TestGenZshCompletionHidden(t *testing.T) { + tcs := []struct { + name string + root *Command + expectedExpressions []string + }{ + { + name: "hidden commmands", + root: func() *Command { + r := &Command{ + Use: "main", + Short: "main short description", + } + s1 := &Command{ + Use: "sub1", + Hidden: true, + Run: emptyRun, + } + s2 := &Command{ + Use: "sub2", + Short: "short sub2 description", + Run: emptyRun, + } + r.AddCommand(s1, s2) + + return r + }(), + expectedExpressions: []string{ + "sub1", + }, + }, + { + name: "hidden flags", + root: func() *Command { + var hidden string + r := &Command{ + Use: "root", + Short: "root short description", + Run: emptyRun, + } + r.Flags().StringVarP(&hidden, "hidden", "H", hidden, "hidden usage") + if err := r.Flags().MarkHidden("hidden"); err != nil { + t.Errorf("Error setting flag hidden: %v\n", err) + } + return r + }(), + expectedExpressions: []string{ + "--hidden", + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + tc.root.Execute() + buf := new(bytes.Buffer) + tc.root.GenZshCompletion(buf) + output := buf.String() + + for _, expr := range tc.expectedExpressions { + if strings.Contains(output, expr) { + t.Errorf("Expected completion (%s) not to contain '%s' but it does", output, expr) + } + } + }) + } } func BenchmarkConstructPath(b *testing.B) { From 2662787697b4d82e32836f2305918a0c22baae69 Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Mon, 26 Feb 2018 22:31:06 +0200 Subject: [PATCH 04/61] zsh-completion: added support for subcommand description. Also make the template more elegant on the way... --- zsh_completions.go | 41 +++++++++++++--------- zsh_completions_test.go | 78 +++++++++++++++++++++++++++++++---------- 2 files changed, 84 insertions(+), 35 deletions(-) diff --git a/zsh_completions.go b/zsh_completions.go index 7fa0df11..9dbc9c7c 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -37,43 +37,50 @@ var ( {{/* should accept Command (that contains subcommands) as parameter */}} {{define "argumentsC" -}} function {{constructPath .}} { - local line + local -a commands - _arguments -C \ -{{range extractFlags . -}} -{{" "}}{{if simpleFlag .}}{{template "simpleFlag" .}}{{else}}{{template "complexFlag" .}}{{end}} \ -{{end}} "1: :({{subCmdList .}})" \ + _arguments -C \{{- range extractFlags .}} + {{if simpleFlag .}}{{template "simpleFlag" .}}{{else}}{{template "complexFlag" .}}{{end}} \{{- end}} + "1: :->cmnds" \ "*::arg:->args" - case $line[1] in {{- range .Commands}}{{if not .Hidden}} - {{cmdName .}}) - {{constructPath .}} - ;; -{{end}}{{end}} esac + case $state in + cmnds) + commands=({{range .Commands}}{{if not .Hidden}} + "{{cmdName .}}:{{.Short}}"{{end}}{{end}} + ) + _describe "command" commands + ;; + esac + + case "$words[1]" in {{- range .Commands}}{{if not .Hidden}} + {{cmdName .}}) + {{constructPath .}} + ;;{{end}}{{end}} + esac } -{{range .Commands}} +{{range .Commands}}{{if not .Hidden}} {{template "selectCmdTemplate" .}} -{{- end}} +{{- end}}{{end}} {{- end}} {{/* should accept Command without subcommands as parameter */}} {{define "arguments" -}} function {{constructPath .}} { -{{with extractFlags . -}} -{{ " _arguments" -}} -{{range .}} \ +{{" _arguments"}}{{range extractFlags .}} \ {{if simpleFlag .}}{{template "simpleFlag" .}}{{else}}{{template "complexFlag" .}}{{end -}} {{end}} -{{end -}} } -{{- end}} +{{end}} +{{/* dispatcher for commands with or without subcommands */}} {{define "selectCmdTemplate" -}} {{if .Hidden}}{{/* ignore hidden*/}}{{else -}} {{if .Commands}}{{template "argumentsC" .}}{{else}}{{template "arguments" .}}{{end}} {{- end}} {{- end}} +{{/* template entry point */}} {{define "Main" -}} #compdef _{{cmdName .}} {{cmdName .}} diff --git a/zsh_completions_test.go b/zsh_completions_test.go index d523762f..c8d80c85 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -70,7 +70,7 @@ func TestGenZshCompletion(t *testing.T) { return r }(), expectedExpressions: []string{ - `\\\n\s+"1: :\(help subcmd1 subcmd2\)" \\\n`, + `commands=\(\n\s+"help:.*\n\s+"subcmd1:.*\n\s+"subcmd2:.*\n\s+\)`, `_arguments \\\n.*"--debug\[description]"`, `_arguments -C \\\n.*"--debug\[description]"`, `function _rootcmd_subcmd1 {`, @@ -185,24 +185,17 @@ func TestGenZshCompletionHidden(t *testing.T) { } } -func BenchmarkConstructPath(b *testing.B) { - c := &Command{ - Use: "main", - Long: "main long description which is very long", - Short: "main short description", - } - d := &Command{ - Use: "hello", - } - e := &Command{ - Use: "world", - } - c.AddCommand(d) - d.AddCommand(e) +func BenchmarkMediumSizeConstruct(b *testing.B) { + root := constructLargeCommandHeirarchy() + // if err := root.GenZshCompletionFile("_mycmd"); err != nil { + // b.Error(err) + // } + for i := 0; i < b.N; i++ { - res := constructPath(e) - if res != "_main_hello_world" { - b.Errorf("expeced path to be '_main_hello_world', got %s", res) + buf := new(bytes.Buffer) + err := root.GenZshCompletion(buf) + if err != nil { + b.Error(err) } } } @@ -232,3 +225,52 @@ func TestExtractFlags(t *testing.T) { t.Errorf("expected Command D to return 2 flags, got %d", len(resD)) } } + +func constructLargeCommandHeirarchy() *Command { + var config, st1, st2 string + var long, debug bool + var in1, in2 int + + r := genTestCommand("mycmd", false) + r.PersistentFlags().StringVarP(&config, "config", "c", config, "config usage") + if err := r.MarkPersistentFlagFilename("config", "*"); err != nil { + panic(err) + } + s1 := genTestCommand("sub1", true) + s1.Flags().BoolVar(&long, "long", long, "long descriptin") + s2 := genTestCommand("sub2", true) + s2.PersistentFlags().BoolVar(&debug, "debug", debug, "debug description") + s3 := genTestCommand("sub3", true) + s3.Hidden = true + s1_1 := genTestCommand("sub1sub1", true) + s1_1.Flags().StringVar(&st1, "st1", st1, "st1 description") + s1_1.Flags().StringVar(&st2, "st2", st2, "st2 description") + s1_2 := genTestCommand("sub1sub2", true) + s1_3 := genTestCommand("sub1sub3", true) + s1_3.Flags().IntVar(&in1, "int1", in1, "int1 descriptionn") + s1_3.Flags().IntVar(&in2, "int2", in2, "int2 descriptionn") + s2_1 := genTestCommand("sub2sub1", true) + s2_2 := genTestCommand("sub2sub2", true) + s2_3 := genTestCommand("sub2sub3", true) + s2_4 := genTestCommand("sub2sub4", true) + s2_5 := genTestCommand("sub2sub5", true) + + s1.AddCommand(s1_1, s1_2, s1_3) + s2.AddCommand(s2_1, s2_2, s2_3, s2_4, s2_5) + r.AddCommand(s1, s2, s3) + r.Execute() + return r +} + +func genTestCommand(name string, withRun bool) *Command { + r := &Command{ + Use: name, + Short: name + " short description", + Long: "Long description for " + name, + } + if withRun { + r.Run = emptyRun + } + + return r +} From e8018e8612d3a4fe2f8a40c01796b15ae705ff22 Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Wed, 28 Feb 2018 12:49:53 +0200 Subject: [PATCH 05/61] zsh-completion template refactoring: - removed redundant function - improved other functions :) - better names for other functions --- zsh_completions.go | 63 +++++++++++----------------------------------- 1 file changed, 14 insertions(+), 49 deletions(-) diff --git a/zsh_completions.go b/zsh_completions.go index 9dbc9c7c..dd23c45f 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -4,7 +4,6 @@ import ( "fmt" "io" "os" - "strings" "text/template" "github.com/spf13/pflag" @@ -12,11 +11,9 @@ import ( var ( funcMap = template.FuncMap{ - "constructPath": constructPath, - "subCmdList": subCmdList, - "extractFlags": extractFlags, - "cmdName": cmdName, - "simpleFlag": simpleFlag, + "genZshFuncName": generateZshCompletionFuncName, + "extractFlags": extractFlags, + "simpleFlag": simpleFlag, } zshCompletionText = ` {{/* for pflag.Flag (specifically annotations) */}} @@ -36,7 +33,8 @@ var ( {{/* should accept Command (that contains subcommands) as parameter */}} {{define "argumentsC" -}} -function {{constructPath .}} { +{{ $cmdPath := genZshFuncName .}} +function {{$cmdPath}} { local -a commands _arguments -C \{{- range extractFlags .}} @@ -47,15 +45,15 @@ function {{constructPath .}} { case $state in cmnds) commands=({{range .Commands}}{{if not .Hidden}} - "{{cmdName .}}:{{.Short}}"{{end}}{{end}} + "{{.Name}}:{{.Short}}"{{end}}{{end}} ) _describe "command" commands ;; esac case "$words[1]" in {{- range .Commands}}{{if not .Hidden}} - {{cmdName .}}) - {{constructPath .}} + {{.Name}}) + {{$cmdPath}}_{{.Name}} ;;{{end}}{{end}} esac } @@ -66,7 +64,7 @@ function {{constructPath .}} { {{/* should accept Command without subcommands as parameter */}} {{define "arguments" -}} -function {{constructPath .}} { +function {{genZshFuncName .}} { {{" _arguments"}}{{range extractFlags .}} \ {{if simpleFlag .}}{{template "simpleFlag" .}}{{else}}{{template "complexFlag" .}}{{end -}} {{end}} @@ -82,7 +80,7 @@ function {{constructPath .}} { {{/* template entry point */}} {{define "Main" -}} -#compdef _{{cmdName .}} {{cmdName .}} +#compdef _{{.Name}} {{.Name}} {{template "selectCmdTemplate" .}} {{end}} @@ -109,39 +107,11 @@ func (c *Command) GenZshCompletion(w io.Writer) error { return tmpl.Execute(w, c) } -func constructPath(c *Command) string { - var path []string - tmpCmd := c - path = append(path, tmpCmd.Name()) - - for { - if !tmpCmd.HasParent() { - break - } - tmpCmd = tmpCmd.Parent() - path = append(path, tmpCmd.Name()) +func generateZshCompletionFuncName(c *Command) string { + if c.HasParent() { + return generateZshCompletionFuncName(c.Parent()) + "_" + c.Name() } - - // reverse path - for left, right := 0, len(path)-1; left < right; left, right = left+1, right-1 { - path[left], path[right] = path[right], path[left] - } - - return "_" + strings.Join(path, "_") -} - -// subCmdList returns a space separated list of subcommands names -func subCmdList(c *Command) string { - var subCmds []string - - for _, cmd := range c.Commands() { - if cmd.Hidden { - continue - } - subCmds = append(subCmds, cmd.Name()) - } - - return strings.Join(subCmds, " ") + return "_" + c.Name() } func extractFlags(c *Command) []*pflag.Flag { @@ -159,11 +129,6 @@ func extractFlags(c *Command) []*pflag.Flag { return flags } -// cmdName returns the command's innvocation -func cmdName(c *Command) string { - return c.Name() -} - func simpleFlag(p *pflag.Flag) bool { return p.Name == "" || p.Shorthand == "" } From ec4b8c974c81c2077fb0f8dcd6cfe0f627b228f9 Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Wed, 28 Feb 2018 17:04:17 +0200 Subject: [PATCH 06/61] zsh-completions: revised flags completion rendering + new features: - If the flags are not bool the completion expects argument. - You don't have to specify file extensions for file completion to work. - Allow multiple occurrences of flag if type is stringArray. Need to verify that these assumption are correct :) --- zsh_completions.go | 79 ++++++++++++++++++++++++++++++----------- zsh_completions_test.go | 31 +++++++++++++--- 2 files changed, 85 insertions(+), 25 deletions(-) diff --git a/zsh_completions.go b/zsh_completions.go index dd23c45f..1d73e272 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "os" + "strings" "text/template" "github.com/spf13/pflag" @@ -11,26 +12,11 @@ import ( var ( funcMap = template.FuncMap{ - "genZshFuncName": generateZshCompletionFuncName, - "extractFlags": extractFlags, - "simpleFlag": simpleFlag, + "genZshFuncName": generateZshCompletionFuncName, + "extractFlags": extractFlags, + "genFlagEntryForZshArguments": genFlagEntryForZshArguments, } zshCompletionText = ` -{{/* for pflag.Flag (specifically annotations) */}} -{{define "flagAnnotations" -}} -{{with index .Annotations "cobra_annotation_bash_completion_filename_extensions"}}:filename:_files{{end}} -{{- end}} - -{{/* for pflag.Flag with short and long options */}} -{{define "complexFlag" -}} -"(-{{.Shorthand}} --{{.Name}})"{-{{.Shorthand}},--{{.Name}}}"[{{.Usage}}]{{template "flagAnnotations" .}}" -{{- end}} - -{{/* for pflag.Flag with either short or long options */}} -{{define "simpleFlag" -}} -"{{with .Name}}--{{.}}{{else}}-{{.Shorthand}}{{end}}[{{.Usage}}]{{template "flagAnnotations" .}}" -{{- end}} - {{/* should accept Command (that contains subcommands) as parameter */}} {{define "argumentsC" -}} {{ $cmdPath := genZshFuncName .}} @@ -38,7 +24,7 @@ function {{$cmdPath}} { local -a commands _arguments -C \{{- range extractFlags .}} - {{if simpleFlag .}}{{template "simpleFlag" .}}{{else}}{{template "complexFlag" .}}{{end}} \{{- end}} + {{genFlagEntryForZshArguments .}} \{{- end}} "1: :->cmnds" \ "*::arg:->args" @@ -66,7 +52,7 @@ function {{$cmdPath}} { {{define "arguments" -}} function {{genZshFuncName .}} { {{" _arguments"}}{{range extractFlags .}} \ - {{if simpleFlag .}}{{template "simpleFlag" .}}{{else}}{{template "complexFlag" .}}{{end -}} + {{genFlagEntryForZshArguments . -}} {{end}} } {{end}} @@ -132,3 +118,56 @@ func extractFlags(c *Command) []*pflag.Flag { func simpleFlag(p *pflag.Flag) bool { return p.Name == "" || p.Shorthand == "" } + +// genFlagEntryForZshArguments returns an entry that matches _arguments +// zsh-completion parameters. It's too complicated to generate in a template. +func genFlagEntryForZshArguments(f *pflag.Flag) string { + if f.Name == "" || f.Shorthand == "" { + return genFlagEntryForSingleOptionFlag(f) + } + return genFlagEntryForMultiOptionFlag(f) +} + +func genFlagEntryForSingleOptionFlag(f *pflag.Flag) string { + var option, multiMark, extras string + + if f.Value.Type() == "stringArray" { + multiMark = "*" + } + + option = "--" + f.Name + if option == "--" { + option = "-" + f.Shorthand + } + extras = genZshFlagEntryExtras(f) + + return fmt.Sprintf(`"%s%s[%s]%s"`, multiMark, option, f.Usage, extras) +} + +func genFlagEntryForMultiOptionFlag(f *pflag.Flag) string { + var options, parenMultiMark, curlyMultiMark, extras string + + if f.Value.Type() == "stringArray" { + parenMultiMark = "*" + curlyMultiMark = "\\*" + } + + options = fmt.Sprintf(`"(%s-%s %s--%s)"{%s-%s,%s--%s}`, + parenMultiMark, f.Shorthand, parenMultiMark, f.Name, curlyMultiMark, f.Shorthand, curlyMultiMark, f.Name) + extras = genZshFlagEntryExtras(f) + + return fmt.Sprintf(`%s"[%s]%s"`, options, f.Usage, extras) +} + +func genZshFlagEntryExtras(f *pflag.Flag) string { + var extras string + + _, pathSpecified := f.Annotations[BashCompFilenameExt] + if pathSpecified { + extras = ":filename:_files" + } else if !strings.HasPrefix(f.Value.Type(), "bool") { + extras = ":" // allow option variable without assisting + } + + return extras +} diff --git a/zsh_completions_test.go b/zsh_completions_test.go index c8d80c85..048c5e51 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -48,7 +48,7 @@ func TestGenZshCompletion(t *testing.T) { }, }, { - name: "command with subcommands", + name: "command with subcommands and flags with values", root: func() *Command { r := &Command{ Use: "rootcmd", @@ -75,7 +75,7 @@ func TestGenZshCompletion(t *testing.T) { `_arguments -C \\\n.*"--debug\[description]"`, `function _rootcmd_subcmd1 {`, `function _rootcmd_subcmd1 {`, - `_arguments \\\n.*"\(-o --option\)"{-o,--option}"\[option description]" \\\n`, + `_arguments \\\n.*"\(-o --option\)"{-o,--option}"\[option description]:" \\\n`, }, }, { @@ -88,20 +88,35 @@ func TestGenZshCompletion(t *testing.T) { Run: emptyRun, } r.Flags().StringVarP(&file, "config", "c", file, "config file") - r.MarkFlagFilename("config", "ext") + r.MarkFlagFilename("config") return r }(), expectedExpressions: []string{ `\n +"\(-c --config\)"{-c,--config}"\[config file]:filename:_files"`, }, }, + { + name: "repeated variables both with and without value", + root: func() *Command { + r := genTestCommand("mycmd", true) + _ = r.Flags().StringArrayP("debug", "d", []string{}, "debug usage") + _ = r.Flags().StringArray("option", []string{}, "options") + return r + }(), + expectedExpressions: []string{ + `"\*--option\[options]`, + `"\(\*-d \*--debug\)"{\\\*-d,\\\*--debug}`, + }, + }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { tc.root.Execute() buf := new(bytes.Buffer) - tc.root.GenZshCompletion(buf) + if err := tc.root.GenZshCompletion(buf); err != nil { + t.Error(err) + } output := buf.Bytes() for _, expr := range tc.expectedExpressions { @@ -173,7 +188,9 @@ func TestGenZshCompletionHidden(t *testing.T) { t.Run(tc.name, func(t *testing.T) { tc.root.Execute() buf := new(bytes.Buffer) - tc.root.GenZshCompletion(buf) + if err := tc.root.GenZshCompletion(buf); err != nil { + t.Error(err) + } output := buf.String() for _, expr := range tc.expectedExpressions { @@ -230,6 +247,7 @@ func constructLargeCommandHeirarchy() *Command { var config, st1, st2 string var long, debug bool var in1, in2 int + var verbose []bool r := genTestCommand("mycmd", false) r.PersistentFlags().StringVarP(&config, "config", "c", config, "config usage") @@ -238,6 +256,8 @@ func constructLargeCommandHeirarchy() *Command { } s1 := genTestCommand("sub1", true) s1.Flags().BoolVar(&long, "long", long, "long descriptin") + s1.Flags().BoolSliceVar(&verbose, "verbose", verbose, "verbose description") + s1.Flags().StringArray("option", []string{}, "various options") s2 := genTestCommand("sub2", true) s2.PersistentFlags().BoolVar(&debug, "debug", debug, "debug description") s3 := genTestCommand("sub3", true) @@ -249,6 +269,7 @@ func constructLargeCommandHeirarchy() *Command { s1_3 := genTestCommand("sub1sub3", true) s1_3.Flags().IntVar(&in1, "int1", in1, "int1 descriptionn") s1_3.Flags().IntVar(&in2, "int2", in2, "int2 descriptionn") + s1_3.Flags().StringArrayP("option", "O", []string{}, "more options") s2_1 := genTestCommand("sub2sub1", true) s2_2 := genTestCommand("sub2sub2", true) s2_3 := genTestCommand("sub2sub3", true) From e9ee8f044615ebe5f59ec8423e3b80e0e05cd7ab Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Wed, 28 Feb 2018 21:09:10 +0200 Subject: [PATCH 07/61] zsh-completion: removed the _dep_ files. They were committed by mistake. --- Gopkg.lock | 132 ----------------------------------------------------- Gopkg.toml | 54 ---------------------- 2 files changed, 186 deletions(-) delete mode 100644 Gopkg.lock delete mode 100644 Gopkg.toml diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index fde98ef9..00000000 --- a/Gopkg.lock +++ /dev/null @@ -1,132 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - name = "github.com/cpuguy83/go-md2man" - packages = ["md2man"] - revision = "20f5889cbdc3c73dbd2862796665e7c465ade7d1" - version = "v1.0.8" - -[[projects]] - name = "github.com/fsnotify/fsnotify" - packages = ["."] - revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" - version = "v1.4.7" - -[[projects]] - branch = "master" - name = "github.com/hashicorp/hcl" - packages = [ - ".", - "hcl/ast", - "hcl/parser", - "hcl/printer", - "hcl/scanner", - "hcl/strconv", - "hcl/token", - "json/parser", - "json/scanner", - "json/token" - ] - revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8" - -[[projects]] - name = "github.com/inconshreveable/mousetrap" - packages = ["."] - revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" - version = "v1.0" - -[[projects]] - name = "github.com/magiconair/properties" - packages = ["."] - revision = "c3beff4c2358b44d0493c7dda585e7db7ff28ae6" - version = "v1.7.6" - -[[projects]] - branch = "master" - name = "github.com/mitchellh/go-homedir" - packages = ["."] - revision = "b8bc1bf767474819792c23f32d8286a45736f1c6" - -[[projects]] - branch = "master" - name = "github.com/mitchellh/mapstructure" - packages = ["."] - revision = "a4e142e9c047c904fa2f1e144d9a84e6133024bc" - -[[projects]] - name = "github.com/pelletier/go-toml" - packages = ["."] - revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8" - version = "v1.1.0" - -[[projects]] - name = "github.com/russross/blackfriday" - packages = ["."] - revision = "4048872b16cc0fc2c5fd9eacf0ed2c2fedaa0c8c" - version = "v1.5" - -[[projects]] - name = "github.com/spf13/afero" - packages = [ - ".", - "mem" - ] - revision = "bb8f1927f2a9d3ab41c9340aa034f6b803f4359c" - version = "v1.0.2" - -[[projects]] - name = "github.com/spf13/cast" - packages = ["."] - revision = "8965335b8c7107321228e3e3702cab9832751bac" - version = "v1.2.0" - -[[projects]] - branch = "master" - name = "github.com/spf13/jwalterweatherman" - packages = ["."] - revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394" - -[[projects]] - branch = "master" - name = "github.com/spf13/pflag" - packages = ["."] - revision = "6a877ebacf28c5fc79846f4fcd380a5d9872b997" - -[[projects]] - branch = "master" - name = "github.com/spf13/viper" - packages = ["."] - revision = "aafc9e6bc7b7bb53ddaa75a5ef49a17d6e654be5" - -[[projects]] - branch = "master" - name = "golang.org/x/sys" - packages = ["unix"] - revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd" - -[[projects]] - branch = "master" - name = "golang.org/x/text" - packages = [ - "internal/gen", - "internal/triegen", - "internal/ucd", - "transform", - "unicode/cldr", - "unicode/norm" - ] - revision = "4e4a3210bb54bb31f6ab2cdca2edcc0b50c420c1" - -[[projects]] - branch = "v2" - name = "gopkg.in/yaml.v2" - packages = ["."] - revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - inputs-digest = "d7aecc8ee6a8b343ebf8c2539348464f2566a18dfc511cd374b2cd76bebb1c6d" - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index 3bf16541..00000000 --- a/Gopkg.toml +++ /dev/null @@ -1,54 +0,0 @@ -# Gopkg.toml example -# -# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" -# -# [prune] -# non-go = false -# go-tests = true -# unused-packages = true - - -[[constraint]] - name = "github.com/cpuguy83/go-md2man" - version = "1.0.8" - -[[constraint]] - name = "github.com/inconshreveable/mousetrap" - version = "1.0.0" - -[[constraint]] - branch = "master" - name = "github.com/mitchellh/go-homedir" - -[[constraint]] - name = "github.com/spf13/pflag" - branch = "master" - -[[constraint]] - name = "github.com/spf13/viper" - branch = "master" - -[[constraint]] - branch = "v2" - name = "gopkg.in/yaml.v2" - -[prune] - go-tests = true - unused-packages = true From df12a0a24975e4c9f278e62927ca08d000b9be71 Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Wed, 28 Feb 2018 21:52:35 +0200 Subject: [PATCH 08/61] zsh-completion: two fixes for identifying flag usage: Fixed after input from @eparis: - Decide on option parameter by checking NoOptDefVal - Slices also could be specified multiple times. --- zsh_completions.go | 11 ++++++++--- zsh_completions_test.go | 19 ++++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/zsh_completions.go b/zsh_completions.go index 1d73e272..1c1c8885 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -131,7 +131,7 @@ func genFlagEntryForZshArguments(f *pflag.Flag) string { func genFlagEntryForSingleOptionFlag(f *pflag.Flag) string { var option, multiMark, extras string - if f.Value.Type() == "stringArray" { + if flagCouldBeSpecifiedMoreThenOnce(f) { multiMark = "*" } @@ -147,7 +147,7 @@ func genFlagEntryForSingleOptionFlag(f *pflag.Flag) string { func genFlagEntryForMultiOptionFlag(f *pflag.Flag) string { var options, parenMultiMark, curlyMultiMark, extras string - if f.Value.Type() == "stringArray" { + if flagCouldBeSpecifiedMoreThenOnce(f) { parenMultiMark = "*" curlyMultiMark = "\\*" } @@ -165,9 +165,14 @@ func genZshFlagEntryExtras(f *pflag.Flag) string { _, pathSpecified := f.Annotations[BashCompFilenameExt] if pathSpecified { extras = ":filename:_files" - } else if !strings.HasPrefix(f.Value.Type(), "bool") { + } else if f.NoOptDefVal == "" { extras = ":" // allow option variable without assisting } return extras } + +func flagCouldBeSpecifiedMoreThenOnce(f *pflag.Flag) bool { + return strings.Contains(f.Value.Type(), "Slice") || + strings.Contains(f.Value.Type(), "Array") +} diff --git a/zsh_completions_test.go b/zsh_completions_test.go index 048c5e51..ba2a3bfb 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -15,6 +15,7 @@ func TestGenZshCompletion(t *testing.T) { name string root *Command expectedExpressions []string + skip string }{ { name: "simple command", @@ -99,7 +100,7 @@ func TestGenZshCompletion(t *testing.T) { name: "repeated variables both with and without value", root: func() *Command { r := genTestCommand("mycmd", true) - _ = r.Flags().StringArrayP("debug", "d", []string{}, "debug usage") + _ = r.Flags().BoolSliceP("debug", "d", []bool{}, "debug usage") _ = r.Flags().StringArray("option", []string{}, "options") return r }(), @@ -108,6 +109,18 @@ func TestGenZshCompletion(t *testing.T) { `"\(\*-d \*--debug\)"{\\\*-d,\\\*--debug}`, }, }, + { + name: "boolSlice should not accept arguments", + root: func() *Command { + r := genTestCommand("mycmd", true) + r.Flags().BoolSlice("verbose", []bool{}, "verbosity level") + return r + }(), + expectedExpressions: []string{ + `"\*--verbose\[verbosity level]"`, + }, + skip: "BoolSlice behaves strangely both with NoOptDefVal and type (identifies as bool)", + }, } for _, tc := range tcs { @@ -119,6 +132,10 @@ func TestGenZshCompletion(t *testing.T) { } output := buf.Bytes() + if tc.skip != "" { + t.Skip("Skipping:", tc.skip) + } + for _, expr := range tc.expectedExpressions { rgx, err := regexp.Compile(expr) if err != nil { From 461a39d5b9353091df9cd9852334113d2d9f22b9 Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Thu, 1 Mar 2018 08:02:33 +0200 Subject: [PATCH 09/61] zsh-completion: removed forgotten function. --- zsh_completions.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/zsh_completions.go b/zsh_completions.go index 1c1c8885..41703468 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -115,10 +115,6 @@ func extractFlags(c *Command) []*pflag.Flag { return flags } -func simpleFlag(p *pflag.Flag) bool { - return p.Name == "" || p.Shorthand == "" -} - // genFlagEntryForZshArguments returns an entry that matches _arguments // zsh-completion parameters. It's too complicated to generate in a template. func genFlagEntryForZshArguments(f *pflag.Flag) string { From dd577bdf3103152e76d48bdb168460f239bdfa29 Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Thu, 1 Mar 2018 08:50:20 +0200 Subject: [PATCH 10/61] zsh-completion: added zsh-completion documentation. --- README.md | 6 ++++++ zsh_completion.md | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 zsh_completion.md diff --git a/README.md b/README.md index ff16e3f6..8a9ace4c 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ etc. * [Suggestions when "unknown command" happens](#suggestions-when-unknown-command-happens) * [Generating documentation for your command](#generating-documentation-for-your-command) * [Generating bash completions](#generating-bash-completions) + * [Generating zsh completions](#generating-zsh-completions) - [Contributing](#contributing) - [License](#license) @@ -719,6 +720,11 @@ Cobra can generate documentation based on subcommands, flags, etc. in the follow Cobra can generate a bash-completion file. If you add more information to your command, these completions can be amazingly powerful and flexible. Read more about it in [Bash Completions](bash_completions.md). +## Generating zsh completions + +Cobra can generate zsh-completion file. Read more about it in +[Zsh Completions](zsh_completions.md). + # Contributing 1. Fork it diff --git a/zsh_completion.md b/zsh_completion.md new file mode 100644 index 00000000..bfedcf19 --- /dev/null +++ b/zsh_completion.md @@ -0,0 +1,26 @@ +## Generating Zsh Completion for your cobra.Command + +Cobra supports native Zsh completion generated from the root `cobra.Command`. +The generated completion script should be put somewhere in your `$fpath` named +`_`. + +### What's Supported + +* Completion for all non-hidden subcommands using their `.Short` description. +* Completion for all non-hidden flags using the following rules: + * Filename completion works by marking the flag with `cmd.MarkFlagFilename...` + family of commands. However, it will ignore specific extensions requested by + this command (see about what's not supported yet below). + * The requirement for argument to the flag is decided by the `.NoOptDefVal` + flag value - if it's empty then completion will expect an argument. + * Flags of one of the various `*Arrary` and `*Slice` types supports multiple + specifications (with or without argument depending on the specific type). + +### What's not yet Supported + +* Positional argument completion are not supported yet. +* Filename completion ignores extension specification. +* Custom completion scripts are not supported yet (We should probably create zsh + specific one, doesn't make sense to re-use the bash one as the functions will + be different). +* Whatever other feature you're looking for and doesn't exist :) From bda855a1a0bf7a7c2d9402a6250d53615dacd294 Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Thu, 1 Mar 2018 08:53:25 +0200 Subject: [PATCH 11/61] zsh-completions: fixed zsh completion markdown file name. --- zsh_completion.md => zsh_completions.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename zsh_completion.md => zsh_completions.md (100%) diff --git a/zsh_completion.md b/zsh_completions.md similarity index 100% rename from zsh_completion.md rename to zsh_completions.md From 50f385938e098a69ec93b3490ff68442ec3d6524 Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Fri, 2 Mar 2018 08:42:52 +0200 Subject: [PATCH 12/61] zsh-completion: added support for filename globbing. --- zsh_completions.go | 11 +++++++---- zsh_completions.md | 4 +--- zsh_completions_test.go | 23 +++++++++++++---------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/zsh_completions.go b/zsh_completions.go index 41703468..2e0d3e38 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -137,7 +137,7 @@ func genFlagEntryForSingleOptionFlag(f *pflag.Flag) string { } extras = genZshFlagEntryExtras(f) - return fmt.Sprintf(`"%s%s[%s]%s"`, multiMark, option, f.Usage, extras) + return fmt.Sprintf(`'%s%s[%s]%s'`, multiMark, option, f.Usage, extras) } func genFlagEntryForMultiOptionFlag(f *pflag.Flag) string { @@ -148,19 +148,22 @@ func genFlagEntryForMultiOptionFlag(f *pflag.Flag) string { curlyMultiMark = "\\*" } - options = fmt.Sprintf(`"(%s-%s %s--%s)"{%s-%s,%s--%s}`, + options = fmt.Sprintf(`'(%s-%s %s--%s)'{%s-%s,%s--%s}`, parenMultiMark, f.Shorthand, parenMultiMark, f.Name, curlyMultiMark, f.Shorthand, curlyMultiMark, f.Name) extras = genZshFlagEntryExtras(f) - return fmt.Sprintf(`%s"[%s]%s"`, options, f.Usage, extras) + return fmt.Sprintf(`%s'[%s]%s'`, options, f.Usage, extras) } func genZshFlagEntryExtras(f *pflag.Flag) string { var extras string - _, pathSpecified := f.Annotations[BashCompFilenameExt] + globs, pathSpecified := f.Annotations[BashCompFilenameExt] if pathSpecified { extras = ":filename:_files" + for _, g := range globs { + extras = extras + fmt.Sprintf(` -g "%s"`, g) + } } else if f.NoOptDefVal == "" { extras = ":" // allow option variable without assisting } diff --git a/zsh_completions.md b/zsh_completions.md index bfedcf19..c218179a 100644 --- a/zsh_completions.md +++ b/zsh_completions.md @@ -9,8 +9,7 @@ The generated completion script should be put somewhere in your `$fpath` named * Completion for all non-hidden subcommands using their `.Short` description. * Completion for all non-hidden flags using the following rules: * Filename completion works by marking the flag with `cmd.MarkFlagFilename...` - family of commands. However, it will ignore specific extensions requested by - this command (see about what's not supported yet below). + family of commands. * The requirement for argument to the flag is decided by the `.NoOptDefVal` flag value - if it's empty then completion will expect an argument. * Flags of one of the various `*Arrary` and `*Slice` types supports multiple @@ -19,7 +18,6 @@ The generated completion script should be put somewhere in your `$fpath` named ### What's not yet Supported * Positional argument completion are not supported yet. -* Filename completion ignores extension specification. * Custom completion scripts are not supported yet (We should probably create zsh specific one, doesn't make sense to re-use the bash one as the functions will be different). diff --git a/zsh_completions_test.go b/zsh_completions_test.go index ba2a3bfb..c5199ed3 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -29,7 +29,7 @@ func TestGenZshCompletion(t *testing.T) { return r }(), expectedExpressions: []string{ - `(?s)function _mycommand {\s+_arguments \\\s+"--debug\[description\]".*--help.*}`, + `(?s)function _mycommand {\s+_arguments \\\s+'--debug\[description\]'.*--help.*}`, "#compdef _mycommand mycommand", }, }, @@ -45,7 +45,7 @@ func TestGenZshCompletion(t *testing.T) { return r }(), expectedExpressions: []string{ - `"\(-d --debug\)"{-d,--debug}"\[debug description\]"`, + `'\(-d --debug\)'{-d,--debug}'\[debug description\]'`, }, }, { @@ -72,15 +72,15 @@ func TestGenZshCompletion(t *testing.T) { }(), expectedExpressions: []string{ `commands=\(\n\s+"help:.*\n\s+"subcmd1:.*\n\s+"subcmd2:.*\n\s+\)`, - `_arguments \\\n.*"--debug\[description]"`, - `_arguments -C \\\n.*"--debug\[description]"`, + `_arguments \\\n.*'--debug\[description]'`, + `_arguments -C \\\n.*'--debug\[description]'`, `function _rootcmd_subcmd1 {`, `function _rootcmd_subcmd1 {`, - `_arguments \\\n.*"\(-o --option\)"{-o,--option}"\[option description]:" \\\n`, + `_arguments \\\n.*'\(-o --option\)'{-o,--option}'\[option description]:' \\\n`, }, }, { - name: "filename completion", + name: "filename completion with and without globs", root: func() *Command { var file string r := &Command{ @@ -90,10 +90,13 @@ func TestGenZshCompletion(t *testing.T) { } r.Flags().StringVarP(&file, "config", "c", file, "config file") r.MarkFlagFilename("config") + r.Flags().String("output", "", "output file") + r.MarkFlagFilename("output", "*.log", "*.txt") return r }(), expectedExpressions: []string{ - `\n +"\(-c --config\)"{-c,--config}"\[config file]:filename:_files"`, + `\n +'\(-c --config\)'{-c,--config}'\[config file]:filename:_files'`, + `:_files -g "\*.log" -g "\*.txt"`, }, }, { @@ -105,8 +108,8 @@ func TestGenZshCompletion(t *testing.T) { return r }(), expectedExpressions: []string{ - `"\*--option\[options]`, - `"\(\*-d \*--debug\)"{\\\*-d,\\\*--debug}`, + `'\*--option\[options]`, + `'\(\*-d \*--debug\)'{\\\*-d,\\\*--debug}`, }, }, { @@ -117,7 +120,7 @@ func TestGenZshCompletion(t *testing.T) { return r }(), expectedExpressions: []string{ - `"\*--verbose\[verbosity level]"`, + `'\*--verbose\[verbosity level]'`, }, skip: "BoolSlice behaves strangely both with NoOptDefVal and type (identifies as bool)", }, From 0d9a33d2da4a2006f3d2293b4a2f0b436228ee54 Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Sat, 3 Mar 2018 14:50:58 +0200 Subject: [PATCH 13/61] zsh-completion: remove temporary file Yet another temporary file that found itself in the repo :( --- zsh_template.tmpl | 56 ----------------------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 zsh_template.tmpl diff --git a/zsh_template.tmpl b/zsh_template.tmpl deleted file mode 100644 index 43846254..00000000 --- a/zsh_template.tmpl +++ /dev/null @@ -1,56 +0,0 @@ -{{define "complexFlag"}}{{ /* for pflag.Flag with short and long options */ -}} -"(-{{.Shorthand}} --{{.Name}})"\{-{{.Shorthand}}, --{{.Name}}\}[{{.Usage}}] -{{- end}} - -{{define "simpleFlag"}}{{ /* for pflag.Flag with either short or long options */ -}} -"{{with .Name}}-{{.}}{{else}}--{{.Shorthand}}{{end}}[{{.Usage}}]" -{{- end}} - -{{define "argumentsC"}} -{{- /* should accept Command (that contains subcommands) as parameter */ -}} -function {{constructPath .}} { - local line - - _arguments -C \ -{{range extractFlags . -}} - {{if simpleFlag .}}{{template "simpleFlag" .}}{{else}}{{template "complexFlag" .}}{{end}} \ -{{end -}} - "1: :({{subCmdList .}})" \ - "*:arg:->args" - - case $line[1] in -{{range .Commands -}} - {{.Use}}) - {{constructPath .}} - ;; -{{end -}} - esac -} -{{range .Commands -}} - -{{template "selectCmdTemplate" .}} -{{end -}} -{{end}} - -{{define "arguments"}} -function {{constructPath .}} { -{{- /* should accept Command without subcommands as parameter */ -}} -{{with extractFlags . -}} - _arguments \ -{{range .}} - {{if simpleFlag .}}{{template "simpleFlag" .}}{{else}}{{template "complexFlag" .}}{{end}} \ -{{end -}} -{{ /* leave this line empty because of the last backslash */ }} -{{end -}} -} -{{end}} - -{{define "selectCmdTemplate" -}} -{{with .Commands}}{{template "argumentsC" .}}{{else}}{{template "arguments" .}}{{end}} -{{end -}} - -{{define "Main"}} -#compdef _{{.Use}} {{.Use}} - -{{template "selectCmdTemplate" .}} -{{end}} From 91e80cc4a4b48b031b65a5cd4024ed5b113c131f Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Sat, 3 Mar 2018 20:34:00 +0200 Subject: [PATCH 14/61] zsh-completion: remove bad test I thought there was a bug in the boolSlice definition but it seems It was my mistake in identifying what's going on. Also removed the provisioning to skip tests (doesn't seem to be needed anymore). --- zsh_completions_test.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/zsh_completions_test.go b/zsh_completions_test.go index c5199ed3..c4e1a95f 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -15,7 +15,6 @@ func TestGenZshCompletion(t *testing.T) { name string root *Command expectedExpressions []string - skip string }{ { name: "simple command", @@ -112,18 +111,6 @@ func TestGenZshCompletion(t *testing.T) { `'\(\*-d \*--debug\)'{\\\*-d,\\\*--debug}`, }, }, - { - name: "boolSlice should not accept arguments", - root: func() *Command { - r := genTestCommand("mycmd", true) - r.Flags().BoolSlice("verbose", []bool{}, "verbosity level") - return r - }(), - expectedExpressions: []string{ - `'\*--verbose\[verbosity level]'`, - }, - skip: "BoolSlice behaves strangely both with NoOptDefVal and type (identifies as bool)", - }, } for _, tc := range tcs { @@ -135,10 +122,6 @@ func TestGenZshCompletion(t *testing.T) { } output := buf.Bytes() - if tc.skip != "" { - t.Skip("Skipping:", tc.skip) - } - for _, expr := range tc.expectedExpressions { rgx, err := regexp.Compile(expr) if err != nil { From 7ce08e227e8c9db9b243dfef1e792af31d19cdd0 Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Sun, 4 Mar 2018 23:56:31 +0200 Subject: [PATCH 15/61] zsh-completion: completion should always parse the root command! It was running on the command it was invoked from which caused some additional helpers (--help, --version) not to be generated. --- zsh_completions.go | 6 ++++-- zsh_completions_test.go | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/zsh_completions.go b/zsh_completions.go index 2e0d3e38..4705a905 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -84,13 +84,15 @@ func (c *Command) GenZshCompletionFile(filename string) error { return c.GenZshCompletion(outFile) } -// GenZshCompletion generates a zsh completion file and writes to the passed writer. +// GenZshCompletion generates a zsh completion file and writes to the passed +// writer. The completion always run on the root command regardless of the +// command it was called from. func (c *Command) GenZshCompletion(w io.Writer) error { tmpl, err := template.New("Main").Funcs(funcMap).Parse(zshCompletionText) if err != nil { return fmt.Errorf("error creating zsh completion template: %v", err) } - return tmpl.Execute(w, c) + return tmpl.Execute(w, c.Root()) } func generateZshCompletionFuncName(c *Command) string { diff --git a/zsh_completions_test.go b/zsh_completions_test.go index c4e1a95f..66b6e692 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -111,6 +111,23 @@ func TestGenZshCompletion(t *testing.T) { `'\(\*-d \*--debug\)'{\\\*-d,\\\*--debug}`, }, }, + { + name: "command should run on the root command so --version and --help will be generated", + root: func() *Command { + r := &Command{ + Use: "mycmd", + Short: "mycmd short description", + Version: "myversion", + } + s := genTestCommand("sub1", true) + r.AddCommand(s) + return s + }(), + expectedExpressions: []string{ + "--version", + "--help", + }, + }, } for _, tc := range tcs { From 7b62c7df786ae4a385717290f9f5b8f2c4edc6b9 Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Mon, 5 Mar 2018 00:46:00 +0200 Subject: [PATCH 16/61] zsh-completion: --version and --help still doesn't work correctly When invoking from subcommand. Modified the test to prove. --- zsh_completions_test.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/zsh_completions_test.go b/zsh_completions_test.go index 66b6e692..b1321c9b 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -15,6 +15,8 @@ func TestGenZshCompletion(t *testing.T) { name string root *Command expectedExpressions []string + invocationArgs []string + skip string }{ { name: "simple command", @@ -112,7 +114,7 @@ func TestGenZshCompletion(t *testing.T) { }, }, { - name: "command should run on the root command so --version and --help will be generated", + name: "generated flags --help and --version should be created even when not executing root cmd", root: func() *Command { r := &Command{ Use: "mycmd", @@ -127,11 +129,19 @@ func TestGenZshCompletion(t *testing.T) { "--version", "--help", }, + invocationArgs: []string{ + "sub1", + }, + skip: "--version and --help are currently not generated when not running on root command", }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { + if tc.skip != "" { + t.Skip(tc.skip) + } + tc.root.Root().SetArgs(tc.invocationArgs) tc.root.Execute() buf := new(bytes.Buffer) if err := tc.root.GenZshCompletion(buf); err != nil { From 66a98807d4036a0b21797ad8434b97a9ce0ae49d Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Mon, 5 Mar 2018 01:09:55 +0200 Subject: [PATCH 17/61] zsh-completion: test to verify that we're always running on root cmd. --- zsh_completions_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/zsh_completions_test.go b/zsh_completions_test.go index b1321c9b..22a66d6d 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -134,6 +134,18 @@ func TestGenZshCompletion(t *testing.T) { }, skip: "--version and --help are currently not generated when not running on root command", }, + { + name: "zsh generation should run on root commannd", + root: func() *Command { + r := genTestCommand("root", false) + s := genTestCommand("sub1", true) + r.AddCommand(s) + return s + }(), + expectedExpressions: []string{ + "function _root {", + }, + }, } for _, tc := range tcs { From 8822449c0fb9575f1d2453e80cd3e55ab8fc2844 Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Sat, 17 Mar 2018 20:55:27 +0200 Subject: [PATCH 18/61] zsh-completion: added escapinng of single quotes in flag description. --- zsh_completions.go | 8 ++++++-- zsh_completions_test.go | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/zsh_completions.go b/zsh_completions.go index 4705a905..59f55312 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -139,7 +139,7 @@ func genFlagEntryForSingleOptionFlag(f *pflag.Flag) string { } extras = genZshFlagEntryExtras(f) - return fmt.Sprintf(`'%s%s[%s]%s'`, multiMark, option, f.Usage, extras) + return fmt.Sprintf(`'%s%s[%s]%s'`, multiMark, option, quoteDescription(f.Usage), extras) } func genFlagEntryForMultiOptionFlag(f *pflag.Flag) string { @@ -154,7 +154,7 @@ func genFlagEntryForMultiOptionFlag(f *pflag.Flag) string { parenMultiMark, f.Shorthand, parenMultiMark, f.Name, curlyMultiMark, f.Shorthand, curlyMultiMark, f.Name) extras = genZshFlagEntryExtras(f) - return fmt.Sprintf(`%s'[%s]%s'`, options, f.Usage, extras) + return fmt.Sprintf(`%s'[%s]%s'`, options, quoteDescription(f.Usage), extras) } func genZshFlagEntryExtras(f *pflag.Flag) string { @@ -177,3 +177,7 @@ func flagCouldBeSpecifiedMoreThenOnce(f *pflag.Flag) bool { return strings.Contains(f.Value.Type(), "Slice") || strings.Contains(f.Value.Type(), "Array") } + +func quoteDescription(s string) string { + return strings.Replace(s, "'", `'\''`, -1) +} diff --git a/zsh_completions_test.go b/zsh_completions_test.go index 22a66d6d..6788797a 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -146,6 +146,17 @@ func TestGenZshCompletion(t *testing.T) { "function _root {", }, }, + { + name: "flag description with single quote (') shouldn't break quotes in completion file", + root: func() *Command { + r := genTestCommand("root", true) + r.Flags().Bool("private", false, "Don't show public info") + return r + }(), + expectedExpressions: []string{ + `--private\[Don'\\''t show public info]`, + }, + }, } for _, tc := range tcs { From d262154093af3bc0c8d63a8dbadaa13e7807360a Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Thu, 22 Mar 2018 19:58:25 +0200 Subject: [PATCH 19/61] zsh-completion: tidy up function and variable names There are many files in the package, renamed all zsh-completion related names to convey that. --- zsh_completions.go | 46 ++++++++++++++++++++--------------------- zsh_completions_test.go | 4 ++-- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/zsh_completions.go b/zsh_completions.go index 59f55312..490eb021 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -11,10 +11,10 @@ import ( ) var ( - funcMap = template.FuncMap{ - "genZshFuncName": generateZshCompletionFuncName, - "extractFlags": extractFlags, - "genFlagEntryForZshArguments": genFlagEntryForZshArguments, + zshCompFuncMap = template.FuncMap{ + "genZshFuncName": zshCompGenFuncName, + "extractFlags": zshCompExtractFlag, + "genFlagEntryForZshArguments": zshCompGenFlagEntryForArguments, } zshCompletionText = ` {{/* should accept Command (that contains subcommands) as parameter */}} @@ -88,21 +88,21 @@ func (c *Command) GenZshCompletionFile(filename string) error { // writer. The completion always run on the root command regardless of the // command it was called from. func (c *Command) GenZshCompletion(w io.Writer) error { - tmpl, err := template.New("Main").Funcs(funcMap).Parse(zshCompletionText) + tmpl, err := template.New("Main").Funcs(zshCompFuncMap).Parse(zshCompletionText) if err != nil { return fmt.Errorf("error creating zsh completion template: %v", err) } return tmpl.Execute(w, c.Root()) } -func generateZshCompletionFuncName(c *Command) string { +func zshCompGenFuncName(c *Command) string { if c.HasParent() { - return generateZshCompletionFuncName(c.Parent()) + "_" + c.Name() + return zshCompGenFuncName(c.Parent()) + "_" + c.Name() } return "_" + c.Name() } -func extractFlags(c *Command) []*pflag.Flag { +func zshCompExtractFlag(c *Command) []*pflag.Flag { var flags []*pflag.Flag c.LocalFlags().VisitAll(func(f *pflag.Flag) { if !f.Hidden { @@ -117,19 +117,19 @@ func extractFlags(c *Command) []*pflag.Flag { return flags } -// genFlagEntryForZshArguments returns an entry that matches _arguments +// zshCompGenFlagEntryForArguments returns an entry that matches _arguments // zsh-completion parameters. It's too complicated to generate in a template. -func genFlagEntryForZshArguments(f *pflag.Flag) string { +func zshCompGenFlagEntryForArguments(f *pflag.Flag) string { if f.Name == "" || f.Shorthand == "" { - return genFlagEntryForSingleOptionFlag(f) + return zshCompGenFlagEntryForSingleOptionFlag(f) } - return genFlagEntryForMultiOptionFlag(f) + return zshCompGenFlagEntryForMultiOptionFlag(f) } -func genFlagEntryForSingleOptionFlag(f *pflag.Flag) string { +func zshCompGenFlagEntryForSingleOptionFlag(f *pflag.Flag) string { var option, multiMark, extras string - if flagCouldBeSpecifiedMoreThenOnce(f) { + if zshCompFlagCouldBeSpecifiedMoreThenOnce(f) { multiMark = "*" } @@ -137,27 +137,27 @@ func genFlagEntryForSingleOptionFlag(f *pflag.Flag) string { if option == "--" { option = "-" + f.Shorthand } - extras = genZshFlagEntryExtras(f) + extras = zshCompGenFlagEntryExtras(f) - return fmt.Sprintf(`'%s%s[%s]%s'`, multiMark, option, quoteDescription(f.Usage), extras) + return fmt.Sprintf(`'%s%s[%s]%s'`, multiMark, option, zshCompQuoteFlagDescription(f.Usage), extras) } -func genFlagEntryForMultiOptionFlag(f *pflag.Flag) string { +func zshCompGenFlagEntryForMultiOptionFlag(f *pflag.Flag) string { var options, parenMultiMark, curlyMultiMark, extras string - if flagCouldBeSpecifiedMoreThenOnce(f) { + if zshCompFlagCouldBeSpecifiedMoreThenOnce(f) { parenMultiMark = "*" curlyMultiMark = "\\*" } options = fmt.Sprintf(`'(%s-%s %s--%s)'{%s-%s,%s--%s}`, parenMultiMark, f.Shorthand, parenMultiMark, f.Name, curlyMultiMark, f.Shorthand, curlyMultiMark, f.Name) - extras = genZshFlagEntryExtras(f) + extras = zshCompGenFlagEntryExtras(f) - return fmt.Sprintf(`%s'[%s]%s'`, options, quoteDescription(f.Usage), extras) + return fmt.Sprintf(`%s'[%s]%s'`, options, zshCompQuoteFlagDescription(f.Usage), extras) } -func genZshFlagEntryExtras(f *pflag.Flag) string { +func zshCompGenFlagEntryExtras(f *pflag.Flag) string { var extras string globs, pathSpecified := f.Annotations[BashCompFilenameExt] @@ -173,11 +173,11 @@ func genZshFlagEntryExtras(f *pflag.Flag) string { return extras } -func flagCouldBeSpecifiedMoreThenOnce(f *pflag.Flag) bool { +func zshCompFlagCouldBeSpecifiedMoreThenOnce(f *pflag.Flag) bool { return strings.Contains(f.Value.Type(), "Slice") || strings.Contains(f.Value.Type(), "Array") } -func quoteDescription(s string) string { +func zshCompQuoteFlagDescription(s string) string { return strings.Replace(s, "'", `'\''`, -1) } diff --git a/zsh_completions_test.go b/zsh_completions_test.go index 6788797a..4ef2e2f4 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -285,8 +285,8 @@ func TestExtractFlags(t *testing.T) { d.Flags().BoolVar(&cmdd, "cmd-d", cmdd, "Command D") c.AddCommand(d) - resC := extractFlags(c) - resD := extractFlags(d) + resC := zshCompExtractFlag(c) + resD := zshCompExtractFlag(d) if len(resC) != 2 { t.Errorf("expected Command C to return 2 flags, got %d", len(resC)) From edbb6712e2cba085bb278feeb1b88ae88079ecdf Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Fri, 23 Mar 2018 13:09:56 +0300 Subject: [PATCH 20/61] zsh-completions: implemented argument completion. --- zsh_completions.go | 149 +++++++++++++++++++++++++++++++++++++++- zsh_completions.md | 17 ++++- zsh_completions_test.go | 128 +++++++++++++++++++++++++++++++--- 3 files changed, 283 insertions(+), 11 deletions(-) diff --git a/zsh_completions.go b/zsh_completions.go index 490eb021..68bb5c6e 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -1,20 +1,29 @@ package cobra import ( + "encoding/json" "fmt" "io" "os" + "sort" "strings" "text/template" "github.com/spf13/pflag" ) +const ( + zshCompArgumentAnnotation = "cobra_annotations_zsh_completion_argument_annotation" + zshCompArgumentFilenameComp = "cobra_annotations_zsh_completion_argument_file_completion" + zshCompArgumentWordComp = "cobra_annotations_zsh_completion_argument_word_completion" +) + var ( zshCompFuncMap = template.FuncMap{ "genZshFuncName": zshCompGenFuncName, "extractFlags": zshCompExtractFlag, "genFlagEntryForZshArguments": zshCompGenFlagEntryForArguments, + "extractArgsCompletions": zshCompExtractArgumentCompletionHintsForRendering, } zshCompletionText = ` {{/* should accept Command (that contains subcommands) as parameter */}} @@ -53,7 +62,8 @@ function {{$cmdPath}} { function {{genZshFuncName .}} { {{" _arguments"}}{{range extractFlags .}} \ {{genFlagEntryForZshArguments . -}} -{{end}} +{{end}}{{range extractArgsCompletions .}} \ + {{.}}{{end}} } {{end}} @@ -73,6 +83,19 @@ function {{genZshFuncName .}} { ` ) +// zshCompArgsAnnotation is used to encode/decode zsh completion for +// arguments to/from Command.Annotations. +type zshCompArgsAnnotation map[int]zshCompArgHint + +type zshCompArgHint struct { + // Indicates the type of the completion to use. One of: + // zshCompArgumentFilenameComp or zshCompArgumentWordComp + Tipe string `json:"type"` + + // A value for the type above (globs for file completion or words) + Options []string `json:"options"` +} + // GenZshCompletionFile generates zsh completion file. func (c *Command) GenZshCompletionFile(filename string) error { outFile, err := os.Create(filename) @@ -95,6 +118,130 @@ func (c *Command) GenZshCompletion(w io.Writer) error { return tmpl.Execute(w, c.Root()) } +// MarkZshCompPositionalArgumentFile marks the specified argument (first +// argument is 1) as completed by file selection. patterns (e.g. "*.txt") are +// optional - if not provided the completion will search for all files. +func (c *Command) MarkZshCompPositionalArgumentFile(argPosition int, patterns ...string) error { + if argPosition < 1 { + return fmt.Errorf("Invalid argument position (%d)", argPosition) + } + annotation, err := c.zshCompGetArgsAnnotations() + if err != nil { + return err + } + if c.zshcompArgsAnnotationnIsDuplicatePosition(annotation, argPosition) { + return fmt.Errorf("Duplicate annotation for positional argument at index %d", argPosition) + } + annotation[argPosition] = zshCompArgHint{ + Tipe: zshCompArgumentFilenameComp, + Options: patterns, + } + return c.zshCompSetArgsAnnotations(annotation) +} + +// MarkZshCompPositionalArgumentWords marks the specified positional argument +// (first argument is 1) as completed by the provided words. At east one word +// must be provided, spaces within words will be offered completion with +// "word\ word". +func (c *Command) MarkZshCompPositionalArgumentWords(argPosition int, words ...string) error { + if argPosition < 1 { + return fmt.Errorf("Invalid argument position (%d)", argPosition) + } + if len(words) == 0 { + return fmt.Errorf("Trying to set empty word list for positional argument %d", argPosition) + } + annotation, err := c.zshCompGetArgsAnnotations() + if err != nil { + return err + } + if c.zshcompArgsAnnotationnIsDuplicatePosition(annotation, argPosition) { + return fmt.Errorf("Duplicate annotation for positional argument at index %d", argPosition) + } + annotation[argPosition] = zshCompArgHint{ + Tipe: zshCompArgumentWordComp, + Options: words, + } + return c.zshCompSetArgsAnnotations(annotation) +} + +func zshCompExtractArgumentCompletionHintsForRendering(c *Command) ([]string, error) { + var result []string + annotation, err := c.zshCompGetArgsAnnotations() + if err != nil { + return nil, err + } + for k, v := range annotation { + s, err := zshCompRenderZshCompArgHint(k, v) + if err != nil { + return nil, err + } + result = append(result, s) + } + if len(c.ValidArgs) > 0 { + if _, positionOneExists := annotation[1]; !positionOneExists { + s, err := zshCompRenderZshCompArgHint(1, zshCompArgHint{ + Tipe: zshCompArgumentWordComp, + Options: c.ValidArgs, + }) + if err != nil { + return nil, err + } + result = append(result, s) + } + } + sort.Strings(result) + return result, nil +} + +func zshCompRenderZshCompArgHint(i int, z zshCompArgHint) (string, error) { + switch t := z.Tipe; t { + case zshCompArgumentFilenameComp: + var globs []string + for _, g := range z.Options { + globs = append(globs, fmt.Sprintf(`-g "%s"`, g)) + } + return fmt.Sprintf(`'%d: :_files %s'`, i, strings.Join(globs, " ")), nil + case zshCompArgumentWordComp: + var words []string + for _, w := range z.Options { + words = append(words, fmt.Sprintf("%q", w)) + } + return fmt.Sprintf(`'%d: :(%s)'`, i, strings.Join(words, " ")), nil + default: + return "", fmt.Errorf("Invalid zsh argument completion annotation: %s", t) + } +} + +func (c *Command) zshcompArgsAnnotationnIsDuplicatePosition(annotation zshCompArgsAnnotation, position int) bool { + _, dup := annotation[position] + return dup +} + +func (c *Command) zshCompGetArgsAnnotations() (zshCompArgsAnnotation, error) { + annotation := make(zshCompArgsAnnotation) + annotationString, ok := c.Annotations[zshCompArgumentAnnotation] + if !ok { + return annotation, nil + } + err := json.Unmarshal([]byte(annotationString), &annotation) + if err != nil { + return annotation, fmt.Errorf("Error unmarshaling zsh argument annotation: %v", err) + } + return annotation, nil +} + +func (c *Command) zshCompSetArgsAnnotations(annotation zshCompArgsAnnotation) error { + jsn, err := json.Marshal(annotation) + if err != nil { + return fmt.Errorf("Error marshaling zsh argument annotation: %v", err) + } + if c.Annotations == nil { + c.Annotations = make(map[string]string) + } + c.Annotations[zshCompArgumentAnnotation] = string(jsn) + return nil +} + func zshCompGenFuncName(c *Command) string { if c.HasParent() { return zshCompGenFuncName(c.Parent()) + "_" + c.Name() diff --git a/zsh_completions.md b/zsh_completions.md index c218179a..95242d34 100644 --- a/zsh_completions.md +++ b/zsh_completions.md @@ -14,10 +14,25 @@ The generated completion script should be put somewhere in your `$fpath` named flag value - if it's empty then completion will expect an argument. * Flags of one of the various `*Arrary` and `*Slice` types supports multiple specifications (with or without argument depending on the specific type). +* Completion of positional arguments using the following rules: + * Argument position for all options below starts at `1`. If argument position + `0` is requested it will raise an error. + * Use `command.MarkZshCompPositionalArgumentFile` to complete filenames. Glob + patterns (e.g. `"*.log"`) are optional - if not specified it will offer to + complete all file types. + * Use `command.MarkZshCompPositionalArgumentWords` to offer specific words for + completion. At least one word is required. + * It's possible to specify completion for some arguments and leave some + unspecified (e.g. offer words for second argument but nothing for first + argument). This will cause no completion for first argument but words + completion for second argument. + * If no argument completion was specified for 1st argument (but optionally was + specified for 2nd) and the command has `ValidArgs` it will be used as + completion options for 1st argument. + * Argument completions only offered for commands with no subcommands. ### What's not yet Supported -* Positional argument completion are not supported yet. * Custom completion scripts are not supported yet (We should probably create zsh specific one, doesn't make sense to re-use the bash one as the functions will be different). diff --git a/zsh_completions_test.go b/zsh_completions_test.go index 4ef2e2f4..976cbfc2 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -58,7 +58,7 @@ func TestGenZshCompletion(t *testing.T) { } d := &Command{ Use: "subcmd1", - Short: "Subcmd1 short descrition", + Short: "Subcmd1 short description", Run: emptyRun, } e := &Command{ @@ -135,7 +135,7 @@ func TestGenZshCompletion(t *testing.T) { skip: "--version and --help are currently not generated when not running on root command", }, { - name: "zsh generation should run on root commannd", + name: "zsh generation should run on root command", root: func() *Command { r := genTestCommand("root", false) s := genTestCommand("sub1", true) @@ -157,6 +157,63 @@ func TestGenZshCompletion(t *testing.T) { `--private\[Don'\\''t show public info]`, }, }, + { + name: "argument completion for file with and without patterns", + root: func() *Command { + r := genTestCommand("root", true) + r.MarkZshCompPositionalArgumentFile(1, "*.log") + r.MarkZshCompPositionalArgumentFile(2) + return r + }(), + expectedExpressions: []string{ + `'1: :_files -g "\*.log"' \\\n\s+'2: :_files`, + }, + }, + { + name: "argument zsh completion for words", + root: func() *Command { + r := genTestCommand("root", true) + r.MarkZshCompPositionalArgumentWords(1, "word1", "word2") + return r + }(), + expectedExpressions: []string{ + `'1: :\("word1" "word2"\)`, + }, + }, + { + name: "argument completion for words with spaces", + root: func() *Command { + r := genTestCommand("root", true) + r.MarkZshCompPositionalArgumentWords(1, "single", "multiple words") + return r + }(), + expectedExpressions: []string{ + `'1: :\("single" "multiple words"\)'`, + }, + }, + { + name: "argument completion when command has ValidArgs and no annotation for argument completion", + root: func() *Command { + r := genTestCommand("root", true) + r.ValidArgs = []string{"word1", "word2"} + return r + }(), + expectedExpressions: []string{ + `'1: :\("word1" "word2"\)'`, + }, + }, + { + name: "argument completion when command has ValidArgs and no annotation for argument at argPosition 1", + root: func() *Command { + r := genTestCommand("root", true) + r.ValidArgs = []string{"word1", "word2"} + r.MarkZshCompPositionalArgumentFile(2) + return r + }(), + expectedExpressions: []string{ + `'1: :\("word1" "word2"\)' \\`, + }, + }, } for _, tc := range tcs { @@ -178,7 +235,7 @@ func TestGenZshCompletion(t *testing.T) { t.Errorf("error compiling expression (%s): %v", expr, err) } if !rgx.Match(output) { - t.Errorf("expeced completion (%s) to match '%s'", buf.String(), expr) + t.Errorf("expected completion (%s) to match '%s'", buf.String(), expr) } } }) @@ -192,7 +249,7 @@ func TestGenZshCompletionHidden(t *testing.T) { expectedExpressions []string }{ { - name: "hidden commmands", + name: "hidden commands", root: func() *Command { r := &Command{ Use: "main", @@ -255,8 +312,61 @@ func TestGenZshCompletionHidden(t *testing.T) { } } +func TestMarkZshCompPositionalArgumentFile(t *testing.T) { + t.Run("Doesn't allow overwriting existing positional argument", func(t *testing.T) { + c := &Command{} + if err := c.MarkZshCompPositionalArgumentFile(1, "*.log"); err != nil { + t.Errorf("Received error when we shouldn't have: %v\n", err) + } + if err := c.MarkZshCompPositionalArgumentFile(1); err == nil { + t.Error("Didn't receive an error when trying to overwrite argument position") + } + }) + + t.Run("Refuses to accept argPosition less then 1", func(t *testing.T) { + c := &Command{} + err := c.MarkZshCompPositionalArgumentFile(0, "*") + if err == nil { + t.Fatal("Error was not thrown when indicating argument position 0") + } + if !strings.Contains(err.Error(), "position") { + t.Errorf("expected error message '%s' to contain 'position'", err.Error()) + } + }) +} + +func TestMarkZshCompPositionalArgumentWords(t *testing.T) { + t.Run("Doesn't allow overwriting existing positional argument", func(t *testing.T) { + c := &Command{} + if err := c.MarkZshCompPositionalArgumentFile(1, "*.log"); err != nil { + t.Errorf("Received error when we shouldn't have: %v\n", err) + } + if err := c.MarkZshCompPositionalArgumentWords(1, "hello"); err == nil { + t.Error("Didn't receive an error when trying to overwrite argument position") + } + }) + + t.Run("Doesn't allow calling without words", func(t *testing.T) { + c := &Command{} + if err := c.MarkZshCompPositionalArgumentWords(0); err == nil { + t.Error("Should not allow saving empty word list for annotation") + } + }) + + t.Run("Refuses to accept argPosition less then 1", func(t *testing.T) { + c := &Command{} + err := c.MarkZshCompPositionalArgumentWords(0, "word") + if err == nil { + t.Fatal("Should not allow setting argument position less then 1") + } + if !strings.Contains(err.Error(), "position") { + t.Errorf("Expected error '%s' to contain 'position' but didn't", err.Error()) + } + }) +} + func BenchmarkMediumSizeConstruct(b *testing.B) { - root := constructLargeCommandHeirarchy() + root := constructLargeCommandHierarchy() // if err := root.GenZshCompletionFile("_mycmd"); err != nil { // b.Error(err) // } @@ -296,7 +406,7 @@ func TestExtractFlags(t *testing.T) { } } -func constructLargeCommandHeirarchy() *Command { +func constructLargeCommandHierarchy() *Command { var config, st1, st2 string var long, debug bool var in1, in2 int @@ -308,7 +418,7 @@ func constructLargeCommandHeirarchy() *Command { panic(err) } s1 := genTestCommand("sub1", true) - s1.Flags().BoolVar(&long, "long", long, "long descriptin") + s1.Flags().BoolVar(&long, "long", long, "long description") s1.Flags().BoolSliceVar(&verbose, "verbose", verbose, "verbose description") s1.Flags().StringArray("option", []string{}, "various options") s2 := genTestCommand("sub2", true) @@ -320,8 +430,8 @@ func constructLargeCommandHeirarchy() *Command { s1_1.Flags().StringVar(&st2, "st2", st2, "st2 description") s1_2 := genTestCommand("sub1sub2", true) s1_3 := genTestCommand("sub1sub3", true) - s1_3.Flags().IntVar(&in1, "int1", in1, "int1 descriptionn") - s1_3.Flags().IntVar(&in2, "int2", in2, "int2 descriptionn") + s1_3.Flags().IntVar(&in1, "int1", in1, "int1 description") + s1_3.Flags().IntVar(&in2, "int2", in2, "int2 description") s1_3.Flags().StringArrayP("option", "O", []string{}, "more options") s2_1 := genTestCommand("sub2sub1", true) s2_2 := genTestCommand("sub2sub2", true) From 601d83077b12a12f839d20881d9e5d3075aaad1b Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Fri, 23 Mar 2018 13:57:53 +0300 Subject: [PATCH 21/61] typo in zsh-completions.md --- zsh_completions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zsh_completions.md b/zsh_completions.md index 95242d34..df9c2eac 100644 --- a/zsh_completions.md +++ b/zsh_completions.md @@ -12,7 +12,7 @@ The generated completion script should be put somewhere in your `$fpath` named family of commands. * The requirement for argument to the flag is decided by the `.NoOptDefVal` flag value - if it's empty then completion will expect an argument. - * Flags of one of the various `*Arrary` and `*Slice` types supports multiple + * Flags of one of the various `*Array` and `*Slice` types supports multiple specifications (with or without argument depending on the specific type). * Completion of positional arguments using the following rules: * Argument position for all options below starts at `1`. If argument position From e2c45ac9eb0cdb823b89c1e868d698dc74822797 Mon Sep 17 00:00:00 2001 From: Haim Ashkenazi Date: Sun, 3 Jun 2018 22:08:30 +0300 Subject: [PATCH 22/61] Started working on Unified API for the various shell completions: - Moved some general function to a more generic shell_completions file. - Added functions to mark flag as directory completion. - Started making the global functions docs more generic (not bash specific) and added compatibility matrix. --- bash_completions.go | 48 ----------------------- shell_completions.go | 85 +++++++++++++++++++++++++++++++++++++++++ zsh_completions.go | 22 +++++++---- zsh_completions_test.go | 15 ++++++++ 4 files changed, 114 insertions(+), 56 deletions(-) create mode 100644 shell_completions.go diff --git a/bash_completions.go b/bash_completions.go index c3c1e501..57bb8e1b 100644 --- a/bash_completions.go +++ b/bash_completions.go @@ -545,51 +545,3 @@ func (c *Command) GenBashCompletionFile(filename string) error { return c.GenBashCompletion(outFile) } - -// MarkFlagRequired adds the BashCompOneRequiredFlag annotation to the named flag if it exists, -// and causes your command to report an error if invoked without the flag. -func (c *Command) MarkFlagRequired(name string) error { - return MarkFlagRequired(c.Flags(), name) -} - -// MarkPersistentFlagRequired adds the BashCompOneRequiredFlag annotation to the named persistent flag if it exists, -// and causes your command to report an error if invoked without the flag. -func (c *Command) MarkPersistentFlagRequired(name string) error { - return MarkFlagRequired(c.PersistentFlags(), name) -} - -// MarkFlagRequired adds the BashCompOneRequiredFlag annotation to the named flag if it exists, -// and causes your command to report an error if invoked without the flag. -func MarkFlagRequired(flags *pflag.FlagSet, name string) error { - return flags.SetAnnotation(name, BashCompOneRequiredFlag, []string{"true"}) -} - -// MarkFlagFilename adds the BashCompFilenameExt annotation to the named flag, if it exists. -// Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided. -func (c *Command) MarkFlagFilename(name string, extensions ...string) error { - return MarkFlagFilename(c.Flags(), name, extensions...) -} - -// MarkFlagCustom adds the BashCompCustom annotation to the named flag, if it exists. -// Generated bash autocompletion will call the bash function f for the flag. -func (c *Command) MarkFlagCustom(name string, f string) error { - return MarkFlagCustom(c.Flags(), name, f) -} - -// MarkPersistentFlagFilename adds the BashCompFilenameExt annotation to the named persistent flag, if it exists. -// Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided. -func (c *Command) MarkPersistentFlagFilename(name string, extensions ...string) error { - return MarkFlagFilename(c.PersistentFlags(), name, extensions...) -} - -// MarkFlagFilename adds the BashCompFilenameExt annotation to the named flag in the flag set, if it exists. -// Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided. -func MarkFlagFilename(flags *pflag.FlagSet, name string, extensions ...string) error { - return flags.SetAnnotation(name, BashCompFilenameExt, extensions) -} - -// MarkFlagCustom adds the BashCompCustom annotation to the named flag in the flag set, if it exists. -// Generated bash autocompletion will call the bash function f for the flag. -func MarkFlagCustom(flags *pflag.FlagSet, name string, f string) error { - return flags.SetAnnotation(name, BashCompCustom, []string{f}) -} diff --git a/shell_completions.go b/shell_completions.go new file mode 100644 index 00000000..ba0af9cb --- /dev/null +++ b/shell_completions.go @@ -0,0 +1,85 @@ +package cobra + +import ( + "github.com/spf13/pflag" +) + +// MarkFlagRequired adds the BashCompOneRequiredFlag annotation to the named flag if it exists, +// and causes your command to report an error if invoked without the flag. +func (c *Command) MarkFlagRequired(name string) error { + return MarkFlagRequired(c.Flags(), name) +} + +// MarkPersistentFlagRequired adds the BashCompOneRequiredFlag annotation to the named persistent flag if it exists, +// and causes your command to report an error if invoked without the flag. +func (c *Command) MarkPersistentFlagRequired(name string) error { + return MarkFlagRequired(c.PersistentFlags(), name) +} + +// MarkFlagRequired adds the BashCompOneRequiredFlag annotation to the named flag if it exists, +// and causes your command to report an error if invoked without the flag. +func MarkFlagRequired(flags *pflag.FlagSet, name string) error { + return flags.SetAnnotation(name, BashCompOneRequiredFlag, []string{"true"}) +} + +// MarkFlagFilename adds the BashCompFilenameExt annotation to the named flag, if it exists. +// Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided. +func (c *Command) MarkFlagFilename(name string, extensions ...string) error { + return MarkFlagFilename(c.Flags(), name, extensions...) +} + +// MarkFlagCustom adds the BashCompCustom annotation to the named flag, if it exists. +// Generated bash autocompletion will call the bash function f for the flag. +func (c *Command) MarkFlagCustom(name string, f string) error { + return MarkFlagCustom(c.Flags(), name, f) +} + +// MarkPersistentFlagFilename instructs the various shell completion +// implementations to limit completions for this persistent flag to the +// specified extensions (patterns). +// +// Shell Completion compatibility matrix: bash, zsh +func (c *Command) MarkPersistentFlagFilename(name string, extensions ...string) error { + return MarkFlagFilename(c.PersistentFlags(), name, extensions...) +} + +// MarkFlagFilename instructs the various shell completion implementations to +// limit completions for this flag to the specified extensions (patterns). +// +// Shell Completion compatibility matrix: bash, zsh +func MarkFlagFilename(flags *pflag.FlagSet, name string, extensions ...string) error { + return flags.SetAnnotation(name, BashCompFilenameExt, extensions) +} + +// MarkFlagCustom instructs the various shell completion implementations to +// limit completions for this flag to the specified extensions (patterns). +// +// Shell Completion compatibility matrix: bash, zsh +func MarkFlagCustom(flags *pflag.FlagSet, name string, f string) error { + return flags.SetAnnotation(name, BashCompCustom, []string{f}) +} + +// MarkFlagDirname instructs the various shell completion implementations to +// complete only directories with this named flag. +// +// Shell Completion compatibility matrix: zsh +func (c *Command) MarkFlagDirname(name string) error { + return MarkFlagDirname(c.Flags(), name) +} + +// MarkPersistentFlagDirname instructs the various shell completion +// implementations to complete only directories with this persistent named flag. +// +// Shell Completion compatibility matrix: zsh +func (c *Command) MarkPersistentFlagDirname(name string) error { + return MarkFlagDirname(c.PersistentFlags(), name) +} + +// MarkFlagDirname instructs the various shell completion implementations to +// complete only directories with this specified flag. +// +// Shell Completion compatibility matrix: zsh +func MarkFlagDirname(flags *pflag.FlagSet, name string) error { + zshPattern := "-(/)" + return flags.SetAnnotation(name, zshCompDirname, []string{zshPattern}) +} diff --git a/zsh_completions.go b/zsh_completions.go index 68bb5c6e..12755482 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -16,6 +16,7 @@ const ( zshCompArgumentAnnotation = "cobra_annotations_zsh_completion_argument_annotation" zshCompArgumentFilenameComp = "cobra_annotations_zsh_completion_argument_file_completion" zshCompArgumentWordComp = "cobra_annotations_zsh_completion_argument_word_completion" + zshCompDirname = "cobra_annotations_zsh_dirname" ) var ( @@ -305,16 +306,21 @@ func zshCompGenFlagEntryForMultiOptionFlag(f *pflag.Flag) string { } func zshCompGenFlagEntryExtras(f *pflag.Flag) string { - var extras string + if f.NoOptDefVal != "" { + return "" + } - globs, pathSpecified := f.Annotations[BashCompFilenameExt] - if pathSpecified { - extras = ":filename:_files" - for _, g := range globs { - extras = extras + fmt.Sprintf(` -g "%s"`, g) + extras := ":" // allow options for flag (even without assistance) + for key, values := range f.Annotations { + switch key { + case zshCompDirname: + extras = fmt.Sprintf(":filename:_files -g %q", values[0]) + case BashCompFilenameExt: + extras = ":filename:_files" + for _, pattern := range values { + extras = extras + fmt.Sprintf(` -g "%s"`, pattern) + } } - } else if f.NoOptDefVal == "" { - extras = ":" // allow option variable without assisting } return extras diff --git a/zsh_completions_test.go b/zsh_completions_test.go index 976cbfc2..e53fa886 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -214,6 +214,21 @@ func TestGenZshCompletion(t *testing.T) { `'1: :\("word1" "word2"\)' \\`, }, }, + { + name: "directory completion for flag", + root: func() *Command { + r := genTestCommand("root", true) + r.Flags().String("test", "", "test") + r.PersistentFlags().String("ptest", "", "ptest") + r.MarkFlagDirname("test") + r.MarkPersistentFlagDirname("ptest") + return r + }(), + expectedExpressions: []string{ + `--test\[test]:filename:_files -g "-\(/\)"`, + `--ptest\[ptest]:filename:_files -g "-\(/\)"`, + }, + }, } for _, tc := range tcs { From 21ccc7b307f4a6453e393a0ce1fe40eedbeffa2d Mon Sep 17 00:00:00 2001 From: Jan Kuehle Date: Mon, 17 Dec 2018 23:01:34 +0000 Subject: [PATCH 23/61] Add basic PowerShell completions --- powershell_completions.go | 100 +++++++++++++++++++++++++++ powershell_completions_test.go | 122 +++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 powershell_completions.go create mode 100644 powershell_completions_test.go diff --git a/powershell_completions.go b/powershell_completions.go new file mode 100644 index 00000000..756c61b9 --- /dev/null +++ b/powershell_completions.go @@ -0,0 +1,100 @@ +// PowerShell completions are based on the amazing work from clap: +// https://github.com/clap-rs/clap/blob/3294d18efe5f264d12c9035f404c7d189d4824e1/src/completions/powershell.rs +// +// The generated scripts require PowerShell v5.0+ (which comes Windows 10, but +// can be downloaded separately for windows 7 or 8.1). + +package cobra + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + + "github.com/spf13/pflag" +) + +var powerShellCompletionTemplate = `using namespace System.Management.Automation +using namespace System.Management.Automation.Language +Register-ArgumentCompleter -Native -CommandName '%s' -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + $commandElements = $commandAst.CommandElements + $command = @( + '%s' + for ($i = 1; $i -lt $commandElements.Count; $i++) { + $element = $commandElements[$i] + if ($element -isnot [StringConstantExpressionAst] -or + $element.StringConstantType -ne [StringConstantType]::BareWord -or + $element.Value.StartsWith('-')) { + break + } + $element.Value + } + ) -join ';' + $completions = @(switch ($command) {%s + }) + $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | + Sort-Object -Property ListItemText +}` + +func generatePowerShellSubcommandCases(out io.Writer, cmd *Command, previousCommandName string) { + var cmdName string + if previousCommandName == "" { + cmdName = cmd.Name() + } else { + cmdName = fmt.Sprintf("%s;%s", previousCommandName, cmd.Name()) + } + + fmt.Fprintf(out, "\n '%s' {", cmdName) + + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + if nonCompletableFlag(flag) { + return + } + usage := escapeStringForPowerShell(flag.Usage) + if len(flag.Shorthand) > 0 { + fmt.Fprintf(out, "\n [CompletionResult]::new('-%s', '%s', [CompletionResultType]::ParameterName, '%s')", flag.Shorthand, flag.Shorthand, usage) + } + fmt.Fprintf(out, "\n [CompletionResult]::new('--%s', '%s', [CompletionResultType]::ParameterName, '%s')", flag.Name, flag.Name, usage) + }) + + for _, subCmd := range cmd.Commands() { + usage := escapeStringForPowerShell(subCmd.Short) + fmt.Fprintf(out, "\n [CompletionResult]::new('%s', '%s', [CompletionResultType]::ParameterValue, '%s')", subCmd.Name(), subCmd.Name(), usage) + } + + fmt.Fprint(out, "\n break\n }") + + for _, subCmd := range cmd.Commands() { + generatePowerShellSubcommandCases(out, subCmd, cmdName) + } +} + +func escapeStringForPowerShell(s string) string { + return strings.Replace(s, "'", "''", -1) +} + +// GenPowerShellCompletion generates PowerShell completion file and writes to the passed writer. +func (c *Command) GenPowerShellCompletion(w io.Writer) error { + buf := new(bytes.Buffer) + + var subCommandCases bytes.Buffer + generatePowerShellSubcommandCases(&subCommandCases, c, "") + fmt.Fprintf(buf, powerShellCompletionTemplate, c.Name(), c.Name(), subCommandCases.String()) + + _, err := buf.WriteTo(w) + return err +} + +// GenPowerShellCompletionFile generates PowerShell completion file. +func (c *Command) GenPowerShellCompletionFile(filename string) error { + outFile, err := os.Create(filename) + if err != nil { + return err + } + defer outFile.Close() + + return c.GenPowerShellCompletion(outFile) +} diff --git a/powershell_completions_test.go b/powershell_completions_test.go new file mode 100644 index 00000000..29b609de --- /dev/null +++ b/powershell_completions_test.go @@ -0,0 +1,122 @@ +package cobra + +import ( + "bytes" + "strings" + "testing" +) + +func TestPowerShellCompletion(t *testing.T) { + tcs := []struct { + name string + root *Command + expectedExpressions []string + }{ + { + name: "trivial", + root: &Command{Use: "trivialapp"}, + expectedExpressions: []string{ + "Register-ArgumentCompleter -Native -CommandName 'trivialapp' -ScriptBlock", + "$command = @(\n 'trivialapp'\n", + }, + }, + { + name: "tree", + root: func() *Command { + r := &Command{Use: "tree"} + + sub1 := &Command{Use: "sub1"} + r.AddCommand(sub1) + + sub11 := &Command{Use: "sub11"} + sub12 := &Command{Use: "sub12"} + + sub1.AddCommand(sub11) + sub1.AddCommand(sub12) + + sub2 := &Command{Use: "sub2"} + r.AddCommand(sub2) + + sub21 := &Command{Use: "sub21"} + sub22 := &Command{Use: "sub22"} + + sub2.AddCommand(sub21) + sub2.AddCommand(sub22) + + return r + }(), + expectedExpressions: []string{ + "'tree'", + "[CompletionResult]::new('sub1', 'sub1', [CompletionResultType]::ParameterValue, '')", + "[CompletionResult]::new('sub2', 'sub2', [CompletionResultType]::ParameterValue, '')", + "'tree;sub1'", + "[CompletionResult]::new('sub11', 'sub11', [CompletionResultType]::ParameterValue, '')", + "[CompletionResult]::new('sub12', 'sub12', [CompletionResultType]::ParameterValue, '')", + "'tree;sub1;sub11'", + "'tree;sub1;sub12'", + "'tree;sub2'", + "[CompletionResult]::new('sub21', 'sub21', [CompletionResultType]::ParameterValue, '')", + "[CompletionResult]::new('sub22', 'sub22', [CompletionResultType]::ParameterValue, '')", + "'tree;sub2;sub21'", + "'tree;sub2;sub22'", + }, + }, + { + name: "flags", + root: func() *Command { + r := &Command{Use: "flags"} + r.Flags().StringP("flag1", "a", "", "") + r.Flags().String("flag2", "", "") + + sub1 := &Command{Use: "sub1"} + sub1.Flags().StringP("flag3", "c", "", "") + r.AddCommand(sub1) + + return r + }(), + expectedExpressions: []string{ + "'flags'", + "[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, '')", + "[CompletionResult]::new('--flag1', 'flag1', [CompletionResultType]::ParameterName, '')", + "[CompletionResult]::new('--flag2', 'flag2', [CompletionResultType]::ParameterName, '')", + "[CompletionResult]::new('sub1', 'sub1', [CompletionResultType]::ParameterValue, '')", + "'flags;sub1'", + "[CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, '')", + "[CompletionResult]::new('--flag3', 'flag3', [CompletionResultType]::ParameterName, '')", + }, + }, + { + name: "usage", + root: func() *Command { + r := &Command{Use: "usage"} + r.Flags().String("flag", "", "this describes the usage of the 'flag' flag") + + sub1 := &Command{ + Use: "sub1", + Short: "short describes 'sub1'", + } + r.AddCommand(sub1) + + return r + }(), + expectedExpressions: []string{ + "[CompletionResult]::new('--flag', 'flag', [CompletionResultType]::ParameterName, 'this describes the usage of the ''flag'' flag')", + "[CompletionResult]::new('sub1', 'sub1', [CompletionResultType]::ParameterValue, 'short describes ''sub1''')", + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + buf := new(bytes.Buffer) + tc.root.GenPowerShellCompletion(buf) + output := buf.String() + + for _, expectedExpression := range tc.expectedExpressions { + if !strings.Contains(output, expectedExpression) { + t.Errorf("Expected completion to contain %q somewhere; got %q", expectedExpression, output) + } + } + }) + } +} From d658160bddb021fa7747c7920de931d8ef8e68c3 Mon Sep 17 00:00:00 2001 From: Jan Kuehle Date: Mon, 17 Dec 2018 23:20:45 +0000 Subject: [PATCH 24/61] Add markdown file explaining support for PowerShell --- powershell_completions.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 powershell_completions.md diff --git a/powershell_completions.md b/powershell_completions.md new file mode 100644 index 00000000..afed8024 --- /dev/null +++ b/powershell_completions.md @@ -0,0 +1,14 @@ +# Generating PowerShell Completions For Your Own cobra.Command + +Cobra can generate PowerShell completion scripts. Users need PowerShell version 5.0 or above, which comes with Windows 10 and can be downloaded separately for Windows 7 or 8.1. They can then write the completions to a file and source this file from their PowerShell profile, which is referenced by the `$Profile` environment variable. See `Get-Help about_Profiles` for more info about PowerShell profiles. + +# What's supported + +- Completion for subcommands using their `.Short` description +- Completion for non-hidden flags using their `.Name` and `.Shorthand` + +# What's not yet supported + +- Command aliases +- Required, filename or custom flags (they will work like normal flags) +- Custom completion scripts From 80ea2901b62e9663b6104d03362f5042b57836b7 Mon Sep 17 00:00:00 2001 From: jharshman Date: Tue, 29 Jan 2019 00:46:54 -0800 Subject: [PATCH 25/61] vgo-support - re-working code generator --- cobra/cmd/init.go | 94 ++++++++++++++++++++++++++++++-------------- cobra/cmd/project.go | 7 ++++ cobra/cmd/root.go | 3 +- cobra/tpl/main.go | 17 ++++++++ 4 files changed, 90 insertions(+), 31 deletions(-) create mode 100644 cobra/tpl/main.go diff --git a/cobra/cmd/init.go b/cobra/cmd/init.go index d65e6c8c..f59be065 100644 --- a/cobra/cmd/init.go +++ b/cobra/cmd/init.go @@ -15,19 +15,24 @@ package cmd import ( "fmt" + "github.com/spf13/cobra/cobra/tpl" "os" "path" "path/filepath" + "text/template" "github.com/spf13/cobra" "github.com/spf13/viper" ) -var initCmd = &cobra.Command{ - Use: "init [name]", - Aliases: []string{"initialize", "initialise", "create"}, - Short: "Initialize a Cobra Application", - Long: `Initialize (cobra init) will create a new application, with a license +var ( + pkgName string + + initCmd = &cobra.Command{ + Use: "init [name]", + Aliases: []string{"initialize", "initialise", "create"}, + Short: "Initialize a Cobra Application", + Long: `Initialize (cobra init) will create a new application, with a license and the appropriate structure for a Cobra-based CLI application. * If a name is provided, it will be created in the current directory; @@ -39,37 +44,66 @@ and the appropriate structure for a Cobra-based CLI application. Init will not use an existing directory with contents.`, - Run: func(cmd *cobra.Command, args []string) { - wd, err := os.Getwd() - if err != nil { - er(err) - } + Run: func(cmd *cobra.Command, args []string) { - var project *Project - if len(args) == 0 { - project = NewProjectFromPath(wd) - } else if len(args) == 1 { - arg := args[0] - if arg[0] == '.' { - arg = filepath.Join(wd, arg) + wd, err := os.Getwd() + if err != nil { + er(err) } - if filepath.IsAbs(arg) { - project = NewProjectFromPath(arg) - } else { - project = NewProject(arg) + + project := &Project{ + AbsolutePath: wd, + PkgName: pkgName, + Legal: getLicense(), + Copyright: copyrightLine(), } - } else { - er("please provide only one argument") - } - initializeProject(project) + // open file for writing + f, err := os.Create(fmt.Sprintf("%s/main.go", project.AbsolutePath)) + if err != nil { + er(err) + } + defer f.Close() - fmt.Fprintln(cmd.OutOrStdout(), `Your Cobra application is ready at -`+project.AbsPath()+` + t := template.Must(template.New("init").Parse(string(tpl.MainTemplate()))) + err = t.Execute(f, project) + if err != nil { + er(err) + } + /* + wd, err := os.Getwd() + if err != nil { + er(err) + } -Give it a try by going there and running `+"`go run main.go`."+` -Add commands to it by running `+"`cobra add [cmdname]`.") - }, + var project *Project + if len(args) == 0 { + project = NewProjectFromPath(wd) + } else if len(args) == 1 { + arg := args[0] + if arg[0] == '.' { + arg = filepath.Join(wd, arg) + } + if filepath.IsAbs(arg) { + project = NewProjectFromPath(arg) + } else { + project = NewProject(arg) + } + } else { + er("please provide only one argument") + } + + initializeProject(project) + */ + + fmt.Printf("Your Cobra applicaton is ready at\n%s\n", project.AbsolutePath) + }, + } +) + +func init() { + initCmd.Flags().StringVar(&pkgName, "pkg-name", "", "fully qualified pkg name") + initCmd.MarkFlagRequired("pkg-name") } func initializeProject(project *Project) { diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index 7ddb8258..138a3802 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -9,6 +9,13 @@ import ( // Project contains name, license and paths to projects. type Project struct { + // v2 + PkgName string + Copyright string + AbsolutePath string + Legal License + + // v1 absPath string cmdPath string srcPath string diff --git a/cobra/cmd/root.go b/cobra/cmd/root.go index 19568f98..624c717c 100644 --- a/cobra/cmd/root.go +++ b/cobra/cmd/root.go @@ -23,7 +23,8 @@ import ( var ( // Used for flags. - cfgFile, userLicense string + cfgFile string + userLicense string rootCmd = &cobra.Command{ Use: "cobra", diff --git a/cobra/tpl/main.go b/cobra/tpl/main.go new file mode 100644 index 00000000..af19ac80 --- /dev/null +++ b/cobra/tpl/main.go @@ -0,0 +1,17 @@ +package tpl + +func MainTemplate() []byte { + return []byte(` +/* +{{ .Copyright }} +{{if .Legal.Header}}{{ .Legal.Header }}{{end}} +*/ +package main + +import "{{ .PkgName }}/cmd" + +func main() { + cmd.Execute() +} +`) +} From c356c6491b84c9d61ca6558ab5a3d2efbe0a751f Mon Sep 17 00:00:00 2001 From: jharshman Date: Tue, 29 Jan 2019 00:48:25 -0800 Subject: [PATCH 26/61] add .idea/* to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1b8c7c26..3b053c59 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ tags *.exe cobra.test + +.idea/* From 26d210e2cd929a8d5f57a08cd0391eae58c4c687 Mon Sep 17 00:00:00 2001 From: jharshman Date: Tue, 29 Jan 2019 01:07:11 -0800 Subject: [PATCH 27/61] vgo - fixing up the root template --- cobra/cmd/init.go | 28 +++++++++++--- cobra/tpl/main.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 118 insertions(+), 6 deletions(-) diff --git a/cobra/cmd/init.go b/cobra/cmd/init.go index f59be065..306a45a9 100644 --- a/cobra/cmd/init.go +++ b/cobra/cmd/init.go @@ -56,20 +56,38 @@ Init will not use an existing directory with contents.`, PkgName: pkgName, Legal: getLicense(), Copyright: copyrightLine(), + // viper + // appname } - // open file for writing - f, err := os.Create(fmt.Sprintf("%s/main.go", project.AbsolutePath)) + // create main.go + mainFile, err := os.Create(fmt.Sprintf("%s/main.go", project.AbsolutePath)) if err != nil { er(err) } - defer f.Close() + defer mainFile.Close() - t := template.Must(template.New("init").Parse(string(tpl.MainTemplate()))) - err = t.Execute(f, project) + mainTemplate := template.Must(template.New("main").Parse(string(tpl.MainTemplate()))) + err = mainTemplate.Execute(mainFile, project) if err != nil { er(err) } + + // create cmd/root.go + rootFile, err := os.Create(fmt.Sprintf("%s/cmd/root.go", project.AbsolutePath)) + if err != nil { + er(err) + } + defer rootFile.Close() + + rootTemplate := template.Must(template.New("root").Parse(string(tpl.RootTemplate()))) + err = rootTemplate.Execute(rootFile, project) + if err != nil { + er(err) + } + + createLicenseFile(project.Legal, project.AbsolutePath) + /* wd, err := os.Getwd() if err != nil { diff --git a/cobra/tpl/main.go b/cobra/tpl/main.go index af19ac80..ee2d528b 100644 --- a/cobra/tpl/main.go +++ b/cobra/tpl/main.go @@ -4,7 +4,7 @@ func MainTemplate() []byte { return []byte(` /* {{ .Copyright }} -{{if .Legal.Header}}{{ .Legal.Header }}{{end}} +{{ if .Legal.Header }}{{ .Legal.Header }}{{ end }} */ package main @@ -15,3 +15,97 @@ func main() { } `) } + +func RootTemplate() []byte { + return []byte(` +/* +{{ .Copyright }} +{{ if .Legal.Header }}{{ .Legal.Header }}{{ end }} +*/ +package cmd + +import ( + "fmt" + "os" + "github.com/spf13/cobra" +{{ if .Viper }} + homedir "github.com/mitchellh/go-homedir" + "github.com/spf13/viper" +{{ end }} +) + +{{ if .Viper }} +var cfgFile string +{{ end }} + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "{{ .AppName }}", + Short: "A brief description of your application", + Long: ` + "`" + `A longer description that spans multiple lines and likely contains +examples and usage of using your application. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.` + "`" + `, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { +{{- if .Viper }} + cobra.OnInitialize(initConfig) +{{ end }} + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. +{{ if .Viper }} + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.{{ .AppName }}.yaml)") +{{ else }} + // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.{{ .AppName }}.yaml)") +{{ end }} + + // Cobra also supports local flags, which will only run + // when this action is called directly. + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + +{{ if .Viper }} +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := homedir.Dir() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // Search config in home directory with name ".{{ .appName }}" (without extension). + viper.AddConfigPath(home) + viper.SetConfigName(".{{ .appName }}") + } + + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + } +} +{{ end }} +`) +} From 17dc9f81420b263d94ea687b9503ce7fedbdece8 Mon Sep 17 00:00:00 2001 From: jharshman Date: Tue, 29 Jan 2019 01:19:08 -0800 Subject: [PATCH 28/61] fixing up templates more --- cobra/cmd/init.go | 4 +-- cobra/cmd/project.go | 2 ++ cobra/tpl/main.go | 86 ++++++++++++++++++++++---------------------- 3 files changed, 47 insertions(+), 45 deletions(-) diff --git a/cobra/cmd/init.go b/cobra/cmd/init.go index 306a45a9..02e303f7 100644 --- a/cobra/cmd/init.go +++ b/cobra/cmd/init.go @@ -56,8 +56,8 @@ Init will not use an existing directory with contents.`, PkgName: pkgName, Legal: getLicense(), Copyright: copyrightLine(), - // viper - // appname + Viper: viper.GetBool("useViper"), + AppName: path.Base(pkgName), } // create main.go diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index 138a3802..3f55732e 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -14,6 +14,8 @@ type Project struct { Copyright string AbsolutePath string Legal License + Viper bool + AppName string // v1 absPath string diff --git a/cobra/tpl/main.go b/cobra/tpl/main.go index ee2d528b..435bf6cf 100644 --- a/cobra/tpl/main.go +++ b/cobra/tpl/main.go @@ -11,7 +11,7 @@ package main import "{{ .PkgName }}/cmd" func main() { - cmd.Execute() + cmd.Execute() } `) } @@ -25,12 +25,12 @@ func RootTemplate() []byte { package cmd import ( - "fmt" - "os" + "fmt" + "os" "github.com/spf13/cobra" {{ if .Viper }} - homedir "github.com/mitchellh/go-homedir" - "github.com/spf13/viper" + homedir "github.com/mitchellh/go-homedir" + "github.com/spf13/viper" {{ end }} ) @@ -40,71 +40,71 @@ var cfgFile string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "{{ .AppName }}", - Short: "A brief description of your application", - Long: ` + "`" + `A longer description that spans multiple lines and likely contains + Use: "{{ .AppName }}", + Short: "A brief description of your application", + Long: ` + "`" + `A longer description that spans multiple lines and likely contains examples and usage of using your application. For example: Cobra is a CLI library for Go that empowers applications. This application is a tool to generate the needed files to quickly create a Cobra application.` + "`" + `, - // Uncomment the following line if your bare application - // has an action associated with it: - // Run: func(cmd *cobra.Command, args []string) { }, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, } // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } } func init() { {{- if .Viper }} - cobra.OnInitialize(initConfig) + cobra.OnInitialize(initConfig) {{ end }} - // Here you will define your flags and configuration settings. - // Cobra supports persistent flags, which, if defined here, - // will be global for your application. + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. {{ if .Viper }} - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.{{ .AppName }}.yaml)") + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.{{ .AppName }}.yaml)") {{ else }} - // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.{{ .AppName }}.yaml)") + // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.{{ .AppName }}.yaml)") {{ end }} - // Cobra also supports local flags, which will only run - // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + // Cobra also supports local flags, which will only run + // when this action is called directly. + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } {{ if .Viper }} // initConfig reads in config file and ENV variables if set. func initConfig() { - if cfgFile != "" { - // Use config file from the flag. - viper.SetConfigFile(cfgFile) - } else { - // Find home directory. - home, err := homedir.Dir() - if err != nil { - fmt.Println(err) - os.Exit(1) - } + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := homedir.Dir() + if err != nil { + fmt.Println(err) + os.Exit(1) + } - // Search config in home directory with name ".{{ .appName }}" (without extension). - viper.AddConfigPath(home) - viper.SetConfigName(".{{ .appName }}") - } + // Search config in home directory with name ".{{ .AppName }}" (without extension). + viper.AddConfigPath(home) + viper.SetConfigName(".{{ .AppName }}") + } - viper.AutomaticEnv() // read in environment variables that match + viper.AutomaticEnv() // read in environment variables that match - // If a config file is found, read it in. - if err := viper.ReadInConfig(); err == nil { - fmt.Println("Using config file:", viper.ConfigFileUsed()) - } + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + } } {{ end }} `) From 69420a9ffa803f0f0990be1fb8ea9ec69fd5fb69 Mon Sep 17 00:00:00 2001 From: jharshman Date: Tue, 29 Jan 2019 19:11:26 -0800 Subject: [PATCH 29/61] vgo - create directory --- cobra/cmd/init.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cobra/cmd/init.go b/cobra/cmd/init.go index 02e303f7..c334121a 100644 --- a/cobra/cmd/init.go +++ b/cobra/cmd/init.go @@ -74,6 +74,9 @@ Init will not use an existing directory with contents.`, } // create cmd/root.go + if _, err = os.Stat(fmt.Sprintf("%s/cmd", project.AbsolutePath)); os.IsNotExist(err) { + os.Mkdir("cmd", 0751) + } rootFile, err := os.Create(fmt.Sprintf("%s/cmd/root.go", project.AbsolutePath)) if err != nil { er(err) From abab9aa52a698bb6fcb945b10968695ec52379aa Mon Sep 17 00:00:00 2001 From: jharshman Date: Tue, 29 Jan 2019 19:34:11 -0800 Subject: [PATCH 30/61] vgo - add Create method to Project struct --- cobra/cmd/init.go | 62 +++----------------------------------------- cobra/cmd/project.go | 39 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 59 deletions(-) diff --git a/cobra/cmd/init.go b/cobra/cmd/init.go index c334121a..2e5bc4e9 100644 --- a/cobra/cmd/init.go +++ b/cobra/cmd/init.go @@ -15,14 +15,11 @@ package cmd import ( "fmt" - "github.com/spf13/cobra/cobra/tpl" + "github.com/spf13/cobra" + "github.com/spf13/viper" "os" "path" "path/filepath" - "text/template" - - "github.com/spf13/cobra" - "github.com/spf13/viper" ) var ( @@ -60,62 +57,9 @@ Init will not use an existing directory with contents.`, AppName: path.Base(pkgName), } - // create main.go - mainFile, err := os.Create(fmt.Sprintf("%s/main.go", project.AbsolutePath)) - if err != nil { + if err := project.Create(); err != nil { er(err) } - defer mainFile.Close() - - mainTemplate := template.Must(template.New("main").Parse(string(tpl.MainTemplate()))) - err = mainTemplate.Execute(mainFile, project) - if err != nil { - er(err) - } - - // create cmd/root.go - if _, err = os.Stat(fmt.Sprintf("%s/cmd", project.AbsolutePath)); os.IsNotExist(err) { - os.Mkdir("cmd", 0751) - } - rootFile, err := os.Create(fmt.Sprintf("%s/cmd/root.go", project.AbsolutePath)) - if err != nil { - er(err) - } - defer rootFile.Close() - - rootTemplate := template.Must(template.New("root").Parse(string(tpl.RootTemplate()))) - err = rootTemplate.Execute(rootFile, project) - if err != nil { - er(err) - } - - createLicenseFile(project.Legal, project.AbsolutePath) - - /* - wd, err := os.Getwd() - if err != nil { - er(err) - } - - var project *Project - if len(args) == 0 { - project = NewProjectFromPath(wd) - } else if len(args) == 1 { - arg := args[0] - if arg[0] == '.' { - arg = filepath.Join(wd, arg) - } - if filepath.IsAbs(arg) { - project = NewProjectFromPath(arg) - } else { - project = NewProject(arg) - } - } else { - er("please provide only one argument") - } - - initializeProject(project) - */ fmt.Printf("Your Cobra applicaton is ready at\n%s\n", project.AbsolutePath) }, diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index 3f55732e..cf66e83e 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -1,10 +1,13 @@ package cmd import ( + "fmt" + "github.com/spf13/cobra/cobra/tpl" "os" "path/filepath" "runtime" "strings" + "text/template" ) // Project contains name, license and paths to projects. @@ -25,6 +28,42 @@ type Project struct { name string } +func (p *Project) Create() error { + + // create main.go + mainFile, err := os.Create(fmt.Sprintf("%s/main.go", p.AbsolutePath)) + if err != nil { + return err + } + defer mainFile.Close() + + mainTemplate := template.Must(template.New("main").Parse(string(tpl.MainTemplate()))) + err = mainTemplate.Execute(mainFile, p) + if err != nil { + return err + } + + // create cmd/root.go + if _, err = os.Stat(fmt.Sprintf("%s/cmd", p.AbsolutePath)); os.IsNotExist(err) { + os.Mkdir("cmd", 0751) + } + rootFile, err := os.Create(fmt.Sprintf("%s/cmd/root.go", p.AbsolutePath)) + if err != nil { + return err + } + defer rootFile.Close() + + rootTemplate := template.Must(template.New("root").Parse(string(tpl.RootTemplate()))) + err = rootTemplate.Execute(rootFile, p) + if err != nil { + return err + } + + // create license + createLicenseFile(p.Legal, p.AbsolutePath) + return nil +} + // NewProject returns Project with specified project name. func NewProject(projectName string) *Project { if projectName == "" { From 5b1685faaa2b08aec83e0ea49d00e0a49735a773 Mon Sep 17 00:00:00 2001 From: jharshman Date: Tue, 29 Jan 2019 20:25:38 -0800 Subject: [PATCH 31/61] vgo - generate license --- cobra/cmd/project.go | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index cf66e83e..ab349830 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -60,10 +60,39 @@ func (p *Project) Create() error { } // create license - createLicenseFile(p.Legal, p.AbsolutePath) - return nil + return createLicenseFile(p.Legal, p.AbsolutePath) } +func (p *Project) createLicenseFile() error { + data := map[string]interface{}{ + "copyright": copyrightLine(), + } + licenseFile, err := os.Create(fmt.Sprintf("%s/LICENSE", p.AbsolutePath)) + if err != nil { + return err + } + + licenseTemplate := template.Must(template.New("license").Parse(p.Legal.Text)) + return licenseTemplate.Execute(licenseFile, data) +} + +//func createLicenseFile(license License, path string) { +// data := make(map[string]interface{}) +// data["copyright"] = copyrightLine() +// +// // Generate license template from text and data. +// text, err := executeTemplate(license.Text, data) +// if err != nil { +// er(err) +// } +// +// // Write license text to LICENSE file. +// err = writeStringToFile(filepath.Join(path, "LICENSE"), text) +// if err != nil { +// er(err) +// } +//} + // NewProject returns Project with specified project name. func NewProject(projectName string) *Project { if projectName == "" { From 91dbcb7ffee662057bde52378f2a79381b4aef6b Mon Sep 17 00:00:00 2001 From: jharshman Date: Tue, 29 Jan 2019 20:26:08 -0800 Subject: [PATCH 32/61] remove commented code --- cobra/cmd/project.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index ab349830..1b97c3d6 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -76,23 +76,6 @@ func (p *Project) createLicenseFile() error { return licenseTemplate.Execute(licenseFile, data) } -//func createLicenseFile(license License, path string) { -// data := make(map[string]interface{}) -// data["copyright"] = copyrightLine() -// -// // Generate license template from text and data. -// text, err := executeTemplate(license.Text, data) -// if err != nil { -// er(err) -// } -// -// // Write license text to LICENSE file. -// err = writeStringToFile(filepath.Join(path, "LICENSE"), text) -// if err != nil { -// er(err) -// } -//} - // NewProject returns Project with specified project name. func NewProject(projectName string) *Project { if projectName == "" { From 44c2d482f6512000718de39a3bbabb5aec856d56 Mon Sep 17 00:00:00 2001 From: jharshman Date: Tue, 29 Jan 2019 20:27:46 -0800 Subject: [PATCH 33/61] fix calling to createLicenseFile --- cobra/cmd/project.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index 1b97c3d6..f3bc9eb8 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -60,7 +60,7 @@ func (p *Project) Create() error { } // create license - return createLicenseFile(p.Legal, p.AbsolutePath) + return p. createLicenseFile() } func (p *Project) createLicenseFile() error { From 73b5215dc72c245abe81cc72b7c1adfc1ebfab70 Mon Sep 17 00:00:00 2001 From: jharshman Date: Tue, 29 Jan 2019 20:28:47 -0800 Subject: [PATCH 34/61] vgo - fix format --- cobra/cmd/project.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index f3bc9eb8..47c63abf 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -60,7 +60,7 @@ func (p *Project) Create() error { } // create license - return p. createLicenseFile() + return p.createLicenseFile() } func (p *Project) createLicenseFile() error { From 4c22a20fd45b379d3e9b9223dd76445e269199e4 Mon Sep 17 00:00:00 2001 From: jharshman Date: Tue, 29 Jan 2019 20:35:27 -0800 Subject: [PATCH 35/61] vgo - remove unused methods --- cobra/cmd/init.go | 164 +--------------------------------------------- 1 file changed, 1 insertion(+), 163 deletions(-) diff --git a/cobra/cmd/init.go b/cobra/cmd/init.go index 2e5bc4e9..b5802e96 100644 --- a/cobra/cmd/init.go +++ b/cobra/cmd/init.go @@ -19,7 +19,6 @@ import ( "github.com/spf13/viper" "os" "path" - "path/filepath" ) var ( @@ -69,165 +68,4 @@ Init will not use an existing directory with contents.`, func init() { initCmd.Flags().StringVar(&pkgName, "pkg-name", "", "fully qualified pkg name") initCmd.MarkFlagRequired("pkg-name") -} - -func initializeProject(project *Project) { - if !exists(project.AbsPath()) { // If path doesn't yet exist, create it - err := os.MkdirAll(project.AbsPath(), os.ModePerm) - if err != nil { - er(err) - } - } else if !isEmpty(project.AbsPath()) { // If path exists and is not empty don't use it - er("Cobra will not create a new project in a non empty directory: " + project.AbsPath()) - } - - // We have a directory and it's empty. Time to initialize it. - createLicenseFile(project.License(), project.AbsPath()) - createMainFile(project) - createRootCmdFile(project) -} - -func createLicenseFile(license License, path string) { - data := make(map[string]interface{}) - data["copyright"] = copyrightLine() - - // Generate license template from text and data. - text, err := executeTemplate(license.Text, data) - if err != nil { - er(err) - } - - // Write license text to LICENSE file. - err = writeStringToFile(filepath.Join(path, "LICENSE"), text) - if err != nil { - er(err) - } -} - -func createMainFile(project *Project) { - mainTemplate := `{{ comment .copyright }} -{{if .license}}{{ comment .license }}{{end}} - -package main - -import "{{ .importpath }}" - -func main() { - cmd.Execute() -} -` - data := make(map[string]interface{}) - data["copyright"] = copyrightLine() - data["license"] = project.License().Header - data["importpath"] = path.Join(project.Name(), filepath.Base(project.CmdPath())) - - mainScript, err := executeTemplate(mainTemplate, data) - if err != nil { - er(err) - } - - err = writeStringToFile(filepath.Join(project.AbsPath(), "main.go"), mainScript) - if err != nil { - er(err) - } -} - -func createRootCmdFile(project *Project) { - template := `{{comment .copyright}} -{{if .license}}{{comment .license}}{{end}} - -package cmd - -import ( - "fmt" - "os" -{{if .viper}} - homedir "github.com/mitchellh/go-homedir"{{end}} - "github.com/spf13/cobra"{{if .viper}} - "github.com/spf13/viper"{{end}} -){{if .viper}} - -var cfgFile string{{end}} - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "{{.appName}}", - Short: "A brief description of your application", - Long: ` + "`" + `A longer description that spans multiple lines and likely contains -examples and usage of using your application. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.` + "`" + `, - // Uncomment the following line if your bare application - // has an action associated with it: - // Run: func(cmd *cobra.Command, args []string) { }, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -func init() { {{- if .viper}} - cobra.OnInitialize(initConfig) -{{end}} - // Here you will define your flags and configuration settings. - // Cobra supports persistent flags, which, if defined here, - // will be global for your application.{{ if .viper }} - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.{{ .appName }}.yaml)"){{ else }} - // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.{{ .appName }}.yaml)"){{ end }} - - // Cobra also supports local flags, which will only run - // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -}{{ if .viper }} - -// initConfig reads in config file and ENV variables if set. -func initConfig() { - if cfgFile != "" { - // Use config file from the flag. - viper.SetConfigFile(cfgFile) - } else { - // Find home directory. - home, err := homedir.Dir() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - // Search config in home directory with name ".{{ .appName }}" (without extension). - viper.AddConfigPath(home) - viper.SetConfigName(".{{ .appName }}") - } - - viper.AutomaticEnv() // read in environment variables that match - - // If a config file is found, read it in. - if err := viper.ReadInConfig(); err == nil { - fmt.Println("Using config file:", viper.ConfigFileUsed()) - } -}{{ end }} -` - - data := make(map[string]interface{}) - data["copyright"] = copyrightLine() - data["viper"] = viper.GetBool("useViper") - data["license"] = project.License().Header - data["appName"] = path.Base(project.Name()) - - rootCmdScript, err := executeTemplate(template, data) - if err != nil { - er(err) - } - - err = writeStringToFile(filepath.Join(project.CmdPath(), "root.go"), rootCmdScript) - if err != nil { - er(err) - } - -} +} \ No newline at end of file From c3b51f3a2e0ea821c0cb8ed8e5850e98394641c5 Mon Sep 17 00:00:00 2001 From: jharshman Date: Tue, 29 Jan 2019 23:41:41 -0800 Subject: [PATCH 36/61] simplify test --- cobra/cmd/add_test.go | 12 ++---------- cobra/cmd/init.go | 2 +- cobra/cmd/init_test.go | 23 ++++++++++++++++++++++- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/cobra/cmd/add_test.go b/cobra/cmd/add_test.go index b920e2b9..94497084 100644 --- a/cobra/cmd/add_test.go +++ b/cobra/cmd/add_test.go @@ -1,15 +1,6 @@ package cmd -import ( - "errors" - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/spf13/viper" -) - +/* // TestGoldenAddCmd initializes the project "github.com/spf13/testproject" // in GOPATH, adds "test" command // and compares the content of all files in cmd directory of testproject @@ -107,3 +98,4 @@ func TestValidateCmdName(t *testing.T) { } } } +*/ diff --git a/cobra/cmd/init.go b/cobra/cmd/init.go index b5802e96..25377558 100644 --- a/cobra/cmd/init.go +++ b/cobra/cmd/init.go @@ -68,4 +68,4 @@ Init will not use an existing directory with contents.`, func init() { initCmd.Flags().StringVar(&pkgName, "pkg-name", "", "fully qualified pkg name") initCmd.MarkFlagRequired("pkg-name") -} \ No newline at end of file +} diff --git a/cobra/cmd/init_test.go b/cobra/cmd/init_test.go index 40eb4038..f61a139f 100644 --- a/cobra/cmd/init_test.go +++ b/cobra/cmd/init_test.go @@ -10,11 +10,32 @@ import ( "github.com/spf13/viper" ) +func TestGoldenInitCmd(t *testing.T) { + wd, _ := os.Getwd() + project := &Project{ + AbsolutePath: wd, + PkgName: "github.com/spf13/testproject", + Legal: getLicense(), + Viper: true, + AppName: "testproject", + } + + err := project.Create() + if err != nil { + t.Fatal(err) + } + + //expectedFiles := []string{"LICENSE", "main.go", "cmd/root.go"} + //for _, f := range expectedFiles { + // // read each file and compare with corresponding golden file + //} +} + // TestGoldenInitCmd initializes the project "github.com/spf13/testproject" // in GOPATH and compares the content of files in initialized project with // appropriate golden files ("testdata/*.golden"). // Use -update to update existing golden files. -func TestGoldenInitCmd(t *testing.T) { +func TTestGoldenInitCmd(t *testing.T) { projectName := "github.com/spf13/testproject" project := NewProject(projectName) defer os.RemoveAll(project.AbsPath()) From 04af6aed80d3a451ea8de88881be0239804d5f42 Mon Sep 17 00:00:00 2001 From: jharshman Date: Wed, 30 Jan 2019 00:02:51 -0800 Subject: [PATCH 37/61] vgo - add todo --- cobra/cmd/init.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cobra/cmd/init.go b/cobra/cmd/init.go index 25377558..a6abd1c3 100644 --- a/cobra/cmd/init.go +++ b/cobra/cmd/init.go @@ -47,6 +47,10 @@ Init will not use an existing directory with contents.`, er(err) } + // todo: + // if . use current directory + // else create named directory and set wd to that + project := &Project{ AbsolutePath: wd, PkgName: pkgName, From e993d53002aa6dca9d85ed6d8633a6984c3eafb1 Mon Sep 17 00:00:00 2001 From: jharshman Date: Wed, 30 Jan 2019 00:20:25 -0800 Subject: [PATCH 38/61] vgo - take named directory or current wd --- cobra/cmd/init.go | 8 +++++--- cobra/cmd/project.go | 10 +++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/cobra/cmd/init.go b/cobra/cmd/init.go index a6abd1c3..63397d11 100644 --- a/cobra/cmd/init.go +++ b/cobra/cmd/init.go @@ -47,9 +47,11 @@ Init will not use an existing directory with contents.`, er(err) } - // todo: - // if . use current directory - // else create named directory and set wd to that + if len(args) > 0 { + if args[0] != "." { + wd = fmt.Sprintf("%s/%s", wd, args[0]) + } + } project := &Project{ AbsolutePath: wd, diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index 47c63abf..34dea56f 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -30,6 +30,14 @@ type Project struct { func (p *Project) Create() error { + // check if AbsolutePath exists + if _, err := os.Stat(p.AbsolutePath); os.IsNotExist(err) { + // create directory + if err := os.Mkdir(p.AbsolutePath, 0754); err != nil { + return err + } + } + // create main.go mainFile, err := os.Create(fmt.Sprintf("%s/main.go", p.AbsolutePath)) if err != nil { @@ -45,7 +53,7 @@ func (p *Project) Create() error { // create cmd/root.go if _, err = os.Stat(fmt.Sprintf("%s/cmd", p.AbsolutePath)); os.IsNotExist(err) { - os.Mkdir("cmd", 0751) + os.Mkdir(fmt.Sprintf("%s/cmd", p.AbsolutePath), 0751) } rootFile, err := os.Create(fmt.Sprintf("%s/cmd/root.go", p.AbsolutePath)) if err != nil { From 642c3c7a0edbb0242be3f380808df5474e377291 Mon Sep 17 00:00:00 2001 From: jharshman Date: Wed, 30 Jan 2019 00:33:51 -0800 Subject: [PATCH 39/61] vgo - compare generated files against golden files --- cobra/cmd/init_test.go | 86 ++++-------------------------------------- 1 file changed, 8 insertions(+), 78 deletions(-) diff --git a/cobra/cmd/init_test.go b/cobra/cmd/init_test.go index f61a139f..f96b6575 100644 --- a/cobra/cmd/init_test.go +++ b/cobra/cmd/init_test.go @@ -1,19 +1,16 @@ package cmd import ( - "errors" - "io/ioutil" + "fmt" "os" "path/filepath" "testing" - - "github.com/spf13/viper" ) func TestGoldenInitCmd(t *testing.T) { wd, _ := os.Getwd() project := &Project{ - AbsolutePath: wd, + AbsolutePath: fmt.Sprintf("%s/testproject", wd), PkgName: "github.com/spf13/testproject", Legal: getLicense(), Viper: true, @@ -25,80 +22,13 @@ func TestGoldenInitCmd(t *testing.T) { t.Fatal(err) } - //expectedFiles := []string{"LICENSE", "main.go", "cmd/root.go"} - //for _, f := range expectedFiles { - // // read each file and compare with corresponding golden file - //} -} - -// TestGoldenInitCmd initializes the project "github.com/spf13/testproject" -// in GOPATH and compares the content of files in initialized project with -// appropriate golden files ("testdata/*.golden"). -// Use -update to update existing golden files. -func TTestGoldenInitCmd(t *testing.T) { - projectName := "github.com/spf13/testproject" - project := NewProject(projectName) - defer os.RemoveAll(project.AbsPath()) - - viper.Set("author", "NAME HERE ") - viper.Set("license", "apache") - viper.Set("year", 2017) - defer viper.Set("author", nil) - defer viper.Set("license", nil) - defer viper.Set("year", nil) - - os.Args = []string{"cobra", "init", projectName} - if err := rootCmd.Execute(); err != nil { - t.Fatal("Error by execution:", err) - } - - expectedFiles := []string{".", "cmd", "LICENSE", "main.go", "cmd/root.go"} - gotFiles := []string{} - - // Check project file hierarchy and compare the content of every single file - // with appropriate golden file. - err := filepath.Walk(project.AbsPath(), func(path string, info os.FileInfo, err error) error { + expectedFiles := []string{"LICENSE", "main.go", "cmd/root.go"} + for _, f := range expectedFiles { + generatedFile := fmt.Sprintf("%s/%s", project.AbsolutePath, f) + goldenFile := fmt.Sprintf("testdata/%s.golden", filepath.Base(f)) + err := compareFiles(generatedFile, goldenFile) if err != nil { - return err + t.Fatal(err) } - - // Make path relative to project.AbsPath(). - // E.g. path = "/home/user/go/src/github.com/spf13/testproject/cmd/root.go" - // then it returns just "cmd/root.go". - relPath, err := filepath.Rel(project.AbsPath(), path) - if err != nil { - return err - } - relPath = filepath.ToSlash(relPath) - gotFiles = append(gotFiles, relPath) - goldenPath := filepath.Join("testdata", filepath.Base(path)+".golden") - - switch relPath { - // Known directories. - case ".", "cmd": - return nil - // Known files. - case "LICENSE", "main.go", "cmd/root.go": - if *update { - got, err := ioutil.ReadFile(path) - if err != nil { - return err - } - if err := ioutil.WriteFile(goldenPath, got, 0644); err != nil { - t.Fatal("Error while updating file:", err) - } - } - return compareFiles(path, goldenPath) - } - // Unknown file. - return errors.New("unknown file: " + path) - }) - if err != nil { - t.Fatal(err) - } - - // Check if some files lack. - if err := checkLackFiles(expectedFiles, gotFiles); err != nil { - t.Fatal(err) } } From 50665e99933b63952bde8df17bfe2113ab6dbe44 Mon Sep 17 00:00:00 2001 From: jharshman Date: Wed, 30 Jan 2019 00:46:53 -0800 Subject: [PATCH 40/61] vgo - update golden templates --- cobra/cmd/init_test.go | 1 + cobra/cmd/testdata/main.go.golden | 29 ++++---- cobra/cmd/testdata/root.go.golden | 118 ++++++++++++++++-------------- cobra/tpl/main.go | 6 +- 4 files changed, 81 insertions(+), 73 deletions(-) diff --git a/cobra/cmd/init_test.go b/cobra/cmd/init_test.go index f96b6575..77145fcb 100644 --- a/cobra/cmd/init_test.go +++ b/cobra/cmd/init_test.go @@ -13,6 +13,7 @@ func TestGoldenInitCmd(t *testing.T) { AbsolutePath: fmt.Sprintf("%s/testproject", wd), PkgName: "github.com/spf13/testproject", Legal: getLicense(), + Copyright: copyrightLine(), Viper: true, AppName: "testproject", } diff --git a/cobra/cmd/testdata/main.go.golden b/cobra/cmd/testdata/main.go.golden index cdbe38d7..4ad570c5 100644 --- a/cobra/cmd/testdata/main.go.golden +++ b/cobra/cmd/testdata/main.go.golden @@ -1,21 +1,22 @@ -// Copyright © 2017 NAME HERE -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/* +Copyright © 2019 NAME HERE +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package main import "github.com/spf13/testproject/cmd" func main() { - cmd.Execute() + cmd.Execute() } diff --git a/cobra/cmd/testdata/root.go.golden b/cobra/cmd/testdata/root.go.golden index d74f4cd4..d3b889ba 100644 --- a/cobra/cmd/testdata/root.go.golden +++ b/cobra/cmd/testdata/root.go.golden @@ -1,89 +1,97 @@ -// Copyright © 2017 NAME HERE -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/* +Copyright © 2019 NAME HERE +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package cmd import ( - "fmt" - "os" + "fmt" + "os" + "github.com/spf13/cobra" + + homedir "github.com/mitchellh/go-homedir" + "github.com/spf13/viper" - homedir "github.com/mitchellh/go-homedir" - "github.com/spf13/cobra" - "github.com/spf13/viper" ) + var cfgFile string + // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "testproject", - Short: "A brief description of your application", - Long: `A longer description that spans multiple lines and likely contains + Use: "testproject", + Short: "A brief description of your application", + Long: `A longer description that spans multiple lines and likely contains examples and usage of using your application. For example: Cobra is a CLI library for Go that empowers applications. This application is a tool to generate the needed files to quickly create a Cobra application.`, - // Uncomment the following line if your bare application - // has an action associated with it: - // Run: func(cmd *cobra.Command, args []string) { }, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, } // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } } func init() { - cobra.OnInitialize(initConfig) + cobra.OnInitialize(initConfig) - // Here you will define your flags and configuration settings. - // Cobra supports persistent flags, which, if defined here, - // will be global for your application. - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.testproject.yaml)") + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. - // Cobra also supports local flags, which will only run - // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.testproject.yaml)") + + + // Cobra also supports local flags, which will only run + // when this action is called directly. + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } + // initConfig reads in config file and ENV variables if set. func initConfig() { - if cfgFile != "" { - // Use config file from the flag. - viper.SetConfigFile(cfgFile) - } else { - // Find home directory. - home, err := homedir.Dir() - if err != nil { - fmt.Println(err) - os.Exit(1) - } + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := homedir.Dir() + if err != nil { + fmt.Println(err) + os.Exit(1) + } - // Search config in home directory with name ".testproject" (without extension). - viper.AddConfigPath(home) - viper.SetConfigName(".testproject") - } + // Search config in home directory with name ".testproject" (without extension). + viper.AddConfigPath(home) + viper.SetConfigName(".testproject") + } - viper.AutomaticEnv() // read in environment variables that match + viper.AutomaticEnv() // read in environment variables that match - // If a config file is found, read it in. - if err := viper.ReadInConfig(); err == nil { - fmt.Println("Using config file:", viper.ConfigFileUsed()) - } + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + } } + diff --git a/cobra/tpl/main.go b/cobra/tpl/main.go index 435bf6cf..b62d0265 100644 --- a/cobra/tpl/main.go +++ b/cobra/tpl/main.go @@ -1,8 +1,7 @@ package tpl func MainTemplate() []byte { - return []byte(` -/* + return []byte(`/* {{ .Copyright }} {{ if .Legal.Header }}{{ .Legal.Header }}{{ end }} */ @@ -17,8 +16,7 @@ func main() { } func RootTemplate() []byte { - return []byte(` -/* + return []byte(`/* {{ .Copyright }} {{ if .Legal.Header }}{{ .Legal.Header }}{{ end }} */ From 3741457400aa1a40bac77971975dc4d99360764e Mon Sep 17 00:00:00 2001 From: jharshman Date: Wed, 30 Jan 2019 00:58:29 -0800 Subject: [PATCH 41/61] add CommandTemplate --- cobra/cmd/project.go | 2 ++ cobra/tpl/main.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index 34dea56f..789a2490 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -19,6 +19,8 @@ type Project struct { Legal License Viper bool AppName string + CmdName string + CmdParent string // v1 absPath string diff --git a/cobra/tpl/main.go b/cobra/tpl/main.go index b62d0265..1b8ff10a 100644 --- a/cobra/tpl/main.go +++ b/cobra/tpl/main.go @@ -107,3 +107,47 @@ func initConfig() { {{ end }} `) } + +func AddCommandTemplate() []byte { + return []byte(`/* +{{ .Copyright }} +{{ if .Legal.Header }}{{ .Legal.Header }}{{ end }} +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// {{ .CmdName }}Cmd represents the {{ .CmdName }} command +var {{ .CmdName }}Cmd = &cobra.Command{ + Use: "{{ .CmdName }}", + Short: "A brief description of your command", + Long: ` + "`" + `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.` + "`" + `, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("{{ .CmdName }} called") + }, +} + +func init() { + {{ .CmdParent }}.AddCommand({{ .CmdName }}Cmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // {{ .CmdName }}Cmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // {{ .CmdName }}Cmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} +`) +} \ No newline at end of file From c7ac101cf82f9a5660532cdd00fb5e908b359c29 Mon Sep 17 00:00:00 2001 From: jharshman Date: Wed, 30 Jan 2019 01:24:26 -0800 Subject: [PATCH 42/61] vgo - fixing up the add op to work with vgo --- cobra/cmd/add.go | 129 +++++++++++++++---------------------------- cobra/cmd/project.go | 12 +++- cobra/tpl/main.go | 2 +- 3 files changed, 54 insertions(+), 89 deletions(-) diff --git a/cobra/cmd/add.go b/cobra/cmd/add.go index fb22096a..ca19cb71 100644 --- a/cobra/cmd/add.go +++ b/cobra/cmd/add.go @@ -16,24 +16,21 @@ package cmd import ( "fmt" "os" - "path/filepath" + "path" "unicode" "github.com/spf13/cobra" ) -func init() { - addCmd.Flags().StringVarP(&packageName, "package", "t", "", "target package name (e.g. github.com/spf13/hugo)") - addCmd.Flags().StringVarP(&parentName, "parent", "p", "rootCmd", "variable name of parent command for this command") -} +var ( + packageName string + parentName string -var packageName, parentName string - -var addCmd = &cobra.Command{ - Use: "add [command name]", - Aliases: []string{"command"}, - Short: "Add a command to a Cobra Application", - Long: `Add (cobra add) will create a new command, with a license and + addCmd = &cobra.Command{ + Use: "add [command name]", + Aliases: []string{"command"}, + Short: "Add a command to a Cobra Application", + Long: `Add (cobra add) will create a new command, with a license and the appropriate structure for a Cobra-based CLI application, and register it to its parent (default rootCmd). @@ -42,28 +39,47 @@ with an initial uppercase letter. Example: cobra add server -> resulting in a new cmd/server.go`, - Run: func(cmd *cobra.Command, args []string) { - if len(args) < 1 { - er("add needs a name for the command") - } + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 1 { + er("add needs a name for the command") + } + + commandName := validateCmdName(args[0]) + + if packageName == "" { + // derive packageName + } - var project *Project - if packageName != "" { - project = NewProject(packageName) - } else { wd, err := os.Getwd() if err != nil { er(err) } - project = NewProjectFromPath(wd) - } - cmdName := validateCmdName(args[0]) - cmdPath := filepath.Join(project.CmdPath(), cmdName+".go") - createCmdFile(project.License(), cmdPath, cmdName) + command := &Command{ + CmdName: commandName, + CmdParent: parentName, + Project: &Project{ + AbsolutePath: fmt.Sprintf("%s/cmd", wd), + AppName: path.Base(packageName), + PkgName: packageName, + Legal: getLicense(), + Copyright: copyrightLine(), + }, + } - fmt.Fprintln(cmd.OutOrStdout(), cmdName, "created at", cmdPath) - }, + err = command.Create() + if err != nil { + er(err) + } + + fmt.Printf("%s created at %s", command.CmdName, command.Project.AbsolutePath) + }, + } +) + +func init() { + addCmd.Flags().StringVarP(&packageName, "package", "t", "", "target package name (e.g. github.com/spf13/hugo)") + addCmd.Flags().StringVarP(&parentName, "parent", "p", "rootCmd", "variable name of parent command for this command") } // validateCmdName returns source without any dashes and underscore. @@ -118,62 +134,3 @@ func validateCmdName(source string) string { } return output } - -func createCmdFile(license License, path, cmdName string) { - template := `{{comment .copyright}} -{{if .license}}{{comment .license}}{{end}} - -package {{.cmdPackage}} - -import ( - "fmt" - - "github.com/spf13/cobra" -) - -// {{.cmdName}}Cmd represents the {{.cmdName}} command -var {{.cmdName}}Cmd = &cobra.Command{ - Use: "{{.cmdName}}", - Short: "A brief description of your command", - Long: ` + "`" + `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.` + "`" + `, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("{{.cmdName}} called") - }, -} - -func init() { - {{.parentName}}.AddCommand({{.cmdName}}Cmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // {{.cmdName}}Cmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // {{.cmdName}}Cmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} -` - - data := make(map[string]interface{}) - data["copyright"] = copyrightLine() - data["license"] = license.Header - data["cmdPackage"] = filepath.Base(filepath.Dir(path)) // last dir of path - data["parentName"] = parentName - data["cmdName"] = cmdName - - cmdScript, err := executeTemplate(template, data) - if err != nil { - er(err) - } - err = writeStringToFile(path, cmdScript) - if err != nil { - er(err) - } -} diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index 789a2490..fe9ea319 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -19,8 +19,6 @@ type Project struct { Legal License Viper bool AppName string - CmdName string - CmdParent string // v1 absPath string @@ -30,6 +28,12 @@ type Project struct { name string } +type Command struct { + CmdName string + CmdParent string + *Project +} + func (p *Project) Create() error { // check if AbsolutePath exists @@ -86,6 +90,10 @@ func (p *Project) createLicenseFile() error { return licenseTemplate.Execute(licenseFile, data) } +func (c *Command) Create() error { + return nil +} + // NewProject returns Project with specified project name. func NewProject(projectName string) *Project { if projectName == "" { diff --git a/cobra/tpl/main.go b/cobra/tpl/main.go index 1b8ff10a..ba4d7cc5 100644 --- a/cobra/tpl/main.go +++ b/cobra/tpl/main.go @@ -150,4 +150,4 @@ func init() { // {{ .CmdName }}Cmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } `) -} \ No newline at end of file +} From 732e4db0d43f782e8b3b576c433db2b811171386 Mon Sep 17 00:00:00 2001 From: jharshman Date: Wed, 30 Jan 2019 01:33:04 -0800 Subject: [PATCH 43/61] vgo - trim some uneeded data from struct --- cobra/cmd/add.go | 9 ++++----- cobra/cmd/project.go | 12 +++++++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cobra/cmd/add.go b/cobra/cmd/add.go index ca19cb71..806a7cc5 100644 --- a/cobra/cmd/add.go +++ b/cobra/cmd/add.go @@ -16,7 +16,6 @@ package cmd import ( "fmt" "os" - "path" "unicode" "github.com/spf13/cobra" @@ -60,10 +59,10 @@ Example: cobra add server -> resulting in a new cmd/server.go`, CmdParent: parentName, Project: &Project{ AbsolutePath: fmt.Sprintf("%s/cmd", wd), - AppName: path.Base(packageName), - PkgName: packageName, - Legal: getLicense(), - Copyright: copyrightLine(), + //AppName: path.Base(packageName), + //PkgName: packageName, + Legal: getLicense(), + Copyright: copyrightLine(), }, } diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index fe9ea319..9b56e849 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -91,7 +91,17 @@ func (p *Project) createLicenseFile() error { } func (c *Command) Create() error { - return nil + cmdFile, err := os.Create(fmt.Sprintf("%s/cmd/%s.go", c.Project.AbsolutePath, c.CmdName)) + if err != nil { + return err + } + defer cmdFile.Close() + + commandTemplate := template.Must(template.New("sub").Parse(string(tpl.AddCommandTemplate()))) + err = commandTemplate.Execute(cmdFile, c.Project.AbsolutePath) + if err != nil { + return err + } } // NewProject returns Project with specified project name. From b8ad19ad0d8befeb56daba8874bc17552c4eb405 Mon Sep 17 00:00:00 2001 From: jharshman Date: Wed, 30 Jan 2019 01:34:31 -0800 Subject: [PATCH 44/61] reorder some operations --- cobra/cmd/add.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cobra/cmd/add.go b/cobra/cmd/add.go index 806a7cc5..599fc604 100644 --- a/cobra/cmd/add.go +++ b/cobra/cmd/add.go @@ -43,17 +43,12 @@ Example: cobra add server -> resulting in a new cmd/server.go`, er("add needs a name for the command") } - commandName := validateCmdName(args[0]) - - if packageName == "" { - // derive packageName - } - wd, err := os.Getwd() if err != nil { er(err) } + commandName := validateCmdName(args[0]) command := &Command{ CmdName: commandName, CmdParent: parentName, From 221bae39865086cc5d0cfbd0c0c4d9853552a9d5 Mon Sep 17 00:00:00 2001 From: jharshman Date: Wed, 30 Jan 2019 01:37:10 -0800 Subject: [PATCH 45/61] depricate package name flag --- cobra/cmd/add.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cobra/cmd/add.go b/cobra/cmd/add.go index 599fc604..4fb46329 100644 --- a/cobra/cmd/add.go +++ b/cobra/cmd/add.go @@ -74,6 +74,7 @@ Example: cobra add server -> resulting in a new cmd/server.go`, func init() { addCmd.Flags().StringVarP(&packageName, "package", "t", "", "target package name (e.g. github.com/spf13/hugo)") addCmd.Flags().StringVarP(&parentName, "parent", "p", "rootCmd", "variable name of parent command for this command") + addCmd.Flags().MarkDeprecated("package", "this operation has been removed.") } // validateCmdName returns source without any dashes and underscore. From 3c42f846c2b95e5a47f3793932a9f98f0e3cc49f Mon Sep 17 00:00:00 2001 From: jharshman Date: Wed, 30 Jan 2019 01:38:24 -0800 Subject: [PATCH 46/61] fix duplicated dir --- cobra/cmd/project.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index 9b56e849..3f7a4c8f 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -91,7 +91,7 @@ func (p *Project) createLicenseFile() error { } func (c *Command) Create() error { - cmdFile, err := os.Create(fmt.Sprintf("%s/cmd/%s.go", c.Project.AbsolutePath, c.CmdName)) + cmdFile, err := os.Create(fmt.Sprintf("%s/%s.go", c.Project.AbsolutePath, c.CmdName)) if err != nil { return err } From 2fea75b02e2e18fab31f754b0f014944b3d9e42d Mon Sep 17 00:00:00 2001 From: jharshman Date: Wed, 30 Jan 2019 01:44:39 -0800 Subject: [PATCH 47/61] vgo - add command working --- cobra/cmd/project.go | 3 ++- cobra/tpl/main.go | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index 3f7a4c8f..9f8dba90 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -98,10 +98,11 @@ func (c *Command) Create() error { defer cmdFile.Close() commandTemplate := template.Must(template.New("sub").Parse(string(tpl.AddCommandTemplate()))) - err = commandTemplate.Execute(cmdFile, c.Project.AbsolutePath) + err = commandTemplate.Execute(cmdFile, c) if err != nil { return err } + return nil } // NewProject returns Project with specified project name. diff --git a/cobra/tpl/main.go b/cobra/tpl/main.go index ba4d7cc5..71f1f450 100644 --- a/cobra/tpl/main.go +++ b/cobra/tpl/main.go @@ -110,8 +110,8 @@ func initConfig() { func AddCommandTemplate() []byte { return []byte(`/* -{{ .Copyright }} -{{ if .Legal.Header }}{{ .Legal.Header }}{{ end }} +{{ .Project.Copyright }} +{{ if .Project.Legal.Header }}{{ .Project.Legal.Header }}{{ end }} */ package cmd From 0bb1506d255f827d36e044d13fa2b5470bca4009 Mon Sep 17 00:00:00 2001 From: jharshman Date: Wed, 30 Jan 2019 01:45:15 -0800 Subject: [PATCH 48/61] remove commented field in struct --- cobra/cmd/add.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cobra/cmd/add.go b/cobra/cmd/add.go index 4fb46329..5b0d9e11 100644 --- a/cobra/cmd/add.go +++ b/cobra/cmd/add.go @@ -54,8 +54,6 @@ Example: cobra add server -> resulting in a new cmd/server.go`, CmdParent: parentName, Project: &Project{ AbsolutePath: fmt.Sprintf("%s/cmd", wd), - //AppName: path.Base(packageName), - //PkgName: packageName, Legal: getLicense(), Copyright: copyrightLine(), }, From 303a3e5160b0d2d72ca96ed9eff9e016fe54b2f9 Mon Sep 17 00:00:00 2001 From: jharshman Date: Wed, 30 Jan 2019 01:49:57 -0800 Subject: [PATCH 49/61] vgo - strip out unused methods --- cobra/cmd/project.go | 197 +------------------------------------- cobra/cmd/project_test.go | 23 +---- 2 files changed, 6 insertions(+), 214 deletions(-) diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index 9f8dba90..852623f6 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -4,9 +4,6 @@ import ( "fmt" "github.com/spf13/cobra/cobra/tpl" "os" - "path/filepath" - "runtime" - "strings" "text/template" ) @@ -20,12 +17,11 @@ type Project struct { Viper bool AppName string - // v1 - absPath string - cmdPath string - srcPath string - license License - name string + //absPath string + //cmdPath string + //srcPath string + //license License + //name string } type Command struct { @@ -104,186 +100,3 @@ func (c *Command) Create() error { } return nil } - -// NewProject returns Project with specified project name. -func NewProject(projectName string) *Project { - if projectName == "" { - er("can't create project with blank name") - } - - p := new(Project) - p.name = projectName - - // 1. Find already created protect. - p.absPath = findPackage(projectName) - - // 2. If there are no created project with this path, and user is in GOPATH, - // then use GOPATH/src/projectName. - if p.absPath == "" { - wd, err := os.Getwd() - if err != nil { - er(err) - } - for _, srcPath := range srcPaths { - goPath := filepath.Dir(srcPath) - if filepathHasPrefix(wd, goPath) { - p.absPath = filepath.Join(srcPath, projectName) - break - } - } - } - - // 3. If user is not in GOPATH, then use (first GOPATH)/src/projectName. - if p.absPath == "" { - p.absPath = filepath.Join(srcPaths[0], projectName) - } - - return p -} - -// findPackage returns full path to existing go package in GOPATHs. -func findPackage(packageName string) string { - if packageName == "" { - return "" - } - - for _, srcPath := range srcPaths { - packagePath := filepath.Join(srcPath, packageName) - if exists(packagePath) { - return packagePath - } - } - - return "" -} - -// NewProjectFromPath returns Project with specified absolute path to -// package. -func NewProjectFromPath(absPath string) *Project { - if absPath == "" { - er("can't create project: absPath can't be blank") - } - if !filepath.IsAbs(absPath) { - er("can't create project: absPath is not absolute") - } - - // If absPath is symlink, use its destination. - fi, err := os.Lstat(absPath) - if err != nil { - er("can't read path info: " + err.Error()) - } - if fi.Mode()&os.ModeSymlink != 0 { - path, err := os.Readlink(absPath) - if err != nil { - er("can't read the destination of symlink: " + err.Error()) - } - absPath = path - } - - p := new(Project) - p.absPath = strings.TrimSuffix(absPath, findCmdDir(absPath)) - p.name = filepath.ToSlash(trimSrcPath(p.absPath, p.SrcPath())) - return p -} - -// trimSrcPath trims at the beginning of absPath the srcPath. -func trimSrcPath(absPath, srcPath string) string { - relPath, err := filepath.Rel(srcPath, absPath) - if err != nil { - er(err) - } - return relPath -} - -// License returns the License object of project. -func (p *Project) License() License { - if p.license.Text == "" && p.license.Name != "None" { - p.license = getLicense() - } - return p.license -} - -// Name returns the name of project, e.g. "github.com/spf13/cobra" -func (p Project) Name() string { - return p.name -} - -// CmdPath returns absolute path to directory, where all commands are located. -func (p *Project) CmdPath() string { - if p.absPath == "" { - return "" - } - if p.cmdPath == "" { - p.cmdPath = filepath.Join(p.absPath, findCmdDir(p.absPath)) - } - return p.cmdPath -} - -// findCmdDir checks if base of absPath is cmd dir and returns it or -// looks for existing cmd dir in absPath. -func findCmdDir(absPath string) string { - if !exists(absPath) || isEmpty(absPath) { - return "cmd" - } - - if isCmdDir(absPath) { - return filepath.Base(absPath) - } - - files, _ := filepath.Glob(filepath.Join(absPath, "c*")) - for _, file := range files { - if isCmdDir(file) { - return filepath.Base(file) - } - } - - return "cmd" -} - -// isCmdDir checks if base of name is one of cmdDir. -func isCmdDir(name string) bool { - name = filepath.Base(name) - for _, cmdDir := range []string{"cmd", "cmds", "command", "commands"} { - if name == cmdDir { - return true - } - } - return false -} - -// AbsPath returns absolute path of project. -func (p Project) AbsPath() string { - return p.absPath -} - -// SrcPath returns absolute path to $GOPATH/src where project is located. -func (p *Project) SrcPath() string { - if p.srcPath != "" { - return p.srcPath - } - if p.absPath == "" { - p.srcPath = srcPaths[0] - return p.srcPath - } - - for _, srcPath := range srcPaths { - if filepathHasPrefix(p.absPath, srcPath) { - p.srcPath = srcPath - break - } - } - - return p.srcPath -} - -func filepathHasPrefix(path string, prefix string) bool { - if len(path) <= len(prefix) { - return false - } - if runtime.GOOS == "windows" { - // Paths in windows are case-insensitive. - return strings.EqualFold(path[0:len(prefix)], prefix) - } - return path[0:len(prefix)] == prefix - -} diff --git a/cobra/cmd/project_test.go b/cobra/cmd/project_test.go index 037f7c55..ed5b054a 100644 --- a/cobra/cmd/project_test.go +++ b/cobra/cmd/project_test.go @@ -1,24 +1,3 @@ package cmd -import ( - "testing" -) - -func TestFindExistingPackage(t *testing.T) { - path := findPackage("github.com/spf13/cobra") - if path == "" { - t.Fatal("findPackage didn't find the existing package") - } - if !hasGoPathPrefix(path) { - t.Fatalf("%q is not in GOPATH, but must be", path) - } -} - -func hasGoPathPrefix(path string) bool { - for _, srcPath := range srcPaths { - if filepathHasPrefix(path, srcPath) { - return true - } - } - return false -} +/* todo: write tests */ From 11aa612384e0e18662f319528bde7b68ffca43ee Mon Sep 17 00:00:00 2001 From: jharshman Date: Wed, 30 Jan 2019 19:47:38 -0800 Subject: [PATCH 50/61] test add --- cobra/cmd/add.go | 8 +-- cobra/cmd/add_test.go | 87 ++++++++----------------------- cobra/cmd/project.go | 2 +- cobra/cmd/testdata/test.go.golden | 27 +++++----- cobra/tpl/main.go | 2 +- 5 files changed, 41 insertions(+), 85 deletions(-) diff --git a/cobra/cmd/add.go b/cobra/cmd/add.go index 5b0d9e11..2d240b8a 100644 --- a/cobra/cmd/add.go +++ b/cobra/cmd/add.go @@ -53,9 +53,9 @@ Example: cobra add server -> resulting in a new cmd/server.go`, CmdName: commandName, CmdParent: parentName, Project: &Project{ - AbsolutePath: fmt.Sprintf("%s/cmd", wd), - Legal: getLicense(), - Copyright: copyrightLine(), + AbsolutePath: wd, + Legal: getLicense(), + Copyright: copyrightLine(), }, } @@ -64,7 +64,7 @@ Example: cobra add server -> resulting in a new cmd/server.go`, er(err) } - fmt.Printf("%s created at %s", command.CmdName, command.Project.AbsolutePath) + fmt.Printf("%s created at %s", command.CmdName, command.AbsolutePath) }, } ) diff --git a/cobra/cmd/add_test.go b/cobra/cmd/add_test.go index 94497084..d9ae0f6d 100644 --- a/cobra/cmd/add_test.go +++ b/cobra/cmd/add_test.go @@ -1,76 +1,32 @@ package cmd -/* -// TestGoldenAddCmd initializes the project "github.com/spf13/testproject" -// in GOPATH, adds "test" command -// and compares the content of all files in cmd directory of testproject -// with appropriate golden files. -// Use -update to update existing golden files. +import ( + "fmt" + "os" + "testing" +) + func TestGoldenAddCmd(t *testing.T) { - projectName := "github.com/spf13/testproject" - project := NewProject(projectName) - defer os.RemoveAll(project.AbsPath()) - viper.Set("author", "NAME HERE ") - viper.Set("license", "apache") - viper.Set("year", 2017) - defer viper.Set("author", nil) - defer viper.Set("license", nil) - defer viper.Set("year", nil) + wd, _ := os.Getwd() + command := &Command{ + CmdName: "test", + CmdParent: parentName, + Project: &Project{ + AbsolutePath: fmt.Sprintf("%s/testproject", wd), + Legal: getLicense(), + Copyright: copyrightLine(), + }, + } - // Initialize the project first. - initializeProject(project) - - // Then add the "test" command. - cmdName := "test" - cmdPath := filepath.Join(project.CmdPath(), cmdName+".go") - createCmdFile(project.License(), cmdPath, cmdName) - - expectedFiles := []string{".", "root.go", "test.go"} - gotFiles := []string{} - - // Check project file hierarchy and compare the content of every single file - // with appropriate golden file. - err := filepath.Walk(project.CmdPath(), func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - // Make path relative to project.CmdPath(). - // E.g. path = "/home/user/go/src/github.com/spf13/testproject/cmd/root.go" - // then it returns just "root.go". - relPath, err := filepath.Rel(project.CmdPath(), path) - if err != nil { - return err - } - relPath = filepath.ToSlash(relPath) - gotFiles = append(gotFiles, relPath) - goldenPath := filepath.Join("testdata", filepath.Base(path)+".golden") - - switch relPath { - // Known directories. - case ".": - return nil - // Known files. - case "root.go", "test.go": - if *update { - got, err := ioutil.ReadFile(path) - if err != nil { - return err - } - ioutil.WriteFile(goldenPath, got, 0644) - } - return compareFiles(path, goldenPath) - } - // Unknown file. - return errors.New("unknown file: " + path) - }) - if err != nil { + if err := command.Create(); err != nil { t.Fatal(err) } - // Check if some files lack. - if err := checkLackFiles(expectedFiles, gotFiles); err != nil { + generatedFile := fmt.Sprintf("%s/cmd/%s.go", command.AbsolutePath, command.CmdName) + goldenFile := fmt.Sprintf("testdata/%s.go.golden", command.CmdName) + err := compareFiles(generatedFile, goldenFile) + if err != nil { t.Fatal(err) } } @@ -98,4 +54,3 @@ func TestValidateCmdName(t *testing.T) { } } } -*/ diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index 852623f6..167c55ff 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -87,7 +87,7 @@ func (p *Project) createLicenseFile() error { } func (c *Command) Create() error { - cmdFile, err := os.Create(fmt.Sprintf("%s/%s.go", c.Project.AbsolutePath, c.CmdName)) + cmdFile, err := os.Create(fmt.Sprintf("%s/cmd/%s.go", c.AbsolutePath, c.CmdName)) if err != nil { return err } diff --git a/cobra/cmd/testdata/test.go.golden b/cobra/cmd/testdata/test.go.golden index ed644275..fb8e0fa9 100644 --- a/cobra/cmd/testdata/test.go.golden +++ b/cobra/cmd/testdata/test.go.golden @@ -1,17 +1,18 @@ -// Copyright © 2017 NAME HERE -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/* +Copyright © 2019 NAME HERE +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package cmd import ( diff --git a/cobra/tpl/main.go b/cobra/tpl/main.go index 71f1f450..5e5a0fae 100644 --- a/cobra/tpl/main.go +++ b/cobra/tpl/main.go @@ -111,7 +111,7 @@ func initConfig() { func AddCommandTemplate() []byte { return []byte(`/* {{ .Project.Copyright }} -{{ if .Project.Legal.Header }}{{ .Project.Legal.Header }}{{ end }} +{{ if .Legal.Header }}{{ .Legal.Header }}{{ end }} */ package cmd From 984374f5b622efc94388e2872915c2d97404adba Mon Sep 17 00:00:00 2001 From: jharshman Date: Wed, 30 Jan 2019 20:09:57 -0800 Subject: [PATCH 51/61] clean up testproject files after test executes --- cobra/cmd/add_test.go | 13 +++++++++++++ cobra/cmd/init_test.go | 7 +++++++ 2 files changed, 20 insertions(+) diff --git a/cobra/cmd/add_test.go b/cobra/cmd/add_test.go index d9ae0f6d..0de1d221 100644 --- a/cobra/cmd/add_test.go +++ b/cobra/cmd/add_test.go @@ -16,9 +16,22 @@ func TestGoldenAddCmd(t *testing.T) { AbsolutePath: fmt.Sprintf("%s/testproject", wd), Legal: getLicense(), Copyright: copyrightLine(), + + // required to init + AppName: "testproject", + PkgName: "github.com/spf13/testproject", + Viper: true, }, } + // init project first + command.Project.Create() + defer func() { + if _, err := os.Stat(command.AbsolutePath); err == nil { + os.RemoveAll(command.AbsolutePath) + } + }() + if err := command.Create(); err != nil { t.Fatal(err) } diff --git a/cobra/cmd/init_test.go b/cobra/cmd/init_test.go index 77145fcb..9540b2d3 100644 --- a/cobra/cmd/init_test.go +++ b/cobra/cmd/init_test.go @@ -8,6 +8,7 @@ import ( ) func TestGoldenInitCmd(t *testing.T) { + wd, _ := os.Getwd() project := &Project{ AbsolutePath: fmt.Sprintf("%s/testproject", wd), @@ -23,6 +24,12 @@ func TestGoldenInitCmd(t *testing.T) { t.Fatal(err) } + defer func() { + if _, err := os.Stat(project.AbsolutePath); err == nil { + os.RemoveAll(project.AbsolutePath) + } + }() + expectedFiles := []string{"LICENSE", "main.go", "cmd/root.go"} for _, f := range expectedFiles { generatedFile := fmt.Sprintf("%s/%s", project.AbsolutePath, f) From 2411ac592a8a714d324edd1abc880a55c17a9de0 Mon Sep 17 00:00:00 2001 From: Joshua Harshman Date: Tue, 7 May 2019 11:22:21 -0600 Subject: [PATCH 52/61] remove unused struct fields --- cobra/cmd/project.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cobra/cmd/project.go b/cobra/cmd/project.go index 167c55ff..dd2f7ea2 100644 --- a/cobra/cmd/project.go +++ b/cobra/cmd/project.go @@ -16,12 +16,6 @@ type Project struct { Legal License Viper bool AppName string - - //absPath string - //cmdPath string - //srcPath string - //license License - //name string } type Command struct { From 9eb9f5c66b643a25cb4c468e3a585c4d3a31c689 Mon Sep 17 00:00:00 2001 From: ialidzhikov Date: Sun, 14 Apr 2019 21:35:42 +0300 Subject: [PATCH 53/61] Add gardenctl to projects build using Cobra Signed-off-by: ialidzhikov --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8a9ace4c..bce0fa7a 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Many of the most widely used Go projects are built using Cobra, such as: [Istio](https://istio.io), [Prototool](https://github.com/uber/prototool), [mattermost-server](https://github.com/mattermost/mattermost-server), +[Gardener](https://github.com/gardener/gardenctl), etc. [![Build Status](https://travis-ci.org/spf13/cobra.svg "Travis CI status")](https://travis-ci.org/spf13/cobra) From 5f23f55c8183489b4e881ff0dc99ac6a746baa0c Mon Sep 17 00:00:00 2001 From: Go Frendi Gunawan Date: Tue, 4 Jun 2019 11:25:58 +0700 Subject: [PATCH 54/61] Update README.md To avoid confusion, it is better to use `localCmd` instead of `rootCmd` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bce0fa7a..60c5a425 100644 --- a/README.md +++ b/README.md @@ -338,7 +338,7 @@ rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose out A flag can also be assigned locally which will only apply to that specific command. ```go -rootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from") +localCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from") ``` ### Local Flag on Parent Commands From 4a716d101b39c61535dbb10aa765dfee95b41085 Mon Sep 17 00:00:00 2001 From: Juan Leni Date: Mon, 11 Feb 2019 07:10:59 +0100 Subject: [PATCH 55/61] Extending redirection to stdout, stderr, stdin --- command.go | 69 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/command.go b/command.go index b257f91b..a00367d3 100644 --- a/command.go +++ b/command.go @@ -177,8 +177,6 @@ type Command struct { // that we can use on every pflag set and children commands globNormFunc func(f *flag.FlagSet, name string) flag.NormalizedName - // output is an output writer defined by user. - output io.Writer // usageFunc is usage func defined by user. usageFunc func(*Command) error // usageTemplate is usage template defined by user. @@ -195,6 +193,13 @@ type Command struct { helpCommand *Command // versionTemplate is the version template defined by user. versionTemplate string + + // inReader is a reader defined by the user that replaces stdin + inReader io.Reader + // outWriter is a writer defined by the user that replaces stdout + outWriter io.Writer + // errWriter is a writer defined by the user that replaces stderr + errWriter io.Writer } // SetArgs sets arguments for the command. It is set to os.Args[1:] by default, if desired, can be overridden @@ -206,7 +211,25 @@ func (c *Command) SetArgs(a []string) { // SetOutput sets the destination for usage and error messages. // If output is nil, os.Stderr is used. func (c *Command) SetOutput(output io.Writer) { - c.output = output + c.outWriter = output +} + +// SetOut sets the destination for usage messages. +// If newOut is nil, os.Stdout is used. +func (c *Command) SetOut(newOut io.Writer) { + c.outWriter = newOut +} + +// SetErr sets the destination for error messages. +// If newErr is nil, os.Stderr is used. +func (c *Command) SetErr(newErr io.Writer) { + c.errWriter = newErr +} + +// SetOut sets the source for input data +// If newIn is nil, os.Stdin is used. +func (c *Command) SetIn(newIn io.Reader) { + c.inReader = newIn } // SetUsageFunc sets usage function. Usage can be defined by application. @@ -267,9 +290,19 @@ func (c *Command) OutOrStderr() io.Writer { return c.getOut(os.Stderr) } +// ErrOrStderr returns output to stderr +func (c *Command) ErrOrStderr() io.Writer { + return c.getErr(os.Stderr) +} + +// ErrOrStderr returns output to stderr +func (c *Command) InOrStdin() io.Reader { + return c.getIn(os.Stdin) +} + func (c *Command) getOut(def io.Writer) io.Writer { - if c.output != nil { - return c.output + if c.outWriter != nil { + return c.outWriter } if c.HasParent() { return c.parent.getOut(def) @@ -277,6 +310,26 @@ func (c *Command) getOut(def io.Writer) io.Writer { return def } +func (c *Command) getErr(def io.Writer) io.Writer { + if c.errWriter != nil { + return c.errWriter + } + if c.HasParent() { + return c.parent.getErr(def) + } + return def +} + +func (c *Command) getIn(def io.Reader) io.Reader { + if c.inReader != nil { + return c.inReader + } + if c.HasParent() { + return c.parent.getIn(def) + } + return def +} + // UsageFunc returns either the function set by SetUsageFunc for this command // or a parent, or it returns a default usage function. func (c *Command) UsageFunc() (f func(*Command) error) { @@ -331,11 +384,11 @@ func (c *Command) Help() error { // UsageString return usage string. func (c *Command) UsageString() string { - tmpOutput := c.output + tmpOutput := c.outWriter bb := new(bytes.Buffer) - c.SetOutput(bb) + c.outWriter = bb c.Usage() - c.output = tmpOutput + c.outWriter = tmpOutput return bb.String() } From 0ea93dd60d8a44862790c8751955bdf65be0c4ce Mon Sep 17 00:00:00 2001 From: Juan Leni Date: Mon, 11 Feb 2019 08:22:54 +0100 Subject: [PATCH 56/61] Fixed linter issues --- command.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command.go b/command.go index a00367d3..58cc8732 100644 --- a/command.go +++ b/command.go @@ -217,12 +217,12 @@ func (c *Command) SetOutput(output io.Writer) { // SetOut sets the destination for usage messages. // If newOut is nil, os.Stdout is used. func (c *Command) SetOut(newOut io.Writer) { - c.outWriter = newOut + c.outWriter = newOut } // SetErr sets the destination for error messages. // If newErr is nil, os.Stderr is used. -func (c *Command) SetErr(newErr io.Writer) { +func (c *Command) SetErr(newErr io.Writer) { c.errWriter = newErr } From 618bc00f8084be91329cbceb8094a7e5190a6c29 Mon Sep 17 00:00:00 2001 From: Juan Leni Date: Mon, 11 Feb 2019 16:06:55 +0100 Subject: [PATCH 57/61] Allow for explicit output to err/stderr --- command.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/command.go b/command.go index 58cc8732..3e97687a 100644 --- a/command.go +++ b/command.go @@ -1121,6 +1121,21 @@ func (c *Command) Printf(format string, i ...interface{}) { c.Print(fmt.Sprintf(format, i...)) } +// PrintErr is a convenience method to Print to the defined Err output, fallback to Stderr if not set. +func (c *Command) PrintErr(i ...interface{}) { + fmt.Fprint(c.ErrOrStderr(), i...) +} + +// PrintErrln is a convenience method to Println to the defined Err output, fallback to Stderr if not set. +func (c *Command) PrintErrln(i ...interface{}) { + c.Print(fmt.Sprintln(i...)) +} + +// PrintErrf is a convenience method to Printf to the defined Err output, fallback to Stderr if not set. +func (c *Command) PrintErrf(format string, i ...interface{}) { + c.Print(fmt.Sprintf(format, i...)) +} + // CommandPath returns the full path to this command. func (c *Command) CommandPath() string { if c.HasParent() { From cb27ce11fba1a33e42233dea2658805c62a17e74 Mon Sep 17 00:00:00 2001 From: Juan Leni Date: Wed, 13 Feb 2019 06:32:16 +0100 Subject: [PATCH 58/61] Deprecate and maintain backwards compatibility --- command.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/command.go b/command.go index 3e97687a..6dbb3e8c 100644 --- a/command.go +++ b/command.go @@ -210,8 +210,10 @@ func (c *Command) SetArgs(a []string) { // SetOutput sets the destination for usage and error messages. // If output is nil, os.Stderr is used. +// Deprecated: Use SetOut and/or SetErr instead func (c *Command) SetOutput(output io.Writer) { c.outWriter = output + c.errWriter = output } // SetOut sets the destination for usage messages. From e35034f0daec26bfcbfd2b6fb258b359a611b6e7 Mon Sep 17 00:00:00 2001 From: Alessio Treglia Date: Tue, 30 Apr 2019 18:41:56 +0100 Subject: [PATCH 59/61] Add tests --- command_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/command_test.go b/command_test.go index 6e483a3e..4f1d3690 100644 --- a/command_test.go +++ b/command_test.go @@ -1381,6 +1381,30 @@ func TestSetOutput(t *testing.T) { } } +func TestSetOut(t *testing.T) { + c := &Command{} + c.SetOut(nil) + if out := c.OutOrStdout(); out != os.Stdout { + t.Errorf("Expected setting output to nil to revert back to stdout") + } +} + +func TestSetErr(t *testing.T) { + c := &Command{} + c.SetErr(nil) + if out := c.ErrOrStderr(); out != os.Stderr { + t.Errorf("Expected setting error to nil to revert back to stderr") + } +} + +func TestSetIn(t *testing.T) { + c := &Command{} + c.SetIn(nil) + if out := c.InOrStdin(); out != os.Stdin { + t.Errorf("Expected setting input to nil to revert back to stdin") + } +} + func TestFlagErrorFunc(t *testing.T) { c := &Command{Use: "c", Run: emptyRun} From b6357260812dd09c49dc7d55042f0c795b419f6b Mon Sep 17 00:00:00 2001 From: Juan Leni Date: Wed, 15 May 2019 18:49:16 +0200 Subject: [PATCH 60/61] considering stderr in UsageString --- command.go | 11 ++++++++++- command_test.go | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/command.go b/command.go index 6dbb3e8c..c7e89830 100644 --- a/command.go +++ b/command.go @@ -384,13 +384,22 @@ func (c *Command) Help() error { return nil } -// UsageString return usage string. +// UsageString returns usage string. func (c *Command) UsageString() string { + // Storing normal writers tmpOutput := c.outWriter + tmpErr := c.errWriter + bb := new(bytes.Buffer) c.outWriter = bb + c.errWriter = bb + c.Usage() + + // Setting things back to normal c.outWriter = tmpOutput + c.errWriter = tmpErr + return bb.String() } diff --git a/command_test.go b/command_test.go index 4f1d3690..258f20a2 100644 --- a/command_test.go +++ b/command_test.go @@ -1405,6 +1405,22 @@ func TestSetIn(t *testing.T) { } } +func TestUsageStringRedirected(t *testing.T) { + c := &Command{} + + c.usageFunc = func(cmd *Command) error { + cmd.Print("[stdout1]") + cmd.PrintErr("[stderr2]") + cmd.Print("[stdout3]") + return nil; + } + + expected := "[stdout1][stderr2][stdout3]" + if got := c.UsageString(); got != expected { + t.Errorf("Expected usage string to consider both stdout and stderr") + } +} + func TestFlagErrorFunc(t *testing.T) { c := &Command{Use: "c", Run: emptyRun} From f2b07da1e2c38d5f12845a4f607e2e1018cbb1f5 Mon Sep 17 00:00:00 2001 From: Juan Leni Date: Wed, 15 May 2019 18:53:39 +0200 Subject: [PATCH 61/61] fixing linter issues --- command_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command_test.go b/command_test.go index 258f20a2..2fa2003c 100644 --- a/command_test.go +++ b/command_test.go @@ -1412,11 +1412,11 @@ func TestUsageStringRedirected(t *testing.T) { cmd.Print("[stdout1]") cmd.PrintErr("[stderr2]") cmd.Print("[stdout3]") - return nil; + return nil } expected := "[stdout1][stderr2][stdout3]" - if got := c.UsageString(); got != expected { + if got := c.UsageString(); got != expected { t.Errorf("Expected usage string to consider both stdout and stderr") } }