diff --git a/tools/cli/files.go b/tools/cli/files.go index 2e0fdc18b..1b4b45409 100644 --- a/tools/cli/files.go +++ b/tools/cli/files.go @@ -123,8 +123,13 @@ func CompleteExecutablesInPath(prefix string, paths ...string) []string { entries, err := os.ReadDir(dir) if err == nil { for _, e := range entries { - if strings.HasPrefix(e.Name(), prefix) && !e.IsDir() && unix.Access(filepath.Join(dir, e.Name()), unix.X_OK) == nil { - ans = append(ans, e.Name()) + if strings.HasPrefix(e.Name(), prefix) { + // A symlink to a directory has IsDir() false but still passes the X_OK + // check, since directories are executable in the access(2) sense. + p := filepath.Join(dir, e.Name()) + if unix.Access(p, unix.X_OK) == nil && !is_dir_or_symlink_to_dir(e, p) { + ans = append(ans, e.Name()) + } } } } diff --git a/tools/cli/files_test.go b/tools/cli/files_test.go index 44e5dbf82..28601c841 100644 --- a/tools/cli/files_test.go +++ b/tools/cli/files_test.go @@ -155,6 +155,9 @@ func TestCompleteExecutables(t *testing.T) { create("two", "two-exec", 0700) os.Symlink(filepath.Join(tdir, "two", "two-exec"), filepath.Join(tdir, "one", "s")) os.Symlink(filepath.Join(tdir, "one", "one-not-exec"), filepath.Join(tdir, "one", "n")) + // A symlink to a directory passes the X_OK check, so it must not be offered as a command. + os.Mkdir(filepath.Join(tdir, "two", "subdir"), 0700) + os.Symlink(filepath.Join(tdir, "two", "subdir"), filepath.Join(tdir, "one", "d")) t.Setenv("PATH", strings.Join([]string{filepath.Join(tdir, "one"), filepath.Join(tdir, "two")}, string(os.PathListSeparator))) test_candidates := func(prefix string, expected ...string) {