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/v2"
)
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`.
Discovery Contracts ¶
Exoskeleton uses a contract system to determine how commands and modules are discovered and built. Four default contracts are provided:
- DirectoryModuleContract: Directories containing a .exoskeleton file
- ExecutableModuleContract: Executables with .exoskeleton extension
- ScriptCommandContract: Shell scripts with magic comments (# SUMMARY:, # HELP:)
- ExecutableCommandContract: Executables that respond to --summary and --help
Users can customize discovery by providing their own contracts via WithContracts():
e, _ := exoskeleton.New(
paths,
exoskeleton.WithContracts(
&MyCustomContract{},
&exoskeleton.DirectoryModuleContract{},
),
)
Index ¶
- Constants
- Variables
- func CompleteCommands(e *Entrypoint, args, env []string) ([]string, shellcomp.Directive, error)
- func CompleteExec(e *Entrypoint, args, env []string) error
- func CompleteFiles(e *Entrypoint, args, env []string) ([]string, shellcomp.Directive, error)
- func Exec(paths []string, options ...Option)
- func GenerateCompletionScript(name, shell string, w io.Writer) (err error)
- func HelpExec(e *Entrypoint, args, _ []string) error
- func IsEmbedded(command Command) bool
- func IsNull(command Command) bool
- func MenuFor(cmd Command, opts *MenuOptions) (string, []error)
- func Usage(cmd Command) string
- func UsageRelativeTo(cmd Command, relativeTo Command) string
- func WhichExec(e *Entrypoint, args, _ []string) error
- type AfterIdentifyFunc
- type Cache
- type Command
- type CommandDescribeError
- type CommandError
- type CommandHelpError
- type CommandNotFoundFunc
- type CommandSummaryError
- type Commands
- type CompleteFunc
- type Contract
- type DirectoryContract
- type DiscoveryContext
- type DiscoveryError
- type EmbeddedCommand
- type Entrypoint
- func (e *Entrypoint) Complete(_ *Entrypoint, args, _ []string) (completions []string, directive shellcomp.Directive, err error)
- func (e *Entrypoint) Exec(_ *Entrypoint, rawArgs, env []string) error
- func (e *Entrypoint) GenerateCompletionScript(shell string, w io.Writer) error
- func (e *Entrypoint) Help() (string, error)
- func (e *Entrypoint) Identify(args []string) (Command, []string, error)
- func (e *Entrypoint) Name() string
- func (e *Entrypoint) Parent() Command
- func (e *Entrypoint) Path() string
- func (e *Entrypoint) Subcommands() (Commands, error)
- func (e *Entrypoint) Summary() (string, error)
- type ErrorFunc
- type ExecFunc
- type ExecutableContract
- type ExecutorFunc
- type ExpandOption
- type FileCache
- type Menu
- type MenuHeadingForFunc
- type MenuItem
- type MenuItems
- type MenuOptions
- type MenuSection
- type MenuSections
- type Option
- func AfterIdentify(fn AfterIdentifyFunc) Option
- func AppendCommands(cmds ...*EmbeddedCommand) Option
- func OnCommandNotFound(fn CommandNotFoundFunc) Option
- func OnError(fn ErrorFunc) Option
- func PrependCommands(cmds ...*EmbeddedCommand) Option
- func WithCache(c Cache) Option
- func WithContracts(contracts ...Contract) Option
- func WithExecutor(value ExecutorFunc) Option
- func WithMaxDepth(value int) Option
- func WithMenuHeadingFor(fn MenuHeadingForFunc) Option
- func WithMenuTemplate(value *template.Template) Option
- func WithModuleMetadataFilename(value string) Option
- func WithName(value string) Option
- type ShellScriptContract
- type StandaloneExecutableContract
- type SummaryFunc
- type SymlinkError
Constants ¶
const CompleteHelp = `` /* 969-byte string literal not displayed */
CompleteHelp is the help text for the built-in 'complete' command.
const HelpHelp = `` /* 269-byte string literal not displayed */
HelpHelp is the help text for the built-in 'help' command.
const WhichHelp = `` /* 423-byte string literal not displayed */
WhichHelp is the help text for the built-in 'which' command.
Variables ¶
var ErrNotApplicable = errors.New("contract does not apply")
ErrNotApplicable indicates that a contract does not apply to a given file/directory. Discovery will try the next contract in the list.
Functions ¶
func CompleteCommands ¶
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 ¶
func CompleteExec(e *Entrypoint, args, env []string) error
CompleteExec implements the 'complete' command.
func CompleteFiles ¶
CompleteFiles is a CompleteFunc that provides completions for files and paths.
func GenerateCompletionScript ¶
GenerateCompletionScript generates a completion script for the given shell ("bash" or "zsh") and writes it to the given writer.
func HelpExec ¶
func HelpExec(e *Entrypoint, args, _ []string) error
HelpExec implements the 'help' command.
func IsEmbedded ¶
IsEmbedded returns true if the given Command is built into the exoskeleton.
func MenuFor ¶
func MenuFor(cmd Command, opts *MenuOptions) (string, []error)
MenuFor renders a menu of commands for a Command with subcommands.
func Usage ¶
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 ¶
UsageRelativeTo returns the usage string for the given command relative to the given command. 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 ¶
func WhichExec(e *Entrypoint, args, _ []string) error
WhichExec implements the 'which' command.
Types ¶
type AfterIdentifyFunc ¶
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 Cache ¶
type Cache interface {
// Fetch returns a cached value for the given command and key, or calls compute()
// to generate it. If compute() returns an error, the error is returned and the
// value is not cached.
//
// The key identifies the operation (e.g., "summary", "describe-commands").
// Implementations typically use cmd.Path() combined with key to form a cache key,
// and may use the file's modification time for cache invalidation.
Fetch(cmd Command, key string, compute func() (string, error)) (string, error)
}
Cache provides caching for expensive string-producing operations. Implementations control their own expiry/invalidation logic.
Cache implementations must be safe for concurrent use.
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 command that contains this command.
//
// For unnested 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' command and 'test”s Parent
// would be the entrypoint itself, 'go'.
Parent() Command
// 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)
// Subcommands returns the list of Commands contained by this command.
// Returns an empty slice for leaf commands (commands without subcommands).
//
// Returns a CommandError if the command does not fulfill the contract
// for providing its subcommands.
Subcommands() (Commands, 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 Command named 'mod'.
type CommandDescribeError ¶
type CommandDescribeError struct{ CommandError }
CommandDescribeError indicates that an executable module did not properly respond to `--describe-commands`
type CommandError ¶
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 ¶
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 ¶
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) Expand ¶
func (c Commands) Expand(fops ...ExpandOption) (Commands, []error)
Expand returns a list of commands, recursively replacing modules with their subcommands up to a given depth, along with any errors returned by modules' Subcommands().
type CompleteFunc ¶
CompleteFunc is called when an built-in command is asked to supply shell completions.
type Contract ¶
type Contract interface {
// BuildCommand constructs a Command using this contract's rules.
// Returns ErrNotApplicable if this contract doesn't handle the file/directory.
// Returns nil, nil if the file/directory should be ignored (e.g., non-executable file).
BuildCommand(path string, info fs.DirEntry, parent Command, d DiscoveryContext) (Command, error)
}
Contract represents an agreement between Exoskeleton and a command about how the command is discovered.
Contracts are tried in order during discovery. The first contract that doesn't return ErrNotApplicable builds the command.
type DirectoryContract ¶
type DirectoryContract struct {
MetadataFilename string
}
DirectoryContract handles directories containing a module metadata file.
The module metadata filename is configured on the Entrypoint and passed through the discoverer, not stored on the contract itself.
func (*DirectoryContract) BuildCommand ¶
func (c *DirectoryContract) BuildCommand(path string, info fs.DirEntry, parent Command, d DiscoveryContext) (Command, error)
type DiscoveryContext ¶
type DiscoveryContext interface {
DiscoverIn(path string, parent Command) (Commands, []error)
Executor() ExecutorFunc
MaxDepth() int
Next() DiscoveryContext
Cache() Cache
}
type DiscoveryError ¶
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 ¶
type EmbeddedCommand struct {
Name string
Summary string
Help string
Exec ExecFunc
Complete CompleteFunc
Commands []*EmbeddedCommand
}
EmbeddedCommand defines a built-in command that can be added to an Entrypoint (as opposed to an executable 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/v2', Identify would return the Get command and the arguments {'-u', 'github.com/square/exoskeleton/v2'}.
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() Command
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 ExecFunc ¶
type ExecFunc func(e *Entrypoint, args, env []string) error
ExecFunc is called when an built-in command is run.
type ExecutableContract ¶
type ExecutableContract struct {
}
ExecutableContract handles executables that respond to --describe-commands.
The extension is hardcoded to ".exoskeleton", and the --describe-commands flag is also hardcoded.
func (*ExecutableContract) BuildCommand ¶
func (c *ExecutableContract) BuildCommand(path string, info fs.DirEntry, parent Command, d DiscoveryContext) (Command, error)
type ExecutorFunc ¶
type ExpandOption ¶
type ExpandOption func(*expandOptions)
func WithDepth ¶
func WithDepth(d int) ExpandOption
func WithoutExpandedModules ¶
func WithoutExpandedModules() ExpandOption
type FileCache ¶
type FileCache struct {
// Path is the location of the cache file (e.g., "~/.myapp/cache.json").
Path string
// ExpiresAfter is the maximum age of a cache entry before it's considered stale.
// If zero, entries never expire based on age (only mtime changes invalidate).
ExpiresAfter time.Duration
// contains filtered or unexported fields
}
FileCache is a file-backed cache with mtime-based invalidation and optional TTL expiration. It persists cache entries to a JSON file and invalidates entries when the source file's mtime changes or the TTL expires.
FileCache is safe for concurrent use. It uses singleflight to deduplicate concurrent calls with the same key.
type Menu ¶
type Menu struct {
Usage string
HelpUsage string
Sections MenuSections
}
Menu is the data passed to MenuOptions.Template when it is executed.
type MenuHeadingForFunc ¶
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 parent Command whose subcommands are being listed and the Command whose heading should be returned.
type MenuOptions ¶
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 the parent Command and a subcommand, returning a
// string to use as a section heading for the subcommand.
// 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 Command with subcommands.
Template *template.Template
}
MenuOptions are the options that control how menus are constructed for modules.
type MenuSection ¶
type MenuSections ¶
type MenuSections []MenuSection
type Option ¶
type Option interface {
Apply(*Entrypoint)
}
An Option applies optional changes to an Exoskeleton Entrypoint.
func AfterIdentify ¶
func AfterIdentify(fn AfterIdentifyFunc) Option
AfterIdentify registers a callback (AfterIdentifyFunc) to be invoked with any command that is successfully identified.
func AppendCommands ¶
func AppendCommands(cmds ...*EmbeddedCommand) 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 ¶
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 ¶
func PrependCommands(cmds ...*EmbeddedCommand) 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 WithCache ¶
WithCache sets a cache for expensive operations like command execution. If not set, no caching is performed.
func WithContracts ¶
WithContracts sets the contracts used during discovery. Contracts are tried in order; the first that doesn't return ErrNotApplicable builds the command.
The default contracts are:
- DirectoryContract (directories that contain the module metadata file)
- ExecutableContract (executables with .exoskeleton extension which must implement --describe-commands)
- ShellScriptContract (shell scripts with magic comments)
- StandaloneExecutableContract (all other executables which must implement --summary)
func WithExecutor ¶
func WithExecutor(value ExecutorFunc) Option
WithExecutor supplies a function that executes a subcommand. The default executor calls `Run()` on the command and returns the error.
func WithMaxDepth ¶
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 ¶
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 ¶
WithModuleMetadataFilename sets the filename to use for module metadata. (Default: ".exoskeleton")
type ShellScriptContract ¶
type ShellScriptContract struct{}
ShellScriptContract handles shell scripts with magic comments.
Scripts are detected by checking for a shebang (#!) at the start of the file. They provide metadata via magic comments like "# SUMMARY:" and "# HELP:".
func (*ShellScriptContract) BuildCommand ¶
func (c *ShellScriptContract) BuildCommand(path string, info fs.DirEntry, parent Command, d DiscoveryContext) (Command, error)
type StandaloneExecutableContract ¶
type StandaloneExecutableContract struct{}
StandaloneExecutableContract handles executable files that respond to --summary and --help.
Note: This contract should be ordered AFTER ScriptCommandContract and ExecutableModuleContract in the contract list, as it matches all executable files.
func (*StandaloneExecutableContract) BuildCommand ¶
func (c *StandaloneExecutableContract) BuildCommand(path string, info fs.DirEntry, parent Command, d DiscoveryContext) (Command, error)
type SummaryFunc ¶
SummaryFunc is a function that is expected to return the heading
type SymlinkError ¶
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
Source Files
¶
- builtin_command.go
- cache.go
- command.go
- commands.go
- complete_cmd.go
- completion_scripts.go
- completions.go
- contract.go
- contract_directory.go
- contract_executable.go
- contract_shell_script.go
- contract_standalone_executable.go
- directory_command.go
- discovery.go
- discovery_error.go
- doc.go
- entrypoint.go
- exec.go
- executable_command.go
- help_cmd.go
- identification.go
- menu.go
- null_command.go
- options.go
- predicates.go
- suggestions.go
- symlink_error.go
- usage.go
- which_cmd.go