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] = ' '
}