cmdio

package module
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Dec 25, 2024 License: MIT Imports: 11 Imported by: 12

README

lesiw.io/cmdio

Go Reference

Package cmdio provides portable interfaces for commands and command runners.

Documentation

Overview

Package cmdio provides portable interfaces for commands and command runners.

A command is an io.ReadWriter. Writing to a command writes to its standard input. Reading from a command reads from its standard output. Commands may optionally implement Logger to capture standard error and Coder to represent exit codes.

Commands are instantiated by a Runner. This package contains several Runner implementations: lesiw.io/cmdio/sys, which runs commands on the local system; lesiw.io/cmdio/ctr, which runs commands in containers; and lesiw.io/cmdio/sub, which runs commands as subcommands.

While most of this package is written to support traditional Go error handling, Must-type functions, such as Runner.MustRun and MustPipe, are provided to support a script-like programming style, where failures result in panics.

Example
package main

import (
	"fmt"
	"log"
	"strings"

	"lesiw.io/cmdio"
	"lesiw.io/cmdio/sys"
)

func main() {
	rnr := sys.Runner().WithEnv(map[string]string{
		"PKGNAME": "cmdio",
	})
	err := rnr.Run("echo", "hello from", rnr.Env("PKGNAME"))
	if err != nil {
		log.Fatal(err)
	}
	if _, err := rnr.Get("true"); err == nil {
		fmt.Println("true always succeeds")
	}
	if _, err := rnr.Get("false"); err != nil {
		fmt.Println("false always fails")
	}
	err = cmdio.Pipe(
		rnr.Command("echo", "pIpEs wOrK tOo"),
		rnr.Command("tr", "A-Z", "a-z"),
	)
	if err != nil {
		log.Fatal(err)
	}
	err = cmdio.Pipe(
		strings.NewReader("Even When Mixed With Other IO"),
		rnr.Command("tr", "A-Z", "a-z"),
	)
	if err != nil {
		log.Fatal(err)
	}
}
Output:

hello from cmdio
true always succeeds
false always fails
pipes work too
even when mixed with other io
Example (Script)
package main

import (
	"strings"

	"lesiw.io/cmdio"
	"lesiw.io/cmdio/sys"
)

func main() {
	rnr := sys.Runner().WithEnv(map[string]string{
		"PKGNAME": "cmdio",
	})
	rnr.MustRun("echo", "hello from", rnr.Env("PKGNAME"))
	if _, err := rnr.Get("true"); err == nil {
		rnr.MustRun("echo", "true always succeeds")
	}
	if _, err := rnr.Get("false"); err != nil {
		rnr.MustRun("echo", "false always fails")
	}
	cmdio.MustPipe(
		rnr.Command("echo", "pIpEs wOrK tOo"),
		rnr.Command("tr", "A-Z", "a-z"),
	)
	cmdio.MustPipe(
		strings.NewReader("Even When Mixed With Other IO"),
		rnr.Command("tr", "A-Z", "a-z"),
	)
}
Output:

hello from cmdio
true always succeeds
false always fails
pipes work too
even when mixed with other io

Index

Examples

Constants

This section is empty.

Variables

View Source
var Trace io.Writer = prefix.NewWriter("+ ", stderr)

Trace is an io.Writer to which command tracing information is written. To disable tracing, set this variable to io.Discard.

Functions

func Copy

func Copy(
	dst io.Writer, src io.Reader, mid ...io.ReadWriter,
) (written int64, err error)

Copy copies the output of each stream into the input of the next stream. When output is finished copying from one stream, the receiving stream is closed if it is an io.Closer.

Example
package main

import (
	"io"
	"log"

	"lesiw.io/cmdio"
	"lesiw.io/cmdio/sys"
)

func main() {
	rnr := sys.Runner()

	defer rnr.Run("rm", "-f", "/tmp/cmdio_test.txt")
	_, err := cmdio.Copy(
		io.Discard,
		rnr.Command("echo", "hello world"),
		rnr.Command("tee", "/tmp/cmdio_test.txt"),
	)
	if err != nil {
		log.Fatal(err)
	}

	err = rnr.Run("cat", "/tmp/cmdio_test.txt")
	if err != nil {
		log.Fatal(err)
	}
}
Output:

hello world

func MustPipe

func MustPipe(src io.Reader, cmd ...io.ReadWriter)

MustPipe pipes I/O streams together and panics on failure.

Example
package main

import (
	"strings"

	"lesiw.io/cmdio"
	"lesiw.io/cmdio/sys"
)

