cmdbox

package module
v0.7.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 13, 2022 License: Apache-2.0 Imports: 9 Imported by: 20

README ΒΆ

🍱 Go cmdbox Composite Commander

GoDoc License Go Report Card

A composite commander for modern human-computer text interactions.

"It's like a modular, multicall BusyBox builder for Go with built in completion and embedded documentation support."

"The utility here is that cmdbox lets you maintain your own personal 'toolbox' with built in auto-complete that you can assemble from various Go modules. Individual commands are isolated and unaware of each other and possibly maintained by other people." (tadasv123)

cmdbox is a lightweight commander for creating commands composed of modular subcommands that can be imported as packages with portable completion and embedded documentation compiled into a single, minimal, multicall binary.

Installation

Normally you would simply import "github.com/rwxrob/cmdbox".

cmdbox.DEBUG = true/CMDBOX_DEBUG=1 Mode

Developers can set the CMDBOX_DEBUG environment variable to anything or the cmdbox.DEBUG package boolean to true to activate a verbose log written to standard error output designed to make cmdbox command modules easier to develop and debug. This is particularly important since traditional Go unit test methods are somewhat more difficult when dealing with things that happen during init() time.

Also consider using cmdbox.Init() during testing to reset the package variable state between synchronous tests.

Advantages

  • Build the command-line interface to your application completely independently from your core API library

  • Automatic and extendible bash tab completion built in

  • Modular, interchangeable, readable subcommands

  • Commands can be used standalone or as multicall-enabled subcommands of a composite binary (think BusyBox)

  • Rich ecosystem of importable cmdbox-* commands

Terminology

A command module is an importable Go module hosted in a Git repository containing both an init function and a standalone command package with a main function allowing it to be used by itself or composed into another command as a subcommand.

When referring to standalone in this context we mean a command module that has been compiled into a single command from its command package with a main function.

How It Works

The magic of cmdbox comes from the clean separation of Commands into their own files --- or even separate packages imported as "side-effects" (_) with their own Git repos --- and composing them into a single multicall binary that contains an internal register of these Commands. This is accomplished through simple, judicious use of the init() function. This approach incurs no more performance hit than would already be required for any other such solution and is 100% safe for concurrency.

By the time the command-line application's main() function is called all it has to do is cmdbox.Execute() the top-level command. A typical main.go needs no more than three lines:

package main
import "github.com/rwxrob/cmdbox"
func main() { cmdbox.Execute() }

The name of the main command may be passed to Execute("mymain") or can be left off and inferred from the name of the binary instead.

The bulk of the code is kept in one or more other modular Command files such as this sample (the package name usually does not matter, cmd is used by convention):


package cmd

import (
    "fmt"
    "github.com/rwxrob/cmdbox"
)

func init() {
    x := cmdbox.Add("greet")
    x.Summary = `print a polite greeting`
    x.Usage = `[NAME]`
    x.Copyright = `Copyright 2021 Rob Muhlestein`
    x.License = `Apache-2`
    x.Version = `v1.0.0`
    x.AddHelp()
    // ...
    x.Method = func(args ...string) error {
        name = "you"
        if len(args) > 0 {
            name = args[0]
        }
        fmt.Printf("Well, hello there, %v!\n",name)
        return nil
    }
}

This code can now be used both as a standalone foo program or imported by other composites with import "github.com/rwxrob/cmdbox-foo" in one of its own Command files (or just the main file).

This modularity allows Commands to freely be exchanged and migrated between different projects and repos, or just factored out entirely and imported freely by anything. If there are naming conflicts, the cmdbox.Rename provides for easy, explicit renames as needed.

The x.AddHelp() helper method will add a nice default h|help command that includes a summary of the legal information.

Motivation

This package scratches several "personal itches" that have come up from the needs of modern human-computer interaction and annoyances with the popular alternatives:

  1. No commanders exist for writing simple interfaces (no dashes)
  2. Separating documentation from a program is a modern anti-pattern
  3. No commanders exist with internal tab completion
  4. Sourcing shell completion is a bloated, modern anti-pattern
  5. kubectl requires sourcing 12637 lines in .bashrc for completion
  6. Applications with cmdbox require 1 line (complete -C foo foo)
  7. Completion resolved by the application itself is more powerful
  8. Tab completion is the simplest way to provide good help to the user
  9. Tab completion resolution is faster and easier in Go
  10. Getopt-style options have always been bad UI/HCI
  11. Modern command-line interfaces depend more on stateful context
  12. Modern command-line programs need to be easily distributed
Times Have Changed

The world has outgrown the original UNIX model for both command design and documentation, presumably due to the resource constraints of the day. These constraints forced shared library dependencies and commands were designed to be as minimal as possible doing "only one thing really well." Combining several actions into a single command (think git clone or go tool dist list) or embedding full command documentation into the executable would have been considered monstrously wasteful at that time. The result was lots of small commands dependent on specific shared libraries and separate systems both for documentation (man pages) and tab completion (/etc/bash_complete). This forced software delivery to invent the package management systems we see today. Shipping everything in a single executable was simply unthinkable.

Today the Go programming language has revolutionized applications development and respectfully embraced modern use of resources. Static linking, multicall binaries, in which an entire embedded filesystem can be included, and cross-compilation, built in concurrency, reflection and documentation are all evidence of how times have changed. It follows then, that our command interface designs and documentation approaches should be equally modernized. There is now plenty of room to compose several commands and subcommands into a single, composite, multicall binary executable. In fact, this has become idiomatic for Go's own tool set and popularized by the container-world's love for BusyBox. Distribution and portability are more important than memory and storage size these days.

Sometimes You Just Need Tab

Usually completion context is triggered simply by tapping tab once or twice from the shell when typing in the command and its different subcommand arguments. Tab completion is supported as a standard in most all modern shells. Tab completion is often all that is needed, even usage would be overkill. A user simply wants to remember what the command name is.

Not only is tab completion desirable, it has become the preferred method of containing complexity. The git application, for example, has very precise rules governing its tab completion based on the subcommand being called. Pressing tab from git branch will only show you the current branches.

Keep the Docs with the Command

Clean, standardized, extensive documentation needs to travel with the command it documents rather than being separate from it. There is no telling what operating systems will emerge nor how (and if) they will implement system documentation of any kind. Indeed, with the use of modern web, containerization, and git hosting documentation the need for any system-level documentation system is sharply diminishing. Better to embed it with the command itself when needed. When building container FROM scratch binaries this enables documentation without bloating the container up to contain the entire man page system. The same is true for tab completion itself.

Modern Human-Computer Interaction

Voice and human-friendly textual command-line interfaces are becoming more and more popular as more humans interact with command line interfaces in chat applications, messaging and more. This influence has permeated to system command design and deprecated the design decision to use traditional getopt-like switches and options, which cannot be voiced or tab-completed easily. (Consider how difficult it is to type or voice a simple dash into most text applications.)

