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. One such runner implementation is lesiw.io/cmdio/sys, which runs commands on the local system.
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 ¶
- Variables
- func Copy(dst io.Writer, src io.Reader, mid ...io.ReadWriter) (written int64, err error)
- func MustPipe(src io.Reader, cmd ...io.ReadWriter)
- func Pipe(src io.Reader, cmd ...io.ReadWriter) error
- type Attacher
- type Coder
- type Commander
- type Enver
- type Logger
- type Result
- type Runner
- func (rnr *Runner) Close() error
- func (rnr *Runner) Command(args ...string) io.ReadWriter
- func (rnr *Runner) Env(name string) (value string)
- func (rnr *Runner) Get(args ...string) (Result, error)
- func (rnr *Runner) MustGet(args ...string) Result
- func (rnr *Runner) MustRun(args ...string)
- func (rnr *Runner) Run(args ...string) error
- func (rnr *Runner) WithContext(ctx context.Context) *Runner
- func (rnr *Runner) WithEnv(env map[string]string) *Runner
Examples ¶
Constants ¶
This section is empty.
Variables ¶
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 ¶
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 ¶
nolint: errcheck
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 Commander ¶
type Commander interface { Command( ctx context.Context, env map[string]string, arg ...string, ) (cmd io.ReadWriter) }
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.
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.
type Enver ¶
An Enver has environment variables.
A Commander that also implements this interface will call Env to retrieve environment variables.
type Logger ¶
A Logger accepts an io.Writer for logging diagnostic information.
Implementing this interface is the idiomatic way for commands to represent standard error.
type Result ¶
type Result struct { Cmd io.ReadWriter Out string Log string Code int }
Result describes the results of a command execution.
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 {
// contains filtered or unexported fields
}
A Runner runs commands.
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 ¶
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 ¶
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. r, err := rnr.Get("busybox", "ls", "/bad_directory") fmt.Println("err:", err) fmt.Println("log:", r.Log) fmt.Println("code:", r.Code) }
Output: err: exit status 1 log: ls: /bad_directory: No such file or directory code: 1
func (*Runner) MustGet ¶
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) Run ¶
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) WithContext ¶
WithContext creates a new runner with the provided context.Context. The new runner will share the same environment and commander as its parent.
func (*Runner) WithEnv ¶
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) ¶
nolint: errcheck
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