~shabbyrobe/cmdy

bfe5b670e0e50e094f19478c529e37046b534dc3 — Blake Williams 3 years ago 85b6e63 v0.8.0
cmdyutil StdinOrFile hyphen flag
3 files changed, 115 insertions(+), 11 deletions(-)

M cmdyutil/stdinorfile.go
A cmdyutil/stdinorfile_test.go
M group.go
M cmdyutil/stdinorfile.go => cmdyutil/stdinorfile.go +34 -9
@@ 11,18 11,43 @@ import (
	"github.com/shabbyrobe/cmdy"
)

type StdinFlag int

const (
	HyphenStdin StdinFlag = 1 << iota
)

// OpenStdinOrFile will check if the program's input is a pipe. If so, it will
// return stdin, otherwise it will return an open file.
//
// NOTE: you should probably use '-' as a filename to trigger stdin (see the
// HyphenStdin flag). If considering stdin by default, your commands might
// not work properly in bash while loops:
//
//	# Potentially bad ('find stuff' goes to 'mycmd' stdin):
//	find stuff | while read line; do mycmd; done
//
//	# Less bad:
//	find stuff | while read line; do </dev/null mycmd; done
//
// If you require '-' to trigger stdin, this won't happen by default.
//
// The returned ReadCloser must always be closed.
func OpenStdinOrFile(ctx cmdy.Context, fileName string) (rdr io.ReadCloser, err error) {
func OpenStdinOrFile(ctx cmdy.Context, fileName string, flag StdinFlag) (rdr io.ReadCloser, err error) {
	var input io.Reader
	var hasInput bool

	// FIXME: ReaderIsPipe is untestable when using BufferedRunner:
	input := ctx.Stdin()
	hasInput := false
	if buf, ok := input.(*bytes.Buffer); ok {
		hasInput = buf.Len() > 0
	} else {
		hasInput = cmdy.ReaderIsPipe(input)
	if flag&HyphenStdin == 0 || fileName == "-" {
		if flag&HyphenStdin != 0 {
			fileName = ""
		}
		input = ctx.Stdin()
		if buf, ok := input.(*bytes.Buffer); ok {
			hasInput = buf.Len() > 0
		} else {
			hasInput = cmdy.ReaderIsPipe(input)
		}
	}

	if hasInput && fileName != "" {


@@ 40,8 65,8 @@ func OpenStdinOrFile(ctx cmdy.Context, fileName string) (rdr io.ReadCloser, err 

// ReadStdinOrFile will check if the program's input is a pipe. If so, it will read from
// stdin, otherwise it will read from fileName.
func ReadStdinOrFile(ctx cmdy.Context, fileName string) (bts []byte, err error) {
	rdr, err := OpenStdinOrFile(ctx, fileName)
func ReadStdinOrFile(ctx cmdy.Context, fileName string, flag StdinFlag) (bts []byte, err error) {
	rdr, err := OpenStdinOrFile(ctx, fileName, flag)
	if err != nil {
		return nil, err
	}

A cmdyutil/stdinorfile_test.go => cmdyutil/stdinorfile_test.go +77 -0
@@ 0,0 1,77 @@
package cmdyutil

import (
	"bytes"
	"context"
	"io"
	"io/ioutil"
	"reflect"
	"testing"

	"github.com/shabbyrobe/cmdy"
)

type testContext struct {
	context.Context
	commandRef cmdy.CommandRef
	stdin      io.Reader
	stdout     io.Writer
	stderr     io.Writer
}

func (t *testContext) RawArgs() []string                    { return nil }
func (t *testContext) Stdin() io.Reader                     { return t.stdin }
func (t *testContext) Stdout() io.Writer                    { return t.stdout }
func (t *testContext) Stderr() io.Writer                    { return t.stderr }
func (t *testContext) Runner() *cmdy.Runner                 { return nil }
func (t *testContext) Stack() cmdy.CommandPath              { return nil }
func (t *testContext) Current() cmdy.CommandRef             { return t.commandRef }
func (t *testContext) Push(name string, cmd cmdy.Command)   {}
func (t *testContext) Pop() (name string, cmd cmdy.Command) { return "", nil }

func ctxWithStdin(data []byte) *testContext {
	var stdin bytes.Buffer
	stdin.Write(data)
	var ctx = testContext{stdin: &stdin}
	return &ctx
}

func assertFileContents(t *testing.T, f io.Reader, exp []byte) {
	t.Helper()
	v, err := ioutil.ReadAll(f)
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(v, exp) {
		t.Fatal()
	}
}

func mustOpenStdinOrFile(t *testing.T, ctx cmdy.Context, fname string, flag StdinFlag) io.ReadCloser {
	t.Helper()
	f, err := OpenStdinOrFile(ctx, fname, flag)
	if err != nil {
		t.Fatal(err)
	}
	return f
}

func TestOpenStdinOrFile(t *testing.T) {
	ctx := ctxWithStdin([]byte("data"))
	f := mustOpenStdinOrFile(t, ctx, "", 0)
	assertFileContents(t, f, []byte("data"))
}

func TestOpenStdinOrFileWithHyphenNameTriesToUseFileWhenFlagNotSet(t *testing.T) {
	ctx := ctxWithStdin([]byte("data"))
	_, err := OpenStdinOrFile(ctx, "-", 0)
	if err != errStdinOrFileBoth {
		t.Fatal()
	}
}

func TestOpenStdinOrFileWithHyphenNameUsesInputWhenFlagSet(t *testing.T) {
	ctx := ctxWithStdin([]byte("data"))
	f := mustOpenStdinOrFile(t, ctx, "-", HyphenStdin)
	assertFileContents(t, f, []byte("data"))
}

M group.go => group.go +4 -2
@@ 184,8 184,10 @@ func (grp *Group) BuildHelp(into *strings.Builder) error {
	}
	sort.Strings(names)

	// +4 == command name, +2 == space between command name and synopsis
	indent := make([]byte, width+4+2)
	const cmdNameIndent = 4
	const cmdSynopsisGap = 2

	indent := make([]byte, width+cmdNameIndent+cmdSynopsisGap)
	for i := 0; i < len(indent); i++ {
		indent[i] = ' '
	}