func main() {
	rnr := sys.Runner()
	cmdio.MustPipe(
		strings.NewReader("hello world"),
		rnr.Command("tr", "a-z", "A-Z"),
	)
}
Output:

HELLO WORLD
Example (Panic)
package main

import (
	"errors"
	"fmt"
	"testing/iotest"

	"lesiw.io/cmdio"
	"lesiw.io/cmdio/sys"
)

func main() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println(r)
		}
	}()
	rnr := sys.Runner()
	cmdio.MustPipe(
		iotest.ErrReader(errors.New("some error")),
		rnr.Command("tr", "a-z", "A-Z"),
	)
}
Output:

some error

<*iotest.errReader> | <- some error
tr a-z A-Z

func Pipe

func Pipe(src io.Reader, cmd ...io.ReadWriter) error

Pipe pipes I/O streams together.

Example (Echo)
package main

import (
	"log"
	"os"

	"lesiw.io/cmdio"
	"lesiw.io/cmdio/sys"
	"lesiw.io/prefix"
)

func main() {
	cmdio.Trace = prefix.NewWriter("+ ", os.Stdout)

	rnr := sys.Runner()
	err := cmdio.Pipe(
		rnr.Command("echo", "hello world"),
		rnr.Command("tr", "a-z", "A-Z"),
	)
	if err != nil {
		log.Fatal(err)
	}
}
Output:

+ echo 'hello world' | tr a-z A-Z
HELLO WORLD
Example (Reader)
package main

import (
	"log"
	"os"
	"strings"

	"lesiw.io/cmdio"
	"lesiw.io/cmdio/sys"
	"lesiw.io/prefix"
)

func main() {
	cmdio.Trace = prefix.NewWriter("+ ", os.Stdout)

	rnr := sys.Runner()
	err := cmdio.Pipe(
		strings.NewReader("hello world"),
		rnr.Command("tr", "a-z", "A-Z"),
	)
	if err != nil {
		log.Fatal(err)
	}
}
Output:

+ <*strings.Reader> | tr a-z A-Z
HELLO WORLD

Types

type Attacher

type Attacher interface {
	Attach() error
}

An Attacher can be connected directly to the controlling terminal. An attached command cannot be written to. It must be readable exactly once. The read must block for the duration of command execution, after which it must exit with 0 bytes read.

type Coder

type Coder interface {
	Code() int
}

A Coder has an exit code.

Implementing this interface is the idiomatic way for commands to represent exit codes.

type Command added in v0.5.0

type Command interface {
	io.ReadWriteCloser
	fmt.Stringer
	Attacher
	Coder
	Logger
}

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 Commander

type Commander interface {
	Command(
		ctx context.Context,
		env map[string]string,
		arg ...string,
	) (cmd Command)
}

A Commander instantiates commands.

The Command function accepts a context.Context, a map of environment variables, and a variable number of arguments representing the command itself. It returns a Command.

type Enver

type Enver interface {
	Env(name string) (value string)
}

An Enver has environment variables.

A Commander that also implements this interface will call Env to retrieve environment variables.

type Logger

type Logger interface {
	Log(io.Writer)
}

A Logger accepts an io.Writer for logging diagnostic information.

Implementing this interface is the idiomatic way for commands to represent standard error.

type NopCommand added in v0.5.0

type NopCommand struct{}

A NopCommand is an empty Command implementation. It is useful for embedding in command implementations that may not choose to implement optional interfaces.

func (NopCommand) Attach added in v0.5.0

func (NopCommand) Attach() error

func (NopCommand) Close added in v0.5.0

func (NopCommand) Close() error

func (NopCommand) Code added in v0.5.0

func (NopCommand) Code() int

func (NopCommand) Log added in v0.5.0

func (NopCommand) Log(io.Writer)

func (NopCommand) Read added in v0.5.0

func (NopCommand) Read([]byte) (int, error)

func (NopCommand) String added in v0.5.0

func (NopCommand) String() string

func (NopCommand) Write added in v0.5.0

func (NopCommand) Write([]byte) (int, error)

type Result

type Result struct {
	Cmd  io.ReadWriter
	Out  string
	Log  string
	Code int
}

Result describes the results of a command execution.

func GetPipe

func GetPipe(src io.Reader, cmd ...io.ReadWriter) (Result, error)

GetPipe pipes I/O streams together and captures the output in a Result.

func MustGetPipe

func MustGetPipe(src io.Reader, cmd ...io.ReadWriter) Result

