exoskeleton

package module
v1.6.2 Latest Latest
Warning

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

Go to latest
Published: Aug 23, 2024 License: Apache-2.0 Imports: 18 Imported by: 1

README

Exoskeleton

Exoskeleton is a library for creating modern multi-CLI applications whose commands are external to them.

Go Reference

Why Exoskeleton?

Exoskeleton is similar to frameworks like Cobra and Oclif in that it simplifies building subcommand-based command line applications (like git clone, git log) by providing:

  • nested subcommands and automatically generated menus of subcommands
  • tab-completion for shells
  • suggestions for mistyped commands ("Did you mean ...?")

Exoskeleton differs from other CLI frameworks in that each subcommand maps to a separate, standalone executable. This allows subcommands to be implemented in different languages and released on different schedules!

Exoskeleton's goal is to create a common entrypoint, better discoverability, and a cohesive experience for a decentralized suite of commandline tools.

Creating an Exoskeleton

Install Exoskeleton by running

go get -u github.com/square/exoskeleton@latest

The simplest Exoskeleton application looks like this:

package main

import (
	"os"

	"github.com/square/exit"
	"github.com/square/exoskeleton"
)

func main() {
	paths := []string{
		// paths
		// you want Exoskeleton to search
		// for subcommands
	}

	// 1. Create a new Command Line application that looks for subcommands in the given paths.
	cli, _ := exoskeleton.New(paths)

	// 2. Identify the subcommand being invoked from the arguments.
	cmd, args, _ := cli.Identify(os.Args[1:])

	// 3. Execute the subcommand.
	err := cmd.Exec(cli, args, os.Environ())

	// 4. Exit the program with the exit code the subcommand returned.
	os.Exit(exit.FromError(err))
}

① An exoskeleton is constructed with an array of paths to search for subcommands. ② It uses the arguments to identify which subcommand is being invoked. ③ The subcommand is executed ④ and the program exits (0 if err is nil, 1 or a semantic exit code otherwise).

To see this in action, take a look at the the Hello World example project.

In the real world, an application might also:

  1. Customize the exoskeleton by passing options to exoskeleton.New
  2. Add business logic between ② Identify and ③ Exec or ③ Exec and ④ os.Exit

[!TIP] At Square, we use the OnCommandNotFound callback to install subcommands on-demand, check for updates after constructing the exoskeleton, and wrap Exec to emit usage metrics.

Subcommands

Creating Subcommands

Subcommands can be either shell scripts or binaries.

  1. They MUST be executable.
  2. They SHOULD output help text to be displayed when the user invokes them with --help.
  3. They MAY respond to --summary by outputting a summary of their purpose to be displayed in a menu of commands.
  4. They MAY respond to --complete <input> by outputting a list of shell-completions for <input>.
Help and Summary Text

Compiled binaries should parse their arguments for the --help and --summary flags. They should do this early in execution before any expensive set up, write the text to standard out, and exit successfully.

