Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add command line autocomplete to the fs commands #1622

Merged
merged 43 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
0095d15
Add autocomplete to fs commands
andersrexdb Jul 22, 2024
e8e93fd
Add tests
andersrexdb Jul 24, 2024
300ab07
Lint
andersrexdb Jul 24, 2024
ba64fd3
Fix TestGlobFileset
andersrexdb Jul 24, 2024
5d6a348
Comments
andersrexdb Jul 24, 2024
e3f6a73
Add onlyDirs
andersrexdb Jul 24, 2024
787d563
Add HasWorkspaceClient
andersrexdb Jul 24, 2024
a43b4af
Add file+dir test
andersrexdb Jul 24, 2024
13eb013
PR comments
andersrexdb Jul 25, 2024
3856332
Programmatically add dbfs:/Volumes
andersrexdb Jul 25, 2024
c73c09b
PR feedback
andersrexdb Jul 27, 2024
363bd1d
Client
andersrexdb Jul 29, 2024
0716f31
Comment
andersrexdb Jul 29, 2024
0341e8b
Cleanup
andersrexdb Jul 29, 2024
a42f121
Support .\ local paths with Windows
andersrexdb Aug 2, 2024
6d10776
Handle local Windows paths
andersrexdb Aug 2, 2024
7c010cc
Support both separators on Windows
andersrexdb Aug 2, 2024
e0d2062
Fix test
andersrexdb Aug 2, 2024
d990fe2
Fix test 2
andersrexdb Aug 2, 2024
1868791
PR feedback
andersrexdb Aug 5, 2024
c4a406c
Remove goroutine
andersrexdb Aug 5, 2024
a909d1f
Doc comments
andersrexdb Aug 5, 2024
6091e0d
Use path and filepath for joining paths
andersrexdb Aug 5, 2024
1591896
Use struct for Validate
andersrexdb Aug 5, 2024
84229be
Lowercase validArgs
andersrexdb Aug 6, 2024
058183e
Add integration test
andersrexdb Aug 6, 2024
5df74cf
Fix error
andersrexdb Aug 6, 2024
0803381
DRY up test
andersrexdb Aug 6, 2024
fb90be9
Use fakeFiler in tests
andersrexdb Aug 6, 2024
d802ba7
Remove PrepFakeFiler
andersrexdb Aug 6, 2024
5261a2c
Remove call to current user
andersrexdb Aug 6, 2024
3514151
Add integration test
andersrexdb Aug 6, 2024
2057aad
Put back GetEnvOrSkipTest
andersrexdb Aug 6, 2024
25e4443
PR feedback
andersrexdb Aug 7, 2024
1b168ae
Cleanup
andersrexdb Aug 7, 2024
c2ae3f0
Simplify path.Dir logic
andersrexdb Aug 7, 2024
e5b32fc
Comments
andersrexdb Aug 7, 2024
81775a4
Cleanup
andersrexdb Aug 7, 2024
57ba288
Add windows test
andersrexdb Aug 7, 2024
0eac4e7
PR comments
andersrexdb Aug 9, 2024
6c85d51
Fix TestGlobFileset
andersrexdb Aug 9, 2024
324e4ab
Try again
andersrexdb Aug 9, 2024
354f4b9
Dont use ../filer in test
andersrexdb Aug 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/fs/cat.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@ func newCatCommand() *cobra.Command {
return cmdio.Render(ctx, r)
}

v := newValidArgs()
cmd.ValidArgsFunction = v.Validate

return cmd
}
5 changes: 5 additions & 0 deletions cmd/fs/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,5 +200,10 @@ func newCpCommand() *cobra.Command {
return c.cpFileToFile(sourcePath, targetPath)
}

v := newValidArgs()
// The copy command has two paths that can be completed (SOURCE_PATH & TARGET_PATH)
v.pathArgCount = 2
cmd.ValidArgsFunction = v.Validate

return cmd
}
56 changes: 55 additions & 1 deletion cmd/fs/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (

"github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/filer"
"github.com/databricks/cli/libs/filer/completer"
"github.com/spf13/cobra"
)

