cmdtab

package module
v0.2.1-alpha Latest Latest
Warning

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

Go to latest
Published: Dec 19, 2020 License: Apache-2.0 Imports: 14 Imported by: 3

README

Go Tab Complete Commander

Canonical source at https://gitlab.com/rwxrob/cmdtab

WIP Go Report Card GoDoc

A commander for modern command-line human-computer interactions.

logo

Tab Complete Commander is a lightweight commander package focused on creating human-friendly terminal command-line interfaces composed of modular subcommands with portable completion and embedded, dynamic documentation.

Installation

Normally you would simply import "gitlab.com/rwxrob/cmdtab" but you can install it for review using the following as well:

go get -u gitlab.com/rwxrob/cmdtab

Advantages

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

  • Automatic and extendible Bash tab completion built in.

  • Modular, interchangeable, readable subcommands.

  • Easiest possible command documentation with support for emphasis with simple Markdown formatting.

Ongoing Project

Although Complete Commander is fully functional now it should still be considered beta until the final TODO items are complete. Most of them are related to better default actions and output formatting.

How It Works

The magic of Complete Commander comes from the clean separation of subcommands into their own files and composing them into an internal command index within the main executable program. This is accomplished through simple, judicious use of the init() function in each file. This approach incurs no more performance hit than would already be required for any other such solution and is 100% concurrency safe.

By the time the command-line application's main() function is called all it has to do is cmd.Execute("mycmd") to execute the top-level command:

  1. Detects and responds to programmable shell completion context
  2. Optionally adds the help, usage, and version builtins
  3. Delegates to any subcommands, or
  4. Runs its own top-level method

Because of the loose coupling, and top-down design a full subcommand node tree can easily be displayed and documented, indeed the builtin commands do exactly that. When care is given to maintain this loose coupling between subcommands a file can be modularly added or moved to any other executable directory creating very clean sharing and composition possibilities.

Motivation

This package scratches several "personal itches" that have come up from the needs of modern human-computer interaction clearly passing what is currently available:

  1. No commanders exist for writing simple commands with human-friendly subcommands
  2. No commanders exist that know anything about tab completion
  3. Tab completion is the simplest way to provide good help to the user
  4. Getopt-style options are ancient, ridiculously bad HCI
  5. More users are voicing their commands rather than typing them
  6. More users are using keyboard input with their thumbs
  7. Modern command-line programs need to be easily distributed
  8. Distributing executables with separate documentation complicates understanding the program
  9. Modern monolithic command approaches allow embedded documentation and better distribution
  10. Operating system package managers are overkill for distributing most command executables
  11. Documentation only needs to cover what the user is specifically interested in
  12. Documentation can be provided dynamically when embedded within the command it documents
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 a separate documentation system (man pages).

Today Go has revolutionized applications development and respectfully embraced modern use of resources. Static linking, cross-compilation, built in concurrency, reflection and documentation are all evidence of how time have changed. It follows then, that our command interface designs and documentation approaches should equally modernize.

There is now plenty of room to compose several commands and subcommands into a single monolithic executable. In fact, this has become idiomatic for Go's own tool set. 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.

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 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.

Embedded documentation has the ability to dynamically sense the environment and context for its use thereby creating more useful, specific help to the user. This means that rather than dumping every single usage option into a massive man page separate from the command (think gpg), such documentation can be broken up and displayed programmatically by the command itself.

Most modern terminals support color (ANSI escapes) and UTF-8 (yes even Windows) and regularly display more than 80 columns of text.

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 Complete Commander 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.

Design Decisions

