Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add plugin listing to "kn --help" #929

Merged
merged 10 commits into from
Jul 14, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/kn/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func run(args []string) error {
}

// Create kn root command and all sub-commands
rootCmd, err := root.NewRootCommand()
rootCmd, err := root.NewRootCommand(pluginManager.HelpTemplateFuncs())
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/kn/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func TestUnknownCommands(t *testing.T) {
}
for _, d := range data {
args := append([]string{"kn"}, d.givenCmdArgs...)
rootCmd, err := root.NewRootCommand()
rootCmd, err := root.NewRootCommand(nil)
os.Args = args
assert.NilError(t, err)
err = validateRootCommand(rootCmd)
Expand Down
2 changes: 1 addition & 1 deletion hack/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ go_test() {

echo "🧪 ${X}Test"
set +e
go test -v ./pkg/... >$test_output 2>&1
go test -v .cmd/... ./pkg/... >$test_output 2>&1
local err=$?
if [ $err -ne 0 ]; then
echo "🔥 ${red}Failure${reset}"
Expand Down
2 changes: 1 addition & 1 deletion hack/generate-docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
)

func main() {
rootCmd, err := root.NewRootCommand()
rootCmd, err := root.NewRootCommand(nil)
if err != nil {
log.Panicf("can not create root command: %v", err)
}
Expand Down
74 changes: 74 additions & 0 deletions pkg/kn/plugin/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import (
"runtime"
"sort"
"strings"
"text/template"

homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

// Interface describing a plugin
Expand Down Expand Up @@ -108,6 +110,12 @@ func (manager *Manager) FindPlugin(parts []string) (Plugin, error) {

// ListPlugins lists all plugins that can be found in the plugin directory or in the path (if configured)
func (manager *Manager) ListPlugins() (PluginList, error) {
return manager.ListPluginsForCommandGroup(nil)
}

// ListPluginsForCommandGroup lists all plugins that can be found in the plugin directory or in the path (if configured),
// and which fits to a command group
func (manager *Manager) ListPluginsForCommandGroup(commandGroupParts []string) (PluginList, error) {
var plugins []Plugin

dirs, err := manager.pluginLookupDirectories()
Expand Down Expand Up @@ -135,6 +143,11 @@ func (manager *Manager) ListPlugins() (PluginList, error) {
continue
}

// Check if plugin matches a command group
if !isPartOfCommandGroup(commandGroupParts, f.Name()) {
continue
}

// Ignore all plugins that are shadowed
if _, ok := hasSeen[name]; !ok {
plugins = append(plugins, &plugin{
Expand All @@ -146,11 +159,29 @@ func (manager *Manager) ListPlugins() (PluginList, error) {
}
}
}

// Sort according to name
sort.Sort(PluginList(plugins))
return plugins, nil
}

func isPartOfCommandGroup(commandGroupParts []string, name string) bool {
if commandGroupParts == nil {
return true
}

commandParts := extractPluginCommandFromFileName(name)
if len(commandParts) != len(commandGroupParts)+1 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get some English comments for these conditions... typically not for comments but when conditions are not obvious...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

return false
}
for i := range commandGroupParts {
if commandParts[i] != commandGroupParts[i] {
return false
}
}
return true
}

// PluginsDir returns the configured directory holding plugins
func (manager *Manager) PluginsDir() string {
return manager.pluginsDir
Expand Down Expand Up @@ -212,6 +243,49 @@ func (manager *Manager) pluginLookupDirectories() ([]string, error) {
return dirs, nil
}

// HelpTemplateFuncs returns a function map which can be used in templates for resolving
// plugin related help messages
func (manager *Manager) HelpTemplateFuncs() *template.FuncMap {
ret := template.FuncMap{
"listPlugins": manager.listPluginsHelpMessage(),
}

return &ret
}

// listPluginsHelpMessage returns a function which returns all plugins that are directly below the given
// command as a properly formatted string
func (manager *Manager) listPluginsHelpMessage() func(cmd *cobra.Command) string {
return func(cmd *cobra.Command) string {
if !cmd.HasSubCommands() {
return ""
}
list, err := manager.ListPluginsForCommandGroup(extractCommandGroup(cmd, []string{}))
if err != nil || len(list) == 0 {
// We don't show plugins if there is an error
return ""
}
var plugins []string
for _, pl := range list {
t := fmt.Sprintf(" %%-%ds %%s", cmd.NamePadding())
desc, _ := pl.Description()
command := (pl.CommandParts())[len(pl.CommandParts())-1]
help := fmt.Sprintf(t, command, desc)
plugins = append(plugins, help)
}
return strings.Join(plugins, "\n")
}
}

// extractCommandGroup constructs the command path as array of strings
func extractCommandGroup(cmd *cobra.Command, parts []string) []string {
if cmd.HasParent() {
parts = extractCommandGroup(cmd.Parent(), parts)
parts = append(parts, cmd.Name())
}
return parts
}

// uniquePathsList deduplicates a given slice of strings without
// sorting or otherwise altering its order in any way.
func uniquePathsList(paths []string) []string {
Expand Down
5 changes: 3 additions & 2 deletions pkg/kn/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"flag"
"fmt"
"strings"
"text/template"

"github.com/pkg/errors"
"github.com/spf13/cobra"
Expand All @@ -41,7 +42,7 @@ import (
)

// NewRootCommand creates the default `kn` command with a default plugin handler
func NewRootCommand() (*cobra.Command, error) {
func NewRootCommand(helpFuncs *template.FuncMap) (*cobra.Command, error) {
p := &commands.KnParams{}
p.Initialize()

Expand Down Expand Up @@ -107,7 +108,7 @@ func NewRootCommand() (*cobra.Command, error) {
groups.AddTo(rootCmd)

// Initialize default `help` cmd early to prevent unknown command errors
groups.SetRootUsage(rootCmd)
groups.SetRootUsage(rootCmd, helpFuncs)

// Add the "options" commands for showing all global options
rootCmd.AddCommand(options.NewOptionsCommand())
Expand Down
6 changes: 3 additions & 3 deletions pkg/kn/root/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
)

func TestNewRootCommand(t *testing.T) {
rootCmd, err := NewRootCommand()
rootCmd, err := NewRootCommand(nil)
assert.NilError(t, err)
assert.Assert(t, rootCmd != nil)

Expand All @@ -51,13 +51,13 @@ func TestNewRootCommand(t *testing.T) {
}

func TestSubCommands(t *testing.T) {
rootCmd, err := NewRootCommand()
rootCmd, err := NewRootCommand(nil)
assert.NilError(t, err)
checkLeafCommand(t, "version", rootCmd)
}

func TestCommandGroup(t *testing.T) {
rootCmd, err := NewRootCommand()
rootCmd, err := NewRootCommand(nil)
assert.NilError(t, err)
commandGroups := []string{
"service", "revision", "plugin", "source", "source apiserver",
Expand Down
9 changes: 4 additions & 5 deletions pkg/templates/command_groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package templates

import (
"text/template"

"github.com/spf13/cobra"
)

Expand All @@ -40,11 +42,8 @@ func (g CommandGroups) AddTo(cmd *cobra.Command) {
}

// SetRootUsage sets our own help and usage function messages to the root command
func (g CommandGroups) SetRootUsage(rootCmd *cobra.Command) {
engine := &templateEngine{
RootCmd: rootCmd,
CommandGroups: g,
}
func (g CommandGroups) SetRootUsage(rootCmd *cobra.Command, extraTemplateFunctions *template.FuncMap) {
engine := newTemplateEngine(rootCmd, g, extraTemplateFunctions)
setHelpFlagsToSubCommands(rootCmd)
rootCmd.SetUsageFunc(engine.usageFunc())
rootCmd.SetHelpFunc(engine.helpFunc())
Expand Down
12 changes: 11 additions & 1 deletion pkg/templates/command_groups_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package templates
import (
"fmt"
"testing"
"text/template"

"github.com/spf13/cobra"
"gotest.tools/assert"
Expand Down Expand Up @@ -48,7 +49,7 @@ func TestAddTo(t *testing.T) {
func TestSetUsage(t *testing.T) {
rootCmd := &cobra.Command{Use: "root", Short: "root", Run: func(cmd *cobra.Command, args []string) {}}
groups.AddTo(rootCmd)
groups.SetRootUsage(rootCmd)
groups.SetRootUsage(rootCmd, getTestFuncMap())

for _, cmd := range rootCmd.Commands() {
assert.Assert(t, cmd.DisableFlagsInUseLine)
Expand All @@ -67,3 +68,12 @@ func TestSetUsage(t *testing.T) {
assert.Equal(t, stdErr, "")
assert.Assert(t, util.ContainsAll(stdOut, "root", "header-1", "header-2"))
}

func getTestFuncMap() *template.FuncMap {
fMap := template.FuncMap{
"listPlugins": func(c *cobra.Command) string {
return ""
},
}
return &fMap
}
22 changes: 19 additions & 3 deletions pkg/templates/template_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,30 @@ import (
type templateEngine struct {
RootCmd *cobra.Command
CommandGroups
functions template.FuncMap
}

// Get the function to show the global options
func NewGlobalOptionsFunc() func(command *cobra.Command) error {
return templateEngine{}.optionsFunc()
return newTemplateEngine(nil, nil, nil).optionsFunc()
}

func (e templateEngine) usageFunc() func(command *cobra.Command) error {
// Create new template engine
func newTemplateEngine(rootCmd *cobra.Command, g CommandGroups, extraFunctions *template.FuncMap) templateEngine {
engine := templateEngine{
RootCmd: rootCmd,
CommandGroups: g,
}
engine.functions = engine.templateFunctions()
if extraFunctions != nil {
for name, function := range *extraFunctions {
engine.functions[name] = function
}
}
return engine
}

func (e templateEngine) usageFunc() func(*cobra.Command) error {
return func(c *cobra.Command) error {
return e.fillTemplate("usage", c, usageTemplate())
}
Expand All @@ -60,7 +76,7 @@ func (e templateEngine) optionsFunc() func(command *cobra.Command) error {

func (e templateEngine) fillTemplate(name string, c *cobra.Command, templ string) error {
t := template.New(name)
t.Funcs(e.templateFunctions())
t.Funcs(e.functions)
_, err := t.Parse(templ)
if err != nil {
fmt.Fprintf(c.ErrOrStderr(), "\nINTERNAL: >>>>> %v\n", err)
Expand Down
13 changes: 5 additions & 8 deletions pkg/templates/template_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type testData struct {
}

func TestUsageFunc(t *testing.T) {
rootCmd, engine := newTemplateEngine()
rootCmd, engine := newTestTemplateEngine()
subCmdWithSubs, _, _ := rootCmd.Find([]string{"g1.1"})
subCmd, _, _ := rootCmd.Find([]string{"g2.1"})

Expand Down Expand Up @@ -71,7 +71,7 @@ func TestUsageFunc(t *testing.T) {
}

func TestHelpFunc(t *testing.T) {
rootCmd, engine := newTemplateEngine()
rootCmd, engine := newTestTemplateEngine()
subCmd := rootCmd.Commands()[0]

data := []testData{
Expand Down Expand Up @@ -101,7 +101,7 @@ func TestHelpFunc(t *testing.T) {
}

func TestOptionsFunc(t *testing.T) {
rootCmd, _ := newTemplateEngine()
rootCmd, _ := newTestTemplateEngine()
subCmd := rootCmd.Commands()[0]
capture := test.CaptureOutput(t)
err := NewGlobalOptionsFunc()(subCmd)
Expand Down Expand Up @@ -135,7 +135,7 @@ func validateSubUsageOutput(t *testing.T, stdOut string, cmd *cobra.Command) {
assert.Assert(t, util.ContainsAll(stdOut, "Use", "root", "options", "global"))
}

func newTemplateEngine() (*cobra.Command, templateEngine) {
func newTestTemplateEngine() (*cobra.Command, templateEngine) {
rootCmd := &cobra.Command{Use: "root", Short: "desc-root", Long: "longdesc-root"}
rootCmd.PersistentFlags().String("global-opt", "", "global option")
cmdGroups := CommandGroups{
Expand All @@ -148,10 +148,7 @@ func newTemplateEngine() (*cobra.Command, templateEngine) {
[]*cobra.Command{newCmd("g2.1"), newCmd("g2.2"), newCmd("g2.3")},
},
}
engine := templateEngine{
RootCmd: rootCmd,
CommandGroups: cmdGroups,
}
engine := newTemplateEngine(rootCmd, cmdGroups, getTestFuncMap())
cmdGroups.AddTo(rootCmd)

// Add a sub-command to first command
Expand Down
8 changes: 8 additions & 0 deletions pkg/templates/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ const (

{{end}}`

// sectionPlugins lists all plugins (if any)
sectionPlugins = `{{$plugins := listPlugins .}}{{ if ne (len $plugins) 0}}Plugins:
{{trimRight $plugins}}

{{end}}
`

// sectionFlags is the help template section that displays the command's flags.
sectionFlags = `{{$visibleFlags := visibleFlags .}}{{ if $visibleFlags.HasFlags}}Options:
{{trimRight (flagsUsages $visibleFlags)}}
Expand Down Expand Up @@ -74,6 +81,7 @@ func usageTemplate() string {
sectionExamples,
sectionCommandGroups,
sectionSubCommands,
sectionPlugins,
sectionFlags,
sectionUsage,
sectionTipsHelp,
Expand Down