cmdchain

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Feb 22, 2023 License: MIT Imports: 11 Imported by: 6

README

Go codecov Go Report Card Mentioned in Awesome Go

go-command-chain

A go library for easy configure and run command chains. Such like pipelining in unix shells.

Example

cat log_file.txt | grep error | wc -l
package main

import (
	"fmt"
	"github.com/rainu/go-command-chain"
)

func main() {
	stdOut, stdErr, err := cmdchain.Builder().
		Join("cat", "log_file.txt").
		Join("grep", "error").
		Join("wc", "-l").
		Finalize().RunAndGet()

	if err != nil {
		panic(err)
	}
	if stdErr != "" {
		panic(stdErr)
	}
	fmt.Printf("Errors found: %s", stdOut)
}

For more examples how to use the command chain see examples.

Why you should use this library?

If you want to execute a complex command pipeline you could come up with the idea of just execute one command: the shell itself such like to following code:

package main

import (
	"os/exec"
)

func main() {
	exec.Command("sh", "-c", "cat log_file.txt | grep error | wc -l").Run()
}

But this procedure has some negative points:

  • you must have installed the shell - in correct version - on the system itself
    • so you are dependent on the shell
  • you have no control over the individual commands - only the parent process (shell command itself)
  • pipelining can be complex (redirection of stderr etc.) - so you have to know the pipeline syntax
    • maybe this syntax is different for shell versions

(advanced) features

input injections

Multiple different input stream for each command can be configured. This can be useful if you want to forward multiple input sources to one command.

package main

import (
	"github.com/rainu/go-command-chain"
	"strings"
)

func main() {
	inputContent1 := strings.NewReader("content from application itself\n")
	inputContent2 := strings.NewReader("another content from application itself\n")

	err := cmdchain.Builder().
		Join("echo", "test").WithInjections(inputContent1, inputContent2).
		Join("grep", "test").
		Join("wc", "-l").
		Finalize().Run()

	if err != nil {
		panic(err)
	}
}
forking of stdout and stderr

Stdout and stderr of each command can be forked to different io.Writer.

package main

import (
	"bytes"
	"github.com/rainu/go-command-chain"
)

func main() {
	echoErr := &bytes.Buffer{}
	echoOut := &bytes.Buffer{}
	grepErr := &bytes.Buffer{}
	
	err := cmdchain.Builder().
		Join("echo", "test").WithOutputForks(echoOut).WithErrorForks(echoErr).
		Join("grep", "test").WithErrorForks(grepErr).
		Join("wc", "-l").
		Finalize().Run()

	if err != nil {
		panic(err)
	}
}

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ChainBuilder

type ChainBuilder interface {
	// Join create a new command by the given name and the given arguments. This command then will join
	// the chain. If there is a command which joined before, their stdout/stderr will redirected to this
	// command in stdin (depending of its configuration). After calling Join the command can be more
	// configured. After calling another Join this command can not be configured again. Instead the
	// configuration of the next command will begin.
	Join(name string, args ...string) CommandBuilder

	// JoinCmd takes the given command and join them to the chain. If there is a command which joined
	// before, their stdout/stderr will redirected to this command in stdin (depending of its configuration).
	// Therefore the input (stdin) and output (stdout/stderr) will be manipulated by the chain building process.
	// The streams must not be configured outside the chain builder. Otherwise the chain building process will
	// be failed after Run will be called. After calling JoinCmd the command can be more configured. After
	// calling another Join this command can not be configured again. Instead the configuration of the
	// next command will begin.
	JoinCmd(cmd *exec.Cmd) CommandBuilder

	// JoinWithContext is like Join but includes a context to the created command. The provided context is used
	// to kill the process (by calling os.Process.Kill) if the context becomes done before the command completes
	// on its own.
	JoinWithContext(ctx context.Context, name string, args ...string) CommandBuilder

	// Finalize will finish the command joining process. After calling this method no command can be joined anymore.
	// Instead final configurations can be made and the chain is ready to run.
	Finalize() FinalizedBuilder
}