Simplicity on the command-line has been an ignored requirement for some time.

Why?

Because only amazing technical people are using the command line?

Perhaps. But that is a very bad reason. Interfaces need not be overly complex or difficult to memorize just because most users are technical.

The cmdbox approach takes these HCI considerations very seriously placing priority on how humans use modern command-line interfaces and developing them accordingly. Human interaction is the single most important design decision for any application, including one that runs on the command line --- especially in the modern world of conversational assistants and chat bot plugins.

Design Decisions

This is a summary of the design decisions made over the course of the project (in no particular order). It is provided in the hopes of addressing other design concerns anyone reviewing this package might have before choosing to use it.

  • Several "cool" things have been dropped. This has resulted in a code base that seems deceptively simple, even trivial. That is by design, and came after many hours of refactoring different approaches over the years. Developers can now create "cool" additions by implementing their own cmdbox-* packages and leaving the core cmdbox package alone. Such an ecosystem is far more beneficial to the community as a whole.

  • Any shell or system that sets the COMP_LINE environment and allows the targeting of the same command being called to complete itself (ex: complete -C foo foo) is supported. This obviously includes only bash at the moment, but could easily be added to other existing and future shells. This established mechanism for communicating completion context is not only mature but simple and trivial to implement.

  • Aliases for Commands can be used for alternative languages as well providing multi-lingual speakable command line interface possibilities.

  • Centralizing messages in a map[string]string allows them to be changed easily by those wishing to support more than the English language in their composite programs.

  • Dashes on the command line have always been a bad idea, even more so now. The world has suffered enough from dubious design decisions made decades ago in the getops world when string sizes were limited and usernames no more than eight characters. Thankfully, we can now dismiss these bad practices. As Unicode has standardized, and subcommand composites such at git, kubectl, docker, gh and others like them have become standard practice --- along with the rise of conversational interfaces that transform any command line into a speakable user interface --- it is time we started giving our programs support for written natural language grammars. Not only will our code and command lines be more readable but more accessible allowing the tried-and-true command line interface model to extend to chat services, conversational assistants, and voice-enabled devices. In fact, cmdbox is ideal for creating modular (but monolithic) chat bots of all kinds.

  • Originally, a custom, Markdown-derived syntax was supported for the text of all Command fields. This has been removed for now and will be reconsidered at a later date with proposals from the community. For now, the convention is to use nothing but regular text with no attempt to provide any type of formatting markup. It is critical that the cmdbox community agree on a markup standard, if any. While cmdbox plans to improve on the austerity of Go doc (allowing for color, emphasis, and such) this has to be considered as carefully as the base cmdbox API itself.

  • Internationalization was a big design consideration from the beginning. All code containing the English defaults is separate from the rest allowing build tools to generate language specific versions. The idea of putting all supported languages into monolithic files with locale templates was considered but quickly dismissed given the potential impact to executable size. Localized builds are a common industry practice. The English naming of the default builtin commands is at least as appropriate as these standard keywords are included in many other contexts help, version, and usage are very ubiquitous. If needed, these can be aliased easily by adding commands that encapsulate them with Call() or the commands themselves can be front-end shells to high-level library API calls.

  • Using structs instead of interfaces makes more sense given the serialization goals to enable quick and easy to read and write documentation embedded in the source.

  • Use of aliases (d|del|rm|delete) allows accessibility even if tab completion is not (yet) supported on a particular platform. This provides minimal multi-language support possibilities as well.

  • At one point the internal map[string]*Command index was private to discourage tight coupling between commands and prevent "innovation" that could fork away simplicity as a core tenet of the cmdbox project. But it was decided that this inflexibility came at too great a cost to potential needs of command creators and cmdbox.Reg() was added.

  • Naming of cmdbox-* module repos allows for easy discovery. Using the conventional cmd/cmd.go package/file name (which is ignored at init time) allows consistency for generators and cataloging.

How Does Completion Work?

Please Bash Programmable Completion and pay specific attention to the complete -C option.

For Bash all you need to add to your ~/.bashrc file is the following:


complete -C foo foo

This will cause the shell to run foo and set the COMP_LINE environment variable every time you tap the tab key once or twice. This allows a cmdbox composite program to detect completion context and only print the words that should be possible for the last word of the command when the tab key was pressed. (Also see Go documentation of the Command.Complete method.)

Machine Learning in Simple Terminal Commands?

Yep. Allowing a completion function allows incredible interesting possibilities with completion that uses full human language semantics rather than just prefixing the most likely candidates. Even a full machine learning code module could be added allowing any possible speech completion. Such considerations seem very absent from the HCI conversation regarding terminal command line usage in the past, but times are changing. Phone, OpenCV, natural language processing and many other innovations are bringing the focus (rightly) back to speakable user interfaces of all kinds.

Does this have anything to do with BusyBox?

cmdbox originally started as cmdtab and was used for more than two years before Rob realized BusyBox was even a thing. The name was changed when it became clear just how close to BusyBox cmdbox is in approach and purpose, even if the design and implementation are completely different.

Here are the similarities:

  • Multicall binaries (hard and symbolic links)
  • Light-weight
  • Can be used as a shell (of sorts)
  • Facilitate small containers

Here are the major differences:

  • Written in Go
  • Modular and composable
  • Rich ecosystem of importable commands
  • Embedded documentation
  • Support for standalone commands
  • No symbolic links required

Why Not Just import flag?

Nothing wrong with that if you are okay with dashes and such. cmdbox is designed for more.

What about Cobra?

Cobra is the Electron of command-line interface libraries.

While Cobra is popular and Steve and the rest of the contributors are wonderful, talented people. Cobra has codified several anti-patterns into rather large applications that have spread like cancer wasting resources and breaking .*rc files with wild abandon. For example, kubectl requires the sourcing of 12637 lines of bash code every time a new shell spawns, and that is only one Cobra application. There are many others.

Considering that Cobra is used for most cloud-native related applications it is not hyperbole to say that tens of thousands of people are evaluating 50,000+ lines of code every time they start a new Bash shell.

While there are ways around this, the simplest has been completely overlooked by the Cobra project: complete -C foo foo. The project now maintains so much technical debt because of these hastily made early design decisions in the race to be the first Go Commander that something radically new is needed.

Beyond that, Cobra missed an amazing opportunity to create a modular ecosystem of composable commands by leveraging init() side-effect imports (_). cmdbox takes full and appropriate advantage of these amazing Go features. Cobra's syntax for init() is anything but clean. cmdbox init() bodies are sexy, simple, and self-documenting. In fact, they usually contain the documentation itself in readable, simple text that is rendered much like an embedded "man" page.

Terminology

  • command - foo or mycmd foo
  • tool - another name for command
  • module - a Go module containing cmdbox
  • composite - final binary composed of multiple commands

