Skip to content

Commit

Permalink
first take: sub command namespacing with usage update
Browse files Browse the repository at this point in the history
  • Loading branch information
Greg Osuri committed Nov 8, 2015
1 parent 8d79579 commit 2de2b70
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 3 deletions.
41 changes: 38 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
# nscommand
# cmdns

nscommand is a go library for [Cobra](https://github.com/spf13/cobra) for adding namespace to subcommands. A namedspaced command is alternative way to structure sub commands, similar to `rake db:migrate` and `heroku apps:create`.
cmdns is a go library for [Cobra](https://github.com/spf13/cobra) for adding namespaces to subcommands. Command namespacing is an alternative way to structure sub commands, similar to `rake db:migrate` and `ovrclk apps:create`.

Cobra a popular library for creating powerful modern CLI applications used by [OvrClk](http://ovrclk.com), [Docker](https://github.com/docker/distribution), [Parse (CLI)](https://parse.com/), [Kubernetes](http://kubernetes.io/), and many more widely used Go projects.
Cobra a popular library for creating powerful modern CLI applications used by [Kubernetes](http://kubernetes.io/), [Docker](https://github.com/docker/distribution), [Parse](https://github.com/ParsePlatform/parse-cli), and many more widely used Go projects.

## Example

```go
ovrclk := &cobra.Command{Use: "ovrclk"}
apps := &cobra.Command{Use: "apps"}
apps.AddCommand(&cobra.Command{Use: "info", Run: runFunc})
ovrclk.AddCommand(apps)

// Enable namespacing
cmdns.Enable(ovrclk)

ovrclk.Execute()
```

The above example will namespace `info` with `apps`. It lets you run:

```sh
$ ovrclk apps:info
```

And, updates the usage function for `ovrclk:apps -h`:

```sh
Available Commands:
apps:info

Use "ovrclk [command] --help" for more information about a command.
```

To disable overiding usage function:

```go
cmdns.SetOverrideUsageFunc(false)
```
153 changes: 153 additions & 0 deletions cmdns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package cmdns

import (
"errors"
"fmt"
"github.com/spf13/cobra"
)

// DefaultCmdNS is the default CmdNS for the package
var DefaultCmdNS = New()

// SetOverrideUsageFunc when set to true will overide the command's usage function with the package's usage function that displays namespaces using the DefaultCmdNS
func SetOverrideUsageFunc(v bool) *CmdNS {
return DefaultCmdNS.SetOverrideUsageFunc(v)
}

// Enable enables namespacing for the command using the DefaultCmdNS
func Enable(cmd *cobra.Command) error {
return DefaultCmdNS.Enable(cmd)
}

// CmdNS is the struct represting the component that namespaces sucommands
type CmdNS struct {
// Namespaces
Namespaces []*Namespace
OverrideUsageFunc bool
}

// New returns a new instance of the CmdNS
func New() *CmdNS {
return &CmdNS{
Namespaces: make([]*Namespace, 0),
OverrideUsageFunc: true,
}
}

// SetOverrideUsageFunc when set to true will overide the command's usage
// function with the package's usage function that displays namespaces
func (c *CmdNS) SetOverrideUsageFunc(v bool) *CmdNS {
c.OverrideUsageFunc = v
return c
}

// Enable enables namespacing for the command's subcommands
func (c *CmdNS) Enable(cmd *cobra.Command) error {
if cmd == nil {
return errors.New("cmdns: cmd cannot be nil")
}
for _, child := range cmd.Commands() {
n := NewNamespace()
n.OverrideUsageFunc = c.OverrideUsageFunc
c.Namespaces = append(c.Namespaces, n)
if err := n.Enable(child); err != nil {
return err
}
}
return nil
}

// Namespace represents a namespace for a command
type Namespace struct {
OverrideUsageFunc bool

cmd *cobra.Command
commands []*cobra.Command
}

// NewNamespace returns a new Namespace
func NewNamespace() *Namespace {
return &Namespace{
OverrideUsageFunc: true,
commands: make([]*cobra.Command, 0),
}
}

// AvailableCommands returns the namespaced commands that are available
func (n *Namespace) AvailableCommands() []*cobra.Command {
return n.commands
}

// Command returns the command for the namespace
func (n *Namespace) Command() *cobra.Command {
return n.cmd
}

// Enable enables namespacing for a sub-commmand and its immediated children.
// It returns an error if the command does not have a parent
func (n *Namespace) Enable(cmd *cobra.Command) error {
if !cmd.HasParent() {
return errors.New("cmdns: command is required a parent")
}

// Do not bind if there are not available sub commands
if !cmd.HasAvailableSubCommands() {
return nil
}

if n.OverrideUsageFunc {
cmd.SetUsageFunc(n.UsageFunc())
}

for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() {
continue
}
// copy the command add it to the root command with a prefix of its parent.
nc := *c
nc.Use = cmd.Name() + ":" + c.Use
c.Parent().AddCommand(&nc)

// hide the command so it does not show in available commands list
nc.Hidden = true
n.commands = append(n.commands, &nc)
}
n.cmd = cmd
return nil
}

// UsageFunc returns the usage function for the command that renders namespaces
func (n *Namespace) UsageFunc() (f func(*cobra.Command) error) {
return func(*cobra.Command) error {
err := tmpl(n.Command().Out(), usageTemplate, n)
if err != nil {
fmt.Print(err)
}
return err
}
}

var usageTemplate = `{{$ns := .}}{{with .Command}}Usage:{{if .Runnable}}
{{.UseLine}}{{if .HasFlags}} [flags]{{end}}{{end}}{{if gt .Aliases 0}}
Aliases:
{{.NameAndAliases}}
{{end}}{{if .HasExample}}
Examples:
{{ .Example }}{{end}}{{ if .HasAvailableSubCommands}}
Available Commands:{{range $ns.AvailableCommands}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{ if .HasLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimRightSpace}}{{end}}{{ if .HasInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimRightSpace}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsHelpCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{ if .HasSubCommands }}
Use "{{.Parent.CommandPath}} [command] --help" for more information about a command.{{end}}{{end}}
`
22 changes: 22 additions & 0 deletions cmdns_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package cmdns_test

import (
"fmt"

"github.com/gosuri/cmdns"
"github.com/spf13/cobra"
)

var runFunc = func(cmd *cobra.Command, args []string) { fmt.Println("run", cmd.Name()) }

func Example() {
ovrclk := &cobra.Command{Use: "ovrclk"}
apps := &cobra.Command{Use: "apps"}
apps.AddCommand(&cobra.Command{Use: "info", Run: runFunc})
ovrclk.AddCommand(apps)

// Enable namespacing
cmdns.Enable(ovrclk)

ovrclk.Execute()
}
3 changes: 3 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package cmdns provides namespacing to Cobra command's subcommands
// Command namespacing is an alternative way to structure sub commands, similar to `rake db:migrate` and `ovrclk apps:create`.
package cmdns
53 changes: 53 additions & 0 deletions example/full/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package main

import (
"fmt"

"github.com/gosuri/cmdns"
"github.com/spf13/cobra"
)

var helpFunc = func(cmd *cobra.Command, args []string) { cmd.Help() }
var runFunc = func(cmd *cobra.Command, args []string) { fmt.Println("run", cmd.Name()) }

func main() {
hugo := &cobra.Command{
Use: "hugo",
Short: "Hugo is a very fast static site generator",
Long: "A Fast and Flexible Static Site Generator built with love by spf13 and friends in Go",
}

newCmd := &cobra.Command{
Use: "new",
Short: "Create new content for your site",
Long: "Create a new content file and automatically set the date and title. It will guess which kind of file to create based on the path provided.",
Run: runFunc,
}
hugo.AddCommand(newCmd)

newSiteCmd := &cobra.Command{
Use: "site",
Short: "Create a new site in the provided directory",
Long: "The new site will have the correct structure, but no content or theme yet",
Run: runFunc,
}
newCmd.AddCommand(newSiteCmd)

newThemeCmd := &cobra.Command{
Use: "theme",
Short: "Create a new theme",
Long: "Create a new theme (skeleton) called [name] in the current directory. New theme is a skeleton.",
Run: runFunc,
}
newCmd.AddCommand(newThemeCmd)

// listCmd := &cobra.Command{Use: "list", Run: runFunc}
// listCmd.AddCommand(&cobra.Command{Use: "list", Run: runFunc})
// listCmd.AddCommand(&cobra.Command{Use: "add", Run: runFunc})
//hugo.AddCommand(listCmd)

cmdns.Enable(hugo)
if err := hugo.Execute(); err != nil {
panic(err)
}
}
23 changes: 23 additions & 0 deletions example/simple/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package main

import (
"fmt"

"github.com/gosuri/cmdns"
"github.com/spf13/cobra"
)

var helpFunc = func(cmd *cobra.Command, args []string) { cmd.Help() }
var runFunc = func(cmd *cobra.Command, args []string) { fmt.Println("run", cmd.Name()) }

func main() {
ovrclk := &cobra.Command{Use: "ovrclk"}
apps := &cobra.Command{Use: "apps"}
apps.AddCommand(&cobra.Command{Use: "info", Run: runFunc})
ovrclk.AddCommand(apps)

// Enable namespacing
cmdns.Enable(ovrclk)

ovrclk.Execute()
}
36 changes: 36 additions & 0 deletions helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package cmdns

import (
"fmt"
"github.com/spf13/cobra"
"io"
"strings"
"text/template"
"unicode"
)

var templateFuncs template.FuncMap = template.FuncMap{
"trim": strings.TrimSpace,
"trimRightSpace": trimRightSpace,
"rpad": rpad,
"gt": cobra.Gt,
"eq": cobra.Eq,
}

func trimRightSpace(s string) string {
return strings.TrimRightFunc(s, unicode.IsSpace)
}

//rpad adds padding to the right of a string
func rpad(s string, padding int) string {
template := fmt.Sprintf("%%-%ds", padding)
return fmt.Sprintf(template, s)
}

// tmpl executes the given template text on data, writing the result to w.
func tmpl(w io.Writer, text string, data interface{}) error {
t := template.New("top")
t.Funcs(templateFuncs)
template.Must(t.Parse(text))
return t.Execute(w, data)
}

0 comments on commit 2de2b70

Please sign in to comment.