MustGetPipe pipes I/O streams together and captures the output in a Result. It panics if any of the copy operations fail.

Example
package main

import (
	"fmt"
	"strings"

	"lesiw.io/cmdio"
	"lesiw.io/cmdio/sys"
)

func main() {
	rnr := sys.Runner()
	r := cmdio.MustGetPipe(
		strings.NewReader("hello world"),
		rnr.Command("tr", "a-z", "A-Z"),
	)
	fmt.Println("out:", r.Out)
	fmt.Println("code:", r.Code)
}
Output:

out: HELLO WORLD
code: 0
Example (Panic)
package main

import (
	"fmt"

	"lesiw.io/cmdio"
	"lesiw.io/cmdio/sys"
)

func main() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println(r)
		}
	}()
	rnr := sys.Runner()
	_ = cmdio.MustGetPipe(
		// Use busybox ls to normalize output.
		rnr.Command("busybox", "ls", "/bad_directory"),
		rnr.Command("tr", "a-z", "A-Z"),
	)
}
Output:

exit status 1

busybox ls /bad_directory | <- exit status 1
tr a-z A-Z

out: <empty>
log:
	ls: /bad_directory: No such file or directory
code: 0

type Runner

type Runner struct {
	Commander
	// contains filtered or unexported fields
}

A Runner runs commands.

func (*Runner) Close

func (rnr *Runner) Close() error

Close closes the underlying Commander if it is an io.Closer.

func (*Runner) Command

func (rnr *Runner) Command(args ...string) io.ReadWriter

Command instantiates a command as an io.ReadWriter.

The command will not be executed until the first time it is read or written to.

Example
package main

import (
	"fmt"
	"io"
	"log"

	"lesiw.io/cmdio/sys"
)

func main() {
	rnr := sys.Runner()
	cmd := rnr.Command("echo", "hello world")
	out, err := io.ReadAll(cmd)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(out))
}
Output:

hello world

func (*Runner) Env

func (rnr *Runner) Env(name string) (value string)

Env returns the value of an environment variable.

By default, it parses the output of an env command. Commander implementations may customize this behavior by implementing Enver.

func (*Runner) Get

func (rnr *Runner) Get(args ...string) (Result, error)

Get executes a command and captures the output in a Result.

Note that checking Result.Code > 0 is not sufficient to determine that the command executed successfully. Commands may choose not to implement Coder, and commands that fail to execute because they cannot be found will have no exit code.

Example
package main

import (
	"fmt"
	"log"

	"lesiw.io/cmdio/sys"
)

func main() {
	rnr := sys.Runner()
	r, err := rnr.Get("echo", "hello world")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("out:", r.Out)
	fmt.Println("code:", r.Code)
}
Output:

out: hello world
code: 0
Example (Error)
package main

import (
	"fmt"

	"lesiw.io/cmdio/sys"
)

func main() {
	rnr := sys.Runner()

	// Use busybox ls to normalize output.
	_, err := rnr.Get("busybox", "ls", "/bad_directory")
	fmt.Println("err:", err)
}
Output:

err: exit status 1
out: <empty>
log:
	ls: /bad_directory: No such file or directory
code: 1

func (*Runner) MustGet

func (rnr *Runner) MustGet(args ...string) Result

MustGet runs a command and captures its output in a Result. It panics with diagnostic output if the command fails.

Example
package main

import (
	"fmt"

	"lesiw.io/cmdio/sys"
)

func main() {
	rnr := sys.Runner()
	r := rnr.MustGet("echo", "hello world")
	fmt.Println("out:", r.Out)
	fmt.Println("code:", r.Code)
}
Output:

out: hello world
code: 0
Example (Panic)
package main

import (
	"fmt"

	"lesiw.io/cmdio/sys"
)

func main() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println(r)
		}
	}()
	rnr := sys.Runner()

	// Use busybox ls to normalize output.
	rnr.MustGet("busybox", "ls", "/bad_directory")
}
Output:

exit status 1
out: <empty>
log:
	ls: /bad_directory: No such file or directory
code: 1

func (*Runner) MustRun

func (rnr *Runner) MustRun(args ...string)

MustRun runs a command and panics on failure.

func (*Runner) Run

func (rnr *Runner) Run(args ...string) error

Run attaches a command to the controlling terminal and executes it.

Example
package main

import (
	"log"

	"lesiw.io/cmdio/sys"
)

