Skip to content

Commit

Permalink
Merge pull request urfave#1836 from dearchap/issue_1720
Browse files Browse the repository at this point in the history
Fix:(issue_1720) Add support for reading args from stdin
  • Loading branch information
dearchap authored Dec 9, 2023
2 parents 2b97d2e + 30879e3 commit 2458b93
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 0 deletions.
89 changes: 89 additions & 0 deletions command.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"bufio"
"context"
"flag"
"fmt"
Expand All @@ -10,6 +11,7 @@ import (
"reflect"
"sort"
"strings"
"unicode"
)

const (
Expand Down Expand Up @@ -125,6 +127,9 @@ type Command struct {
MutuallyExclusiveFlags []MutuallyExclusiveFlags
// Arguments to parse for this command
Arguments []Argument
// Whether to read arguments from stdin
// applicable to root command only
ReadArgsFromStdin bool

// categories contains the categorized commands and is populated on app startup
categories CommandCategories
Expand Down Expand Up @@ -340,6 +345,83 @@ func (cmd *Command) ensureHelp() {
}
}

func (cmd *Command) parseArgsFromStdin() ([]string, error) {
type state int
const (
STATE_SEARCH_FOR_TOKEN state = -1
STATE_IN_STRING state = 0
)

st := STATE_SEARCH_FOR_TOKEN
linenum := 1
token := ""
args := []string{}

breader := bufio.NewReader(cmd.Reader)

outer:
for {
ch, _, err := breader.ReadRune()
if err == io.EOF {
switch st {
case STATE_SEARCH_FOR_TOKEN:
if token != "--" {
args = append(args, token)
}
case STATE_IN_STRING:
// make sure string is not empty
for _, t := range token {
if !unicode.IsSpace(t) {
args = append(args, token)
}
}
}
break outer
}
if err != nil {
return nil, err
}
switch st {
case STATE_SEARCH_FOR_TOKEN:
if unicode.IsSpace(ch) || ch == '"' {
if ch == '\n' {
linenum++
}
if token != "" {
// end the processing here
if token == "--" {
break outer
}
args = append(args, token)
token = ""
}
if ch == '"' {
st = STATE_IN_STRING
}
continue
}
token += string(ch)
case STATE_IN_STRING:
if ch != '"' {
token += string(ch)
} else {
if token != "" {
args = append(args, token)
token = ""
}
/*else {
//TODO. Should we pass in empty strings ?
}*/
st = STATE_SEARCH_FOR_TOKEN
}
}
}

tracef("parsed stdin args as %v (cmd=%[2]q)", args, cmd.Name)

return args, nil
}

// Run is the entry point to the command graph. The positional
// arguments are parsed according to the Flag and Command
// definitions and the matching Action functions are run.
Expand All @@ -353,6 +435,13 @@ func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) {
}

if cmd.parent == nil {
if cmd.ReadArgsFromStdin {
if args, err := cmd.parseArgsFromStdin(); err != nil {
return err
} else {
osArgs = append(osArgs, args...)
}
}
// handle the completion flag separately from the flagset since
// completion could be attempted after a flag, but before its value was put
// on the command line. this causes the flagset to interpret the completion
Expand Down
165 changes: 165 additions & 0 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3862,3 +3862,168 @@ func TestCommand_ParentCommand_Set(t *testing.T) {
t.Errorf("expect nil. set parent context flag return err: %s", err)
}
}

func TestCommandReadArgsFromStdIn(t *testing.T) {

tests := []struct {
name string
input string
args []string
expectedInt int64
expectedFloat float64
expectedSlice []string
expectError bool
}{
{
name: "empty",
input: "",
args: []string{"foo"},
expectedInt: 0,
expectedFloat: 0.0,
expectedSlice: []string{},
},
{
name: "empty2",
input: `
`,
args: []string{"foo"},
expectedInt: 0,
expectedFloat: 0.0,
expectedSlice: []string{},
},
{
name: "intflag-from-input",
input: "--if=100",
args: []string{"foo"},
expectedInt: 100,
expectedFloat: 0.0,
expectedSlice: []string{},
},
{
name: "intflag-from-input2",
input: `
--if
100`,
args: []string{"foo"},
expectedInt: 100,
expectedFloat: 0.0,
expectedSlice: []string{},
},
{
name: "multiflag-from-input",
input: `
--if
100
--ff 100.1
--ssf hello
--ssf
"hello
123
44"
`,
args: []string{"foo"},
expectedInt: 100,
expectedFloat: 100.1,
expectedSlice: []string{"hello", "hello\t\n 123\n44"},
},
{
name: "end-args",
input: `
--if
100
--
--ff 100.1
--ssf hello
--ssf
hell02
`,
args: []string{"foo"},
expectedInt: 100,
expectedFloat: 0,
expectedSlice: []string{},
},
{
name: "invalid string",
input: `
"
`,
args: []string{"foo"},
expectedInt: 0,
expectedFloat: 0,
expectedSlice: []string{},
},
{
name: "invalid string2",
input: `
--if
"
`,
args: []string{"foo"},
expectError: true,
},
{
name: "incomplete string",
input: `
--ssf
"
hello
`,
args: []string{"foo"},
expectedSlice: []string{"hello"},
},
}

for _, tst := range tests {
t.Run(tst.name, func(t *testing.T) {
r := require.New(t)

fp, err := os.CreateTemp("", "readargs")
r.NoError(err)
_, err = fp.Write([]byte(tst.input))
r.NoError(err)
fp.Close()

cmd := buildMinimalTestCommand()
cmd.ReadArgsFromStdin = true
cmd.Reader, err = os.Open(fp.Name())
r.NoError(err)
cmd.Flags = []Flag{
&IntFlag{
Name: "if",
},
&FloatFlag{
Name: "ff",
},
&StringSliceFlag{
Name: "ssf",
},
}

actionCalled := false
cmd.Action = func(ctx context.Context, c *Command) error {
r.Equal(tst.expectedInt, c.Int("if"))
r.Equal(tst.expectedFloat, c.Float("ff"))
r.Equal(tst.expectedSlice, c.StringSlice("ssf"))
actionCalled = true
return nil
}

err = cmd.Run(context.Background(), tst.args)
if !tst.expectError {
r.NoError(err)
r.True(actionCalled)
} else {
r.Error(err)
}

})
}
}
3 changes: 3 additions & 0 deletions godoc-current.txt
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,9 @@ type Command struct {
MutuallyExclusiveFlags []MutuallyExclusiveFlags
// Arguments to parse for this command
Arguments []Argument
// Whether to read arguments from stdin
// applicable to root command only
ReadArgsFromStdin bool

// Has unexported fields.
}
Expand Down
3 changes: 3 additions & 0 deletions testdata/godoc-v3.x.txt
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,9 @@ type Command struct {
MutuallyExclusiveFlags []MutuallyExclusiveFlags
// Arguments to parse for this command
Arguments []Argument
// Whether to read arguments from stdin
// applicable to root command only
ReadArgsFromStdin bool

// Has unexported fields.
}
Expand Down

0 comments on commit 2458b93

Please sign in to comment.