Skip to content

Commit

Permalink
Add completions for files and directories
Browse files Browse the repository at this point in the history
  • Loading branch information
bdhess committed Jul 9, 2017
1 parent f021aed commit 6a4a763
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 57 deletions.
83 changes: 80 additions & 3 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,95 +255,122 @@ func TestBashCompletionOptions(t *testing.T) {
return []string{"arg-4-opt-1", "arg-4-opt-2"}
}).String()

four := a.Command("four", "")
four.Arg("arg-5", "").ExistingFileOrDir()
four.Arg("arg-6", "").HintOptions("/usr", "/usr/local").ExistingDir()

five := a.Command("five", "")
five.Flag("flag-5", "").ExistingFile()

cases := []struct {
Args string
ExpectedFlags []string
ExpectedWords []string
}{
{
Args: "--completion-bash",
ExpectedWords: []string{"help", "one", "three", "two"},
ExpectedFlags: []string(nil),
ExpectedWords: []string{"five", "four", "help", "one", "three", "two"},
},
{
Args: "--completion-bash --",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"--flag-0", "--flag-1", "--help"},
},
{
Args: "--completion-bash --fla",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"--flag-0", "--flag-1", "--help"},
},
{
// No options available for flag-0, return to cmd completion
Args: "--completion-bash --flag-0",
ExpectedWords: []string{"help", "one", "three", "two"},
ExpectedFlags: []string(nil),
ExpectedWords: []string{"five", "four", "help", "one", "three", "two"},
},
{
Args: "--completion-bash --flag-0 --",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"--flag-0", "--flag-1", "--help"},
},
{
Args: "--completion-bash --flag-1",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"opt1", "opt2", "opt3"},
},
{
Args: "--completion-bash --flag-1 opt",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"opt1", "opt2", "opt3"},
},
{
Args: "--completion-bash --flag-1 opt1",
ExpectedWords: []string{"help", "one", "three", "two"},
ExpectedFlags: []string(nil),
ExpectedWords: []string{"five", "four", "help", "one", "three", "two"},
},
{
Args: "--completion-bash --flag-1 opt1 --",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"--flag-0", "--flag-1", "--help"},
},

// Try Subcommand
{
Args: "--completion-bash two",
ExpectedFlags: []string(nil),
ExpectedWords: []string(nil),
},
{
Args: "--completion-bash two --",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"--help", "--flag-2", "--flag-3", "--flag-0", "--flag-1"},
},
{
Args: "--completion-bash two --flag",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"--help", "--flag-2", "--flag-3", "--flag-0", "--flag-1"},
},
{
Args: "--completion-bash two --flag-2",
ExpectedFlags: []string(nil),
ExpectedWords: []string(nil),
},
{
// Top level flags carry downwards
Args: "--completion-bash two --flag-1",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"opt1", "opt2", "opt3"},
},
{
// Top level flags carry downwards
Args: "--completion-bash two --flag-1 opt",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"opt1", "opt2", "opt3"},
},
{
// Top level flags carry downwards
Args: "--completion-bash two --flag-1 opt1",
ExpectedFlags: []string(nil),
ExpectedWords: []string(nil),
},
{
Args: "--completion-bash two --flag-3",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"opt4", "opt5", "opt6"},
},
{
Args: "--completion-bash two --flag-3 opt",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"opt4", "opt5", "opt6"},
},
{
Args: "--completion-bash two --flag-3 opt4",
ExpectedFlags: []string(nil),
ExpectedWords: []string(nil),
},
{
Args: "--completion-bash two --flag-3 opt4 --",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"--help", "--flag-2", "--flag-3", "--flag-0", "--flag-1"},
},

Expand All @@ -352,41 +379,91 @@ func TestBashCompletionOptions(t *testing.T) {
// After a command with an arg with no options, nothing should be
// shown
Args: "--completion-bash three ",
ExpectedFlags: []string(nil),
ExpectedWords: []string(nil),
},
{
// After a command with an arg, explicitly starting a flag should
// complete flags
Args: "--completion-bash three --",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"--flag-0", "--flag-1", "--flag-4", "--help"},
},
{
// After a command with an arg that does have completions, they
// should be shown
Args: "--completion-bash three arg1 ",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"arg-2-opt-1", "arg-2-opt-2"},
},
{
// After a command with an arg that does have completions, but a
// flag is started, flag options should be completed
Args: "--completion-bash three arg1 --",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"--flag-0", "--flag-1", "--flag-4", "--help"},
},
{
// After a command with an arg that has no completions, and isn't first,
// nothing should be shown
Args: "--completion-bash three arg1 arg2 ",
ExpectedFlags: []string(nil),
ExpectedWords: []string(nil),
},
{
// After a command with a different arg that also has completions,
// those different options should be shown
Args: "--completion-bash three arg1 arg2 arg3 ",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"arg-4-opt-1", "arg-4-opt-2"},
},
{
// After a command with all args listed, nothing should complete
Args: "--completion-bash three arg1 arg2 arg3 arg4",
ExpectedFlags: []string(nil),
ExpectedWords: []string(nil),
},
// Test file/directory completion
{
// Expect arg-5
Args: "--completion-bash four",
ExpectedFlags: []string{"-f", "-d"},
ExpectedWords: []string(nil),
},
{
// Expect arg-6
Args: "--completion-bash four /tmp/file1",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"/usr", "/usr/local"},
},
{
// After a command with an arg that does have completions, but a
// flag is started, flag options should be completed
Args: "--completion-bash four /tmp/file1 --",
ExpectedWords: []string{"--flag-0", "--flag-1", "--help"},
},
{
// Args satisfied
Args: "--completion-bash four /tmp/file1 /usr/local",
ExpectedFlags: []string(nil),
ExpectedWords: []string(nil),
},
// File flag completion
{
Args: "--completion-bash five",
ExpectedFlags: []string(nil),
ExpectedWords: []string(nil),
},
{
Args: "--completion-bash five --",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"--flag-0", "--flag-1", "--flag-5", "--help"},
},
{
// In a multi-arg scenario, completion should still be available
// even when the argument is minimally satisfied
Args: "--completion-bash five --flag-5",
ExpectedFlags: []string{"-f", "-d"},
ExpectedWords: []string(nil),
},
}
Expand Down
1 change: 0 additions & 1 deletion args.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ func (a *argGroup) init() error {
type ArgClause struct {
actionMixin
parserMixin
completionsMixin
envarMixin
name string
help string
Expand Down
11 changes: 11 additions & 0 deletions args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,14 @@ func TestSubcommandArgRequiredWithEnvar(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, 123, *flag)
}

func TestArgCompletionEnum(t *testing.T) {
app := newTestApp()
cmd := app.Command("command", "")
arg := cmd.Arg("t", "").Required()
arg.Enum("a", "b", "c")

completion := arg.resolveCompletions()
assert.Empty(t, completion.flags)
assert.Equal(t, []string{"a", "b", "c"}, completion.words)
}
11 changes: 0 additions & 11 deletions flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ loop:
type FlagClause struct {
parserMixin
actionMixin
completionsMixin
envarMixin
name string
shorthand rune
Expand Down Expand Up @@ -230,16 +229,6 @@ func (a *FlagClause) HintOptions(options ...string) *FlagClause {
return a
}

func (a *FlagClause) EnumVar(target *string, options ...string) {
a.parserMixin.EnumVar(target, options...)
a.addBuiltinCompletionWords(options...)
}

func (a *FlagClause) Enum(options ...string) (target *string) {
a.addBuiltinCompletionWords(options...)
return a.parserMixin.Enum(options...)
}

// Default values for this flag. They *must* be parseable by the value of the flag.
func (f *FlagClause) Default(values ...string) *FlagClause {
f.defaultValues = values
Expand Down
50 changes: 50 additions & 0 deletions parsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Settings interface {
}

type parserMixin struct {
completionsMixin
value Value
required bool
}
Expand Down Expand Up @@ -137,26 +138,73 @@ func (p *parserMixin) IPVar(target *net.IP) {
// ExistingFile sets the parser to one that requires and returns an existing file.
func (p *parserMixin) ExistingFileVar(target *string) {
p.SetValue(newExistingFileValue(target))
p.addBuiltinCompletionFlags(fileCompletionFlag)
}

// ExistingDir sets the parser to one that requires and returns an existing directory.
func (p *parserMixin) ExistingDirVar(target *string) {
p.SetValue(newExistingDirValue(target))
p.addBuiltinCompletionFlags(dirCompletionFlag)
}

// ExistingDir sets the parser to one that requires and returns an existing directory.
func (p *parserMixin) ExistingFileOrDirVar(target *string) {
p.SetValue(newExistingFileOrDirValue(target))
p.addBuiltinCompletionFlags(fileCompletionFlag, dirCompletionFlag)
}

// FileVar opens an existing file.
func (p *parserMixin) FileVar(target **os.File) {
p.SetValue(newFileValue(target, os.O_RDONLY, 0))
p.addBuiltinCompletionFlags(fileCompletionFlag)
}

// OpenFileVar calls os.OpenFile(flag, perm)
func (p *parserMixin) OpenFileVar(target **os.File, flag int, perm os.FileMode) {
p.SetValue(newFileValue(target, flag, perm))
p.addBuiltinCompletionFlags(fileCompletionFlag)
}

// ExistingFiles accumulates string values into a slice.
func (p *parserMixin) ExistingFiles() (target *[]string) {
target = new([]string)
p.ExistingFilesVar(target)
return
}

func (p *parserMixin) ExistingFilesVar(target *[]string) {
p.SetValue(newAccumulator(target, func(v interface{}) Value {
return newExistingFileValue(v.(*string))
}))
p.addBuiltinCompletionFlags(fileCompletionFlag)
}

// ExistingDirs accumulates string values into a slice.
func (p *parserMixin) ExistingDirs() (target *[]string) {
target = new([]string)
p.ExistingDirsVar(target)
return
}

func (p *parserMixin) ExistingDirsVar(target *[]string) {
p.SetValue(newAccumulator(target, func(v interface{}) Value {
return newExistingDirValue(v.(*string))
}))
p.addBuiltinCompletionFlags(dirCompletionFlag)
}

// ExistingFilesOrDirs accumulates string values into a slice.
func (p *parserMixin) ExistingFilesOrDirs() (target *[]string) {
target = new([]string)
p.ExistingFilesOrDirsVar(target)
return
}

func (p *parserMixin) ExistingFilesOrDirsVar(target *[]string) {
p.SetValue(newAccumulator(target, func(v interface{}) Value {
return newExistingFileOrDirValue(v.(*string))
}))
p.addBuiltinCompletionFlags(fileCompletionFlag, dirCompletionFlag)
}

// URL provides a valid, parsed url.URL.
Expand Down Expand Up @@ -186,6 +234,7 @@ func (p *parserMixin) Enum(options ...string) (target *string) {
// EnumVar allows a value from a set of options.
func (p *parserMixin) EnumVar(target *string, options ...string) {
p.SetValue(newEnumFlag(target, options...))
p.addBuiltinCompletionWords(options...)
}

// Enums allows a set of values from a set of options.
Expand All @@ -198,6 +247,7 @@ func (p *parserMixin) Enums(options ...string) (target *[]string) {
// EnumVar allows a value from a set of options.
func (p *parserMixin) EnumsVar(target *[]string, options ...string) {
p.SetValue(newEnumsFlag(target, options...))
p.addBuiltinCompletionWords(options...)
}

// A Counter increments a number each time it is encountered.
Expand Down
3 changes: 0 additions & 3 deletions values.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@
{"name": "Duration", "type": "time.Duration", "no_value_parser": true},
{"name": "IP", "type": "net.IP", "no_value_parser": true},
{"name": "TCPAddr", "Type": "*net.TCPAddr", "plural": "TCPList", "no_value_parser": true},
{"name": "ExistingFile", "Type": "string", "plural": "ExistingFiles", "no_value_parser": true},
{"name": "ExistingDir", "Type": "string", "plural": "ExistingDirs", "no_value_parser": true},
{"name": "ExistingFileOrDir", "Type": "string", "plural": "ExistingFilesOrDirs", "no_value_parser": true},
{"name": "Regexp", "Type": "*regexp.Regexp", "parser": "regexp.Compile(s)"},
{"name": "ResolvedIP", "Type": "net.IP", "parser": "resolveHost(s)", "help": "Resolve a hostname or IP to an IP."},
{"name": "HexBytes", "Type": "[]byte", "parser": "hex.DecodeString(s)", "help": "Bytes as a hex string."}
Expand Down
Loading

0 comments on commit 6a4a763

Please sign in to comment.