Conventions

  • Prefix repos with cmdbox- for easy discovery.
  • Use x for Command pointer func init() {x := cmdbox.Add("name")}
  • Reuse x when adding multiple commands in same init()
  • Use x for Command method receivers func (x *Command) ...

Real World Examples

Copyright 2021 Robert S. Muhlestein (mailto:rob@rwx.gg)
Licensed under Apache-2.0

"cmdbox" and "cmdbox" are legal trademarks of Robert S. Muhlestein but can be used freely to refer to the cmdbox project https://github.com/rwxrob/cmdbox without limitation. To avoid potential developer confusion, intentionally using these trademarks to refer to other projects --- free or proprietary --- is prohibited.

Documentation ΒΆ

Overview ΒΆ

Package cmdbox is a multicall, modular commander with embedded

documentation and tab completion handling that prioritizes modern, simple human-computer interactions from the command line.

Package cmdbox is a multicall, modular commander with embedded tab

completion and locale-driven documentation, that prioritizes modern, speakable human-computer interactions from the command line.

Index ΒΆ

Examples ΒΆ

Constants ΒΆ

This section is empty.

Variables ΒΆ

View Source
var BadType = func(v interface{}) error {
	return fmt.Errorf(m_bad_type, v)
}

BadType returns an error containing the bad type attempted.

View Source
var CallerRequired = func() error {
	return fmt.Errorf(m_missing_caller)
}

CallerRequired retuns an error indicating a Command was used incorrectly (as designed by the developer) and that is requires being called from something else.

View Source
var Color = true

Color sets the default output mode for interactive terminals. Set to false to force uncolored output for testing, etc. Non-interactive terminals have color disabled by default (unless ForceColor is set).

View Source
var DEBUG bool

DEBUG is set when os.Getenv("CMDBOX_DEBUG") is set to anything. Produces verbose debugging logs to stderr to help cmdbox users develop robust tools.

View Source
var DefaultComplete = CompFunc(CompleteCommand)

DefaultComplete is assigned CompleteCommand by default but can be assigned any valid CompFunc to override it. This function is called to perform completion for any Command that does not implement its own Command.CompFunc.

View Source
var DoNotExit bool

DoNotExit is a utility for disabling any call to os.Exit via any caller in order to trap panics and use them for testing, etc.

View Source
var ForceColor = false

ForceColor forces color output no matter what the default Color value is. This can be used for testing or associating with configuration parameters (for example, when a user has a pager that supports color output).

View Source
var Harmless = func(msg ...string) error {
	if len(msg) > 0 {
		return fmt.Errorf("%v", msg[0])
	}
	return fmt.Errorf("")
}

Harmless returns an error that is mostly designed to trigger an error exit status. This is useful for help and commands like it to help the user disambiguate significant output from just help and other error output.

View Source
var MissingArg = func(name string) error {
	return fmt.Errorf(m_missing_arg, name)
}

MissingArg returns an error stating that the name of the parameter for which no argument was found.

Reg is the internal register (map) of Commands. See CommandMap and Add. Use caution when manipulating Reg directly.

View Source
var SyntaxError = func(msg string) error {
	return fmt.Errorf(m_syntax_error, msg)
}

SyntaxError returns an error with the message stating the problem.

View Source
var TrapPanic = func() {
	if r := recover(); r != nil {
		ExitError(r)
	}
}

TrapPanic recovers from any panic and more gracefully displays the error as an exit message. It is used to gaurantee that no cmdbox composite command will ever panic (exiting instead). It can be redefined to behave differently or set to an empty func() to allow the panic to blow up with its full trace log.

View Source
var TrapPanicNoExit = func() {
	if r := recover(); r != nil {
		fmt.Println(r)
	}
}

TrapPanicNoExit is same as TrapPanic without exiting. It prints the panic instead (useful for testing).

View Source
var UnexpectedArg = func(name string) error {
	return fmt.Errorf(m_unexpected_arg, name)
}

UnexpectedArg returns an error stating that the argument passed was unexpected in the given context.

View Source
var Unimplemented = func(a string) error {
	return fmt.Errorf(m_unimplemented, a)
}

Unimplemented returns an unimplemented error for the Command passed. This function may be overriden by command modules from their init() and main() procedures to change behavior for everything in the composite command. See "unimplemented" in Messages.

View Source
var Unresolvable = func(msg string) error {
	return fmt.Errorf(m_unresolvable, msg)
}

Unresolvable returns an error stating the command method could not be found in the internal registry.

View Source
var UsageError = func(x *Command) error {
	return fmt.Errorf("usage: %v %v", x.Name, x.Usage)
}

UsageError returns an error containing the usage string suitable for printing directly. This function may be overriden by CmdBox command modules from their init and main methods to change behavior for everything in the composite command.

Functions ΒΆ

func Call ΒΆ

func Call(caller *Command, name string, args ...string) error

Call allows any Command in the internal register to be called directly by name. The first argument is an optional pointer to the calling Command, the second is the required name, and the third is an optional list of string arguments (or nil). Resolve is first called to get the Command from the internal registry and lookup the proper Method and any argument shifting required. If no Method is returned Call returns Unimplemented. Otherwise, Method is called with its arguments and error result returned. See command.Call, Resolve, Command, Execute, and ExampleCall as well.

Example (Caller_Subcommand) ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing

	caller := cmdbox.Add("foo", "h|help")

	x := cmdbox.Add("foo help")

	x.Method = func(args ...string) error {
		fmt.Printf("help for foo %v\n", args)
		return nil
	}

	cmdbox.Call(caller, "help")
	cmdbox.Call(nil, "foo help")
	cmdbox.Call(caller, "help", "with", "args")

}
Output:

help for foo []
help for foo []
help for foo [with args]
Example (Nil_Caller) ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing

	x := cmdbox.Add("greet")

	x.Method = func(args ...string) error {
		fmt.Println("hello")
		return nil
	}

	cmdbox.Call(nil, "greet")

}
Output:

hello

func CompleteCommand ΒΆ added in v0.0.8

func CompleteCommand(x *Command) []string

CompleteCommand takes a pointer to a Command (x) returning a list of lexigraphically sorted combination of strings from x.Commands that are found in the internal register and x.Params that match the current completion context with any x.Hidden strings removed. Returns an empty list if anything fails. Note that no assertion validating that the specified command names exist in the register. See the Command.Complete method and comp subpackage.

func Delete ΒΆ added in v0.1.0

func Delete(names ...string)

Delete deletes one or more commands from the internal register.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	cmdbox.Add("foo")
	cmdbox.Add("bar")
	fmt.Println(cmdbox.Names())
	cmdbox.Delete("bar")
	fmt.Println(cmdbox.Names())

}
Output:

[bar foo]
[foo]