func main() {
	rnr := sys.Runner()
	err := rnr.Run("echo", "hello world")
	if err != nil {
		log.Fatal(err)
	}
}
Output:

hello world

func (*Runner) WithCommand added in v0.7.0

func (rnr *Runner) WithCommand(cmd string, rnr2 *Runner) *Runner

WithCommand creates a new Runner with cmd handled by the provided Runner. The new Runner will otherwise be identical to its parent.

Example
package main

import (
	"fmt"
	"log"

	"lesiw.io/cmdio"
	"lesiw.io/cmdio/sub"
	"lesiw.io/cmdio/sys"
)

func main() {
	rnr := sys.Runner()
	box := sub.WithRunner(rnr, "busybox")
	rnr = rnr.WithCommand("dos2unix", box)
	rnr = rnr.WithCommand("unix2dos", box)
	r, err := cmdio.GetPipe(
		rnr.Command("printf", "hello\r\nworld\r\n"),
		rnr.Command("dos2unix"),
	)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(r.Out)
}
Output:

hello
world
Example (Env)
package main

import (
	"log"

	"lesiw.io/cmdio/sub"
	"lesiw.io/cmdio/sys"
)

func main() {
	rnr := sys.Runner()
	box := sub.WithRunner(rnr, "busybox")
	rnr = rnr.WithCommand("sh", box)
	err := rnr.WithEnv(map[string]string{"PKGNAME": "cmdio"}).
		Run("sh", "-c", `echo "hello from $PKGNAME"`)
	if err != nil {
		log.Fatal(err)
	}
}
Output:

hello from cmdio

func (*Runner) WithCommander added in v0.6.0

func (rnr *Runner) WithCommander(cdr Commander) *Runner

WithCommander creates a new Runner with the provided Commander. The new Runner will have a copy of the parent Runner's env and shares the same context as its parent.

func (*Runner) WithContext

func (rnr *Runner) WithContext(ctx context.Context) *Runner

WithContext creates a new Runner with the provided context.Context. The new Runner will have a copy of the parent Runner's env and shares the same commander as its parent.

func (*Runner) WithEnv

func (rnr *Runner) WithEnv(env map[string]string) *Runner

WithEnv creates a new Runner with the provided env. The new Runner will share the same context and commander as its parent.

PWD conventionally sets the working directory.

Example
package main

import (
	"fmt"

	"lesiw.io/cmdio/sys"
)

func main() {
	rnr := sys.Runner().WithEnv(map[string]string{
		"HOME": "/",
	})
	fmt.Println("rnr(HOME):", rnr.Env("HOME"))
}
Output:

rnr(HOME): /
Example (Multiple)
package main

import (
	"fmt"

	"lesiw.io/cmdio/sys"
)

func main() {
	rnr := sys.Runner().WithEnv(map[string]string{
		"HOME": "/",
		"FOO":  "bar",
	})
	fmt.Println("rnr(HOME):", rnr.Env("HOME"))
	rnr2 := rnr.WithEnv(map[string]string{
		"HOME": "/home/example",
	})
	fmt.Println("rnr(HOME):", rnr.Env("HOME"))
	fmt.Println("rnr2(HOME):", rnr2.Env("HOME"))
	fmt.Println("rnr(FOO):", rnr.Env("FOO"))
	fmt.Println("rnr2(FOO):", rnr2.Env("FOO"))
}
Output:

rnr(HOME): /
rnr(HOME): /
rnr2(HOME): /home/example
rnr(FOO): bar
rnr2(FOO): bar
Example (Pwd)
package main

import (
	"log"
	"os"

	"lesiw.io/cmdio"
	"lesiw.io/cmdio/sys"
	"lesiw.io/prefix"
)

func main() {
	cmdio.Trace = prefix.NewWriter("+ ", os.Stdout)
	rnr := sys.Runner()

	defer rnr.Run("rm", "-r", "/tmp/cmdio_dir_test")
	err := rnr.Run("mkdir", "/tmp/cmdio_dir_test")
	if err != nil {
		log.Fatal(err)
	}

	err = rnr.WithEnv(map[string]string{
		"PWD": "/tmp/cmdio_dir_test",
	}).Run("pwd")
	if err != nil {
		log.Fatal(err)
	}
}
Output:

+ mkdir /tmp/cmdio_dir_test
+ PWD=/tmp/cmdio_dir_test pwd
/tmp/cmdio_dir_test
+ rm -r /tmp/cmdio_dir_test

Directories

Path Synopsis
x
busybox Module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL
JackTT - Gopher 🇻🇳