Skip to content

Commit 2458b93

Browse files
authored
Merge pull request #1836 from dearchap/issue_1720
Fix:(issue_1720) Add support for reading args from stdin
2 parents 2b97d2e + 30879e3 commit 2458b93

File tree

4 files changed

+260
-0
lines changed

4 files changed

+260
-0
lines changed

command.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli
22

33
import (
4+
"bufio"
45
"context"
56
"flag"
67
"fmt"
@@ -10,6 +11,7 @@ import (
1011
"reflect"
1112
"sort"
1213
"strings"
14+
"unicode"
1315
)
1416

1517
const (
@@ -125,6 +127,9 @@ type Command struct {
125127
MutuallyExclusiveFlags []MutuallyExclusiveFlags
126128
// Arguments to parse for this command
127129
Arguments []Argument
130+
// Whether to read arguments from stdin
131+
// applicable to root command only
132+
ReadArgsFromStdin bool
128133

129134
// categories contains the categorized commands and is populated on app startup
130135
categories CommandCategories
@@ -340,6 +345,83 @@ func (cmd *Command) ensureHelp() {
340345
}
341346
}
342347

348+
func (cmd *Command) parseArgsFromStdin() ([]string, error) {
349+
type state int
350+
const (
351+
STATE_SEARCH_FOR_TOKEN state = -1
352+
STATE_IN_STRING state = 0
353+
)
354+
355+
st := STATE_SEARCH_FOR_TOKEN
356+
linenum := 1
357+
token := ""
358+
args := []string{}
359+
360+
breader := bufio.NewReader(cmd.Reader)
361+
362+
outer:
363+
for {
364+
ch, _, err := breader.ReadRune()
365+
if err == io.EOF {
366+
switch st {
367+
case STATE_SEARCH_FOR_TOKEN:
368+
if token != "--" {
369+
args = append(args, token)
370+
}
371+
case STATE_IN_STRING:
372+
// make sure string is not empty
373+
for _, t := range token {
374+
if !unicode.IsSpace(t) {
375+
args = append(args, token)
376+
}
377+
}
378+
}
379+
break outer
380+
}
381+
if err != nil {
382+
return nil, err
383+
}
384+
switch st {
385+
case STATE_SEARCH_FOR_TOKEN:
386+
if unicode.IsSpace(ch) || ch == '"' {
387+
if ch == '\n' {
388+
linenum++
389+
}
390+
if token != "" {
391+
// end the processing here
392+
if token == "--" {
393+
break outer
394+
}
395+
args = append(args, token)
396+
token = ""
397+
}
398+
if ch == '"' {
399+
st = STATE_IN_STRING
400+
}
401+
continue
402+
}
403+
token += string(ch)
404+
case STATE_IN_STRING:
405+
if ch != '"' {
406+
token += string(ch)
407+
} else {
408+
if token != "" {
409+
args = append(args, token)
410+
token = ""
411+
}
412+
/*else {
413+
//TODO. Should we pass in empty strings ?
414+
}*/
415+
st = STATE_SEARCH_FOR_TOKEN
416+
}
417+
}
418+
}
419+
420+
tracef("parsed stdin args as %v (cmd=%[2]q)", args, cmd.Name)
421+
422+
return args, nil
423+
}
424+
343425
// Run is the entry point to the command graph. The positional
344426
// arguments are parsed according to the Flag and Command
345427
// definitions and the matching Action functions are run.
@@ -353,6 +435,13 @@ func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) {
353435
}
354436

355437
if cmd.parent == nil {
438+
if cmd.ReadArgsFromStdin {
439+
if args, err := cmd.parseArgsFromStdin(); err != nil {
440+
return err
441+
} else {
442+
osArgs = append(osArgs, args...)
443+
}
444+
}
356445
// handle the completion flag separately from the flagset since
357446
// completion could be attempted after a flag, but before its value was put
358447
// on the command line. this causes the flagset to interpret the completion

command_test.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3862,3 +3862,168 @@ func TestCommand_ParentCommand_Set(t *testing.T) {
38623862
t.Errorf("expect nil. set parent context flag return err: %s", err)
38633863
}
38643864
}
3865+
3866+
func TestCommandReadArgsFromStdIn(t *testing.T) {
3867+
3868+
tests := []struct {
3869+
name string
3870+
input string
3871+
args []string
3872+
expectedInt int64
3873+
expectedFloat float64
3874+
expectedSlice []string
3875+
expectError bool
3876+
}{
3877+
{
3878+
name: "empty",
3879+
input: "",
3880+
args: []string{"foo"},
3881+
expectedInt: 0,
3882+
expectedFloat: 0.0,
3883+
expectedSlice: []string{},
3884+
},
3885+
{
3886+
name: "empty2",
3887+
input: `
3888+
3889+
`,
3890+
args: []string{"foo"},
3891+
expectedInt: 0,
3892+
expectedFloat: 0.0,
3893+
expectedSlice: []string{},
3894+
},
3895+
{
3896+
name: "intflag-from-input",
3897+
input: "--if=100",
3898+
args: []string{"foo"},
3899+
expectedInt: 100,
3900+
expectedFloat: 0.0,
3901+
expectedSlice: []string{},
3902+
},
3903+
{
3904+
name: "intflag-from-input2",
3905+
input: `
3906+
--if
3907+
3908+
100`,
3909+
args: []string{"foo"},
3910+
expectedInt: 100,
3911+
expectedFloat: 0.0,
3912+
expectedSlice: []string{},
3913+
},
3914+
{
3915+
name: "multiflag-from-input",
3916+
input: `
3917+
--if
3918+
3919+
100
3920+
--ff 100.1
3921+
3922+
--ssf hello
3923+
--ssf
3924+
3925+
"hello
3926+
123
3927+
44"
3928+
`,
3929+
args: []string{"foo"},
3930+
expectedInt: 100,
3931+
expectedFloat: 100.1,
3932+
expectedSlice: []string{"hello", "hello\t\n 123\n44"},
3933+
},
3934+
{
3935+
name: "end-args",
3936+
input: `
3937+
--if
3938+
3939+
100
3940+
--
3941+
--ff 100.1
3942+
3943+
--ssf hello
3944+
--ssf
3945+
3946+
hell02
3947+
`,
3948+
args: []string{"foo"},
3949+
expectedInt: 100,
3950+
expectedFloat: 0,
3951+
expectedSlice: []string{},
3952+
},
3953+
{
3954+
name: "invalid string",
3955+
input: `
3956+
"
3957+
`,
3958+
args: []string{"foo"},
3959+
expectedInt: 0,
3960+
expectedFloat: 0,
3961+
expectedSlice: []string{},
3962+
},
3963+
{
3964+
name: "invalid string2",
3965+
input: `
3966+
--if
3967+
"
3968+
`,
3969+
args: []string{"foo"},
3970+
expectError: true,
3971+
},
3972+
{
3973+
name: "incomplete string",
3974+
input: `
3975+
--ssf
3976+
"
3977+
hello
3978+
`,
3979+
args: []string{"foo"},
3980+
expectedSlice: []string{"hello"},
3981+
},
3982+
}
3983+
3984+
for _, tst := range tests {
3985+
t.Run(tst.name, func(t *testing.T) {
3986+
r := require.New(t)
3987+
3988+
fp, err := os.CreateTemp("", "readargs")
3989+
r.NoError(err)
3990+
_, err = fp.Write([]byte(tst.input))
3991+
r.NoError(err)
3992+
fp.Close()
3993+
3994+
cmd := buildMinimalTestCommand()
3995+
cmd.ReadArgsFromStdin = true
3996+
cmd.Reader, err = os.Open(fp.Name())
3997+
r.NoError(err)
3998+
cmd.Flags = []Flag{
3999+
&IntFlag{
4000+
Name: "if",
4001+
},
4002+
&FloatFlag{
4003+
Name: "ff",
4004+
},
4005+
&StringSliceFlag{
4006+
Name: "ssf",
4007+
},
4008+
}
4009+
4010+
actionCalled := false
4011+
cmd.Action = func(ctx context.Context, c *Command) error {
4012+
r.Equal(tst.expectedInt, c.Int("if"))
4013+
r.Equal(tst.expectedFloat, c.Float("ff"))
4014+
r.Equal(tst.expectedSlice, c.StringSlice("ssf"))
4015+
actionCalled = true
4016+
return nil
4017+
}
4018+
4019+
err = cmd.Run(context.Background(), tst.args)
4020+
if !tst.expectError {
4021+
r.NoError(err)
4022+
r.True(actionCalled)
4023+
} else {
4024+
r.Error(err)
4025+
}
4026+
4027+
})
4028+
}
4029+
}

godoc-current.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,9 @@ type Command struct {
394394
MutuallyExclusiveFlags []MutuallyExclusiveFlags
395395
// Arguments to parse for this command
396396
Arguments []Argument
397+
// Whether to read arguments from stdin
398+
// applicable to root command only
399+
ReadArgsFromStdin bool
397400

398401
// Has unexported fields.
399402
}

testdata/godoc-v3.x.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,9 @@ type Command struct {
394394
MutuallyExclusiveFlags []MutuallyExclusiveFlags
395395
// Arguments to parse for this command
396396
Arguments []Argument
397+
// Whether to read arguments from stdin
398+
// applicable to root command only
399+
ReadArgsFromStdin bool
397400

398401
// Has unexported fields.
399402
}

0 commit comments

Comments
 (0)