From 37d2df45c8e47e26d0767af3b0a5c114af3f9a72 Mon Sep 17 00:00:00 2001 From: Bradley Hess Date: Sun, 9 Jul 2017 14:11:03 -0400 Subject: [PATCH] Add completions for files and directories Fixes #156. Also fixes a bug in completion of an incomplete flag. An incomplete flag should take priority over a not-yet-entered command. --- app_test.go | 125 +++++++++++++++++++++++++++++++++++++++++++++++++-- args_test.go | 11 +++++ clause.go | 3 ++ cmd.go | 27 ++++++----- 4 files changed, 149 insertions(+), 17 deletions(-) diff --git a/app_test.go b/app_test.go index a086898..ceedf52 100644 --- a/app_test.go +++ b/app_test.go @@ -261,6 +261,14 @@ 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 @@ -268,88 +276,108 @@ func TestBashCompletionOptions(t *testing.T) { }{ { 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"}, }, @@ -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), }, } diff --git a/args_test.go b/args_test.go index b40ecca..305ab2e 100644 --- a/args_test.go +++ b/args_test.go @@ -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") + + result := arg.resolveCompletions() + assert.Empty(t, result.flags) + assert.Equal(t, []string{"a", "b", "c"}, result.words) +} diff --git a/clause.go b/clause.go index 776b8a0..6568edc 100644 --- a/clause.go +++ b/clause.go @@ -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)) } diff --git a/cmd.go b/cmd.go index 1ea3b76..63fffd0 100644 --- a/cmd.go +++ b/cmd.go @@ -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) { @@ -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 {