func Dups ΒΆ added in v0.1.0

func Dups() []string

Dups returns key strings of duplicates (which can then be easily renamed). Keys are sorted in lexicographic order. See Rename.

func Execute ΒΆ

func Execute(a ...string)

Execute is the main entrypoint into a CmdBox composite command and is always called from a main() function. In fact, most won't need the optional argument at all since it is inferred by the name of the executable. See ExecutedAs.

package main
import "github.com/rwxrob/cmdbox"
func main() { cmdbox.Execute() }

Execute also traps all panics and eventually Calls the Command matching the inferred name from the Reg Commands register. If completion context is detected (see comp.Yes), Execute calls x.Complete instead of Calling it. Execute is guaranteed to always exit the program cleanly. See Call, TrapPanic, and Command.

Example (No_Method) ΒΆ
package main

import (
	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	cmdbox.ExitOff()
	defer cmdbox.ExitOn()

	cmdbox.Add("foo", "some")
	cmdbox.Execute("foo")

}
Output:

usage: foo some

func ExecutedAs ΒΆ added in v0.0.10

func ExecutedAs() string

ExecutedAs returns the multicall inferred name of the executable as it was called during the init() phase. The multicall approach (akin to BusyBox) allows the binary to be renamed, hard or soft linked, or copied, effectively changing the behavior simply by changing the resulting changed name. For security reasons this name may never be changed at runtime. When the Execute function is called without any arguments the ExecutedAs value is inferred automatically. Irrelevant suffixes are removed (currently, only .exe).

func Exit ΒΆ

func Exit()

Exit calls os.Exit(0) unless DoNotExit has been set to true.

func ExitError ΒΆ

func ExitError(err ...interface{})

ExitError prints err and exits with 1 return value unless DoNotExit has been set to true.

func ExitOff ΒΆ added in v0.7.5

func ExitOff()

ExitOff sets DoNotExit to false.

func ExitOn ΒΆ added in v0.7.5

func ExitOn()

ExitOn sets DoNotExit to true.

func ExitSyntaxError ΒΆ added in v0.7.8

func ExitSyntaxError(a string)

ExitSyntaxError prints the bad syntax and calls ExitError().

func ExitUnimplemented ΒΆ

func ExitUnimplemented(a string)

ExitUnimplemented calls Unimplemented and calls ExitError().

func Init ΒΆ added in v0.0.16

func Init()

Init initializes (or re-initialized) the package status and empties the internal commands register (without changing its reference). Init is primarily intended for testing to reset the cmdbox package.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	fmt.Println(cmdbox.Names())
	cmdbox.Add("foo")
	fmt.Println(cmdbox.Names())
	cmdbox.Init()
	fmt.Println(cmdbox.Names())

}
Output:

[]
[foo]
[]

func JSON ΒΆ

func JSON() string

JSON serializes the current internal package register of commands as JSON which can then be used to present documentation of the composite command in different forms. Also see YAML. Empty values are always omitted.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	fmt.Println(cmdbox.JSON())
	cmdbox.Add("foo")
	fmt.Println(cmdbox.JSON())
}
Output:

func Names ΒΆ added in v0.1.5

func Names() []string

Names returns a sorted list of all Command names in the internal register.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	cmdbox.Add("foo")
	cmdbox.Add("bar")
	fmt.Println(cmdbox.Names())

}
Output:

[bar foo]

func Print ΒΆ added in v0.0.16

func Print()

Print is shortcut for fmt.Println(cmdbox.YAML()) which is mostly only useful during testing.

func Rename ΒΆ added in v0.0.4

func Rename(from, to string)

Rename renames a Command in the Reg register by adding the new name with the same *Command and deleting the old one. This is useful when a name conflict causes New to append and underscore (_) to the duplicate's name. Rename can be called from init() at any point after the duplicate has been added to resolve the conflict. Note the order of init() execution --- while predictable --- is not always apparent. When in doubt do Rename from main(). Rename is safe for concurrency.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	cmdbox.Add("foo")
	cmdbox.Add("foo")
	cmdbox.Rename("foo_", "bar")
	fmt.Println(cmdbox.Names())

}
Output:

[bar foo]

func Set ΒΆ added in v0.1.0

func Set(name string, x *Command)

Set sets the internal register for the given key to the given *Command pointer in a way that is safe for concurrency. Replaces entries that already exist. Note that although this allows register key names to refer to commands that have an actual x.Name that differs from the key this is discouraged, which is why Add and Rename should generally be used instead. Also see Add and Get.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	foo := cmdbox.NewCommand("foo")
	cmdbox.Set("foo", foo)
	bar := cmdbox.NewCommand("bar")
	cmdbox.Set("bar", bar)
	cmdbox.Set("bar", foo)
	newbar := cmdbox.Get("bar")
	fmt.Println(newbar.Name)

}
Output:

foo

func YAML ΒΆ added in v0.1.0

func YAML() string

YAML serializes the current internal package register of commands as YAML which can then be used to present documentation of the composite command in different forms. Empty values are always omitted.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	cmdbox.Add("foo")
	fmt.Print(cmdbox.YAML())
}
Output:

Types ΒΆ

type Command ΒΆ

type Command struct {
	Name        string          `json:"name,omitempty" yaml:",omitempty"`
	Summary     string          `json:"summary,omitempty" yaml:",omitempty"`
	Usage       string          `json:"usage,omitempty" yaml:",omitempty"`
	Version     string          `json:"version,omitempty" yaml:",omitempty"`
	Copyright   string          `json:"copyright,omitempty" yaml:",omitempty"`
	License     string          `json:"license,omitempty" yaml:",omitempty"`
	Description string          `json:"description,omitempty" yaml:",omitempty"`
	Site        string          `json:"site,omitempty" yaml:",omitempty"`
	Source      string          `json:"source,omitempty" yaml:",omitempty"`
	Issues      string          `json:"issues,omitempty" yaml:",omitempty"`
	Commands    *util.StringMap `json:"commands,omitempty" yaml:",omitempty"`
	Params      []string        `json:"params,omitempty" yaml:",omitempty"`
	Hidden      []string        `json:"hidden,omitempty" yaml:",omitempty"`
	Default     string          `json:"default,omitempty" yaml:",omitempty"`
	// Title()
	// Legal()
	CompFunc   CompFunc `json:"-" yaml:"-"`
	Caller     *Command `json:"-" yaml:"-"`
	Method     Method   `json:"-" yaml:"-"`
	sync.Mutex `json:"-" yaml:"-"`
}

Command contains a Method or delegates to one or more other Commands by name. Typically a Command is created within an init() function by calling cmdbox.New:

import "github.com/rwxrob/cmdbox"

