diff --git a/tools/completion/files.go b/tools/completion/files.go index 1e20f478b..d9065fcba 100644 --- a/tools/completion/files.go +++ b/tools/completion/files.go @@ -8,6 +8,8 @@ import ( "os" "path/filepath" "strings" + + "golang.org/x/sys/unix" ) var _ = fmt.Print @@ -54,3 +56,21 @@ func complete_files(prefix string, callback CompleteFilesCallback) error { return nil } + +func complete_executables_in_path(prefix string, paths ...string) []string { + ans := make([]string, 0, 1024) + if len(paths) == 0 { + paths = filepath.SplitList(os.Getenv("PATH")) + } + for _, dir := range paths { + 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()) + } + } + } + } + return ans +} diff --git a/tools/completion/files_test.go b/tools/completion/files_test.go index 3cdcff754..99b68cd94 100644 --- a/tools/completion/files_test.go +++ b/tools/completion/files_test.go @@ -9,6 +9,7 @@ import ( "path/filepath" "reflect" "sort" + "strings" "testing" ) @@ -19,11 +20,16 @@ func TestCompleteFiles(t *testing.T) { defer os.Chdir(cwd) } os.Chdir(tdir) - os.Create(filepath.Join(tdir, "one.txt")) - os.Create(filepath.Join(tdir, "two.txt")) + + create := func(parts ...string) { + f, _ := os.Create(filepath.Join(tdir, filepath.Join(parts...))) + f.Close() + } + create("one.txt") + create("two.txt") os.Mkdir(filepath.Join(tdir, "odir"), 0700) - os.Create(filepath.Join(tdir, "odir", "three.txt")) - os.Create(filepath.Join(tdir, "odir", "four.txt")) + create("odir", "three.txt") + create("odir", "four.txt") test_candidates := func(prefix string, expected ...string) { if expected == nil { @@ -77,3 +83,36 @@ func TestCompleteFiles(t *testing.T) { test_candidates("x") } + +func TestCompleteExecutables(t *testing.T) { + tdir := t.TempDir() + create := func(base string, name string, mode os.FileMode) { + f, _ := os.OpenFile(filepath.Join(tdir, base, name), os.O_CREATE, mode) + f.Close() + } + os.Mkdir(filepath.Join(tdir, "one"), 0700) + os.Mkdir(filepath.Join(tdir, "two"), 0700) + + create("", "not-in-path", 0700) + create("one", "one-exec", 0700) + create("one", "one-not-exec", 0600) + 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")) + + t.Setenv("PATH", strings.Join([]string{filepath.Join(tdir, "one"), filepath.Join(tdir, "two")}, string(os.PathListSeparator))) + test_candidates := func(prefix string, expected ...string) { + if expected == nil { + expected = make([]string, 0) + } + actual := complete_executables_in_path(prefix) + sort.Strings(expected) + sort.Strings(actual) + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("Did not get expected completion candidates for prefix: %#v\nExpected: %#v\nActual: %#v", prefix, expected, actual) + } + } + test_candidates("", "one-exec", "two-exec", "s") + test_candidates("o", "one-exec") + test_candidates("x") +}