func filerForPath(ctx context.Context, fullPath string) (filer.Filer, string, error) {
Expand Down Expand Up @@ -46,6 +48,58 @@ func filerForPath(ctx context.Context, fullPath string) (filer.Filer, string, er
return f, path, err
}

const dbfsPrefix string = "dbfs:"

func isDbfsPath(path string) bool {
return strings.HasPrefix(path, "dbfs:/")
return strings.HasPrefix(path, dbfsPrefix)
}

type validArgs struct {
mustWorkspaceClientFunc func(cmd *cobra.Command, args []string) error
filerForPathFunc func(ctx context.Context, fullPath string) (filer.Filer, string, error)
pathArgCount int
onlyDirs bool
}

func newValidArgs() *validArgs {
return &validArgs{
mustWorkspaceClientFunc: root.MustWorkspaceClient,
filerForPathFunc: filerForPath,
pathArgCount: 1,
onlyDirs: false,
}
}

func (v *validArgs) Validate(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
cmd.SetContext(root.SkipPrompt(cmd.Context()))

if len(args) >= v.pathArgCount {
return nil, cobra.ShellCompDirectiveNoFileComp
}

err := v.mustWorkspaceClientFunc(cmd, args)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}

filer, toCompletePath, err := v.filerForPathFunc(cmd.Context(), toComplete)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}

completer := completer.New(cmd.Context(), filer, v.onlyDirs)

// Dbfs should have a prefix and always use the "/" separator
isDbfsPath := isDbfsPath(toComplete)
if isDbfsPath {
completer.SetPrefix(dbfsPrefix)
completer.SetIsLocalPath(false)
}

completions, directive, err := completer.CompletePath(toCompletePath)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}

return completions, directive
}
89 changes: 89 additions & 0 deletions cmd/fs/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ package fs
import (
"context"
"runtime"
"strings"
"testing"

"github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/filer"
"github.com/databricks/databricks-sdk-go/experimental/mocks"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -60,3 +64,88 @@ func TestFilerForWindowsLocalPaths(t *testing.T) {
testWindowsFilerForPath(t, ctx, `d:\abc`)
testWindowsFilerForPath(t, ctx, `f:\abc\ef`)
}

func mockMustWorkspaceClientFunc(cmd *cobra.Command, args []string) error {
return nil
}

func setupCommand(t *testing.T) (*cobra.Command, *mocks.MockWorkspaceClient) {
m := mocks.NewMockWorkspaceClient(t)
ctx := context.Background()
ctx = root.SetWorkspaceClient(ctx, m.WorkspaceClient)

cmd := &cobra.Command{}
cmd.SetContext(ctx)

return cmd, m
}

func setupTest(t *testing.T) (*validArgs, *cobra.Command, *mocks.MockWorkspaceClient) {
cmd, m := setupCommand(t)

fakeFilerForPath := func(ctx context.Context, fullPath string) (filer.Filer, string, error) {
fakeFiler := filer.NewFakeFiler(map[string]filer.FakeFileInfo{
"dir": {FakeName: "root", FakeDir: true},
"dir/dirA": {FakeDir: true},
"dir/dirB": {FakeDir: true},
"dir/fileA": {},
})
return fakeFiler, strings.TrimPrefix(fullPath, "dbfs:/"), nil
}

v := newValidArgs()
v.filerForPathFunc = fakeFilerForPath
v.mustWorkspaceClientFunc = mockMustWorkspaceClientFunc

return v, cmd, m
}

func TestGetValidArgsFunctionDbfsCompletion(t *testing.T) {
v, cmd, _ := setupTest(t)
completions, directive := v.Validate(cmd, []string{}, "dbfs:/dir/")
assert.Equal(t, []string{"dbfs:/dir/dirA/", "dbfs:/dir/dirB/", "dbfs:/dir/fileA"}, completions)
assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive)
}

func TestGetValidArgsFunctionLocalCompletion(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip()
}

v, cmd, _ := setupTest(t)
completions, directive := v.Validate(cmd, []string{}, "dir/")
assert.Equal(t, []string{"dir/dirA/", "dir/dirB/", "dir/fileA", "dbfs:/"}, completions)
assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive)
}

func TestGetValidArgsFunctionLocalCompletionWindows(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip()
}

v, cmd, _ := setupTest(t)
completions, directive := v.Validate(cmd, []string{}, "dir/")
assert.Equal(t, []string{"dir\\dirA\\", "dir\\dirB\\", "dir\\fileA", "dbfs:/"}, completions)
assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive)
}

func TestGetValidArgsFunctionCompletionOnlyDirs(t *testing.T) {
v, cmd, _ := setupTest(t)
v.onlyDirs = true
completions, directive := v.Validate(cmd, []string{}, "dbfs:/dir/")
assert.Equal(t, []string{"dbfs:/dir/dirA/", "dbfs:/dir/dirB/"}, completions)
assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive)
}