ChainBuilder contains methods for joining new commands to the current cain or finalize them.

type CommandApplier added in v0.2.0

type CommandApplier func(index int, command *exec.Cmd)

CommandApplier is a function which will get the command's index and the command's reference

type CommandBuilder

type CommandBuilder interface {
	ChainBuilder

	// Apply will call the given CommandApplier with the previously joined command. The CommandApplier can do anything
	// with the previously joined command. The CommandApplier will be called directly so the command which the applier
	// will be received has included all changes which made before this function call.
	// ATTENTION: Be aware of the changes the CommandApplier will make. This can clash with the changes the building
	// pipeline will make!
	Apply(CommandApplier) CommandBuilder

	// ApplyBeforeStart will call the given CommandApplier with the previously joined command. The CommandApplier can do
	// anything with the previously joined command. The CommandApplier will be called before the command will be started
	// so the command is almost finished (all streams are configured and so on).
	// ATTENTION: Be aware of the changes the CommandApplier will make. This can clash with the changes the building
	// pipeline will make!
	ApplyBeforeStart(CommandApplier) CommandBuilder

	// ForwardError will configure the previously joined command to redirect all its stderr output to the next
	// command's input. If WithErrorForks is also used, the stderr output of the previously joined command will
	// be redirected to both: stdin of the next command AND the configured fork(s).
	// If ForwardError is not used, the stderr output of the previously joined command will be dropped. But if
	// WithErrorForks is used, the stderr output will be redirected to the configured fork(s).
	ForwardError() CommandBuilder

	// DiscardStdOut will configure the previously joined command to drop all its stdout output. So the stdout does NOT
	// redirect to the next command's stdin. If WithOutputForks is also used, the output of the previously joined
	// command will be redirected to this fork(s). It will cause an invalid stream configuration error if the stderr is
	// also discarded (which is the default case)! So it should be used in combination of ForwardError.
	DiscardStdOut() CommandBuilder

	// WithOutputForks will configure the previously joined command to redirect their stdout output to the configured
	// target(s). The configured writer will be written in parallel so streaming is possible. If the previously
	// joined command is also configured to redirect its stdout to the next command's input, the stdout output will
	// redirected to both: stdin of the next command AND the configured fork(s).
	// ATTENTION: If one of the given writer will be closed before the command ends the command will be exited. This is
	// because of the this method uses the io.MultiWriter. And it will close the writer if on of them is closed.
	WithOutputForks(targets ...io.Writer) CommandBuilder

	// WithAdditionalOutputForks is similar to WithOutputForks except that the given targets will be added to the
	// command and not be overwritten.
	WithAdditionalOutputForks(targets ...io.Writer) CommandBuilder

	// WithErrorForks will configure the previously joined command to redirect their stderr output to the configured
	// target(s). The configured writer will be written in parallel so streaming is possible. If the previously
	// joined command is also configured to redirect its stderr to the next command's input, the stderr output will
	// redirected to both: stdin of the next command AND the configured fork(s).
	// ATTENTION: If one of the given writer will be closed before the command ends the command will be exited. This is
	// because of the this method uses the io.MultiWriter. And it will close the writer if on of them is closed.
	WithErrorForks(targets ...io.Writer) CommandBuilder

	// WithAdditionalErrorForks is similar to WithErrorForks except that the given targets will be added to the
	// command and not be overwritten.
	WithAdditionalErrorForks(targets ...io.Writer) CommandBuilder

	// WithInjections will configure the previously joined command to read from the given sources AND the predecessor
	// command's stdout or stderr (depending on the configuration). This streams (stdout/stderr of predecessor command
	// and the given sources) will read in parallel (not sequential!). So be aware of concurrency issues.
	// If this behavior is not wanted, me the io.MultiReader is a better choice.
	WithInjections(sources ...io.Reader) CommandBuilder

	// WithEnvironment will configure the previously joined command to use the given environment variables. Key-value
	// pair(s) must be passed as arguments. Where the first represents the key and the second the value of the
	// environment variable.
	WithEnvironment(envMap ...interface{}) CommandBuilder

	// WithEnvironmentMap will configure the previously joined command to use the given environment variables.
	WithEnvironmentMap(envMap map[interface{}]interface{}) CommandBuilder

	// WithAdditionalEnvironment will do almost the same thing as WithEnvironment expecting that the given key-value
	// pairs will be joined with the environment variables of the current process.
	WithAdditionalEnvironment(envMap ...interface{}) CommandBuilder

	// WithAdditionalEnvironmentMap will do almost the same thing as WithEnvironmentMap expecting that the given
	// values will be joined with the environment variables of the current process.
	WithAdditionalEnvironmentMap(envMap map[interface{}]interface{}) CommandBuilder

	// WithWorkingDirectory will configure the previously joined command to use the specifies the working directory.
	// Without setting the working directory, the calling process's current directory will be used.
	WithWorkingDirectory(workingDir string) CommandBuilder

	// WithErrorChecker will configure the previously joined command to use the given error checker. In some cases
	// the commands will return a non-zero exit code, which will normally cause an error at the FinalizedBuilder.Run().
	// To avoid that you can use a ErrorChecker to ignore these kind of errors. There exists a set of functions which
	// create a such ErrorChecker: IgnoreExitCode, IgnoreExitErrors, IgnoreAll, IgnoreNothing
	WithErrorChecker(ErrorChecker) CommandBuilder
}