func init() {
    // use x by convention
    x := cmdbox.New("greet","hi","hello")
    x.Method = func(args []string) error {
        if len(args) == 0 {
            args = append(args, "hi")
        }
        switch args[0] {
        case "hello":
            fmt.Println("*Hello!*")
        case "hi":
            fmt.Println("*Hello!*")
        default:
            return x.UsageError()
        }
        return nil
    }
}

Providing the method, documentation, and tab completion rules in a single file providing a tight, clean view of the Command that is easy for humans and computers to quickly parse. Such Commands can also be placed into their own "side-effect" packages for importing into other code using the underscore identifier making for potentially very succinct commands.

import (
      "github.com/rwxrob/cmdbox"
      _ "github.com/rwxrob/cmdbox-greet"
)

func main() { cmdbox.Run() }

Or, it can be combined with others and composed into an entirely new monolith one of its commands:

import (
      "github.com/rwxrob/cmdbox"
      _ "github.com/rwxrob/cmdbox-greet"
      _ "github.com/rwxrob/cmdbox-timer"
      _ "github.com/rwxrob/cmdbox-pomo"
)

func init() {
      x := cmdbox.New("skeeziks","greet","timer","pomo")
      x.Usage = `[greet|timer|pomo]` // default: greet
      x.Summary = `a simple command line assistant`
      // notice no Method
      // ...
}

This modularity allows a CmdBox monolith to compose in an unlimited number of commands. Hence the name CmdBox, a nod to BusyBox which does something similar but without the ability for sub-command composition and coding in Go.

A Command also includes all the documentation for the method as well as rules about how to handle tab completion so there is no need for extra shell code to be "eval"ed. In Bash, this happens by using the -C option of complete telling Bash to use the same command that it is completing for completion itself.

complete -C skeeziks skeeziks

And then the following will complete with the list passed to New():

skeeziks g<TAB>
skeeziks greet

Additional builtin Commands are automatically composed into the monolith as well (unless disabled):

skeeziks h<TAB>
skeeziks help

Tab completion rules default to the list of Commands and Params, but can be overriden per Command by defining and assigning an anonymous closure function to the CompFunc field (see CompFunc type).

If a Command.CompFunc is not assigned then Command.Complete will delegate to the package cmdbox.CompFunc passing it a pointer to the Command. In this way the default completion behavior of all Commands can be easily tested and changed, even at run time.

This allows for dynamic tab completion possibilities that have nothing to do with sub-Commands and can access program and system state for their determination.

Conventions for Text Field Values ΒΆ

When providing the text of the different fields of the the Command struct please keep the following conventional considerations in mind:

* Imagine your text being spoken by a conversational assistant * Avoid the use of acronymns and excessive punctuation * Resist the temptation to assign const and vars outside init

Fields such as Usage are obvious exceptions to these conventions.

Description ΒΆ