func TestGetValidArgsFunctionNotCompletedArgument(t *testing.T) {
cmd, _ := setupCommand(t)

v := newValidArgs()
v.pathArgCount = 0
v.mustWorkspaceClientFunc = mockMustWorkspaceClientFunc

completions, directive := v.Validate(cmd, []string{}, "dbfs:/")

assert.Nil(t, completions)
assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive)
}
4 changes: 4 additions & 0 deletions cmd/fs/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,9 @@ func newLsCommand() *cobra.Command {
`))
}

v := newValidArgs()
v.onlyDirs = true
cmd.ValidArgsFunction = v.Validate

return cmd
}
4 changes: 4 additions & 0 deletions cmd/fs/mkdir.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,9 @@ func newMkdirCommand() *cobra.Command {
return f.Mkdir(ctx, path)
}

v := newValidArgs()
v.onlyDirs = true
cmd.ValidArgsFunction = v.Validate

return cmd
}
3 changes: 3 additions & 0 deletions cmd/fs/rm.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,8 @@ func newRmCommand() *cobra.Command {
return f.Delete(ctx, path)
}

v := newValidArgs()
cmd.ValidArgsFunction = v.Validate

return cmd
}
27 changes: 27 additions & 0 deletions internal/completer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package internal

import (
"context"
"fmt"
"strings"
"testing"

_ "github.com/databricks/cli/cmd/fs"
"github.com/databricks/cli/libs/filer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func setupCompletionFile(t *testing.T, f filer.Filer) {
err := f.Write(context.Background(), "dir1/file1.txt", strings.NewReader("abc"), filer.CreateParentDirectories)
require.NoError(t, err)
}

func TestAccFsCompletion(t *testing.T) {
f, tmpDir := setupDbfsFiler(t)
setupCompletionFile(t, f)

stdout, _ := RequireSuccessfulRun(t, "__complete", "fs", "ls", tmpDir)
expectedOutput := fmt.Sprintf("%s/dir1/\n:2\n", tmpDir)
assert.Equal(t, expectedOutput, stdout.String())
}
95 changes: 95 additions & 0 deletions libs/filer/completer/completer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package completer

import (
"context"
"path"
"path/filepath"
"strings"

"github.com/databricks/cli/libs/filer"
"github.com/spf13/cobra"
)

type completer struct {
ctx context.Context

// The filer to use for completing remote or local paths.
filer filer.Filer

// CompletePath will only return directories when onlyDirs is true.
onlyDirs bool

// Prefix to prepend to completions.
prefix string

// Whether the path is local or remote. If the path is local we use the `filepath`
// package for path manipulation. Otherwise we use the `path` package.
isLocalPath bool
}

// General completer that takes a filer to complete remote paths when TAB-ing through a path.
func New(ctx context.Context, filer filer.Filer, onlyDirs bool) *completer {
return &completer{ctx: ctx, filer: filer, onlyDirs: onlyDirs, prefix: "", isLocalPath: true}
}

func (c *completer) SetPrefix(p string) {
c.prefix = p
}

func (c *completer) SetIsLocalPath(i bool) {
c.isLocalPath = i
}

func (c *completer) CompletePath(p string) ([]string, cobra.ShellCompDirective, error) {
trailingSeparator := "/"
joinFunc := path.Join

// Use filepath functions if we are in a local path.
if c.isLocalPath {
joinFunc = filepath.Join
trailingSeparator = string(filepath.Separator)
}

// If the user is TAB-ing their way through a path and the
// path ends in a trailing slash, we should list nested directories.
// If the path is incomplete, however, then we should list adjacent
// directories.
dirPath := p
if !strings.HasSuffix(p, trailingSeparator) {
dirPath = path.Dir(p)
}

entries, err := c.filer.ReadDir(c.ctx, dirPath)
if err != nil {
return nil, cobra.ShellCompDirectiveError, err
}

completions := []string{}
for _, entry := range entries {
if c.onlyDirs && !entry.IsDir() {
continue
}

// Join directory path and entry name
completion := joinFunc(dirPath, entry.Name())

// Prepend prefix if it has been set
if c.prefix != "" {
completion = joinFunc(c.prefix, completion)
}

// Add trailing separator for directories.
if entry.IsDir() {
completion += trailingSeparator
}

completions = append(completions, completion)
}

// If the path is local, we add the dbfs:/ prefix suggestion as an option
if c.isLocalPath {
completions = append(completions, "dbfs:/")
}

return completions, cobra.ShellCompDirectiveNoSpace, err
}
Loading
Loading