CommandBuilder contains methods for configuring the previous joined command.

type ErrorChecker added in v0.2.0

type ErrorChecker func(index int, command *exec.Cmd, err error) bool

ErrorChecker is a function which will receive the command's error. His purposes is to check if the given error can be ignored. If the function return true the given error is a "real" error and will NOT be ignored!

func IgnoreAll added in v0.2.0

func IgnoreAll() ErrorChecker

IgnoreAll will return an ErrorChecker. This will ignore all error.

func IgnoreExitCode added in v0.2.0

func IgnoreExitCode(allowedCodes ...int) ErrorChecker

IgnoreExitCode will return an ErrorChecker. This will ignore all exec.ExitError which have any of the given exit codes.

func IgnoreExitErrors added in v0.2.0

func IgnoreExitErrors() ErrorChecker

IgnoreExitErrors will return an ErrorChecker. This will ignore all exec.ExitError.

func IgnoreNothing added in v0.3.0

func IgnoreNothing() ErrorChecker

IgnoreNothing will return an ErrorChecker. This will ignore no error.

type FinalizedBuilder

type FinalizedBuilder interface {

	// WithOutput configures the stdout stream(s) for the last command in the chain. If there is more than one target
	// given io.MultiWriter will be used as command's stdout. So in that case if there was one of the given targets
	// closed before the chain normally ends, the chain will be exited. This is because of the behavior of the
	// io.MultiWriter.
	WithOutput(targets ...io.Writer) FinalizedBuilder

	// WithAdditionalOutput is similar to WithOutput except that the given targets will be added to the
	// command and not be overwritten.
	WithAdditionalOutput(targets ...io.Writer) FinalizedBuilder

	// WithError configures the stderr stream(s) for the last command in the chain. If there is more than one target
	// given io.MultiWriter will be used as command's stdout. So in that case if there was one of the given targets
	// closed before the chain normally ends, the chain will be exited. This is because of the behavior of the
	// io.MultiWriter.
	WithError(targets ...io.Writer) FinalizedBuilder

	// WithAdditionalError is similar to WithError except that the given targets will be added to the
	// command and not be overwritten.
	WithAdditionalError(targets ...io.Writer) FinalizedBuilder

	// WithGlobalErrorChecker will configure the complete chain to use the given error checker. If there is an error
	// checker configured for a special command, this error checker will be skipped for these one. In some cases
	// the commands will return a non-zero exit code, which will normally cause an error at the Run().
	// To avoid that you can use a ErrorChecker to ignore these kind of errors. There exists a set of functions which
	// create a such ErrorChecker: IgnoreExitCode, IgnoreExitErrors, IgnoreAll, IgnoreNothing
	WithGlobalErrorChecker(ErrorChecker) FinalizedBuilder

	// Run will execute the command chain. It will start all underlying commands and wait after completion of all of
	// them. If the building of the chain was failed, an error will returned before the commands are started! In that
	// case an MultipleErrors will be returned. If any command starting failed, the run will the error (single) of
	// starting. All previously started commands should be exited in that case. Following commands will not be started.
	// If any error occurs while commands are running, a MultipleErrors will return within all errors per
	// command.
	Run() error

	// RunAndGet works like Run in addition the function will return the stdout and stderr of the command chain. Be
	// careful with this convenience function because the stdout and stderr will be stored in memory!
	RunAndGet() (string, string, error)

	// String returns a string representation of the command chain.
	String() string
}

FinalizedBuilder contains methods for configuration the the finalized chain. At this step the chain can be running.

type FirstCommandBuilder

type FirstCommandBuilder interface {
	ChainBuilder

	// WithInput configures the input stream(s) for the first command in the chain. If multiple streams are
	// configured, this streams will read in parallel (not sequential!). So be aware of concurrency issues.
	// If this behavior is not wanted, me the io.MultiReader is a better choice.
	WithInput(sources ...io.Reader) ChainBuilder
}

FirstCommandBuilder contains methods for building the chain. Especially it contains configuration which can be made only for the first command in the chain.

func Builder

func Builder() FirstCommandBuilder

Builder creates a new command chain builder. This build flow will configure the commands more or less instantaneously. If any error occurs while building the chain you will receive them when you finally call Run of this chain.

Example
output := &bytes.Buffer{}

//it's the same as in shell: ls -l | grep README | wc -l
err := cmdchain.Builder().
	Join("ls", "-l").
	Join("grep", "README").
	Join("wc", "-l").
	Finalize().
	WithOutput(output).
	Run()

if err != nil {
	panic(err)
}
println(output.String())
Output:

Example (DiscardStdOut)
//this will drop the stdout from echo .. so grep will receive no input
//Attention: it must be used in combination with ForwardError - otherwise
//it will cause a invalid stream configuration error!
err := cmdchain.Builder().
	Join("echo", "test").DiscardStdOut().ForwardError().
	Join("grep", "test").
	Join("wc", "-l").
	Finalize().Run()

if err != nil {
	panic(err)
}
Output:

Example (Finalize)
//it's the same as in shell: ls -l | grep README
err := cmdchain.Builder().
	Join("ls", "-l").
	Join("grep", "README").
	Finalize().Run()

if err != nil {
	panic(err)
}
Output:

Example (ForwardError)
//it's the same as in shell: echoErr "test" |& grep test
err := cmdchain.Builder().
	Join("echoErr", "test").ForwardError().
	Join("grep", "test").
	Join("wc", "-l").
	Finalize().Run()

if err != nil {
	panic(err)
}
Output:

Example (Join)
//it's the same as in shell: ls -l | grep README
err := cmdchain.Builder().
	Join("ls", "-l").
	Join("grep", "README").
	Finalize().Run()

if err != nil {
	panic(err)
}
Output:

Example (JoinCmd)
//it's the same as in shell: ls -l | grep README
grepCmd := exec.Command("grep", "README")

//do NOT manipulate the command's streams!

err := cmdchain.Builder().
	Join("ls", "-l").
	JoinCmd(grepCmd).
	Finalize().Run()

if err != nil {
	panic(err)
}
Output:

Example (JoinWithContext)
//the "ls" command will be killed after 1 second
ctx, cancelFn := context.WithTimeout(context.Background(), 1*time.Second)
defer cancelFn()

//it's the same as in shell: ls -l | grep README
err := cmdchain.Builder().
	JoinWithContext(ctx, "ls", "-l").
	Join("grep", "README").
	Finalize().Run()

if err != nil {
	panic(err)
}
Output:

Example (Run)
output := &bytes.Buffer{}

//it's the same as in shell: ls -l | grep README | wc -l
err := cmdchain.Builder().
	Join("ls", "-l").
	Join("grep", "README").
	Join("wc", "-l").
	Finalize().
	WithOutput(output).
	Run()

if err != nil {
	panic(err)
}
println(output.String())
Output:

Example (RunAndGet)
//it's the same as in shell: ls -l | grep README | wc -l
sout, serr, err := cmdchain.Builder().
	Join("ls", "-l").
	Join("grep", "README").
	Join("wc", "-l").
	Finalize().
	RunAndGet()

if err != nil {
	panic(err)
}
println("OUTPUT: " + sout)
println("ERROR: " + serr)
Output:

Example (WithAdditionalEnvironment)
//it's the same as in shell: TEST=VALUE TEST2=2 env | grep TEST | wc -l
err := cmdchain.Builder().
	Join("env").WithAdditionalEnvironment("TEST", "VALUE", "TEST2", 2).
	Join("grep", "TEST").
	Finalize().Run()

if err != nil {
	panic(err)
}
Output:

Example (WithError)
//it's the same as in shell: echoErr "test" 2> /tmp/error

target, err := os.OpenFile("/tmp/error", os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
	panic(err)
}

err = cmdchain.Builder().
	Join("echoErr", "test").
	Finalize().WithError(target).Run()

if err != nil {
	panic(err)
}
Output:

Example (WithErrorForks)
//it's the same as in shell: echoErr "test" |& tee <fork> | grep test | wc -l
errorFork := &bytes.Buffer{}

err := cmdchain.Builder().
	Join("echoErr", "test").ForwardError().WithErrorForks(errorFork).
	Join("grep", "test").
	Join("wc", "-l").
	Finalize().Run()

if err != nil {
	panic(err)
}
println(errorFork.String())
Output:

Example (WithInjections)
//it's the same as in shell: echo -e "test\ntest" | grep test | wc -l
inputContent := strings.NewReader("test\n")

err := cmdchain.Builder().
	Join("echoErr", "test").WithInjections(inputContent).
	Join("grep", "test").
	Join("wc", "-l").
	Finalize().Run()

if err != nil {
	panic(err)
}
Output:

Example (WithInput)
inputContent := strings.NewReader("test\n")

//it's the same as in shell: echo "test" | grep test
err := cmdchain.Builder().
	WithInput(inputContent).
	Join("grep", "test").
	Finalize().Run()

if err != nil {
	panic(err)
}
Output:

Example (WithOutput)
//it's the same as in shell: echo "test" | grep test > /tmp/output

target, err := os.OpenFile("/tmp/output", os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
	panic(err)
}

err = cmdchain.Builder().
	Join("echo", "test").
	Join("grep", "test").
	Finalize().WithOutput(target).Run()

if err != nil {
	panic(err)
}
Output:

Example (WithOutputForks)
//it's the same as in shell: echo "test" | tee <fork> | grep test | wc -l
outputFork := &bytes.Buffer{}

err := cmdchain.Builder().
	Join("echo", "test").WithOutputForks(outputFork).
	Join("grep", "test").
	Join("wc", "-l").
	Finalize().Run()

if err != nil {
	panic(err)
}
println(outputFork.String())
Output:

type MultipleErrors

type MultipleErrors struct {
	// contains filtered or unexported fields
}

MultipleErrors fusions multiple errors into one error. All underlying errors can be accessed. Normally the errors are saved by commands sequence. So if the first command in the chain occurs an error, this error will be placed at first in the error list.

func (MultipleErrors) Error

func (e MultipleErrors) Error() string

Error fusions all error messages of the underlying errors and return them.

func (MultipleErrors) Errors

func (e MultipleErrors) Errors() []error

Errors returns the underlying errors.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL