1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-11-26 09:00:57 +02:00

Removed github.com/ionrock/procs for better code coverage

This commit is contained in:
mjarkk 2018-10-27 16:57:34 +02:00
parent 9a99748d3b
commit ed564adb4a
19 changed files with 3 additions and 1304 deletions

View File

@ -5,10 +5,10 @@ package commands
import (
"bufio"
"errors"
"os"
"os/exec"
"regexp"
"github.com/ionrock/procs"
"github.com/kr/pty"
"github.com/mgutz/str"
)
@ -22,10 +22,8 @@ func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(s
splitCmd := str.ToArgv(command)
cmd := exec.Command(splitCmd[0], splitCmd[1:]...)
cmd.Env = procs.Env(map[string]string{
"LANG": "en_US.utf8",
"LC_ALL": "en_US.UTF-8",
}, true)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "LANG=en_US.utf8", "LC_ALL=en_US.UTF-8")
tty, err := pty.Start(cmd)

View File

@ -1,30 +0,0 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
[[constraint]]
branch = "master"
name = "github.com/apoydence/onpar"
[[constraint]]
branch = "master"
name = "github.com/flynn/go-shlex"

View File

@ -1,13 +0,0 @@
Copyright 2017 Eric Larson <eric@ionrock.org>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,32 +0,0 @@
SOURCEDIR=.
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
test: dep
dep ensure
go test .
race: dep
dep ensure
go test -race .
dep:
ifeq (, $(shell which dep))
go get -u github.com/golang/dep/cmd/dep
endif
all: prelog cmdtmpl procmon
prelog: $(SOURCES)
go build ./cmd/prelog
cmdtmpl: $(SOURCES)
go build ./cmd/cmdtmpl
procmon: $(SOURCES)
go build ./cmd/procmon
clean:
rm -f prelog
rm -f cmdtmpl
rm -f procmon

View File

