dippy's cmd package provides an abstraction for running arbitrary commands. It's fairly simple, but it does good job of allowing for unit testing as well as encapsulating concerns about local vs. remote commands.
type Runner interface {
Execute(ctx context.Context, c *Cmd) error
}
A Runner is anything that can execute a command. This package defines a few useful kinds of runners, but other types in dippy also implement Runner by delegating to one of those.
type Cmd struct {
Argv []string
Stdout io.Writer
Stderr io.Writer
}
Cmd captures the basic details of a command to be run by a runner. It doesn't include every possible field someone might want to set on a command: only the ones actually used by dippy. The Stdout field in particular is used so that Runners only need to implement a single method without worrying about whether the output is being captured or not.
A Cmd is usually not constructed by users of the package. Instead, they will use one of the following two functions that create the Cmd and execute it in one go.
func Run(ctx context.Context, r Runner, name string, args ...string) error {
return r.Execute(ctx, &Cmd{
Argv: append([]string{name}, args...),
Stdout: os.Stdout,
Stderr: os.Stderr,
})
}
Run is used to run a command without capturing its output. Both stdout and stderr are inherited from dippy, so the output will be displayed to the user.
// Output runs a command through the given runner, capturing and returning its stdout. Stderr will not be captured.
func Output(ctx context.Context, r Runner, name string, args ...string) ([]byte, error) {
var b bytes.Buffer
err := r.Execute(ctx, &Cmd{
Argv: append([]string{name}, args...),
Stdout: &b,
Stderr: os.Stderr,
})
return b.Bytes(), err
}
Output is used to run a command while capturing and returning its stdout. This is done by creating a byte buffer for the command to write to and passing that as the stdout writer. The stderr is still inherited from dippy, so that output will be shown to the user.
//| file: packages/dippy/cmd/runner.go package cmd import ( "bytes" "context" "io" "os" ) <<Runner>> <<Cmd>> <<Run>> <<Output>>
//| file: packages/dippy/cmd/local_runner.go
package cmd
import (
"context"
"os/exec"
)
type LocalRunner struct{}
func (_ LocalRunner) Execute(ctx context.Context, c *Cmd) error {
cmd := exec.CommandContext(ctx, c.Argv[0], c.Argv[1:]...)
cmd.Stdout = c.Stdout
cmd.Stderr = c.Stderr
return cmd.Run()
}
A LocalRunner is a very small shim over exec.CommandContext. The arguments and output streams are passed along and the command is run.
type SSHRunner struct {
*ssh.Client
}
An SSHRunner wraps an SSH client and uses it to run commands on a particular remote host.
func NewSSHRunner(host string, user string, key ssh.Signer, hostKeyCallback ssh.HostKeyCallback) (*SSHRunner, error) {
config := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(key),
},
HostKeyCallback: hostKeyCallback,
HostKeyAlgorithms: []string{
ssh.CertAlgoED25519v01,
ssh.KeyAlgoED25519,
ssh.KeyAlgoRSA,
},
}
client, err := ssh.Dial("tcp", host+":22", config)
if err != nil {
return nil, fmt.Errorf("dialing %s via ssh: %w", host, err)
}
return &SSHRunner{Client: client}, nil
}
NewSSHRunner creates a new SSH runner. It requires a few pieces of information to do so:
With this information, it constructs an SSH client configuration and dials the host. If that goes well, it returns a new runner.
func (r *SSHRunner) Execute(ctx context.Context, c *Cmd) error {
session, err := r.NewSession()
if err != nil {
return fmt.Errorf("creating ssh session: %w", err)
}
defer session.Close()
session.Stdout = c.Stdout
session.Stderr = c.Stderr
return session.Run(shellescape.QuoteCommand(c.Argv))
}
Execute runs a command on the SSH connection. Each command run over the same SSH connection is handled by it's own session, so a new one is created. The output streams are configured based on the command that was given, and then the session is told to run the given command. While arguments are passed as a list when running locally, for SSH they are passed as a single string which the server will usually pass to a shell. For that reason, a shellescape package is used to quote the arguments of the command before passing it along.
//| file: packages/dippy/cmd/ssh_runner.go package cmd import ( "context" "fmt" "al.essio.dev/pkg/shellescape" "golang.org/x/crypto/ssh" ) <<SSHRunner>> <<NewSSHRunner>> <<SSHRunner.Execute>>
text/gemini;lang=en-USThis content has been proxied by September (UNKNO).