Shell scripts which start with the shebang (#!) may respond to --help and --summary flags or may choose to document themselves with magic comments (# HELP: <help text follows>, # SUMMARY: <summary line follows>). See examples/hello_world/libexec/ls and examples/hello_world/libexec/rm for examples.

Completions

Exoskeleton uses shellcomp (the API that Cobra developed) to separate shell-specific logic for implementing completions from the logic for producing the suggestions themselves.

Shellcomp scripts invoke exoskeleton with --complete <WHATEVER THE USER TYPED> and expect to receive a list of suggestions on standard output.

[!NOTE] Imagine git is implemented with Exoskeleton.

If the user types

$ git che<tab>

then the shellcomp scripts will execute:

$ git complete che

and Exoskeleton will suggest completions from the list of commands it knows and output:

checkout
:4

and the shellcomp scripts will complete the command git checkout.

If the user types

$ git checkout lail/<tab>

then the shellcomp scripts will execute:

$ git complete checkout lail/

and Exoskeleton will dispatch the completion to the checkout command, executing this:

$ $(git which checkout) --complete -- checkout lail/

and, at this point, if git checkout handles --complete, it may list branches that start with lail/.

See shellcomp's docs for implementing completions for a subcommand.

Call exoskeleton.GenerateCompletionScript to generate the shellcomp scripts for your project.

[!TIP] At Square, we call this function in our Makefile and distribute artifacts for Bash and Zsh with releases.

Menus

As each subcommand maps to a standalone executable, submenus map to directories.

  1. The directory MUST contain a file named .exoskeleton (the file name is configurable).

Take a look at the dir module in the Hello World example project.

Documentation

Overview

Package exoskeleton allows you to create modern multi-CLI applications whose subcommands are external to them.

You use it by defining an Entrypoint and a list of paths where external subcommands may be found.

Here's an example:

Assuming we have two executables that respond to `--summary` like this:

   $ ~/libexec/rm --summary
   Remove directory entries

   $ ~/libexec/ls --summary
   List directory contents

And a binary, `example`, that is implemented like this:

   package main

   import (
       "os"

       "github.com/square/exoskeleton"
   )

   func main() {
       exoskeleton.Exec([]string{os.Getenv("HOME") + "/libexec"})
   }

Then our example program will behave like this:

   $ ./example
   USAGE
      example <command> [<args>]

   COMMANDS
      ls  List directory contents
      rm  Remove directory entries

   Run example help <command> to print information on a specific command.

And running `example ls` will forward execution to `~/libexec/ls`.

Index

Constants

View Source
const CompleteHelp = `` /* 969-byte string literal not displayed */

CompleteHelp is the help text for the built-in 'complete' command.

View Source
const HelpHelp = `` /* 269-byte string literal not displayed */

HelpHelp is the help text for the built-in 'help' command.

View Source
const WhichHelp = `` /* 423-byte string literal not displayed */

WhichHelp is the help text for the built-in 'which' command.

Variables

This section is empty.

Functions

func CompleteCommands

func CompleteCommands(e *Entrypoint, args, env []string) ([]string, shellcomp.Directive, error)

CompleteCommands is a CompleteFunc that provides completions for command names. It is used by commands like 'help' and 'which' which expect their arguments to be command names.

func CompleteExec added in v1.5.0

func CompleteExec(e *Entrypoint, args, env []string) error

CompleteExec implements the 'complete' command.

func CompleteFiles added in v1.1.1

func CompleteFiles(e *Entrypoint, args, env []string) ([]string, shellcomp.Directive, error)

CompleteFiles is a CompleteFunc that provides completions for files and paths.

func Exec

func Exec(paths []string, options ...Option)

Exec constructs an Entrypoint with the given paths and options and executes it.

func GenerateCompletionScript

func GenerateCompletionScript(name, shell string, w io.Writer) (err error)

GenerateCompletionScript generates a completion script for the given shell ("bash" or "zsh") and writes it to the given writer.

func HelpExec added in v1.5.0

func HelpExec(e *Entrypoint, args, _ []string) error

HelpExec implements the 'help' command.

func IsEmbedded added in v1.1.1

func IsEmbedded(command Command) bool

IsEmbedded returns true if the given Command is built into the exoskeleton.

func IsModule added in v1.1.1

func IsModule(command Command) bool

IsModule returns true if the given Command is a Module and false if it is not.

func IsNull

func IsNull(command Command) bool

IsNull returns true if the given Command is a NullCommand and false if it is not.

func MenuFor(m Module, opts *MenuOptions) (string, []error)

MenuFor renders a menu of commands for a Module.

func Usage

func Usage(cmd Command) string

Usage returns the usage string for the given command. For example, given the Tidy command in the Go CLI, Usage(Tidy) would be 'go mod tidy'.

func UsageRelativeTo

func UsageRelativeTo(cmd Command, m Module) string

UsageRelativeTo returns the usage string for the given command relative to the given module. For example, given the Tidy command in the Go CLI ('go mod tidy'), UsageRelativeTo(Tidy, Mod) would be 'tidy' and UsageRelativeTo(Tidy, Go) would be 'mod tidy'.

func WhichExec added in v1.5.0

func WhichExec(e *Entrypoint, args, _ []string) error

WhichExec implements the 'which' command.

Types

type AfterIdentifyFunc added in v1.6.0

type AfterIdentifyFunc func(*Entrypoint, Command, []string)

AfterIdentifyFunc is a function that is called after a command is identified. It accepts the Command and the remaining arguments that were not used to identify the command.

type Command

type Command interface {
	// Path returns the location of the executable that defines the command.
	// For built-in commands, it returns the path to the entrypoint executable itself.
	// It is used by the built-in command 'which'.
	Path() string

	// Name returns the name of the command.
	Name() string

	// Parent returns the module that contains the command.
	//
	// For unnamespaced commands, Parent returns the Entrypoint. For the Entrypoint,
	// Parent returns nil.
	//
	// In the Go CLI, 'go test' and 'go mod tidy' are both commands. If modeled with
	// Exoskeleton, 'tidy”s Parent would be the 'mod' Module and 'test”s Parent
	// would be the entrypoint itself, 'go'.
	Parent() Module

	// Exec executes the command.
	Exec(e *Entrypoint, args, env []string) error

	// Complete asks the command to return completions.
	// It is used by the built-in command 'complete' which provides shell completions.
	Complete(e *Entrypoint, args, env []string) ([]string, shellcomp.Directive, error)

	// Summary returns the (short!) description of the command to be displayed
	// in menus.
	//
	// Returns a CommandError if the command does not fulfill the contract
	// for providing its summary.
	Summary() (string, error)

	// Help returns the help text for the command.
	//
	// Returns a CommandError if the command does not fulfill the contract
	// for providing its help.
	Help() (string, error)
}

Command is a command in your CLI application.

In the Go CLI, 'go test' and 'go mod tidy' are both commands. Exoskeleton would model 'test' and 'tidy' as Commands. 'tidy' would be nested one level deeper than 'test', beneath a Module named 'mod'.

type CommandDescribeError added in v1.4.0

type CommandDescribeError struct{ CommandError }

CommandDescribeError indicates that an executable module did not properly respond to `--describe-commands`

type CommandError

type CommandError struct {
	Message string
	Cause   error
	Command Command
}

CommandError records an error that occurred with a command's implementation of its interface

func (CommandError) Error

func (e CommandError) Error() string

func (CommandError) Unwrap

func (e CommandError) Unwrap() error

type CommandHelpError added in v1.4.0

type CommandHelpError struct{ CommandError }

CommandHelpError indicates that a command did not properly implement the interface for providing help

type CommandNotFoundFunc

type CommandNotFoundFunc func(*Entrypoint, Command)

CommandNotFoundFunc is a function that accepts a Null Command object. It is called when a command is not found.

type CommandSummaryError added in v1.4.0

type CommandSummaryError struct{ CommandError }

CommandSummaryError indicates that a command did not properly implement the interface for providing a summary

type Commands

type Commands []Command

func (Commands) Find

func (c Commands) Find(cmdName string) Command

Find returns the first command with a given name.

func (Commands) Flatten

func (c Commands) Flatten() (Commands, []error)

Flatten returns a list of commands, recursively replacing modules with their subcommands, along with any errors returned by modules' Subcommands().

type CompleteFunc

type CompleteFunc func(e *Entrypoint, args, env []string) ([]string, shellcomp.Directive, error)

CompleteFunc is called when an built-in command is asked to supply shell completions.

type DiscoveryError

type DiscoveryError struct {
	Cause error
	Path  string
}

DiscoveryError records an error that occurred while discovering commands in a directory.

func (DiscoveryError) Error

func (e DiscoveryError) Error() string

func (DiscoveryError) Unwrap

func (e DiscoveryError) Unwrap() error

type EmbeddedCommand added in v1.1.0

type EmbeddedCommand struct {
	Name     string
	Summary  string
	Help     string
	Exec     ExecFunc
	Complete CompleteFunc
}

EmbeddedCommand defines a built-in command that can be added to an Entrypoint (as opposed to an executable external to the Entrypoint).

type EmbeddedModule added in v1.1.0

type EmbeddedModule struct {
	Name     string
	Summary  string
	Commands []interface{}
}

EmbeddedCommand defines a built-in module that can be added to an Entrypoint (as opposed to a directory external to the Entrypoint).

type Entrypoint

type Entrypoint struct {
	// contains filtered or unexported fields
}

Entrypoint is the root of an exoskeleton CLI application.

func New

func New(paths []string, options ...Option) (*Entrypoint, error)

New searches the given paths and constructs an Entrypoint with a list of commands discovered in those paths. It also accepts options that can be used to customize the behavior of the Entrypoint.

func (*Entrypoint) Complete

func (e *Entrypoint) Complete(_ *Entrypoint, args, _ []string) (completions []string, directive shellcomp.Directive, err error)

func (*Entrypoint) Exec

func (e *Entrypoint) Exec(_ *Entrypoint, rawArgs, env []string) error

func (*Entrypoint) GenerateCompletionScript

func (e *Entrypoint) GenerateCompletionScript(shell string, w io.Writer) error

GenerateCompletionScript generates a completion script for the given shell ("bash" or "zsh") and writes it to the given writer.

func (*Entrypoint) Help

func (e *Entrypoint) Help() (string, error)

func (*Entrypoint) Identify

func (e *Entrypoint) Identify(args []string) (Command, []string, error)

Identify identifies the command being invoked.

The function also returns any arguments that were not used to identify the command. For example, if the Go CLI were implemented with Exoskeleton, and we ran it with the rawArgs 'get -u github.com/square/exoskeleton', Identify would return the Get command and the arguments {'-u', 'github.com/square/exoskeleton'}.

If no command is identified, Identify invokes CommandNotFound callbacks and returns NullCommand.

Returns a CommandError if the command does not fulfill the contract for providing its subcommands.

func (*Entrypoint) Name

func (e *Entrypoint) Name() string

func (*Entrypoint) Parent

func (e *Entrypoint) Parent() Module

func (*Entrypoint) Path

func (e *Entrypoint) Path() string

func (*Entrypoint) Subcommands

func (e *Entrypoint) Subcommands() (Commands, error)

func (*Entrypoint) Summary

func (e *Entrypoint) Summary() (string, error)

type ErrorFunc

type ErrorFunc func(*Entrypoint, error)

ErrorFunc is called when an error occurs.

type ExecFunc

type ExecFunc func(e *Entrypoint, args, env []string) error

ExecFunc is called when an built-in command is run.

type Menu struct {
	Usage     string
	HelpUsage string
	Sections  MenuSections
}

Menu is the data passed to MenuOptions.Template when it is executed.

type MenuHeadingForFunc func(Module, Command) string

MenuHeadingForFunc is a function that is expected to return the heading under which a command should be listed when it is printed in a menu. (The default value is "COMMANDS".)

It accepts the Module whose subcommands are being listed and the Command whose heading should be returned.

type MenuItem struct {
	Name    string
	Summary string
	Heading string
	Width   int
}
type MenuItems []*MenuItem
func (m MenuItems) Len() int
func (m MenuItems) Less(i, j int) bool
func (m MenuItems) MaxWidth() (longestCommand int)
func (m MenuItems) Swap(i, j int)
type MenuOptions struct {
	// Depth describes how recursively a menu should be constructed. Its default
	// value is 0, which indicates that the menu should list only the commands
	// that are descendants of the module. A value of 1 would list descendants one
	// level deep, a value of 2 would list descendants two levels deep, etc. A value
	// -1 lists all descendants.
	Depth int

	// HeadingFor accepts a Command the Module the menu is being prepared for
	// and returns a string to use as a section heading for the Command.
	// The default function returns "COMMANDS".
	HeadingFor MenuHeadingForFunc

	// SummaryFor accepts a Command and returns its summary and, optionally, an error.
	// The default function invokes Summary() on the provided Command.
	SummaryFor SummaryFunc

	// Template is executed with the constructed exoskeleton.Menu to render
	// help content for a Module.
	Template *template.Template
}

MenuOptions are the options that control how menus are constructed for modules.

type MenuSection struct {
	Heading   string
	MenuItems MenuItems
}
type MenuSections []MenuSection

type Module

type Module interface {
	Command

	// Subcommands returns the list of Commands that are contained by this module.
	//
	// For example, in the Go CLI, 'go mod' is a Module and its Subcommands would
	// be 'download', 'edit', 'graph', 'init', 'tidy', 'vendor', 'verify', and 'why'.
	//
	// Returns a CommandError if the command does not fulfill the contract
	// for providing its subcommands.
	Subcommands() (Commands, error)
}

Module is a namespace of commands in your CLI application.

In the Go CLI, 'go mod tidy' is a command. Exoskeleton would module 'mod' as a Module and 'tidy' as a Command that's nested beneath it.

Executing a Module produces a menu of its subcommands.

type Option

type Option interface {
	Apply(*Entrypoint)
}

An Option applies optional changes to an Exoskeleton Entrypoint.

func AfterIdentify added in v1.6.0

func AfterIdentify(fn AfterIdentifyFunc) Option

AfterIdentify registers a callback (AfterIdentifyFunc) to be invoked with any command that is successfully identified.

func AppendCommands added in v1.1.0

func AppendCommands(cmds ...interface{}) Option

AppendCommands adds new embedded commands to the Entrypoint. The commands are added to the end of the list and will have the lowest precedence: an executable with the same name as one of these commands would override it.

func OnCommandNotFound

func OnCommandNotFound(fn CommandNotFoundFunc) Option

OnCommandNotFound registers a callback (CommandNotFoundFunc) to be invoked when the command a user attempted to execute is not found. The callback is also invoked when the user asks for help on a command that can not be found.

func OnError

func OnError(fn ErrorFunc) Option

OnError registers a callback (ErrorFunc) to be invoked when a nonfatal error occurs.

These are recoverable errors such as

  • a broken symlink is encountered in one of the paths being searched
  • a command exits unnsuccessfully when invoked with --summary or --help

func PrependCommands added in v1.1.0

func PrependCommands(cmds ...interface{}) Option

PrependCommands adds new embedded commands to the Entrypoint. The command are added to the front of the list and will have the highest precedence: an executable with the same name as one of these commands would be overridden by it.

func WithMaxDepth

func WithMaxDepth(value int) Option

WithMaxDepth sets the maximum depth of the command tree.

A value of 0 prohibits any submodules. All subcommands are leaves of the Entrypoint. (i.e. If the Go CLI were an exoskeleton, 'go doc' would be allowed, 'go mod tidy' would not.)

A value of -1 (the default value) means there is no maximum depth.

func WithMenuHeadingFor

func WithMenuHeadingFor(fn MenuHeadingForFunc) Option

WithMenuHeadingFor allows you to supply a function that determines the heading a Command should be listed under in the main menu.

func WithMenuTemplate added in v1.6.0

func WithMenuTemplate(value *template.Template) Option

WithMenuTemplate sets the template that will be used to render help for modules. The template will be executed with an instance of exoskeleton.Menu as its data.

func WithModuleMetadataFilename

func WithModuleMetadataFilename(value string) Option

WithModuleMetadataFilename sets the filename to use for module metadata. (Default: ".exoskeleton")

func WithName added in v1.4.1

func WithName(value string) Option

WithName sets the name of the entrypoint. (By default, this is the basename of the executable.)

type SummaryFunc added in v1.6.0

type SummaryFunc func(Command) (string, error)

SummaryFunc is a function that is expected to return the heading

type SymlinkError

type SymlinkError struct {
	Cause error
	Path  string
}

SymlinkError records an error that occurred while following a symlink.

func (SymlinkError) Error

func (e SymlinkError) Error() string

func (SymlinkError) Unwrap

func (e SymlinkError) Unwrap() error

Directories

Path Synopsis
pkg
shellcomp
The constants in this file are taken from https://github.com/spf13/cobra/blob/v1.5.0/completions.go
The constants in this file are taken from https://github.com/spf13/cobra/blob/v1.5.0/completions.go

Jump to

Keyboard shortcuts

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