Skip to content

Commit

Permalink
Add cmdio.Command interface
Browse files Browse the repository at this point in the history
Due to interface assertions not working on embedded interface types,
code that inspected the capabilities of an underlying command
represented as an io.ReadWriter failed, requiring manual passthrough of
all implemented methods.

This was not a sustainable way of handling methods with differing
sets of command implementations.

cmdio.Command represents the broadest possible interface of any
command. For implementations that choose not to implement optional
interfaces, NopCommand is provided as an embeddable type to
automatically fulfill the interface with dummy implementations, which
can be selectively overridden as desired.
  • Loading branch information
lesiw committed Oct 14, 2024
1 parent 588b24c commit 1856e83
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 37 deletions.
24 changes: 4 additions & 20 deletions ctr/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ package ctr

import (
"context"
"fmt"
"io"

"lesiw.io/cmdio"
)

type cmd struct {
io.ReadWriter
cmdio.Command
cdr *cdr
ctx context.Context
env map[string]string
Expand All @@ -18,7 +16,7 @@ type cmd struct {

func newCmd(
cdr *cdr, ctx context.Context, env map[string]string, args ...string,
) io.ReadWriter {
) cmdio.Command {
c := &cmd{
ctx: ctx,
env: env,
Expand All @@ -31,26 +29,12 @@ func newCmd(

func (c *cmd) Attach() error {
c.setCmd(true)
if a, ok := c.ReadWriter.(cmdio.Attacher); ok {
if a, ok := c.Command.(cmdio.Attacher); ok {
return a.Attach()
}
return nil
}

func (c *cmd) Close() error {
if cl, ok := c.ReadWriter.(io.Closer); ok {
return cl.Close()
}
return nil
}

func (c *cmd) String() string {
if s, ok := c.ReadWriter.(fmt.Stringer); ok {
return s.String()
}
return fmt.Sprintf("<%T>", c)
}

func (c *cmd) setCmd(attach bool) {
cmd := []string{"container", "exec", "-i"}
if attach {
Expand All @@ -65,5 +49,5 @@ func (c *cmd) setCmd(attach bool) {
}
cmd = append(cmd, c.cdr.ctrid)
cmd = append(cmd, c.arg...)
c.ReadWriter = c.cdr.rnr.Commander.Command(c.ctx, nil, cmd...)
c.Command = c.cdr.rnr.Commander.Command(c.ctx, nil, cmd...)
}
3 changes: 1 addition & 2 deletions ctr/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"crypto/sha1"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
Expand All @@ -29,7 +28,7 @@ type cdr struct {

func (c *cdr) Command(
ctx context.Context, env map[string]string, args ...string,
) io.ReadWriter {
) cmdio.Command {
return newCmd(c, ctx, env, args...)
}

Expand Down
43 changes: 33 additions & 10 deletions io.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package cmdio

import (
"context"
"fmt"
"io"
"os"

Expand All @@ -28,21 +29,13 @@ import (
//
// The Command function accepts a [context.Context], a map of environment
// variables, and a variable number of arguments representing the command
// itself.
//
// The command must not begin execution until the first time it is read from or
// written to. It must return [io.EOF] once execution has completed and all
// output has been consumed.
//
// In general, the Write method will correspond to standard in and the Read
// method will correspond to standard out. Commands may implement [Logger] to
// represent standard error.
// itself. It returns a [Command].
type Commander interface {
Command(
ctx context.Context,
env map[string]string,
arg ...string,
) (cmd io.ReadWriter)
) (cmd Command)
}

// An Enver has environment variables.
Expand Down Expand Up @@ -77,6 +70,36 @@ type Attacher interface {
Attach() error
}

// A [Command] is the broadest possible command interface.
//
// Commands must not begin execution until the first time they are read from or
// written to. They must return [io.EOF] once execution has completed and all
// output has been consumed.
//
// In general, the Write method will correspond to standard in, the Read
// method will correspond to standard out, and an [io.Writer] may be passed
// to Log for handling standard error.
type Command interface {
io.ReadWriteCloser
fmt.Stringer
Attacher
Coder
Logger
}

// A [NopCommand] is an empty [Command] implementation. It is useful for
// embedding in command implementations that may not choose to implement
// optional interfaces.
type NopCommand struct{}

func (NopCommand) Read([]byte) (int, error) { return 0, nil }
func (NopCommand) Write([]byte) (int, error) { return 0, nil }
func (NopCommand) Close() error { return nil }
func (NopCommand) String() string { return "<nop>" }
func (NopCommand) Attach() error { return nil }
func (NopCommand) Code() int { return 0 }
func (NopCommand) Log(io.Writer) {}

// Trace is an [io.Writer] to which command tracing information is written.
// To disable tracing, set this variable to [io.Discard].
var Trace io.Writer = prefix.NewWriter("+ ", stderr)
Expand Down
3 changes: 1 addition & 2 deletions sub/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package sub

import (
"context"
"io"

"lesiw.io/cmdio"
"lesiw.io/cmdio/sys"
Expand All @@ -15,7 +14,7 @@ type cdr struct {

func (c *cdr) Command(
ctx context.Context, env map[string]string, args ...string,
) io.ReadWriter {
) cmdio.Command {
return c.rnr.Commander.Command(ctx, env, append(c.cmd, args...)...)
}

Expand Down
8 changes: 7 additions & 1 deletion sys/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ import (
"slices"
"strings"
"sync"

"lesiw.io/cmdio"
)

type cmd struct {
cmdio.NopCommand

ctx context.Context
cmd *exec.Cmd
env map[string]string
Expand All @@ -38,7 +42,9 @@ func (c *cmd) Attach() error {
return nil
}

func newCmd(ctx context.Context, env map[string]string, args ...string) *cmd {
func newCmd(
ctx context.Context, env map[string]string, args ...string,
) cmdio.Command {
c := new(cmd)
c.ctx = ctx
c.cmd = exec.CommandContext(ctx, args[0], args[1:]...)
Expand Down
3 changes: 1 addition & 2 deletions sys/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package sys

import (
"context"
"io"

"lesiw.io/cmdio"
)
Expand All @@ -11,7 +10,7 @@ type cdr struct{}

func (cdr) Command(
ctx context.Context, env map[string]string, args ...string,
) io.ReadWriter {
) cmdio.Command {
return newCmd(ctx, env, args...)
}

Expand Down

0 comments on commit 1856e83

Please sign in to comment.