@ -1,168 +0,0 @@
# Procs
[![](https://travis-ci.org/ionrock/procs.svg?branch=master)](https://travis-ci.org/ionrock/procs)
[![Go Report Card](https://goreportcard.com/badge/github.com/ionrock/procs)](https://goreportcard.com/report/github.com/ionrock/procs)
[![GoDoc](https://godoc.org/github.com/ionrock/procs?status.svg)](https://godoc.org/github.com/ionrock/procs)
Procs is a library to make working with command line applications a
little nicer.
The primary use case is when you have to use a command line client in
place of an API. Often times you want to do things like output stdout
within your own logs or ensure that every time the command is called,
there are a standard set of flags that are used.
## Basic Usage
The majority of this functionality is intended to be included the
procs.Process.
### Defining a Command
A command can be defined by a string rather than a []string. Normally,
this also implies that the library will run the command in a shell,
exposing a potential man in the middle attack. Rather than using a
shell, procs [lexically
parses](https://github.com/flynn-archive/go-shlex) the command for the
different arguments. It also allows for pipes in order to string
commands together.
```go
p := procs.NewProcess("kubectl get events | grep dev")
```
You can also define a new `Process` by passing in predefined commands.
```go
cmds := []*exec.Cmd{
exec.Command("kubectl", "get", "events"),
exec.Command("grep", "dev"),
}
p := procs.Process{Cmds: cmds}
```
### Output Handling
One use case that is cumbersome is using the piped output from a
command. For example, lets say we wanted to start a couple commands
and have each command have its own prefix in stdout, while still
capturing the output of the command as-is.
```go
p := procs.NewProcess("cmd1")
p.OutputHandler = func(line string) string {
fmt.Printf("cmd1 | %s\n")
return line
}
out, _ := p.Run()
fmt.Println(out)
```
Whatever is returned from the `OutputHandler` will be in the buffered
output. In this way you can choose to filter or skip output buffering
completely.
You can also define a `ErrHandler` using the same signature to get the
same filtering for stderr.
### Environment Variables
Rather than use the `exec.Cmd` `[]string` environment variables, a
`procs.Process` uses a `map[string]string` for environment variables.
```go
p := procs.NewProcess("echo $FOO")
p.Env = map[string]string{"FOO": "foo"}
```
Also, environment variables defined by the `Process.Env` can be
expanded automatically using the `os.Expand` semantics and the
provided environment.
There is a `ParseEnv` function that can help to merge the parent
processes' environment with any new values.
```go
env := ParseEnv(os.Environ())
env["USER"] = "foo"
```
Finally, if you are building commands manually, the `Env` function can
take a `map[string]string` and convert it to a `[]string` for use with
an `exec.Cmd`. The `Env` function also accepts a `useEnv` bool to help
include the parent process environment.
```go
cmd := exec.Command("knife", "cookbook", "show", cb)
cmd.Env = Env(map[string]string{"USER": "knife-user"}, true)
```
## Example Applications
Take a look in the [`cmd`](./cmd/) dir for some simple applications
that use the library. You can also `make all` to build them. The
examples below assume you've built them locally.
### Prelog
The `prelog` command allows running a command and prefixing the output
with a value.
```bash
$ ./prelog -prefix foo -- echo 'hello world!'
Running the command
foo | hello world!
Accessing the output without a prefix.
hello world!
Running the command with Start / Wait
foo | hello world!
```
### Cmdtmpl
The `cmdtmpl` command uses the `procs.Builder` to create a command
based on some paramters. It will take a `data.yml` file and
`template.yml` file to create a command.
```bash
$ cat example/data.json
{
"source": "https://my.example.org",
"user": "foo",
"model": "widget",
"action": "create",
"args": "-f new -i improved"
}
$ cat example/template.json
[
"mysvc ${model} ${action} ${args}",
"--endpoint ${source}",
"--username ${user}"
]
$ ./cmdtmpl -data example/data.json -template example/template.json
Command: mysvc foo widget create -f new -i imporoved --endpoint https://my.example.org --username foo
$ ./cmdtmpl -data example/data.json -template example/template.json -field user=bar
Command: mysvc foo widget create -f new -i imporoved --endpoint https://my.example.org --username bar
```
### Procmon
The `procmon` command acts like
[foreman](https://github.com/ddollar/foreman) with the difference
being it uses a JSON file with key value pairs instead of a
Procfile. This example uses the `procs.Manager` to manage a set of
`procs.Processes`.
```bash
$ cat example/procfile.json
{
"web": "python -m SimpleHTTPServer"
}
$ ./procmon -procfile example/procfile.json
web | Starting web with python -m SimpleHTTPServer
```
You can then access http://localhost:8000 to see the logs. You can
also kill the child process and see `procmon` recognizing it has
exited and exit itself.

View File

@ -1,58 +0,0 @@
package procs
import (
"os"
"strings"
)
// Builder helps construct commands using templates.
type Builder struct {
Context map[string]string
Templates []string
}
func (b *Builder) getConfig(ctx map[string]string) func(string) string {
return func(key string) string {
if v, ok := ctx[key]; ok {
return v
}
return ""
}
}
func (b *Builder) expand(v string, ctx map[string]string) string {
return os.Expand(v, b.getConfig(ctx))
}
// Command returns the result of the templates as a single string.
func (b *Builder) Command() string {
parts := []string{}
for _, t := range b.Templates {
parts = append(parts, b.expand(t, b.Context))
}
return strings.Join(parts, " ")
}
// CommandContext returns the result of the templates as a single
// string, but allows providing an environment context as a
// map[string]string for expansions.
func (b *Builder) CommandContext(ctx map[string]string) string {
// Build our environment context by starting with our Builder
// context and overlay the passed in context map.
env := make(map[string]string)
for k, v := range b.Context {
env[k] = b.expand(v, b.Context)
}
for k, v := range ctx {
env[k] = b.expand(v, env)
}
parts := []string{}
for _, t := range b.Templates {
parts = append(parts, b.expand(t, env))
}
return strings.Join(parts, " ")
}

View File

@ -1,58 +0,0 @@
package procs_test
import (
"testing"
"github.com/ionrock/procs"
)
func TestCommandContext(t *testing.T) {
b := &procs.Builder{
Context: map[string]string{
"options": "-Fj -s https://example.com/chef -k knife.pem",
},
Templates: []string{
"knife",
"${model} ${action}",
"${args}",
"${options}",
},
}
cmd := b.CommandContext(map[string]string{
"model": "data bag",
"action": "from file",
"args": "foo data_bags/foo/bar.json",
})
expected := "knife data bag from file foo data_bags/foo/bar.json -Fj -s https://example.com/chef -k knife.pem"
if cmd != expected {
t.Fatalf("failed building command: %q != %q", cmd, expected)
}
}
func TestCommand(t *testing.T) {
b := &procs.Builder{
Context: map[string]string{
"options": "-Fj -s https://example.com/chef -k knife.pem",
"model": "data bag",
"action": "from file",
"args": "foo data_bags/foo/bar.json",
},
Templates: []string{
"knife",
"${model} ${action}",
"${args}",
"${options}",
},
}
cmd := b.CommandContext(map[string]string{})
expected := "knife data bag from file foo data_bags/foo/bar.json -Fj -s https://example.com/chef -k knife.pem"
if cmd != expected {
t.Fatalf("failed building command: %q != %q", cmd, expected)
}
}

View File

@ -1,48 +0,0 @@
package procs
import (
"fmt"
"os"
"strings"
)
// ParseEnv takes an environment []string and converts it to a map[string]string.
func ParseEnv(environ []string) map[string]string {
env := make(map[string]string)
for _, e := range environ {
pair := strings.SplitN(e, "=", 2)
// There is a chance we can get an env with empty values
if len(pair) == 2 {
env[pair[0]] = pair[1]
}
}
return env
}
// Env takes a map[string]string and converts it to a []string that
// can be used with exec.Cmd. The useEnv boolean flag will include the
// current process environment, overlaying the provided env
// map[string]string.
func Env(env map[string]string, useEnv bool) []string {
envlist := []string{}
// update our env by loading our env and overriding any values in
// the provided env.
if useEnv {
environ := ParseEnv(os.Environ())
for k, v := range env {
environ[k] = v
}
env = environ
}
for key, val := range env {
if key == "" {
continue
}
envlist = append(envlist, fmt.Sprintf("%s=%s", key, val))
}
return envlist
}

View File

@ -1,83 +0,0 @@
package procs_test
import (
"fmt"
"os"
"os/exec"
"strings"
"testing"
"github.com/ionrock/procs"
)
func TestParseEnv(t *testing.T) {
env := []string{
"FOO=bar",
"BAZ=`echo 'hello=world'`",
}
m := procs.ParseEnv(env)
v, ok := m["FOO"]
if !ok {
t.Errorf("error missing FOO from env: %#v", m)
}
if v != "bar" {
t.Errorf("error FOO != bar: %s", v)
}
v, ok = m["BAZ"]
if !ok {
t.Errorf("error missing BAZ from env: %#v", m)
}
expectBaz := "`echo 'hello=world'`"
if v != expectBaz {
t.Errorf("error BAZ != %s: %s", expectBaz, v)
}
}
func TestEnvBuilder(t *testing.T) {
env := procs.Env(map[string]string{
"FOO": "bar",
"BAZ": "hello world",
}, false)
if len(env) != 2 {
t.Errorf("error loading env: %s", env)
}
}
func helperEnvCommand(env map[string]string) *exec.Cmd {
cmd := exec.Command(os.Args[0], "-test.run=TestEnvBuilderOverrides")
cmd.Env = procs.Env(env, false)
return cmd
}
func TestEnvBuilderOverrides(t *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
for _, envvar := range procs.Env(map[string]string{"FOO": "override"}, true) {
fmt.Println(envvar)
}
}
func TestEnvBuilderWithEnv(t *testing.T) {
cmd := helperEnvCommand(map[string]string{
"GO_WANT_HELPER_PROCESS": "1",
"FOO": "default",
})
out, err := cmd.Output()
if err != nil {
t.Fatalf("error running helper: %s", err)
}
env := procs.ParseEnv(strings.Split(string(out), "\n"))
if env["FOO"] != "override" {
t.Errorf("error overriding envvar: %s", string(out))
}
}

View File

@ -1,33 +0,0 @@
package procs_test
import (
"fmt"
"github.com/ionrock/procs"
)
func Example() {
b := procs.Builder{
Context: map[string]string{
"NAME": "eric",
},
Templates: []string{
"echo $NAME |",
"grep $NAME",
},
}
cmd := b.Command()
fmt.Println(cmd)
p := procs.NewProcess(cmd)
p.Run()
out, _ := p.Output()
fmt.Println(string(out))
// Output:
// echo eric | grep eric
// eric
}

View File

@ -1,23 +0,0 @@
package procs_test
import (
"fmt"
"os/exec"
"github.com/ionrock/procs"
)
func Example_predefinedCmds() {
p := procs.Process{
Cmds: []*exec.Cmd{
exec.Command("echo", "foo"),
exec.Command("grep", "foo"),
},
}
p.Run()
out, _ := p.Output()
fmt.Println(string(out))
// Output:
// foo
}

View File

@ -1,43 +0,0 @@
package procs_test
import (
"fmt"
"github.com/ionrock/procs"
)
func ExampleSplitCommand() {
parts := procs.SplitCommand("echo 'hello world'")
for i, p := range parts {
fmt.Printf("%d %s\n", i+1, p)
}
// Output:
// 1 echo
// 2 hello world
}
func ExampleSplitCommandEnv() {
env := map[string]string{
"GREETING": "hello",
"NAME": "world!",
"PASSWORD": "secret",
}
getenv := func(key string) string {
if v, ok := env[key]; ok && key != "PASSWORD" {
return v
}
return ""
}
parts := procs.SplitCommandEnv("echo '$GREETING $NAME $PASSWORD'", getenv)
for i, p := range parts {
fmt.Printf("%d %s\n", i+1, p)
}
// Output:
// 1 echo
// 2 hello world!
}

View File

@ -1,119 +0,0 @@
package procs
import (
"fmt"
"sync"
)
// Manager manages a set of Processes.
type Manager struct {
Processes map[string]*Process
lock sync.Mutex
}
// NewManager creates a new *Manager.
func NewManager() *Manager {
return &Manager{
Processes: make(map[string]*Process),
}
}
// StdoutHandler returns an OutHandler that will ensure the underlying
// process has an empty stdout buffer and logs to stdout a prefixed value
// of "$name | $line".
func (m *Manager) StdoutHandler(name string) OutHandler {
return func(line string) string {
fmt.Printf("%s | %s\n", name, line)
return ""
}
}
// StderrHandler returns an OutHandler that will ensure the underlying
// process has an empty stderr buffer and logs to stdout a prefixed value
// of "$name | $line".
func (m *Manager) StderrHandler(name string) OutHandler {
return func(line string) string {
fmt.Printf("%s | %s\n", name, line)
return ""
}
}
// Start and managed a new process using the default handlers from a
// string.
func (m *Manager) Start(name, cmd string) error {
m.lock.Lock()
defer m.lock.Unlock()
p := NewProcess(cmd)
p.OutputHandler = m.StdoutHandler(name)
p.ErrHandler = m.StderrHandler(name)
err := p.Start()
if err != nil {
return err
}
m.Processes[name] = p
return nil
}
// StartProcess starts and manages a predifined process.
func (m *Manager) StartProcess(name string, p *Process) error {
m.lock.Lock()
defer m.lock.Unlock()
err := p.Start()
if err != nil {
return err
}
m.Processes[name] = p
return nil
}
// Stop will try to stop a managed process. If the process does not
// exist, no error is returned.
func (m *Manager) Stop(name string) error {
p, ok := m.Processes[name]
// We don't mind stopping a process that doesn't exist.
if !ok {
return nil
}
return p.Stop()
}
// Remove will try to stop and remove a managed process.
func (m *Manager) Remove(name string) error {
m.lock.Lock()
defer m.lock.Unlock()
err := m.Stop(name)
if err != nil {
return err
}
// Note that if the stop fails we don't remove it from the map of
// processes to avoid losing the reference.
delete(m.Processes, name)
return nil
}
// Wait will block until all managed processes have finished.
func (m *Manager) Wait() error {
wg := &sync.WaitGroup{}
wg.Add(len(m.Processes))
for _, p := range m.Processes {
go func(proc *Process) {
defer wg.Done()
proc.Wait()
}(p)
}
wg.Wait()
return nil
}

View File

@ -1,55 +0,0 @@
package procs_test
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"testing"
"github.com/ionrock/procs"
)
func TestManagerStartHelper(t *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
port := flag.String("p", "12212", "port to serve on")
directory := flag.String("d", ".", "the directory of static file to host")
flag.Parse()
http.Handle("/", http.FileServer(http.Dir(*directory)))
log.Printf("Serving %s on HTTP port: %s\n", *directory, *port)
log.Fatal(http.ListenAndServe(":"+*port, nil))
os.Exit(0)
}
func TestManagerStart(t *testing.T) {
m := procs.NewManager()
err := m.Start("test", fmt.Sprintf("%s -test.run=TestManagerStartHelper", os.Args[0]))
if err != nil {
t.Errorf("failed to start test process: %s", err)
}
if len(m.Processes) != 1 {
t.Error("failed to add process")
}
err = m.Stop("test")
if err != nil {
t.Errorf("error stopping process: %s", err)
}
err = m.Remove("test")
if err != nil {
t.Errorf("error removing process: %s", err)
}
if len(m.Processes) != 0 {
t.Error("failed to remove processes")
}
}

View File

@ -1,36 +0,0 @@
package procs
import (
"log"
"os"
"strings"
shlex "github.com/flynn/go-shlex"
)
// SplitCommand parses a command and splits it into lexical arguments
// like a shell, returning a []string that can be used as arguments to
// exec.Command.
func SplitCommand(cmd string) []string {
return SplitCommandEnv(cmd, nil)
}
// SplitCommandEnv parses a command and splits it into lexical
// arguments like a shell, returning a []string that can be used as
// arguments to exec.Command. It also allows providing an expansion
// function that will be used when expanding values within the parsed
// arguments.
func SplitCommandEnv(cmd string, getenv func(key string) string) []string {
parts, err := shlex.Split(strings.TrimSpace(cmd))
if err != nil {
log.Fatal(err)
}
if getenv != nil {
for i, p := range parts {
parts[i] = os.Expand(p, getenv)
}
}
return parts
}

View File

@ -1,44 +0,0 @@
package procs_test
import (
"testing"
"github.com/apoydence/onpar"
. "github.com/apoydence/onpar/expect"
. "github.com/apoydence/onpar/matchers"
"github.com/ionrock/procs"
)
func matchSplitCommand(t *testing.T, parts, expected []string) {
for i, part := range parts {
Expect(t, part).To(Equal(expected[i]))
}
}
func TestSplitCommand(t *testing.T) {
o := onpar.New()
defer o.Run(t)
o.Group("split with pipe", func() {
o.BeforeEach(func(t *testing.T) (*testing.T, []string, []string) {
parts := procs.SplitCommand("echo 'foo' | grep o")
expected := []string{"echo", "foo", "|", "grep", "o"}
return t, parts, expected
})
o.Spec("pass with a pipe", matchSplitCommand)
})
o.Group("replace with specific context", func() {
o.BeforeEach(func(t *testing.T) (*testing.T, []string, []string) {
parts := procs.SplitCommandEnv("echo ${FOO}", func(k string) string {
return "bar"
})
expected := []string{"echo", "bar"}
return t, parts, expected
})
o.Spec("expand values found in provided env", matchSplitCommand)
})
}

View File

@ -1,307 +0,0 @@
// Procs is a library to make working with command line applications a
// little nicer.
//
// The goal is to expand on the os/exec package by providing some
// features usually accomplished in a shell, without having to resort to
// a shell. Procs also tries to make working with output simpler by
// providing a simple line handler API over working with io pipes.
//
// Finally, while the hope is that procs provides some convenience, it
// is also a goal to help make it easier to write more secure
// code. For example, avoiding a shell and the ability to manage the
// environment as a map[string]string are both measures that intend to
// make it easier to accomplish things like avoiding outputting
// secrets and opening the door for MITM attacks. With that said, it is
// always important to consider the security implications, especially
// when you are working with untrusted input or sensitive data.
package procs
import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"os"
"os/exec"
"sync"
)
// OutHandler defines the interface for writing output handlers for
// Process objects.
type OutHandler func(string) string
// Process is intended to be used like exec.Cmd where possible.
type Process struct {
// CmdString takes a string and parses it into the relevant cmds
CmdString string
// Cmds is the list of command delmited by pipes.
Cmds []*exec.Cmd
// Env provides a map[string]string that can mutated before
// running a command.
Env map[string]string
// Dir defines the directory the command should run in. The
// Default is the current dir.
Dir string
// OutputHandler can be defined to perform any sort of processing
// on the output. The simple interface is to accept a string (a
// line of output) and return a string that will be included in the
// buffered output and/or output written to stdout.'
//
// For example defining the Process as:
//
// prefix := "myapp"
// p := &procs.Process{
// OutputHandler: func(line string) string {
// return fmt.Sprintf("%s | %s", prefix, line)
// },
// }
//
// This would prefix the stdout lines with a "myapp | ".
//
// By the default, this function is nil and will be skipped, with
// the unchanged line being added to the respective output buffer.
OutputHandler OutHandler
// ErrHandler is a OutputHandler for stderr.
ErrHandler OutHandler
// When no output is given, we'll buffer output in these vars.
errBuffer bytes.Buffer
outBuffer bytes.Buffer
// When a output handler is provided, we ensure we're handling a
// single line at at time.
outputWait *sync.WaitGroup
}
// NewProcess creates a new *Process from a command string.
//
// It is assumed that the user will mutate the resulting *Process by
// setting the necessary attributes.
func NewProcess(command string) *Process {
return &Process{CmdString: command}
}
// internal expand method to use the proc env.
func (p *Process) expand(s string) string {
return os.Expand(s, func(key string) string {
v, _ := p.Env[key]
return v
})
}
// addCmd adds a new command to the list of commands, ensuring the Dir
// and Env have been added to the underlying *exec.Cmd instances.
func (p *Process) addCmd(cmdparts []string) {
var cmd *exec.Cmd
if len(cmdparts) == 1 {
cmd = exec.Command(cmdparts[0])
} else {
cmd = exec.Command(cmdparts[0], cmdparts[1:]...)
}
if p.Dir != "" {
cmd.Dir = p.Dir
}
if p.Env != nil {
env := []string{}
for k, v := range p.Env {
env = append(env, fmt.Sprintf("%s=%s", k, p.expand(v)))
}
cmd.Env = env
}
p.Cmds = append(p.Cmds, cmd)
}
// findCmds parses the CmdString to find the commands that should be
// run by spliting the lexically parsed command by pipes ("|").
func (p *Process) findCmds() {
// Skip if the cmd set is already set. This allows manual creation
// of piped commands.
if len(p.Cmds) > 0 {
return
}
if p.CmdString == "" {
return
}
parts := SplitCommand(p.CmdString)
for i := range parts {
parts[i] = p.expand(parts[i])
}
cmd := []string{}
for _, part := range parts {
if part == "|" {
p.addCmd(cmd)
cmd = []string{}
} else {
cmd = append(cmd, part)
}
}
p.addCmd(cmd)
}
// lineReader takes will read a line in the io.Reader and write to the
// Process output buffer and use any OutputHandler that exists.
func (p *Process) lineReader(wg *sync.WaitGroup, r io.Reader, w *bytes.Buffer, handler OutHandler) {
defer wg.Done()
reader := bufio.NewReader(r)
var buffer bytes.Buffer
for {
buf := make([]byte, 1024)
n, err := reader.Read(buf)
if err != nil {
return
}
buf = buf[:n]
for {
i := bytes.IndexByte(buf, '\n')
if i < 0 {
break
}
buffer.Write(buf[0:i])
outLine := buffer.String()
if handler != nil {
outLine = handler(outLine)
}
w.WriteString(outLine)
buffer.Reset()
buf = buf[i+1:]
}
buffer.Write(buf)
}
}
// checkErr shortens the creation of the pipes by bailing out with a
// log.Fatal.
func checkErr(msg string, err error) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
}
}
func (p *Process) setupPipes() error {
last := len(p.Cmds) - 1
if last != 0 {
for i, cmd := range p.Cmds[:last] {
var err error
p.Cmds[i+1].Stdin, err = cmd.StdoutPipe()
if err != nil {
fmt.Printf("error creating stdout pipe: %s\n", err)
return err
}
cmd.Stderr = &p.errBuffer
}
}
cmd := p.Cmds[last]
stdout, err := cmd.StdoutPipe()
if err != nil {
fmt.Printf("error creating stdout pipe: %s\n", err)
return err
}
stderr, err := cmd.StderrPipe()
if err != nil {
fmt.Printf("error creating stderr pipe: %s\n", err)
return err
}
p.outputWait = new(sync.WaitGroup)
p.outputWait.Add(2)
// These close the stdout/err channels
go p.lineReader(p.outputWait, stdout, &p.outBuffer, p.OutputHandler)
go p.lineReader(p.outputWait, stderr, &p.errBuffer, p.ErrHandler)
return nil
}
// Run executes the cmds and returns the output as a string and any error.
func (p *Process) Run() error {
if err := p.Start(); err != nil {
return err
}
return p.Wait()
}
// Start will start the list of cmds.
func (p *Process) Start() error {
p.findCmds()
p.setupPipes()
for i, cmd := range p.Cmds {
err := cmd.Start()
if err != nil {
defer func() {
for _, precmd := range p.Cmds[0:i] {
precmd.Wait()
}
}()
return err
}
}
return nil
}
// Wait will block, waiting for the commands to finish.
func (p *Process) Wait() error {
if p.outputWait != nil {
p.outputWait.Wait()
}
var err error
for _, cmd := range p.Cmds {
err = cmd.Wait()
}
return err
}
// Stop tries to stop the process.
func (p *Process) Stop() error {
for _, cmd := range p.Cmds {
// ProcessState means it is already exited.
if cmd.ProcessState != nil {
continue
}
err := cmd.Process.Kill()
if err != nil {
return err
}
}
return nil
}
// Output returns the buffered output as []byte.
func (p *Process) Output() ([]byte, error) {
return p.outBuffer.Bytes(), nil
}
// ErrOutput returns the buffered stderr as []byte
func (p *Process) ErrOutput() ([]byte, error) {
return p.errBuffer.Bytes(), nil
}

View File

@ -1,143 +0,0 @@
package procs_test
import (
"bytes"
"fmt"
"os"
"os/exec"
"testing"
"github.com/ionrock/procs"
)
func newProcess() *procs.Process {
return &procs.Process{
Cmds: []*exec.Cmd{
exec.Command("echo", "foo"),
exec.Command("grep", "foo"),
},
}
}
func TestProcess(t *testing.T) {
p := newProcess()
err := p.Run()
if err != nil {
t.Fatalf("error running program: %s", err)
}
out, _ := p.Output()
if !bytes.Equal(bytes.TrimSpace(out), []byte("foo")) {
t.Errorf("wrong output: expected foo but got %s", out)
}
}
func TestProcessWithOutput(t *testing.T) {
p := newProcess()
p.OutputHandler = func(line string) string {
return fmt.Sprintf("x | %s", line)
}
err := p.Run()
if err != nil {
t.Fatalf("error running program: %s", err)
}
expected := []byte("x | foo")
out, _ := p.Output()
if !bytes.Equal(bytes.TrimSpace(out), expected) {
t.Errorf("wrong output: expected %q but got %q", expected, out)
}
}
func TestProcessStartAndWait(t *testing.T) {
p := newProcess()
p.Start()
p.Wait()
out, _ := p.Output()
expected := []byte("foo")
if !bytes.Equal(bytes.TrimSpace(out), expected) {
t.Errorf("wrong output: expected %q but got %q", expected, out)
}
}
func TestProcessStartAndWaitWithOutput(t *testing.T) {
p := newProcess()
p.OutputHandler = func(line string) string {
return fmt.Sprintf("x | %s", line)
}
p.Start()
p.Wait()
out, _ := p.Output()
expected := []byte("x | foo")
if !bytes.Equal(bytes.TrimSpace(out), expected) {
t.Errorf("wrong output: expected %q but got %q", expected, out)
}
}
func TestProcessFromString(t *testing.T) {
p := procs.NewProcess("echo 'foo'")
err := p.Run()
if err != nil {
t.Fatalf("error running program: %s", err)
}
out, _ := p.Output()
if !bytes.Equal(bytes.TrimSpace(out), []byte("foo")) {
t.Errorf("wrong output: expected foo but got %s", out)
}
}
func TestProcessFromStringWithPipe(t *testing.T) {
p := procs.NewProcess("echo 'foo' | grep foo")
err := p.Run()
if err != nil {
t.Fatalf("error running program: %s", err)
}
out, _ := p.Output()
if !bytes.Equal(bytes.TrimSpace(out), []byte("foo")) {
t.Errorf("wrong output: expected foo but got %s", out)
}
}
func TestStderrOutput(t *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
fmt.Fprintln(os.Stdout, "stdout output")
fmt.Fprintln(os.Stderr, "stderr output")
os.Exit(1)
}
func TestProcessPipeWithFailures(t *testing.T) {
// This will run a piped command with a failure part way
// through. We want to be sure we get output on stderr.
p := procs.NewProcess(fmt.Sprintf("echo 'foo' | %s -test.run=TestStderrOutput | grep foo", os.Args[0]))
p.Env = map[string]string{"GO_WANT_HELPER_PROCESS": "1"}
err := p.Run()
if err == nil {
t.Fatal("expected error running program")
}
out, _ := p.Output()
expected := []byte("") // expecting no output b/c the grep foo won't run
if !bytes.Equal(out, expected) {
t.Errorf("wrong stdout output: expected '%s' but got '%s'", expected, out)
}
errOut, _ := p.ErrOutput()
expected = []byte("stderr output")
if !bytes.Equal(bytes.TrimSpace(errOut), expected) {
t.Errorf("wrong stderr output: expected '%s' but got '%s'", expected, out)
}
}

6
vendor/vendor.json vendored
View File

@ -8,12 +8,6 @@
"revision": "3f9db97f856818214da2e1057f8ad84803971cff",
"revisionTime": "2015-05-15T14:53:56Z"
},
{
"checksumSHA1": "AcczqNgc2/sR/+4gklvFe8zs2eE=",
"path": "github.com/ionrock/procs",
"revision": "f53ef5630f1a1fd42c4e528a04cdaa81265cc66d",
"revisionTime": "2018-01-02T00:55:58Z"
},
{
"checksumSHA1": "KU+GT3javo9S9oVEJfqQUKPPUwo=",
"path": "github.com/kr/pty",