Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"strings"

"github.com/block/opencli-go"
"github.com/square/exit"
"github.com/square/exoskeleton/v2/pkg/shellcomp"
)
Expand Down Expand Up @@ -187,6 +188,12 @@ type commandDescriptor struct {
Summary *string `json:"summary,omitempty"`
Commands []*commandDescriptor `json:"commands,omitempty"`
DefaultCommand string `json:"defaultCommand,omitempty"`

// openCLI holds the node's OpenCLI metadata (arguments, options, description,
// examples, exit codes, ...) captured from --help-opencli. Its Commands field
// is not populated; children are represented by Commands above. It is nil for
// commands discovered via contracts other than OpenCLI.
openCLI *opencli.Command
}

func readSummaryFromShellScript(cmd *shellScriptCommand) (string, error) {
Expand Down
27 changes: 27 additions & 0 deletions contract_opencli.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ import (
// file extension. Any executable file is eligible.
type OpenCLIContract struct{}

// OpenCLIDescriber is implemented by Commands that can describe themselves
// using the OpenCLI model (github.com/block/opencli-go). Not every Command
// implements it; reflect on a discovered Command to obtain its metadata:
//
// if d, ok := cmd.(OpenCLIDescriber); ok {
// if c, _ := d.OpenCLICommand(); c != nil {
// // c.Arguments, c.Options, c.Description, c.Examples, ...
// }
// }
//
// OpenCLICommand returns nil when the command has no OpenCLI metadata (for
// example, a command discovered via a contract other than OpenCLI).
//
// The returned Command describes only this node; its Commands field is not
// populated. Walk Subcommands() to describe the command tree.
type OpenCLIDescriber interface {
OpenCLICommand() (*opencli.Command, error)
}

func (c *OpenCLIContract) BuildCommand(path string, info fs.DirEntry, parent Command, d DiscoveryContext) (Command, error) {
if info.IsDir() {
return nil, ErrNotApplicable
Expand Down Expand Up @@ -118,6 +137,14 @@ func opencliToDescriptor(cmd opencli.Command) *commandDescriptor {
if cmd.DefaultCommand != nil {
d.DefaultCommand = *cmd.DefaultCommand
}

// Retain the node's OpenCLI metadata so it can be surfaced via
// OpenCLIDescriber. Strip Commands: children are represented by
// d.Commands and reachable through Subcommands().
nodeLocal := cmd
nodeLocal.Commands = nil
d.openCLI = &nodeLocal

if len(cmd.Commands) > 0 {
d.Commands = make([]*commandDescriptor, len(cmd.Commands))
for i, sub := range cmd.Commands {
Expand Down
48 changes: 48 additions & 0 deletions contract_opencli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,54 @@ func TestOpenCLICommandDiscovery(t *testing.T) {
assert.Equal(t, "tidy", cmds[1].DefaultSubcommand().Name())
}

func TestOpenCLICommandExposesMetadata(t *testing.T) {
contract := &OpenCLIContract{}
path := filepath.Join(fixtures, "opencli-tool")
info, err := os.Lstat(path)
assert.NoError(t, err)

d := &discoverer{maxDepth: -1, executor: defaultExecutor}
cmd, err := contract.BuildCommand(path, fs.FileInfoToDirEntry(info), nil, d)
assert.NoError(t, err)

// The root command is an OpenCLIDescriber.
describer, ok := cmd.(OpenCLIDescriber)
assert.True(t, ok)

// Root node metadata (triggers discovery). Commands are not populated on
// the node; they are reachable via Subcommands().
root, err := describer.OpenCLICommand()
assert.NoError(t, err)
assert.Equal(t, "opencli-tool", root.Name)
assert.Equal(t, "An OpenCLI tool", *root.Summary)
assert.Empty(t, root.Commands)

cmds, err := cmd.Subcommands()
assert.NoError(t, err)

// The "hidden-cmd" subcommand carries options, arguments, and the hidden flag.
hidden, ok := cmds[2].(OpenCLIDescriber)
assert.True(t, ok)
node, err := hidden.OpenCLICommand()
assert.NoError(t, err)
assert.True(t, node.Hidden)
assert.Len(t, node.Options, 1)
assert.Equal(t, "--verbose", node.Options[0].Name)
assert.Equal(t, []string{"-v"}, node.Options[0].Aliases)
assert.Len(t, node.Arguments, 1)
assert.Equal(t, "file", node.Arguments[0].Name)
assert.Empty(t, node.Commands)
}

func TestOpenCLICommandForNonOpenCLIContract(t *testing.T) {
// A command that was not discovered via the OpenCLI contract has no
// OpenCLI metadata; its richer fields come from the Command interface.
cmd := &executableCommand{name: "plain", aliases: []string{"p"}}
node, err := cmd.OpenCLICommand()
assert.NoError(t, err)
assert.Nil(t, node)
}

func TestParseOpenCLI(t *testing.T) {
cmd := &executableCommand{path: "/test"}
out := `{
Expand Down
23 changes: 23 additions & 0 deletions executable_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"os/exec"

"github.com/block/opencli-go"
"github.com/square/exoskeleton/v2/pkg/shellcomp"
)

Expand All @@ -29,6 +30,7 @@ type executableCommand struct {
cache Cache
describe describeFunc
contract string
openCLI *opencli.Command
}

func (cmd *executableCommand) Parent() Command { return cmd.parent }
Expand Down Expand Up @@ -172,10 +174,30 @@ func (cmd *executableCommand) discover() error {

cmd.summary = descriptor.Summary
cmd.defaultSubcommand = descriptor.DefaultCommand
cmd.openCLI = descriptor.openCLI
cmd.cmds = toCommands(cmd, descriptor.Commands, nil, cmd.discoverer)
return nil
}

// OpenCLICommand returns this command's OpenCLI metadata (github.com/block/opencli-go),
// or nil when it has none — for example, a command discovered via a contract
// other than OpenCLI.
//
// The returned Command describes only this node. Its Commands field is not
// populated; walk Subcommands() to describe the command tree.
func (cmd *executableCommand) OpenCLICommand() (*opencli.Command, error) {
if cmd.openCLI == nil && cmd.discoverer != nil && cmd.cmds == nil {
if err := cmd.discover(); err != nil {
return nil, err
}
}
if cmd.openCLI == nil {
return nil, nil
}
node := *cmd.openCLI
return &node, nil
}

func toCommands(parent *executableCommand, descriptors []*commandDescriptor, args []string, d DiscoveryContext) Commands {
cmds := Commands{}
for _, descriptor := range descriptors {
Expand All @@ -188,6 +210,7 @@ func toCommands(parent *executableCommand, descriptors []*commandDescriptor, arg
aliases: descriptor.Aliases,
summary: descriptor.Summary,
defaultSubcommand: descriptor.DefaultCommand,
openCLI: descriptor.openCLI,
executor: parent.executor,
cache: parent.cache,
contract: parent.contract,
Expand Down
Loading