This is a summary of the design decisions made roughly in the order they were made. It is provided in the hopes of addressing other design concerns anyone reviewing this package might have before choosing to use it.

  • To this day command options plague developers and users by different ways of dealing with single or double dashes, the equals sign, single letter options and more. Most are also not friendly to the use of UTF-8 runes in the upper ranges.

  • Even through getopt can be problematic (having had to code for it and around it for two decades and still has nightmares about gpg's interface) MapOpts is available for those who insist on using them giving the command author the choice. Parsing traditional getopt-type options and switches was originally dropped since the goals of this project are to deprecate such designs in favor of modern HCI approaches to command-line user interfaces. But, choice prevails over opinion in this case. Still, please don't use -f <file> when the tab-completable file <file> could be used instead. Even a single dash - is nearly impossible to voice-to-text command-line interface.

  • Semantic emphasis *bold*, **italic**, ***bold-italic*** are the only inline formatting options allowed. This seems prudent given the fundamental requirement for readability as well as the complication of other inline formatting and escapes. These three inline formats have been supported by all system documentation formats from the beginning and play nicely with Go's back-ticked raw-strings. Users can change the colors of these on terminals that use term(cap|info)-aware man pages and less/more (i.e. LESS_TERMCAP_?? or COMPCMD_??) without any need to provide theming in the package itself.

  • The decision to not use other Markdown for formatting was easy given the minimal three formats allowed. The decision to keep documentation 100% compatible with CommonMark was a no-brainer decision.

  • Pager application (i.e. less, more) detection seems practical since most everyone would want such when help output exceeds the page height (when such can be determined). There is a TODO to add fall-back builtin pager support.

  • Allowing more than 80 columns and never hard-wrapping lines allows the user to decide the preferred width of documentation. Lines that are hard-wrapped are unreadable when the column count drops to less than that of the hard-wrapping. With TMUX small pane widths are common. (This is one thing Go documentation seriously failed to consider.)

  • 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().

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

  • Including a fair amount of output formatting and printing code seemed appropriate given that one of the three main goals of the project was to produce consistent command output formatting. It had been suggested to put such into another package instead but this package is so small that ultimately turned out to be overkill. Besides, the formatting used by cmd is highly specific to making output look good on a terminal as it relates to command documentation.

  • Rather than hard-code a dependency on Bash completion, every effort has been made to decouple completion from any specific shell completion API (despite the many references to Bash, which dominates currently). As long as any shell completion implementation sets an environment variable containing the full line to be completed package cmd will always be able to sense completion context and perform. This puts the completion logic safely embedded into the command that needs the completion and exposes as little dependency on shell completion methods as possible.

  • Inferring the main (top-level) command to use was considered to be the first argument (os.Args[0]) for some time but further research revealed it can never be relied upon fully. It also initially seemed clever to change the behavior of an executable simply by changing its name but this was quickly dismissed when clearer thinking prevailed concerning this and other security concerns. Therefore, the first command must be passed as an argument to cmd.Execute("mytopcmd"). It is simple enough, however, to compile other executables with different main commands. Indeed, only the argument to need change to do so.

  • Rather than add the complication of wrapping lines in a block and indenting the proper number of spaces for a given terminal width (which could be resized), the choice to keep all blocks beginning from the leftmost column was made. This makes the best use of screen space, is consistent with the help documentation from Go itself, and allows the output to be sized to any width without complication. Instead, emphasis has been given to the headers of the specific blocks. When indentation is truly needed raw-text (initial four spaces) can be used.

  • Full DocOpt parsing of usage was considered and dismissed. DocOpt solves a different problem with different priorities (which do no include consideration of getopt-style command lines interfaces as an anti-pattern in terms of HCI). In fact, the only thing that should every really been in a usage string are lists of subcommands or arguments called out by name with <something> both of which are automatically formatted when detected. In other words, no usage should ever include - or -- prefixed anything. Everything should be voice-able. This makes even the most complicated commands extremely easy to understand quickly.

  • Support for all-caps keywords (ex: [OPTIONS]) in usage strings was considered and dismissed because angle-bracket notation already covers that and having single suggested usage format is simpler. It is also far easier for the formatting parser to determine what should be italicized using angle-brackets.

  • Decided against any paged output of any special hidden builtin to allow combination with other shell scripts when quick customization is needed. Paged output remains for the main help, usage, and version builtins however.

  • Removed all color and formatting from the special hidden builtin output since is will mostly likely be used by shell scripts that further parse it, for example to email all the authors.

  • Emphasize(), Wrap(), and Indent() have been made public in addition to Format() for convenient use by command authors, but the values for italic, bold, and bolditalic have not been. This is to preserve the look and feel of commands that use Emphasize() and put the power to control appearance into the hands of users instead of developers.

  • At one point the internal map[string]*Command index was private to discourage tight coupling between commands. But it was decided that this inflexibility came at too great a cost to potential needs of command creators in the future who might want to inspect the Index directly themselves without necessarily doing anything to is, say to use some sort of prefix convention. Has() was kept as a convenience. This does not change the fact that subcommands should be as independent and uncoupled as possible. In the extreme case when subcommands need to communicate they should use system environment variables as would any other two commands normally.

How Does Completion Work?

Reading about [Bash Programmable Completion](https://duck.com/lite?kae=t&q=Bash Programmable Completion) is probably a good idea.

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


complete -C mycmd mycmd

This will cause the shell to run mycmd and set the COMP_LINE environment variable every time you tap the tab key once or twice. This allows Complete Commander 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.

The cmd package then sees completion context it resolves it by calling Complete(). See the package docs for more on the specific algorithm used, but generally it does the following:

  • Prints the output of any completion function found, or
  • Recursively looks up any matching Subcommand names and prints them

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. Such considerations seem very absent from the HCI conversation regarding terminal command line usage.

Automatic (Builtin) Subcommands

Unless specifically disabled with OmitBuiltins (although the size to add these builtins is trivial), the following internal subcommands are added to any executable that is created using the cmd package:

  • help [<subcmd>]
  • usage
  • version

These subcommands are so common they have become something of an unspoken standard for modern commands.

Several other utility subcommands are also builtin for help integrating with shell environments, producing documentation in other formats, and more:


_authors       list names and authors
_bash_complete print line to add for bash completion
_builtins      list all cmd package builtins names and summaries
_cmdversion    print the cmd package version
_complete      force completion context
_copyrights    list names and copyrights
_descriptions  list names and descriptions
_examples      list names and examples
_gits          list names and git source repos
_help_json     dump help documentation as JSON
_index         list all names and summaries from cmd package index
_issues        list names and issue reporting URLs
_licenses      list names and licenses
_names         list names, main first
_summaries     list names and summaries
_usages        list names and usages
_versions      list names and versions

Help Documentation

Help documentation is inspired by the look and design of UNIX man pages so as to feel comfortable to those using such documentation for commands for decades. It contains all the details for the given command or <subcmd>. Rather than dump all the documentation into a single page, however, details of subcommands can be displayed separately.

Usage Documentation

Usage is when double-tab doesn't provide enough hints about how the command is to be used. Usage is meant to provide only minimal usage output while 'help' provides the full detailed information for the command. If an additional argument is provided the detailed help for that specific subcommand will be provided. If the subcommand does not exist it will be ignored and the main help information shown instead.

Version Documentation

Having a version subcommand in particular is well-defined as being the place to put all legal and authorship information in addition to just the version. Such is required by most all free software and open-source licenses.

TODO

Here's some stuff we know I want to add but haven't made issues or time for yet:

  • fix broken subcommands of subcommands with tab completion and usage
  • support <file> syntax formatting rendered as all caps upper without the angle brackets and underlined, or just leave if no support in terminal for underlining to make compat visually with man pages
  • recursive usage building by combining x.Usage into one line
  • better color terminal detection and support, configurable
  • add:addmember - aliases to allow sl member add but addmember in Index
  • _help_md - output a markdown file containing doc info
  • _help_html - output a standalone HTML5 document with doc info
  • _help_http - serves the _help_html document locally
  • internal paging when less not found (go-pager?)
  • assume use of the standard logger log, trap and beautify output

Documentation

Overview

Package cmd provides a lightweight commander focused on modern human-computer interaction through terminal command-line interfaces composed of modular subcommands with portable completion, and embedded, dynamic documentation.

Index

Examples

Constants

View Source
const Version = `v0.1.0`

Version contains the semantic version of the cmd package used. This value is printed with the version builtin subcommand.

Variables

View Source
var Args = os.Args[1:]

Args returns a reliable collection of arguments to the executable.

WARNING: Although the first the element of os.Args is usually the binary of the compiled program executed it is never reliable and significantly differs depending on operating system and method of program execution. The first argument is therefore stripped completely leaving only the arguments to be processed. The cmd.Args package variable can also be set during testing to check cmd.Execute() behavior.

View Source
var CompLine string

CompLine is set if a completion context from the shell is detected. (For Bash it is COMP_LINE. See Programmable Completion in the bash man page.)

View Source
var DisableEmphasis bool

DisableEmphasis turns off emphasis (italic, bold, bolditalic) from Format(). Note that this is package wide and must be turned on again after being disabled. Generally emphasis should be left on to provide a consistent user experience for anything built with this package

View Source
var FixCompLine bool = true

FixCompLine activates an attempt to correct the CompLine to work best with completion. For example, when an executable that uses the cmd package is renamed or is called as a path and not just the single command name. True by default. Set to false to leave the CompLine exactly as it is detected but note that depending on a specific form of CompLine may not be consistent across operating systems.

View Source
var Index = map[string]*Command{}

Index contains all the commands available as subcommands with one of them being set to Main. Commands are created and registered with New().

WARNING: Index is made public only to cover the unforeseen needs of command creators to inspect their own Command set but should not be relied upon heavily to avoid unnecessary coupling dependencies between commands. While the Index type is guaranteed never to change, direct manipulation of the Index is strongly discouraged. Use one of the helper package methods instead (or request the addition of one).

View Source
var KeepAlive bool

KeepAlive allows developers to stop Execute() from exiting. It should not be used for any purpose other than testing and should be kept out of any test examples.

View Source
var OmitBuiltins bool

OmitBuiltins turns off the injection of the Builtin subcommands into the Main command when Execute is called. It can be assigned in any init() or from main() before calling Execute().

View Source
var PagedDefStatus = `Line %lb [<space>(down), b(ack), h(elp), q(quit)]`

PagedDefStatus is the status line passed to `less` to provide information at the bottom of the screen prompting the user what to do. Helpful with implementing help in languages besides English.

View Source
var PagedOut = true

PagedOut sets the default use of a pager application and calling PrintPaged() instead of Print() for non-hidden builtin subcommands.

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 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 WinSize winsz

Functions

func Builtins

func Builtins() []string

Builtins are subcommands that are added to every Main command when Execute is called. This can be prevented by setting OmitBuiltins to false.

Most of the builtins are hidden (beginning with underscore '_') but the following are so standardized they are included by default:

* help [subcmd] - very long, formatted documentation * version - version, copyright, license, authors, git source

Any of these can be overridden by command authors simply by naming their own version the same. This may be desirable when creating commands in other languages although keeping these standard English names is strongly recommended due to their ubiquitous usage.

The following are hidden but can be promoted by encapsulating them in other subcommands each with its own file, name, title, and documentation:

func Call

func Call(name string, args []string) error

Call allows any indexed subcommand to be called directly by name. Avoid using this method as much as possible since it creates very tight coupling dependencies between commands. It is included primarily publicly so that builtin commands like help, usage, and version can be wrapped with internationalized aliases.

func Complete

func Complete()

Complete calls complete on the Main command passing it CompLine. No verification of Main's existence is checked. The CompLine is always changed to match the actual name of the Main command even if the executable name has been changed or called as an alias. This ensures proper tab completion no matter what the actual executable is called.

Example (Delegated)
package main

import (
	cmd "gitlab.com/rwxrob/cmdtab"
)

func main() {
	list := cmd.New("list")
	list.Completion = func(compline string) []string {
		// could do fancier context analysis of compline
		return []string{"all", "today", "yesterday"}
	}
	cmd.Main = list // simulate Execute()
	cmd.New("add")
	cmd.New("remove")
	cmd.New("exe", "list", "add", "remove")
	cmd.CompLine = "exe list" // set by shell
	cmd.Complete()

}
Output:

all
today
yesterday
Example (Fullsub)
package main

import (
	"fmt"

	cmd "gitlab.com/rwxrob/cmdtab"
)

func main() {
	cmd.New("list")
	cmd.New("add")
	cmd.New("remove")
	cmd.Main = cmd.New("exe", "list", "add", "remove")
	cmd.CompLine = "exe list" // set by shell
	cmd.Complete()
	fmt.Println("nothing")
}
Output:

nothing
Example (Plainnosubs)
package main

import (
	cmd "gitlab.com/rwxrob/cmdtab"
)

func main() {
	cmd.Main = cmd.New("exe")
	cmd.CompLine = "exe" // set by shell
	cmd.Complete()
}
Output:

Example (Plansubs)
package main

import (
	"fmt"

	cmd "gitlab.com/rwxrob/cmdtab"
)

func main() {
	cmd.New("list")
	cmd.New("add")
	cmd.New("remove")
	exe := cmd.New("exe1", "list", "add", "remove")
	cmd.Main = exe // simulate Execute()
	fmt.Println(exe.Has("list"))
	fmt.Println(exe.Has("add"))
	fmt.Println(exe.Has("remove"))
	fmt.Println(exe.Has("missing"))
	cmd.CompLine = "exe" // set by shell
	cmd.Complete()
}
Output:

true
true
true
false
list
add
remove
Example (Presubs)
package main

import (
	cmd "gitlab.com/rwxrob/cmdtab"
)

func main() {
	cmd.New("list")
	cmd.New("add")
	cmd.New("remove")
	cmd.Main = cmd.New("exe", "list", "add", "remove")
	cmd.CompLine = "exe l" // set by shell
	cmd.Complete()
}
Output:

list
Example (Subsubs)
package main

import (
	cmd "gitlab.com/rwxrob/cmdtab"
)

func main() {
	cmd.New("sched", "all", "today")
	cmd.Main = cmd.New("exe2", "sched", "add", "remove")
	cmd.CompLine = "exe2 sched" // set by shell
	cmd.Complete()

}
Output:

all
today

func ConvertToJSON

func ConvertToJSON(thing interface{}) string

ConvertToJSON converts any object to its JSON string equivalent with two spaces of human-readable indenting. While this inflates the size for most purposes this is desirable even when dealing with large data sets. Technologies like GraphQL and offline-first progressive web apps have reduced the concern for total size of JSON. Usually human readability is more important. If an error is encountered while marshalling an ERROR key will be created with the string value of the error as its value.

Example
package main

import (
	"fmt"

	cmd "gitlab.com/rwxrob/cmdtab"
)

func main() {

	sample := map[string]interface{}{}
	sample["int"] = 1
	sample["float"] = 1
	sample["string"] = "some thing"
	sample["map"] = map[string]interface{}{"blah": "another"}
	sample["array"] = []string{"blah", "another"}

	fmt.Println(cmd.ConvertToJSON(sample))

}
Output:


{
  "array": [
    "blah",
    "another"
  ],
  "float": 1,
  "int": 1,
  "map": {
    "blah": "another"
  },
  "string": "some thing"
}

func Dump

func Dump(stuff ...interface{})

Dump simply dumps the stuff passed to it to standard output. Use for debugging. Use Print for general printing.

func Emphasize

func Emphasize(buf string) string

func Exec

func Exec(args ...string) error

Exec (not to be confused with Execute) will check for the existance of the first argument as an executable on the system and then execute it using syscall.Exec(), which replaces the currently running program with the new one in all respects (stdin, stdout, stderr, process ID, etc).

Note that although this is exceptionally faster and cleaner than calling any of the os/exec variations it may be less compatible with different operating systems.

func Execute

func Execute(name string)

Execute traps all panics (see Panic), detects completion and does it, or sets Main to the command name passed, injects the Builtin subcommands (unless OmitBuiltins is true), looks up the named command from the command Index and calls it passing cmd.Args. Execute alway exits the program.

func Exit

func Exit()

Exit just exits with 0 return value.

func ExitError

func ExitError(err ...interface{})

ExitError prints err and exits with 1 return value.

func ExitExec

func ExitExec(xnargs ...string) error

ExitExec exits the currently running Go program and hands off memory and control to the executable passed as the first in a string of arguments along with the arguments to pass along to the called executable. This is only supported on systems that support Go's syscall.Exec() and underlying execve() system call.

func ExitUnimplemented

func ExitUnimplemented(thing interface{})

ExitUnimplemented calls Unimplemented and calls ExitError().

func Format

func Format(input string, indent, width int) (output string)

Format takes a command documentation format string (an extremely limited version of Markdown that is also Godoc friendly) and transforms it as follows:

* Initial and trailing blank lines are removed.

  • Indentation is removed - the number of spaces preceeding the first word of the first line are ignored in every line (including raw text blocks).
  • Raw text ignored - any line beginning with four or more spaces (after convenience indentation is removed) will be kept as it is exactly (code examples, etc.) but should never exceed 80 characters (including the spaces).
  • Blocks are unwrapped - any non-blank (without three or less initial spaces) will be trimmed line following a line will be joined to the preceding line recursively (unless hard break).
  • Hard breaks kept - like Markdown any line that ends with two or more spaces will automatically force a line return.
  • URL links argument names and anything else within angle brackets (<url>), will trigger italics in both text blocks and usage sections.
  • Italic, Bold, and BoldItalic inline emphasis using one, two, or three stars respectivly will be observed and cannot be intermixed or intraword. Each opener must be preceded by a UNICODE space (or nothing) and followed by a non-space rune. Each closer must be preceded by a non-space rune and followed by a UNICODE space (or nothing).

For historic reasons the following environment variables will be observed if found (and also provide color support for the less pager utility):

* italic - LESS_TERMCAP_so * bold - LESS_TERMCAP_md * bolditalic = LESS_TERMCAP_mb

func Has

func Has(name string) bool

Has looks for the named subcommand in the Index.

func Hidden

func Hidden() map[string]*Command

Hidden returns a new map containing only pointers to the hidden Commands.

func Indent

func Indent(buf string, spaces int) string

Indent indents each line the set number of spaces.

func JSON

func JSON() string

JSON returns a JSON representation of the state of the cmd package including the main command and all subcommands from the internal index. This can be useful when providing documentation in a structured data format that can be easily shared and rendered in different ways. The json builtin simply calls this and prints it. Empty values are always omitted. (See Command.MarshalJSON() as well.)

func MapOpts

func MapOpts(stuff []string) (opts map[string]string, args []string)

MapOpts takes an array of getopt like argument strings, examines them for options, and returns the options as a map and the remaining args as an array. To be clear:

  • single dash options cannot have a value (-g)
  • single dash options can be combined (-tgz)
  • double dash options can have value (--template template.html)
  • double dash options can use equals (--template=template.html)
  • options with no value will have empty string
  • values with quotes will contain quotes (--template='template.html')

Note that there are many other getopt libraries. This is a minimal implementation of the most common getopt library. The most notable distinction is that single dash options cannot ever have values.

IMPORTANT: While MapOpts is useful and encouraged when called from within a Command.Method implementation requiring dashed options, very often --- in our emerging conversational user interface world --- it is discouraged to use any dashed options at all. Instead create natural language options and arguments that can be spoken into an interface rather than typed and detected by context instead of looking for the dash, which is extremely unfriendly to voice interfaces of any kind. Nevertheless the choice is up to you.

Example
package main

import (
	"fmt"

	cmd "gitlab.com/rwxrob/cmdtab"
)

func main() {

	// For a given command:
	//
	//   foo README.md meta.yml \
	//     --template=template \
	//     "--withquotes='with quote'" \
	//     --name "Mr. Rob" \
	//     - anotherarg -- wasarg \
	//     -abc -x -y -z \
	//     -t notavalue
	//

	cmdArgs := []string{
		"foo", "README.md", "meta.yml",
		"--template=template.html",
		"--withquotes='with quotes'",
		"--name", "Mr. Rob",
		"anotherarg", "--", "wasarg", "-",
		"-abc", "-x", "-y", "-z",
		"-t", "notavalue",
	}

	opts, args := cmd.MapOpts(cmdArgs[1:])

	fmt.Println("Options:")
	fmt.Println(cmd.ConvertToJSON(opts))

	fmt.Println("Arguments:")
	fmt.Println(cmd.ConvertToJSON(args))

}
Output:


Options:
{
  "a": "",
  "b": "",
  "c": "",
  "name": "Mr. Rob",
  "t": "",
  "template": "template.html",
  "withquotes": "'with quotes'",
  "x": "",
  "y": "",
  "z": ""
}
Arguments:
[
  "README.md",
  "meta.yml",
  "anotherarg",
  "--",
  "wasarg",
  "-",
  "notavalue"
]

func Print

func Print(stuff ...interface{})

Print calls Sprint and prints it.

func PrintPaged

func PrintPaged(buf, status string)

PrintPaged prints a string to the system pager (usually less) using the second argument as the custom status string (usually at the bottom). Control is returned to the calling program after completion. If no pager application is detected the regular Print() will be called intead. If status string is empty PagedDefStatus will be used (use " " to empty). Currently only the less pager is supported.

func Println

func Println(stuff ...interface{})

Println calls Sprint and prints it with a line return.

Example
package main

import (
	cmd "gitlab.com/rwxrob/cmdtab"
)

func main() {

	stringer := func() string { return "something 1" }

	cmd.Println("something 1")
	cmd.Println("some%v %v", "thing", 1)
	cmd.Println(stringer)
	cmd.Println()
	cmd.Println(nil)

}
Output:

something 1
something 1
something 1

func Run

func Run(args ...string) error

Run checks for existance of first argument as an executable on the system and then runs it without exiting in a way that is supported across different operating systems. The stdin, stdout, and stderr are connected directly to that of the calling program. Use more specific exec alternatives if intercepting stdout and stderr are desired.

func Sprint

func Sprint(stuff ...interface{}) string

Sprint returns nothing if empty, acts like fmt.Sprint if it has one argument, or acts like Sprintf if it has more than one argument. Print can also print Stringers. Use Dump instead for debugging.

Example
package main

import (
	"fmt"

	cmd "gitlab.com/rwxrob/cmdtab"
)

func main() {

	stringer := func() string { return "something 1" }

	fmt.Println(cmd.Sprint("something 1"))
	fmt.Println(cmd.Sprint("some%v %v", "thing", 1))
	fmt.Println(cmd.Sprint(stringer))
	fmt.Println(cmd.Sprint())

}
Output:

something 1
something 1
something 1

func String

func String(thing Stringer) string

String adds 'func() string' to normal Go string coercion as well as converting nil to the empty string "".

Example
package main

import (
	"fmt"

	cmd "gitlab.com/rwxrob/cmdtab"
)

type ImmaStringer struct{}

func (s ImmaStringer) String() string {
	return "Hello"
}

func main() {

	f := func() string { return "Hello" }
	fmt.Println(cmd.String(f))

	s := "Hello"
	fmt.Println(cmd.String(s))

	st := ImmaStringer{} // st.String()
	fmt.Println(cmd.String(st))

}
Output:

Hello
Hello
Hello

func Unimplemented

func Unimplemented(thing interface{}) error

Unimplemented just returns an unimplemented error for the thing passed.

func Visible

func Visible() map[string]*Command

Visible returns a new map containing only pointers to the visible Commands.

func WaitForInterrupt

func WaitForInterrupt(cancel context.CancelFunc)

WaitForInterrupt just blocks until an interrupt signal is received. It should only be called from the main goroutine. It takes a single context.CancelFunc that is designed to signal everything to stop cleanly before exiting.

func Wrap

func Wrap(buf string, width int) string

Wrap wraps the string to the given width using spaces to separate words. If passed a negative width will effectively join all words in the buffer into a single line with no wrapping.

Types

type Command

type Command struct {
	Name        string              // <= 14 recommended
	Summary     string              // < 65 recommended
	Version     Stringer            // semantic version (0.1.3)
	Usage       Stringer            // following docopt syntax
	Description Stringer            // long form
	Examples    Stringer            // long form
	SeeAlso     Stringer            // links, other commands, etc.
	Author      Stringer            // email format, commas for multiple
	Git         Stringer            // same as Go
	Issues      Stringer            // full web URL
	Copyright   Stringer            // legal copyright statement, if any
	License     Stringer            // released under license(s), if any
	Other       map[string]Stringer // long form

	Method     func(args []string) error
	Parameters Stringer
	Completion func(compline string) []string

	Default string // default subcommand
	// contains filtered or unexported fields
}
var Main *Command

Main contains the main command passed to Execute to start the program. While it can be changed by Subcommands it usually should not be.

func New

func New(name string, subcmds ...string) *Command

New initializes a new command and subcommands (adding them to the internal subcommand index) and returns a pointer to the command. Note that the subcmds added do not create a new Command in the Index, they are merely added to the list returned by Subcommands.

func (*Command) Add

func (c *Command) Add(names ...string)

Add adds new subcommands by name skipping any it already has. It is up to developers to ensure that the named subcommands has been added to the internal package index with New().

func (*Command) Call

func (c *Command) Call(args []string) error

Call calls its own Method or delegates to one of the Command's subcommands. If a Default has been set and the first argument does not appear to be a subcommand then delegate to Default subcommand by name.

func (*Command) Complete

func (c *Command) Complete(compline string)

Complete prints the completion replies for the current context (See Programmble Completion in the bash man page.) The line is passed exactly as detected leaving the maximum flexibility for parsing and matching up to the Completion function. The Completion method will be delegated if defined. Otherwise, the Subcommands are used to provide traditional prefix completion recursively.

func (*Command) Has

func (c *Command) Has(name string) bool

Has looks for the named Subcommand.

func (Command) Hidden

func (c Command) Hidden() bool

Hidden simple returns if the command name begins with '_' or not.

func (Command) MarshalJSON

func (c Command) MarshalJSON() ([]byte, error)

MarshalJSON fulfills the Go JSON marshalling requirements and is called by String. Empty values are always omitted. Strings are trimmed and long strings such as Description and Examples are written as basic Markdown (which any Markdown engine will able to render. See cmd.Format() for specifics).

Example
package main

import (
	"fmt"

	cmd "gitlab.com/rwxrob/cmdtab"
)

func main() {

	c := cmd.New("mycmd", "subcmd1", "subcmd2")
	c.Author = `Rob`
	c.Copyright = `© Rob`
	c.Description = `Just for testing.`
	c.Examples = `examples`
	c.Git = `gitlab.com/rwxrob/cmdtab`
	c.Issues = `https://gitlab.com/rwxrob/cmdtab/issues`
	c.License = `Apache 2.0`
	//c.SeeAlso = ``
	c.Summary = `summary`
	//c.Usage = ``
	c.Version = `v1.0.0`

	// without these Usage won't return because they do not exist
	cmd.New("subcmd1")
	cmd.New("subcmd2")

	fmt.Println(c)

}
Output:

{
  "Author": "Rob",
  "Copyright": "© Rob",
  "Description": "Just for testing.",
  "Examples": "examples",
  "Git": "gitlab.com/rwxrob/cmdtab",
  "Issues": "https://gitlab.com/rwxrob/cmdtab/issues",
  "License": "Apache 2.0",
  "Name": "mycmd",
  "Subcommands": [
    "subcmd1",
    "subcmd2"
  ],
  "Summary": "summary",
  "Version": "v1.0.0"
}

func (Command) SprintCommandSummaries

func (c Command) SprintCommandSummaries() string

SprintCommandSummaries returns a printable string that includes a bold Command.Name for each line along with the summary string, if any, for that subcommand. This is helpful when creating custom builtin help commands.

func (Command) SprintUsage

func (c Command) SprintUsage() string

SprintUsage returns a usage string (without emphasis) that includes a bold Command.Name for each line along with the main command usage and each individual SprintUsage of every subcommand that is not hidden. If indentation is needed this can be passed to Indent(). To replace emphasis with terminal escapes for printing to colored terminals pass to Emphasize().

func (Command) String

func (c Command) String() string

Fulfills the Stinger interface rendering a Command as a JSON string.

func (*Command) SubcommandUsage

func (c *Command) SubcommandUsage() []string

SubcommandUsage returns the Usage strings for each Subcommand. This is useful when creating usages that have additional notes or formatting when it is desirable to loop through the subcommand usage strings. The order of usage strings is gauranteed to match the order of Subcommands() even if the usage for a particular subcommand is empty.

func (*Command) Subcommands

func (c *Command) Subcommands() []string

Subcommands returns the subcommands added with Add().

func (Command) Title

func (c Command) Title() string

func (*Command) Unimplemented

func (c *Command) Unimplemented() error

Unimplemented calls Unimplemented passing the name of the command. Useful for temporarily notifying users of commands in beta that something has not yet been implemented.

func (*Command) UsageError

func (c *Command) UsageError() error

func (Command) VersionLine

func (c Command) VersionLine() string

VersionLine 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. VersionLine() is used by the version builtin command to aggregate all the version information into a single output.

Example
package main

import (
	"fmt"

	cmd "gitlab.com/rwxrob/cmdtab"
)

func main() {

	c := cmd.New("foo")
	fmt.Println(c.VersionLine())
	c.Version = `v1.0.0`
	fmt.Println(c.VersionLine())
	c.Copyright = `© Rob`
	fmt.Println(c.VersionLine())
	c.License = `Apache 2.0`
	fmt.Println(c.VersionLine())
	c.Copyright = ""
	fmt.Println(c.VersionLine())

}
Output:


foo v1.0.0
foo v1.0.0 © Rob
foo v1.0.0 © Rob (Apache 2.0)
foo v1.0.0 (Apache 2.0)

type Stringer

type Stringer interface{} // the one time i miss generics

Stringer is anything that can be coerced into a string (see fmt.Sprintf) plus any function with no arguments that returns a string (func() string).

Jump to

Keyboard shortcuts

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