The Description contains a long Command description and can span multiple paragraphs, even pages. Use of backtick quotes (`) is preferred and text may be indended and wrapped anywhere. Paragraphs are recognized by a blank line between them. Keep Go documentation style guidelines in mind when writing them (headers on single line, four space indent for verbatim text, etc.)

Params ΒΆ

The Params list is for completion as well, specifically for things that are neither Commands nor actions to be handled by the Method but would be nice to have included in completion. Unlike Commands and Aliases, Params do not need to be valid names (see valid package). This allows them to be used for things such as default numeric values that may begin with a number or dash and other things that contain punctuation. These should not, however, be used to bypass the core requirement for speakable Commands and Aliases, and whenever possible, arguments as well. (For more complex completion, assign a custom x.CompFunc function.)

Hidden ΒΆ

The Hidden list is to keep commands from being completed or showing up in any general help documentation. They have to be specifically used or passed to help directly.

Examples ΒΆ

For examples of different Command structs search on GitHub for any project beginning with cmdbox- such as the following:

* https://github.com/rwxrob/cmdbox-greet * httpe://github.com/rwxrob/cmdbox-pomo

var Main *Command

Main is always set to the main command that was used for Execute. This can be useful from certain subcommands to query or call directly.

func Add ΒΆ added in v0.1.0

func Add(name string, a ...string) *Command

Add creates a new Command, adds it to the Reg internal register, and returns a pointer to it (assigned to 'x' by convention). The Add function is guaranteed to never return nil but will panic if invalid arguments are passed (see Validation below). Further initialization can be done with direct assignments to fields of x from within the init() function. Depending on the situation, you may find having a single init() that reuses x to Add multiple commands is desirable. Other times, you may wish to keep one Command per file usually named after the command.

First Argument is Command Name ΒΆ

The first argument is *always* the main Name by which it will be called and becomes x.Name. This uniquely identifies the Command and becomes the key used by cmdbox.Call to lookup the Command from the internal register for completion and execution. By convention, these command names should be complete words with no punctuation except for dot and applications will panic with names that do not follow this convention. It is sometimes desirable to group multiple commands using traditional dotted notation.

Command Names May Contain Two Words ΒΆ

The Name may contain two complete names separated by a single space. Only main commands should have a single name. Subcommands should always have two names to avoid naming collisions in Reg from other imported cmdbox modules and facilitate default tab completion. The first name is also the name of the parent command. This also removes indirection when called from Execute.

x := cmdbox.Add("foo help")
x.Summary = `output help information for foo`

Commands May Have Subcommands ΒΆ

Any variadic arguments that follow will be directly passed to x.Add(). This provides a succinct summary of how the command may be called. As a convenience a h|help command can be added with x.AddHelp(), but is not included automatically. Make sure to AddHelp after Version, Copyright, and License have been set so that the LEGAL section will populate correctly.

x := cmdbox.Add("foo", "bar")
x.Copyright = "2021 (c) Rob Muhlestein"
x.AddHelp()

Subcommands May Have Aliases ΒΆ

Each argument passed in the list may be in signature form (rather than just a name) meaning it may have one or more aliases prefixed and bar-delimited which are added to the x.Commands Map:

x := cmdbox.Add("foo", "b|bar")

Aliases will appear in the help documentation (from AddHelp) but will not be options for completion even though they are valid commands. This is to prevent the completion list from getting overwhelming.

Default Usage Automatically Inferred ΒΆ

Since it is so common to declare everything up front for a new Command the x.Default will be set to an optional joined list of all commands and aliases as if the following where explicitly assigned:

x.Usage = `[b|bar]`

Of course, this can always be overridden explicitly by Command authors using the same assignment.

Command Method Has Priority ΒΆ

When the Command (x) is called from cmdbox.Call the x.Commands Map is used to delegate the call to a matching Command in the internal register if and only if the Command itself does not have a Method defined. Please read that last sentence carefully. See Call and x.Call for more about this delegation and how key names are matched to the internal register.

All but top-level Commands will usually assign a x.Method to handle the work of the Command. By convention the arguments should be named "args" if something is expected and "none" if not. The returned error type usually remains unnamed.

x.Method = func(args ...string) error {
  fmt.Println("would do something")
  return nil
}

No Command Method Will Trigger Default Delegation ΒΆ

If the Command does not have an x.Method of its own, then the list of arguments passed to Add is assumed to be the signatures for other registered Commands.

No Assertion of Command Registration ΒΆ

Add does not validate that a potential command has been registered because the state of the internal register cannot be predicted at the specific time any init function is called. Not all Commands may yet have been registered before any other Add is called. This means runtime testing is required to check for errant calls to unregistered Commands (which otherwise produce a relatively harmless "unimplemented" error.)

Duplicate Names Append Underscore ΒΆ

Because CmdBox composite commands may be composed of packages imported from a rich eco-system of command module packages it is possible that two CmdBox modules might use conflicting names and need some resolution by the composite developer who is importing them.

Rather than override any Command previously added with an identical Name, Add simply adds an underscore (_) to the end of the name allowing it to be identified with Dups. Developer will know of such conflicts in advance and be able to easily correct them by calling the Rename function before Execute.

Example (Simple) ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	cmdbox.Add("foo")
	fmt.Println(cmdbox.Names())

}
Output:

[foo]
Example (With_Duplicates) ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	cmdbox.Add("foo")
	cmdbox.Add("foo")
	fmt.Println(cmdbox.Dups())
	fmt.Println(cmdbox.Names())

}
Output:

[foo_]
[foo foo_]
Example (With_Subcommands) ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	cmdbox.Add("foo")
	cmdbox.Add("foo bar")
	fmt.Println(cmdbox.Names())

}
Output:

[foo foo bar]

func Get ΒΆ added in v0.1.0

func Get(name string) *Command

Get returns the *Command for key name if found.

Example ΒΆ
package main

import (
	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	cmdbox.Add("foo", "bar")
	cmdbox.Get("foo").Print()

}
Output:

name: foo
usage: bar
commands:
  bar: bar

func NewCommand ΒΆ added in v0.1.0

func NewCommand(name string, a ...string) *Command

NewCommand returns pointer to new initialized Command. See the New package function instead for creating a new Command that is also added to the Register. Minimal validation is done on the name and all arguments (subcommands, actions) to ensure a consistent user experience for all CmdBox commands (see valid subpackage for more). The Usage string is set to the default with UpdateUsage. Since calling NewCommand involves critical syntax checks a panic is thrown if invalid.

Example (Commands) ΒΆ
package main

import (
	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	x := cmdbox.NewCommand("foo", "h|help", "l|ls|list")
	x.Print()
}
Output:

name: foo
usage: '[h|help|l|list|ls]'
commands:
  h: help
  help: help
  l: list
  list: list
  ls: list
default: help
Example (Dup) ΒΆ
package main

import (
	"github.com/rwxrob/cmdbox"
)

func main() {
	x := cmdbox.NewCommand("foo_")
	x.Print()
}
Output:

name: foo_
commands: {}
default: help
Example (Invalid) ΒΆ
package main

import (
	"github.com/rwxrob/cmdbox"
)

func main() {
	defer cmdbox.TrapPanicNoExit()
	cmdbox.NewCommand("Foo") // see valid/name.go for more

}
Output:

syntax error: invalid name (must be lowercase word): Foo
Example (Simple) ΒΆ
package main

import (
	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	x := cmdbox.NewCommand("foo")
	x.Print()
}
Output:

name: foo
commands: {}
Example (Subcommand) ΒΆ
package main

import (
	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	x := cmdbox.NewCommand("foo bar")
	x.Print()
}
Output:

name: foo bar
commands: {}

func Slice ΒΆ added in v0.1.0

func Slice(names ...string) []*Command

Slice returns a slice of *Command pointers and fetched from the internal register that match the key names passed. If an entry is not found it is simply skipped. Will return an empty slice if none found.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	cmdbox.Add("foo")
	cmdbox.Add("foo help")
	cmdbox.Add("bar")
	for _, x := range cmdbox.Slice("foo", "bar") {
		fmt.Println(x.Name)
	}

}
Output:

foo
bar

func (*Command) Add ΒΆ

func (x *Command) Add(sigs ...string)

Add adds the list of Command signatures passed. A command signature consists of one or more more aliases separated by a bar (|) with the final word being the name of the actual Command. Aliases are a useful way to provide shortcuts when tab completion is not available and should generally be considered for every Command. Single letter aliases are common and encouraged.

Note that Add does not validate inclusion in the internal Register (Reg) since in many cases there may not yet be a Register entry, and in the case of actions handled entirely by the Command itself there never will be. See Command.Commands and Command.Run.

Example ΒΆ
package main

import (
	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	x := cmdbox.NewCommand("foo")
	x.Add("l|ls|list", "h|help", "version")
	x.Print()
}
Output:

name: foo
commands:
  h: help
  help: help
  l: list
  list: list
  ls: list
  version: version
default: help

func (*Command) AddHelp ΒΆ added in v0.7.8

func (x *Command) AddHelp()

AddHelp adds a basic h|help subcommand sets x.Default to it if unset. As of v0.7.7 help is no longer automatically added to allows the lightest binaries possible. See PrintHelp and Help as well.

func (*Command) Call ΒΆ

func (x *Command) Call(name string, args ...string) error

Call is a convenience method that calls cmdbox.Call(x,"foo",args...).

func (*Command) CommandRequired ΒΆ added in v0.7.8

func (x *Command) CommandRequired() bool

CommandRequired returns true if x.Default is not set and there also is no x.Method found. Such commands always require a explicit subcommand or will produce a usage error.

func (*Command) Complete ΒΆ

func (x *Command) Complete()

Complete prints the possible strings based on the current Command and completion context. If the Commands CompFunc has been assigned (not nil) it is called and passed its own pointer. If CompFunc has not been assigned (is nil) then cmdbox.DefaultComplete is called instead. This allows Command authors to control their own completion or simply use the default. It also allows changing the default by assigning to the package cmdbox.DefaultComplete before calling cmdbox.Execute.

Example (Commands) ΒΆ
package main

import (
	"github.com/rwxrob/cmdbox"
	"github.com/rwxrob/cmdbox/comp"
)

func main() {
	cmdbox.Init() // just for testing
	x := cmdbox.NewCommand("foo")
	x.Add("l|ls|list")
	comp.This = "l"
	x.Complete()

}
Output:

list
Example (CompFunc) ΒΆ
package main

import (
	"github.com/rwxrob/cmdbox"
	"github.com/rwxrob/cmdbox/comp"
)

func main() {
	cmdbox.Init() // just for testing
	x := cmdbox.NewCommand("foo")
	x.CompFunc = func(x *cmdbox.Command) []string {
		// deliberatly always return the same thing
		// could add filter logic here
		return []string{"bar", "this"}
	}
	x.Complete()
	comp.This = "b"
	x.Complete()
}
Output:

bar
this
bar
this

func (*Command) Help ΒΆ added in v0.2.0

func (x *Command) Help() string

Help returns a formatted string suitable for printing either to a file or to an interactive terminal. For a more structured form of the same information see YAML, JSON, Print, and PrintHelp.

func (*Command) JSON ΒΆ added in v0.1.0

func (x *Command) JSON() string

JSON is shortcut for json.Marshal(x). See util.ToJSON.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	x := cmdbox.NewCommand("foo", "help")
	fmt.Println(x.JSON())
	fmt.Println(x.String())
	fmt.Println(x)
}
Output:

{"name":"foo","usage":"[help]","commands":{"help":"help"},"default":"help"}
{"name":"foo","usage":"[help]","commands":{"help":"help"},"default":"help"}
{"name":"foo","usage":"[help]","commands":{"help":"help"},"default":"help"}

func (*Command) Legal ΒΆ added in v0.2.0

func (x *Command) Legal() string

Legal returns a single line with the combined values of the Name, Version, Copyright, and License. If Version is empty or nil an empty string is returned instead. Legal() is used by the version builtin command to aggregate all the version information into a single output.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	x := cmdbox.NewCommand("foo")
	fmt.Println(x.Legal())
	x.Version = "v0.0.1"
	fmt.Println(x.Legal())
	x.Copyright = "Copyright 2021 Rob"
	fmt.Println(x.Legal())
	x.License = "Apache-2"
	fmt.Println(x.Legal())
}
Output:


foo
Copyright 2021 Rob
foo v0.0.1
Copyright 2021 Rob
License Apache-2

func (*Command) MissingArg ΒΆ added in v0.6.3

func (x *Command) MissingArg(a string) error

MissingArg returns cmdbox.MissingArg

func (*Command) Print ΒΆ added in v0.1.0

func (x *Command) Print()

Print outputs as YAML (nice when testing).

func (*Command) PrintHelp ΒΆ added in v0.2.0

func (x *Command) PrintHelp()

PrintHelp simply prints what Help returns.

func (*Command) Resolve ΒΆ added in v0.5.0

func (x *Command) Resolve(name string) *Command

Resolve looks up the Command from the register based on the name passed. First it looks for a fully qualified entry in the register (x.Name + " " + name), then it just looks for the name alone. Returns nil if nothing found in the register. This method is particularly important because at init time when Commands are Added to the register their subcommands may not yet have been registered. Resolve allows this lookup to happen reliably later in runtime.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing

	x := cmdbox.Add("foo", "bar")

	b := cmdbox.Add("bar")
	fmt.Println(b.Name)

	r := x.Resolve("bar")
	if r != nil {
		fmt.Println(r.Name)
	}

}
Output:

bar
bar
Example (Bork) ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing

	x := cmdbox.Add("foo me", "bork")

	r := x.Resolve("bork")
	fmt.Println(r)

}
Output:

<nil>
Example (Subcommand) ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing

	x := cmdbox.Add("foo me", "bar")

	b := cmdbox.Add("bar")
	fmt.Println(b.Name)

	r := x.Resolve("bar")
	if r != nil {
		fmt.Println(r.Name)
	}

}
Output:

bar
bar

func (*Command) ResolveDelegate ΒΆ added in v0.3.0

func (x *Command) ResolveDelegate(args []string) *Command

ResolveDelegate returns a Command pointer looked up from the internal register based on the following priority from more

  1. x.Caller.Name + " " + arg[0] as name
  2. cmdbox.Main.Name + " " + arg[0] as name
  3. arg[0] as name

This is a specialized lookup for Commands that are designed to operate on other Commands in the registry. See Help and Legal for examples. See Resolve for simpler resolution of Commands.

func (*Command) Sigs ΒΆ added in v0.5.0

func (x *Command) Sigs() *util.StringMap

Sigs returns a StringMap keyed to the Command.Names with signatures as values suitable for printing usage information. Hidden commands are omitted.

Example ΒΆ
package main

import (
	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	x := cmdbox.Add("foo", "h|help", "version", "bar")
	x.Sigs().Print()

}
Output:

bar: bar
help: h|help
version: version

func (Command) String ΒΆ

func (x Command) String() string

String fullfills fmt.Stringer interface as JSON.

func (*Command) Title ΒΆ

func (x *Command) Title() string

Title returns a dynamic field of Name and Summary combined (if exists).

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	x := cmdbox.NewCommand("foo")
	fmt.Println(x.Title())
	x.Summary = "just a foo"
	fmt.Println(x.Title())
}
Output:

foo
foo - just a foo

func (*Command) Titles ΒΆ added in v0.5.0

func (x *Command) Titles(indent, max int) string

Titles returns a single string with the titles of each subcommand indented and with a maximum title signature length for justification. Hidden commands are not included.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	x := cmdbox.Add("foo", "bar", "other", "p|print", "c|comp|complete")

	b := cmdbox.Add("bar")
	b.Summary = `does bar stuff`

	h := cmdbox.Add("other")
	h.Summary = `other stuff`

	p := cmdbox.Add("print")
	p.Summary = `prints stuff`

	c := cmdbox.Add("complete")
	c.Summary = `complete stuff`

	fmt.Println(x.Titles(0, 0))
	fmt.Println(x.Titles(2, 0))
	fmt.Println(x.Titles(4, 7))

}
Output:

bar             - does bar stuff
c|comp|complete - complete stuff
other           - other stuff
p|print         - prints stuff
  bar             - does bar stuff
  c|comp|complete - complete stuff
  other           - other stuff
  p|print         - prints stuff
    bar     - does bar stuff
    c|comp|complete - complete stuff
    other   - other stuff
    p|print - prints stuff

func (*Command) UnexpectedArg ΒΆ added in v0.6.3

func (x *Command) UnexpectedArg(a string) error

UnexpectedArg returns cmdbox.UnexpectedArg

func (*Command) Unimplemented ΒΆ

func (x *Command) Unimplemented(a string) error

Unimplemented is a convenience method that delegates calls to cmdbox.Unimplemented.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	x := cmdbox.NewCommand("foo")
	fmt.Println(x.Unimplemented("help"))
}
Output:

unimplemented: help

func (*Command) UpdateUsage ΒΆ added in v0.4.0

func (x *Command) UpdateUsage()

UpdateUsage will set x.Usage to the default, which is all of the Commands joined with bar (|) and wrapped in either brackets ([]) or parenthesis (()) depending on whether a x.Default has been set or the command has it's own Method (effectively the default).

func (*Command) UsageError ΒΆ

func (x *Command) UsageError() error

UsageError is a convenience method that delegates calls to cmdbox.UsageError.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing

	x := cmdbox.NewCommand("foo")
	x.Usage = "unique usage here"
	fmt.Println(x.UsageError())

}
Output:

usage: foo unique usage here

func (*Command) YAML ΒΆ added in v0.1.0

func (x *Command) YAML() string

YAML is shortcut for yaml.Marshal(x). See util.ToYAML.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	x := cmdbox.NewCommand("foo")
	fmt.Print(x.YAML())
	x.Print()
}
Output:

name: foo
commands: {}
default: help
name: foo
commands: {}
default: help

type CommandMap ΒΆ added in v0.0.18

type CommandMap struct {
	M map[string]*Command
	sync.Mutex
}

CommandMap encapsulates a map[string]*Command and embeds a sync.Mutex for locking making it safe for concurrency. The internal map is exported (M) in the event developers want more direct control without using the CommandMap methods that ensure concurrency safety. See the Register function for more.

func NewCommandMap ΒΆ added in v0.3.0

func NewCommandMap() *CommandMap

NewCommandMap returns a new CommandMap with the internal map[string]*Command initialized.

func (*CommandMap) Delete ΒΆ added in v0.3.0

func (m *CommandMap) Delete(keys ...string)

Delete removes one or more entries from the map in a way that is safe for concurrency.

func (CommandMap) Dups ΒΆ added in v0.3.0

func (m CommandMap) Dups() []string

Dups returns key strings of duplicates (which can then be easily renamed). Keys are sorted in lexicographic order. See Rename.

func (*CommandMap) Get ΒΆ added in v0.3.0

func (m *CommandMap) Get(key string) *Command

Get returns a Command pointer by key name safe for concurrency. Returns nil if not found.

func (*CommandMap) Init ΒΆ added in v0.3.0

func (m *CommandMap) Init()

Init initializes (or re-initialized) the CommandMap deleting all its values (without changing its reference).

func (CommandMap) JSON ΒΆ added in v0.3.0

func (m CommandMap) JSON() string

JSON is shortcut for json.Marshal(m). See ToJSON.

func (CommandMap) Names ΒΆ added in v0.0.18

func (m CommandMap) Names() []string

Names returns a sorted list of all Command names.

func (CommandMap) Print ΒΆ added in v0.3.0

func (m CommandMap) Print()

Print outputs as YAML (nice when testing).

func (CommandMap) Rename ΒΆ added in v0.3.0

func (m CommandMap) Rename(from, to string)

Rename renames a Command in the Register by adding the new name with the same *Command and deleting the old one. This is useful when a name conflict causes New to append and underscore (_) to the duplicate's name. Rename can be called from init() at any point after the duplicate has been added to resolve the conflict. Note the order of init() execution --- while predictable --- is not always apparent. When in doubt do Rename from main() to be sure. Rename is safe for concurrency.

func (*CommandMap) Set ΒΆ added in v0.3.0

func (m *CommandMap) Set(key string, val *Command)

Set set a value by key name safe in a way that is for concurrency.

func (CommandMap) Slice ΒΆ added in v0.3.0

func (m CommandMap) Slice(names ...string) []*Command

Slice returns a slice of *Command pointers and fetched from the internal register that match the key names passed. If an entry is not found it is simply skipped. Will return an empty slice if none found.

func (CommandMap) String ΒΆ added in v0.0.18

func (m CommandMap) String() string

String fullfills fmt.Stringer interface as JSON.

func (CommandMap) YAML ΒΆ added in v0.3.0

func (m CommandMap) YAML() string

YAML is shortcut for yaml.Marshal(m). See ToYAML.

type CompFunc ΒΆ added in v0.0.8

type CompFunc func(x *Command) []string

CompFunc defines a first-class function type for tab completion closures that sense the completion context, and return a list of completion strings. By managing completion logic as first-class functions we allow for easier completion testing and modularity. An empty string slice must always be returned even on failure.

type Method ΒΆ added in v0.1.0

type Method func(args ...string) error

Method represents a function to be used as Command.Method values. By convention, use "args" when arguments are expected and "none" when not.

func Resolve ΒΆ added in v0.1.6

func Resolve(caller *Command, name string, args []string) (Method,
	[]string)

Resolve looks up a Command from the internal Reg register based on the caller and the name. If the Name of the caller and name passed, joined with a space (a fully qualified entry) is found then that is used instead of just the name. Otherwise, just the name is looked up (which might itself already be fully qualified). The returned Command (x) is examined further to decide which Method and Args to return:

  • If x.Method defined, call and return it with args unaltered

  • If first arg in x.Commands, recursively Call with shifted args

  • First with x.Name + " " + cmd

  • Then with just cmd

  • If x.Default defined, recursively Call with shifted args

  • First with x.Name + " " + x.Default

  • Then with just x.Default

  • Return nil and args

By convention, passing a nil as the caller indicates the Command was called from something besides another Command, usually the cmdbox package itself or test cases. See Call, Command, ExampleResolve for more.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/rwxrob/cmdbox"
)

func main() {
	cmdbox.Init() // just for testing
	defer cmdbox.TrapPanicNoExit()

	gr := cmdbox.Add("greet", "fr|french", "ru|russian")
	gr.Summary = "main greet composite, no method"

	fr := cmdbox.Add("greet french")
	fr.Method = func(args ...string) error {
		fmt.Print("Salut")
		return nil
	}

	ru := cmdbox.Add("greet russian")
	ru.Method = func(args ...string) error {
		fmt.Print("Privyet")
		return nil
	}

	tests := []struct {
		caller *cmdbox.Command
		name   string
		args   []string
	}{
		{nil, "greet", nil},            // usage
		{nil, "greet", []string{"h"}},  // usage
		{nil, "greet", []string{"hi"}}, // usage
		{nil, "greet russian", []string{"hi"}},
		{gr, "russian", []string{"hi"}},
	}

	for _, t := range tests {
		method, args := cmdbox.Resolve(t.caller, t.name, t.args)
		if method != nil {
			method(args...)
			fmt.Printf("%v %q\n", t.name, args)
			continue
		}
		fmt.Printf("failed: %v %q\n", t.name, t.args)
	}

}
Output:

failed: greet []
failed: greet ["h"]
failed: greet ["hi"]
Privyetgreet russian ["hi"]
Privyetrussian ["hi"]

Directories ΒΆ

Path Synopsis
Package comp is the Bash tab completion subpackage of CmdBox.
Package comp is the Bash tab completion subpackage of CmdBox.
Package complib is the tab completion function library for CmdBox.
Package complib is the tab completion function library for CmdBox.
esc
Package util contains utilities used by CmdBox itself or that are frequently needed and useful when writing Command.Method implementations.
Package util contains utilities used by CmdBox itself or that are frequently needed and useful when writing Command.Method implementations.
The valid package simply and efficiently minimally validates command * line components passed into CmdBox commands, aliases, and such.
The valid package simply and efficiently minimally validates command * line components passed into CmdBox commands, aliases, and such.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL