I’m a big fan of using processes in programs to implement functionality. There are often times existing programs that do the intended behavior and do it well. Unfortunately, integrating command line applications into other programs can be painful. There are times you want to expose output from the underlying commands, or parse it for specific output. It can be difficult to build commands as well in a way that is consistent. None of these problems are overwhelmingly difficult to overcome, but at the same time, it can take a good deal of work when you just want to execute a command. The goal of procs is to help, and hopefully, make it a little easier to work with processes in general.

First off, there have been times where I wanted a consistent way to build commands. I usually would end up wrapping how I call the commands in some object that handles the actual calls and there is always some command builder step that puts together the pieces to create the command. In procs, that is accomplished with a Builder . Here is an example using the knife command from Chef. This is a good example because options always have to happen after the arguments and you often want a consistent set of arguments for every call.

b := procs.Builder{

Templates: []string{

knifeCmd, "$model $action",

"$flags $globalFlags",

},

Context: map[string]string{

"globalFlags": "-Fj",

},

} syncEnvCmd := b.CommandContext(map[string]string{

"model": "environment",

"action": "from file environments/stage.json",

} p := procs.NewProcess(syncEnvCmd)

The context in this case acts like a set of environment variable that can be used to expand variables in the set of templates. The templates are joined together into a single string. The reasoning for the list of templates is to ensure that it is easy to build up the list of elements, sharing values through variables. For example, you can see I set a knifeCmd variable that might be computed to use the full path to the executable.

It should also be noted that the procs.NewProcess function accepts a string and not a list of strings. The string is lexically as a shell command using the go-shlex library. Once it has been parsed, the resulting set of strings is analyzed for any pipes. Procs will then do the needful to connect the stdin and stdout pipes. You can see the resulting commands by creating a new procs.Process and looking at the Cmds attribute.

p := procs.NewProcess("echo 'foo' | grep foo | tr f b")

for _, c := range p.Cmds {

fmt.Println(c.Args)

}

Another use case I often have when calling commands in my applications is reporting output regarding the command to the user, in addition to evaluating the result. This process of creating the necessary pipes and line reading capabilities that also hooks things into whatever output my program is using always ends up being a pain in the neck. In procs, we have the OutHandler pattern to make this much easier.

In the proc repo, I created a foreman like implementation to allow spawning a set of processes using a JSON object with key/value pairs like a Procfile. The output will use the name and a “|” before outputting the line to stdout. Here is the main.go file to show how it works.

The prefixHandler returns a OutHandler function which is just a function that accepts a string (a line of output) and returns a string. It prints the prefixed line to stdout and then returns an empty string, signifying that we don’t want to capture any output in the output buffer. The same functionality is available for stderr as well.

Lastly, there is a procs.Manager type that allows for simple management of a set of proc.Process objects. This provides a basic primitive for starting sidecar applications and building a service using multiple small processes, while managing them as a whole or individually.

There are probably corner cases that I’m missing, so if you try it out and find something please let me know. The same goes for features that would be helpful. For example, I can imagine it would be helpful having better support for status codes, sending signals, managing daemons, etc.

If you try it out and have any feedback please feel free to submit an issue or open a pull request!