Skip to content

Commit 5d46ac9

Browse files
authored
custom comp: do not complete flags after args when interspersed is false (#1308)
If the interspersed option is set false and one arg is already set all following arguments are counted as arg and not parsed as flags. Because of that we should not offer flag completion. The same applies to arguments followed after `--`. Signed-off-by: Paul Holzinger <[email protected]>
1 parent 3c8a19e commit 5d46ac9

File tree

2 files changed

+259
-11
lines changed

2 files changed

+259
-11
lines changed

completions.go

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ const (
2121
// can be instructed to have once completions have been provided.
2222
type ShellCompDirective int
2323

24+
type flagCompError struct {
25+
subCommand string
26+
flagName string
27+
}
28+
29+
func (e *flagCompError) Error() string {
30+
return "Subcommand '" + e.subCommand + "' does not support flag '" + e.flagName + "'"
31+
}
32+
2433
const (
2534
// ShellCompDirectiveError indicates an error occurred and completions should be ignored.
2635
ShellCompDirectiveError ShellCompDirective = 1 << iota
@@ -224,18 +233,35 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
224233
// This is important because if we are completing a flag value, we need to also
225234
// remove the flag name argument from the list of finalArgs or else the parsing
226235
// could fail due to an invalid value (incomplete) for the flag.
227-
flag, finalArgs, toComplete, err := checkIfFlagCompletion(finalCmd, finalArgs, toComplete)
228-
if err != nil {
229-
// Error while attempting to parse flags
230-
return finalCmd, []string{}, ShellCompDirectiveDefault, err
231-
}
236+
flag, finalArgs, toComplete, flagErr := checkIfFlagCompletion(finalCmd, finalArgs, toComplete)
237+
238+
// Check if interspersed is false or -- was set on a previous arg.
239+
// This works by counting the arguments. Normally -- is not counted as arg but
240+
// if -- was already set or interspersed is false and there is already one arg then
241+
// the extra added -- is counted as arg.
242+
flagCompletion := true
243+
_ = finalCmd.ParseFlags(append(finalArgs, "--"))
244+
newArgCount := finalCmd.Flags().NArg()
232245

233246
// Parse the flags early so we can check if required flags are set
234247
if err = finalCmd.ParseFlags(finalArgs); err != nil {
235248
return finalCmd, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
236249
}
237250

238-
if flag != nil {
251+
realArgCount := finalCmd.Flags().NArg()
252+
if newArgCount > realArgCount {
253+
// don't do flag completion (see above)
254+
flagCompletion = false
255+
}
256+
// Error while attempting to parse flags
257+
if flagErr != nil {
258+
// If error type is flagCompError and we don't want flagCompletion we should ignore the error
259+
if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) {
260+
return finalCmd, []string{}, ShellCompDirectiveDefault, flagErr
261+
}
262+
}
263+
264+
if flag != nil && flagCompletion {
239265
// Check if we are completing a flag value subject to annotations
240266
if validExts, present := flag.Annotations[BashCompFilenameExt]; present {
241267
if len(validExts) != 0 {
@@ -262,7 +288,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
262288
// When doing completion of a flag name, as soon as an argument starts with
263289
// a '-' we know it is a flag. We cannot use isFlagArg() here as it requires
264290
// the flag name to be complete
265-
if flag == nil && len(toComplete) > 0 && toComplete[0] == '-' && !strings.Contains(toComplete, "=") {
291+
if flag == nil && len(toComplete) > 0 && toComplete[0] == '-' && !strings.Contains(toComplete, "=") && flagCompletion {
266292
var completions []string
267293

268294
// First check for required flags
@@ -375,7 +401,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
375401

376402
// Find the completion function for the flag or command
377403
var completionFn func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
378-
if flag != nil {
404+
if flag != nil && flagCompletion {
379405
completionFn = c.Root().flagCompletionFunctions[flag]
380406
} else {
381407
completionFn = finalCmd.ValidArgsFunction
@@ -459,6 +485,7 @@ func checkIfFlagCompletion(finalCmd *Command, args []string, lastArg string) (*p
459485
var flagName string
460486
trimmedArgs := args
461487
flagWithEqual := false
488+
orgLastArg := lastArg
462489

463490
// When doing completion of a flag name, as soon as an argument starts with
464491
// a '-' we know it is a flag. We cannot use isFlagArg() here as that function
@@ -517,9 +544,8 @@ func checkIfFlagCompletion(finalCmd *Command, args []string, lastArg string) (*p
517544

518545
flag := findFlag(finalCmd, flagName)
519546
if flag == nil {
520-
// Flag not supported by this command, nothing to complete
521-
err := fmt.Errorf("Subcommand '%s' does not support flag '%s'", finalCmd.Name(), flagName)
522-
return nil, nil, "", err
547+
// Flag not supported by this command, the interspersed option might be set so return the original args
548+
return nil, args, orgLastArg, &flagCompError{subCommand: finalCmd.Name(), flagName: flagName}
523549
}
524550

525551
if !flagWithEqual {

completions_test.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1749,6 +1749,228 @@ func TestValidArgsFuncChildCmdsWithDesc(t *testing.T) {
17491749
}
17501750
}
17511751

1752+
func TestFlagCompletionWithNotInterspersedArgs(t *testing.T) {
1753+
rootCmd := &Command{Use: "root", Run: emptyRun}
1754+
childCmd := &Command{
1755+
Use: "child",
1756+
Run: emptyRun,
1757+
ValidArgsFunction: func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
1758+
return []string{"--validarg", "test"}, ShellCompDirectiveDefault
1759+
},
1760+
}
1761+
childCmd2 := &Command{
1762+
Use: "child2",
1763+
Run: emptyRun,
1764+
ValidArgs: []string{"arg1", "arg2"},
1765+
}
1766+
rootCmd.AddCommand(childCmd, childCmd2)
1767+
childCmd.Flags().Bool("bool", false, "test bool flag")
1768+
childCmd.Flags().String("string", "", "test string flag")
1769+
_ = childCmd.RegisterFlagCompletionFunc("string", func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
1770+
return []string{"myval"}, ShellCompDirectiveDefault
1771+
})
1772+
1773+
// Test flag completion with no argument
1774+
output, err := executeCommand(rootCmd, ShellCompRequestCmd, "child", "--")
1775+
if err != nil {
1776+
t.Errorf("Unexpected error: %v", err)
1777+
}
1778+
1779+
expected := strings.Join([]string{
1780+
"--bool\ttest bool flag",
1781+
"--string\ttest string flag",
1782+
":4",
1783+
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")
1784+
1785+
if output != expected {
1786+
t.Errorf("expected: %q, got: %q", expected, output)
1787+
}
1788+
1789+
// Test that no flags are completed after the -- arg
1790+
output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "--", "-")
1791+
if err != nil {
1792+
t.Errorf("Unexpected error: %v", err)
1793+
}
1794+
1795+
expected = strings.Join([]string{
1796+
"--validarg",
1797+
"test",
1798+
":0",
1799+
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
1800+
1801+
if output != expected {
1802+
t.Errorf("expected: %q, got: %q", expected, output)
1803+
}
1804+
1805+
// Test that no flags are completed after the -- arg with a flag set
1806+
output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "--bool", "--", "-")
1807+
if err != nil {
1808+
t.Errorf("Unexpected error: %v", err)
1809+
}
1810+
1811+
expected = strings.Join([]string{
1812+
"--validarg",
1813+
"test",
1814+
":0",
1815+
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
1816+
1817+
if output != expected {
1818+
t.Errorf("expected: %q, got: %q", expected, output)
1819+
}
1820+
1821+
// set Interspersed to false which means that no flags should be completed after the first arg
1822+
childCmd.Flags().SetInterspersed(false)
1823+
1824+
// Test that no flags are completed after the first arg
1825+
output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "arg", "--")
1826+
if err != nil {
1827+
t.Errorf("Unexpected error: %v", err)
1828+
}
1829+
1830+
expected = strings.Join([]string{
1831+
"--validarg",
1832+
"test",
1833+
":0",
1834+
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
1835+
1836+
if output != expected {
1837+
t.Errorf("expected: %q, got: %q", expected, output)
1838+
}
1839+
1840+
// Test that no flags are completed after the fist arg with a flag set
1841+
output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "--string", "t", "arg", "--")
1842+
if err != nil {
1843+
t.Errorf("Unexpected error: %v", err)
1844+
}
1845+
1846+
expected = strings.Join([]string{
1847+
"--validarg",
1848+
"test",
1849+
":0",
1850+
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
1851+
1852+
if output != expected {
1853+
t.Errorf("expected: %q, got: %q", expected, output)
1854+
}
1855+
1856+
// Check that args are still completed after --
1857+
output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "--", "")
1858+
if err != nil {
1859+
t.Errorf("Unexpected error: %v", err)
1860+
}
1861+
1862+
expected = strings.Join([]string{
1863+
"--validarg",
1864+
"test",
1865+
":0",
1866+
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
1867+
1868+
if output != expected {
1869+
t.Errorf("expected: %q, got: %q", expected, output)
1870+
}
1871+
1872+
// Check that args are still completed even if flagname with ValidArgsFunction exists
1873+
output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "--", "--string", "")
1874+
if err != nil {
1875+
t.Errorf("Unexpected error: %v", err)
1876+
}
1877+
1878+
expected = strings.Join([]string{
1879+
"--validarg",
1880+
"test",
1881+
":0",
1882+
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
1883+
1884+
if output != expected {
1885+
t.Errorf("expected: %q, got: %q", expected, output)
1886+
}
1887+
1888+
// Check that args are still completed even if flagname with ValidArgsFunction exists
1889+
output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child2", "--", "a")
1890+
if err != nil {
1891+
t.Errorf("Unexpected error: %v", err)
1892+
}
1893+
1894+
expected = strings.Join([]string{
1895+
"arg1",
1896+
"arg2",
1897+
":4",
1898+
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")
1899+
1900+
if output != expected {
1901+
t.Errorf("expected: %q, got: %q", expected, output)
1902+
}
1903+
1904+
// Check that --validarg is not parsed as flag after --
1905+
output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "--", "--validarg", "")
1906+
if err != nil {
1907+
t.Errorf("Unexpected error: %v", err)
1908+
}
1909+
1910+
expected = strings.Join([]string{
1911+
"--validarg",
1912+
"test",
1913+
":0",
1914+
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
1915+
1916+
if output != expected {
1917+
t.Errorf("expected: %q, got: %q", expected, output)
1918+
}
1919+
1920+
// Check that --validarg is not parsed as flag after an arg
1921+
output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "arg", "--validarg", "")
1922+
if err != nil {
1923+
t.Errorf("Unexpected error: %v", err)
1924+
}
1925+
1926+
expected = strings.Join([]string{
1927+
"--validarg",
1928+
"test",
1929+
":0",
1930+
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
1931+
1932+
if output != expected {
1933+
t.Errorf("expected: %q, got: %q", expected, output)
1934+
}
1935+
1936+
// Check that --validarg is added to args for the ValidArgsFunction
1937+
childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
1938+
return args, ShellCompDirectiveDefault
1939+
}
1940+
output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "--", "--validarg", "")
1941+
if err != nil {
1942+
t.Errorf("Unexpected error: %v", err)
1943+
}
1944+
1945+
expected = strings.Join([]string{
1946+
"--validarg",
1947+
":0",
1948+
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
1949+
1950+
if output != expected {
1951+
t.Errorf("expected: %q, got: %q", expected, output)
1952+
}
1953+
1954+
// Check that --validarg is added to args for the ValidArgsFunction and toComplete is also set correctly
1955+
childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
1956+
return append(args, toComplete), ShellCompDirectiveDefault
1957+
}
1958+
output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "--", "--validarg", "--toComp=ab")
1959+
if err != nil {
1960+
t.Errorf("Unexpected error: %v", err)
1961+
}
1962+
1963+
expected = strings.Join([]string{
1964+
"--validarg",
1965+
"--toComp=ab",
1966+
":0",
1967+
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
1968+
1969+
if output != expected {
1970+
t.Errorf("expected: %q, got: %q", expected, output)
1971+
}
1972+
}
1973+
17521974
func TestFlagCompletionInGoWithDesc(t *testing.T) {
17531975
rootCmd := &Command{
17541976
Use: "root",

0 commit comments

Comments
 (0)