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 ¶
- 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 Command
- type Commander
- type Enver
- type Logger
- type NopCommand
- 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) WithCommand(cmd string, rnr2 *Runner) *Runner
- func (rnr *Runner) WithCommander(cdr Commander) *Runner
- func (rnr *Runner) WithContext(ctx context.Context) *Runner
- func (rnr *Runner) WithEnv(env map[string]string) *Runner
Examples ¶
- Package
- Package (Script)
- Copy
- MustGetPipe
- MustGetPipe (Panic)
- MustPipe
- MustPipe (Panic)
- Pipe (Echo)
- Pipe (Reader)
- Runner.Command
- Runner.Get
- Runner.Get (Error)
- Runner.MustGet
- Runner.MustGet (Panic)
- Runner.Run
- Runner.WithCommand
- Runner.WithCommand (Env)
- Runner.WithEnv
- Runner.WithEnv (Multiple)
- Runner.WithEnv (Pwd)
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 ¶
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
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 ¶
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 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) String ¶ added in v0.5.0
func (NopCommand) String() string
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 { Commander // 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. _, 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 ¶
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) WithCommand ¶ added in v0.7.0
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
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 ¶
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 ¶
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