Skip to content

Commit

Permalink
Add completions for files and directories
Browse files Browse the repository at this point in the history
Fixes alecthomas#156. Also fixes a bug in completion of an incomplete flag. An
incomplete flag should take priority over a not-yet-entered command.
  • Loading branch information
bdhess committed Jul 9, 2017
1 parent f01bf33 commit e340df9
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 17 deletions.
125 changes: 122 additions & 3 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,95 +261,123 @@ 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()
five.Command("sub-1", "").Flag("flag-6", "").ExistingDir()

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 @@ -358,41 +386,132 @@ 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{"sub-1"},
},
{
Args: "--completion-bash five --",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"--flag-0", "--flag-1", "--flag-5", "--help"},
},
{
Args: "--completion-bash five --flag-5",
ExpectedFlags: []string{"-f"},
ExpectedWords: []string(nil),
},
{
// Even though this _may_ be complete, the expected behavior is to
// continue to build a file path. This would work a lot better if
// we could detect trailing whitespace in the arg string we're
// provided (see the case below).
Args: "--completion-bash five --flag-5 /tmp/file1",
ExpectedFlags: []string{"-f"},
ExpectedWords: []string(nil),
},
{
// Theoretical continuation of the above. This won't happen in practice,
// since trailing whitespace will be ignored by command line parsing.
Args: "--completion-bash five --flag-5 /tmp/file1 ",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"sub-1"},
},
{
Args: "--completion-bash five --flag-5 /tmp/file1 sub-1",
ExpectedFlags: []string(nil),
ExpectedWords: []string(nil),
},
{
Args: "--completion-bash five --flag-5 /tmp/file1 sub-1 --",
ExpectedFlags: []string(nil),
ExpectedWords: []string{"--flag-0", "--flag-1", "--flag-6", "--help"},
},
{
Args: "--completion-bash five --flag-5 /tmp/file1 sub-1 --flag-6",
ExpectedFlags: []string{"-d"},
ExpectedWords: []string(nil),
},
{
// Same as before; may not be complete
Args: "--completion-bash five --flag-5 /tmp/file1 sub-1 --flag-6 /tmp/dir1",
ExpectedFlags: []string{"-d"},
ExpectedWords: []string(nil),
},
{
// Another theoretical continuation
Args: "--completion-bash five --flag-5 /tmp/file1 sub-1 --flag-6 /tmp/dir1 ",
ExpectedFlags: []string(nil),
ExpectedWords: []string(nil),
},
}
Expand Down
11 changes: 11 additions & 0 deletions args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,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)
}
3 changes: 3 additions & 0 deletions clause.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,16 +283,19 @@ func (c *Clause) BytesVar(target *units.Base2Bytes) {

// ExistingFile sets the parser to one that requires and returns an existing file.
func (c *Clause) ExistingFileVar(target *string) {
c.addBuiltinCompletionFlags(fileCompletionFlag)
c.SetValue(newExistingFileValue(target))
}

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

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

Expand Down
27 changes: 13 additions & 14 deletions cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,28 +54,27 @@ func (c *cmdMixin) CmdCompletion(context *ParseContext) completion {
return options
}

func (c *cmdMixin) FlagCompletion(flagName string, flagValue string) (result completion, flagMatch bool, optionMatch bool) {
func (c *cmdMixin) FlagCompletion(flagName string, flagValue string) (result completion, nameMatch bool, valueMatch bool) {
// Check if flagName matches a known flag.
// If it does, show the options for the flag
// Otherwise, show all flags

options := completion{}

allFlags := []string{}
for _, flag := range c.flagGroup.flagOrder {
// Loop through each flag and determine if a match exists
if flag.name == flagName {
// User typed entire flag. Need to look for matches in the wordlist.
options = flag.resolveCompletions()
if len(options.words) == 0 {
// No wordlist Options to Choose From, Assume Match.
return options, true, true
// User typed entire flag name. Let's check if the value matches.
completions := flag.resolveCompletions()
if completions.empty() {
// No completions specified; assume the value matches
return completions, true, true
}

// Loop options to find if the user specified value matches
// Loop words to find if the user specified value matches
isPrefix := false
matched := false

for _, opt := range options.words {
for _, opt := range completions.words {
if flagValue == opt {
matched = true
} else if strings.HasPrefix(opt, flagValue) {
Expand All @@ -85,16 +84,16 @@ func (c *cmdMixin) FlagCompletion(flagName string, flagValue string) (result com

// Matched Flag Directly
// Flag Value Not Prefixed, and Matched Directly
return options, true, !isPrefix && matched
return completions, true, !isPrefix && matched
}

if !flag.hidden {
options.words = append(options.words, "--"+flag.name)
allFlags = append(allFlags, "--"+flag.name)
}
}
// No Flag directly matched.
return options, false, false

// No Flag directly matched.
return completion{words: allFlags}, false, false
}

type cmdGroup struct {
Expand Down

0 comments on commit e340df9

Please sign in to comment.