diff --git a/CHANGELOG.md b/CHANGELOG.md index 44e108b73..6effb3710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ - Allow adjacent slashes to be used as keys when uploading to remote. ([#459](https://github.com/peak/s5cmd/pull/459)) - Debian packages are provided on [releases page](https://github.com/peak/s5cmd/releases) ([#380](https://github.com/peak/s5cmd/issues/380)) - Upgraded minimum required Go version to 1.17. - +- Improve auto-completion support of s5cmd for `zsh` and `bash`, start supporting `pwsh` and stop the support for `fish`. Now s5cmd can complete bucket names, s3 keys in a bucket and the local files. However, `install-completion` flag no longer _installs_ the completion script to `*rc` files instead it merely gives instructions to install autocompletion and provides the autocompletion script ([#500](https://github.com/peak/s5cmd/pull/500)). #### Bugfixes - Fixed a bug where (`--stat`) prints unnecessarily when used with help and version commands ([#452](https://github.com/peak/s5cmd/issues/452)) diff --git a/README.md b/README.md index f4fc7f776..db5626a40 100644 --- a/README.md +++ b/README.md @@ -435,14 +435,28 @@ While executing the commands, `s5cmd` detects the region according to the follow ### Shell auto-completion -Shell completion is supported for bash, zsh and fish. +Shell completion is supported for bash, pwsh (PowerShell) and zsh. -To enable auto-completion, run: +Run `s5cmd --install-completion` to obtain the appropriate auto-completion script for your shell, note that `install-completion` does not install the auto-completion but merely gives the instructions to install. The name is kept as it is for backward compatibility. - s5cmd --install-completion +To actually enable auto-completion: +#### in bash and zsh: + you should add auto-completion script to `.bashrc` and `.zshrc` file. +#### in pwsh: +you should save the autocompletion script to a file named `s5cmd.ps1` and add the full path of "s5cmd.ps1" file to profile file (which you can locate with `$profile`) -This will add a few lines to your shell configuration file. After installation, -restart your shell to activate the changes. + +Finally, restart your shell to activate the changes. + +> **Note** +The environment variable `SHELL` must be accurate for the autocompletion to function properly. That is it should point to `bash` binary in bash, to `zsh` binary in zsh and to `pwsh` binary in PowerShell. + + +> **Note** +The autocompletion is tested with following versions of the shells: \ +***zsh*** 5.8.1 (x86_64-apple-darwin21.0) \ +GNU ***bash***, version 5.1.16(1)-release (x86_64-apple-darwin21.1.0) \ +***PowerShell*** 7.2.6 ### Google Cloud Storage support diff --git a/command/app.go b/command/app.go index c7a728b86..283d843aa 100644 --- a/command/app.go +++ b/command/app.go @@ -6,7 +6,6 @@ import ( "os" "strings" - cmpinstall "github.com/posener/complete/cmd/install" "github.com/urfave/cli/v2" "github.com/peak/s5cmd/log" @@ -23,8 +22,9 @@ const ( ) var app = &cli.App{ - Name: appName, - Usage: "Blazing fast S3 and local filesystem execution tool", + Name: appName, + Usage: "Blazing fast S3 and local filesystem execution tool", + EnableBashCompletion: true, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "json", @@ -60,7 +60,7 @@ var app = &cli.App{ }, &cli.BoolFlag{ Name: "install-completion", - Usage: "install completion for your shell", + Usage: "get completion installation instructions for your shell (only available for bash, pwsh, and zsh)", }, &cli.BoolFlag{ Name: "dry-run", @@ -154,13 +154,9 @@ var app = &cli.App{ }, Action: func(c *cli.Context) error { if c.Bool("install-completion") { - if cmpinstall.IsInstalled(appName) { - return nil - } - - return cmpinstall.Install(appName) + printAutocompletionInstructions(os.Getenv("SHELL")) + return nil } - args := c.Args() if args.Present() { cli.ShowCommandHelp(c, args.First()) @@ -228,9 +224,5 @@ func AppCommand(name string) *cli.Command { func Main(ctx context.Context, args []string) error { app.Commands = Commands() - if maybeAutoComplete() { - return nil - } - return app.RunContext(ctx, args) } diff --git a/command/auto_complete.go b/command/auto_complete.go new file mode 100644 index 000000000..7c37df72e --- /dev/null +++ b/command/auto_complete.go @@ -0,0 +1,241 @@ +package command + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/peak/s5cmd/storage" + "github.com/peak/s5cmd/storage/url" + "github.com/urfave/cli/v2" +) + +const zsh = `autoload -Uz compinit +compinit + +_s5cmd_cli_zsh_autocomplete() { + local -a opts + local cur + cur=${words[-1]} + opts=("${(@f)$(${words[@]:0:#words[@]-1} "${cur}" --generate-bash-completion)}") + + if [[ "${opts[1]}" != "" ]]; then + _describe 'values' opts + else + _files + fi +} + +compdef _s5cmd_cli_zsh_autocomplete s5cmd +` + +const bash = `# prepare autocompletion suggestions for s5cmd and save them to COMPREPLY array +_s5cmd_cli_bash_autocomplete() { + if [[ "${COMP_WORDS[0]}" != "source" ]]; then + COMPREPLY=() + local opts cur cmd` + + // get current word (cur) and prepare command (cmd) + ` + cur="${COMP_WORDS[COMP_CWORD]}" + cmd="${COMP_LINE:0:$COMP_POINT}"` + + + // if we want to complete the second argument and we didn't start writing + // yet then we should pass an empty string as another argument. Otherwise + // the white spaces will be discarded and the program will make suggestions + // as if it is completing the first argument. + // Beware that we want to pass an empty string so we intentionally write + // as it is. Fixes of SC2089 and SC2090 are not what we want. + // see also https://www.shellcheck.net/wiki/SC2090 + ` + [ "${COMP_LINE:COMP_POINT-1:$COMP_POINT}" == " " ] \ + && cmd="${cmd} ''" ` + + + // execute the command with '--generate-bash-completion' flag to obtain + // possible completion values for current word. + // ps. SC2090 is not wanted. + ` + opts=$($cmd --generate-bash-completion)` + + + // prepare completion array with possible values and filter those do not start with cur. + // if no completion is found then fallback to default completion of shell. + ` + + while IFS='' read -r line; + do + COMPREPLY+=("$line"); + done \ + < <(compgen -o bashdefault -o default -o nospace -W "${opts}" -- "${cur}") + + return 0 + fi +} + +# call the _s5cmd_cli_bash_autocomplete to complete s5cmd command. +complete -o nospace -F _s5cmd_cli_bash_autocomplete s5cmd +` + +const pwsh = `$fn = $($MyInvocation.MyCommand.Name) +$name = $fn -replace "(.*)\.ps1$", '$1' +Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock { + param($commandName, $wordToComplete, $cursorPosition) + $other = "$wordToComplete --generate-bash-completion" + Invoke-Expression $other | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } +} +` + +func getBashCompleteFn(cmd *cli.Command, isOnlyRemote, isOnlyBucket bool) func(ctx *cli.Context) { + isOnlyRemote = isOnlyRemote || isOnlyBucket + return func(ctx *cli.Context) { + arg := parseArgumentToComplete(ctx) + + if strings.HasPrefix(arg, "-") { + cli.DefaultCompleteWithFlags(cmd)(ctx) + return + } + + if isOnlyRemote || strings.HasPrefix(arg, "s3://") { + u, err := url.New(arg) + if err != nil { + u = &url.URL{Type: 0, Scheme: "s3"} + } + + c := ctx.Context + client, err := storage.NewRemoteClient(c, u, NewStorageOpts(ctx)) + if err != nil { + return + } + + shell := filepath.Base(os.Getenv("SHELL")) + printS3Suggestions(c, shell, client, u, arg, isOnlyBucket) + return + } + } +} + +// constantCompleteWithDefault returns a complete function which prints the argument, itself, which is to be completed. +// If the argument is empty string it uses the defaultCompletions to make suggestions. +func constantCompleteWithDefault(shell, arg string, defaultCompletions ...string) { + if arg == "" { + for _, str := range defaultCompletions { + fmt.Println(formatSuggestionForShell(shell, str, arg)) + } + } else { + fmt.Println(formatSuggestionForShell(shell, arg, arg)) + } +} + +func printS3Suggestions(c context.Context, shell string, client *storage.S3, u *url.URL, arg string, isOnlyBucket bool) { + if u.Bucket == "" || (u.IsBucket() && !strings.HasSuffix(arg, "/")) || isOnlyBucket { + printListBuckets(c, shell, client, u, arg) + } else { + printListNURLSuggestions(c, shell, client, u, 20, arg) + } +} + +func printListBuckets(ctx context.Context, shell string, client *storage.S3, u *url.URL, argToBeCompleted string) { + buckets, err := client.ListBuckets(ctx, u.Bucket) + if err != nil { + return + } + + for _, bucket := range buckets { + fmt.Println(formatSuggestionForShell(shell, "s3://"+bucket.Name+"/", argToBeCompleted)) + } +} + +func printListNURLSuggestions(ctx context.Context, shell string, client *storage.S3, u *url.URL, count int, argToBeCompleted string) { + if u.IsBucket() { + var err error + u, err = url.New(u.Absolute() + "/") + if err != nil { + return + } + } + + i := 0 + for obj := range (*client).List(ctx, u, false) { + if i > count { + break + } + if obj.Err != nil { + return + } + fmt.Println(formatSuggestionForShell(shell, obj.URL.Absolute(), argToBeCompleted)) + i++ + } +} + +func printAutocompletionInstructions(shell string) { + var script string + baseShell := filepath.Base(shell) + instructions := `# To enable autocompletion you should add the following script to startup scripts of your shell. +# It is probably located at ~/.` + baseShell + "rc" + + switch baseShell { + case "zsh": + script = zsh + case "bash": + script = bash + case "pwsh": + script = pwsh + instructions = `# To enable autocompletion you should save the following script to a file named "s5cmd.ps1" and execute it. +# To persist it you should add the path of "s5cmd.ps1" file to profile file (which you can locate with $profile) to automatically execute "s5cmd.ps1" on every shell start up.` + default: + instructions = `# We couldn't recognize your SHELL "` + baseShell + `". +# Shell completion is supported only for bash, pwsh and zsh. +# Make sure that your SHELL environment variable is set accurately.` + } + + fmt.Println(instructions) + fmt.Println(script) +} + +func formatSuggestionForShell(baseShell, suggestion, argToBeCompleted string) string { + switch baseShell { + case "bash": + var prefix string + suggestions := make([]string, 0, 2) + if i := strings.LastIndex(argToBeCompleted, ":"); i >= 0 && baseShell == "bash" { + // include the original suggestion in case that COMP_WORDBREAKS does not contain : + // or that the argToBeCompleted was quoted. + // Bash doesn't split on : when argument is quoted even if : is in COMP_WORDBREAKS + suggestions = append(suggestions, suggestion) + prefix = argToBeCompleted[0 : i+1] + } + suggestions = append(suggestions, strings.TrimPrefix(suggestion, prefix)) + return strings.Join(suggestions, "\n") + case "zsh": + // replace every colon : with \: if shell is zsh + // colons are used as a seperator for the autocompletion script + // so "literal colons in completion must be quoted with a backslash" + // see also https://zsh.sourceforge.io/Doc/Release/Completion-System.html#:~:text=This%20is%20followed,as%20name1%3B + return strings.ReplaceAll(suggestion, ":", `\:`) + default: + return suggestion + } +} + +func parseArgumentToComplete(ctx *cli.Context) string { + var arg string + args := ctx.Args() + l := args.Len() + + if l > 0 { + arg = args.Get(l - 1) + } + + // argument may start with a quotation mark, in this case we want to trim + // that before checking if it has prefix 's3://'. + // Beware that we only want to trim the first char, not all of the leading + // quotation marks, because those quotation marks may be actual characters. + if strings.HasPrefix(arg, "'") { + arg = strings.TrimPrefix(arg, "'") + } else { + arg = strings.TrimPrefix(arg, "\"") + } + return arg +} diff --git a/command/autocomplete.go b/command/autocomplete.go deleted file mode 100644 index ab3e2659e..000000000 --- a/command/autocomplete.go +++ /dev/null @@ -1,42 +0,0 @@ -package command - -import ( - "github.com/posener/complete" - "github.com/urfave/cli/v2" -) - -func adaptCommand(cmd *cli.Command) complete.Command { - return complete.Command{ - Flags: adaptFlags(cmd.Flags), - // TODO(ig): add args predictors - } -} - -func adaptFlags(flags []cli.Flag) complete.Flags { - completionFlags := make(complete.Flags) - - for _, flag := range flags { - for _, flagname := range flag.Names() { - if len(flagname) == 1 { - flagname = "-" + flagname - } else { - flagname = "--" + flagname - } - completionFlags[flagname] = complete.PredictNothing - } - } - return completionFlags -} - -func maybeAutoComplete() bool { - cmpCommands := make(complete.Commands) - for _, cmd := range app.Commands { - cmpCommands[cmd.Name] = adaptCommand(cmd) - } - - completionCmd := complete.Command{ - Flags: adaptFlags(app.Flags), - Sub: cmpCommands, - } - return complete.New(appName, completionCmd).Complete() -} diff --git a/command/cat.go b/command/cat.go index 0e66afddb..2a3b1242c 100644 --- a/command/cat.go +++ b/command/cat.go @@ -28,7 +28,7 @@ Examples: ` func NewCatCommand() *cli.Command { - return &cli.Command{ + cmd := &cli.Command{ Name: "cat", HelpName: "cat", Usage: "print remote object content", @@ -60,6 +60,8 @@ func NewCatCommand() *cli.Command { }.Run(c.Context) }, } + cmd.BashComplete = getBashCompleteFn(cmd, true, false) + return cmd } // Cat holds cat operation flags and states. diff --git a/command/cp.go b/command/cp.go index cdf193771..b6b766d65 100644 --- a/command/cp.go +++ b/command/cp.go @@ -213,7 +213,7 @@ func NewCopyCommandFlags() []cli.Flag { } func NewCopyCommand() *cli.Command { - return &cli.Command{ + cmd := &cli.Command{ Name: "cp", HelpName: "cp", Usage: "copy objects", @@ -233,6 +233,9 @@ func NewCopyCommand() *cli.Command { return NewCopy(c, false).Run(c.Context) }, } + + cmd.BashComplete = getBashCompleteFn(cmd, false, false) + return cmd } // Copy holds copy operation flags and states. diff --git a/command/du.go b/command/du.go index 871910108..380ab47c1 100644 --- a/command/du.go +++ b/command/du.go @@ -36,7 +36,7 @@ Examples: ` func NewSizeCommand() *cli.Command { - return &cli.Command{ + cmd := &cli.Command{ Name: "du", HelpName: "du", Usage: "show object size usage", @@ -80,6 +80,9 @@ func NewSizeCommand() *cli.Command { }.Run(c.Context) }, } + + cmd.BashComplete = getBashCompleteFn(cmd, false, false) + return cmd } // Size holds disk usage (du) operation flags and states. diff --git a/command/ls.go b/command/ls.go index 43a7dffda..2f1183301 100644 --- a/command/ls.go +++ b/command/ls.go @@ -48,7 +48,7 @@ Examples: ` func NewListCommand() *cli.Command { - return &cli.Command{ + cmd := &cli.Command{ Name: "ls", HelpName: "ls", Usage: "list buckets and objects", @@ -105,6 +105,9 @@ func NewListCommand() *cli.Command { }.Run(c.Context) }, } + + cmd.BashComplete = getBashCompleteFn(cmd, false, false) + return cmd } // List holds list operation flags and states. diff --git a/command/mb.go b/command/mb.go index 9a70f9937..6b5eabe03 100644 --- a/command/mb.go +++ b/command/mb.go @@ -3,6 +3,9 @@ package command import ( "context" "fmt" + "os" + "path/filepath" + "strings" "github.com/urfave/cli/v2" @@ -27,7 +30,7 @@ Examples: ` func NewMakeBucketCommand() *cli.Command { - return &cli.Command{ + cmd := &cli.Command{ Name: "mb", HelpName: "mb", Usage: "make bucket", @@ -51,6 +54,17 @@ func NewMakeBucketCommand() *cli.Command { }.Run(c.Context) }, } + cmd.BashComplete = func(ctx *cli.Context) { + arg := parseArgumentToComplete(ctx) + if strings.HasPrefix(arg, "-") { + cli.DefaultCompleteWithFlags(cmd)(ctx) + } else { + shell := filepath.Base(os.Getenv("SHELL")) + constantCompleteWithDefault(shell, arg, "s3://") + } + } + + return cmd } // MakeBucket holds bucket creation operation flags and states. diff --git a/command/mv.go b/command/mv.go index fbc4e359d..0693fd3da 100644 --- a/command/mv.go +++ b/command/mv.go @@ -39,7 +39,7 @@ Examples: ` func NewMoveCommand() *cli.Command { - return &cli.Command{ + cmd := &cli.Command{ Name: "mv", HelpName: "mv", Usage: "move/rename objects", @@ -55,4 +55,7 @@ func NewMoveCommand() *cli.Command { return NewCopy(c, true).Run(c.Context) }, } + + cmd.BashComplete = getBashCompleteFn(cmd, false, false) + return cmd } diff --git a/command/rb.go b/command/rb.go index df8eac01a..db4723cd1 100644 --- a/command/rb.go +++ b/command/rb.go @@ -26,7 +26,7 @@ Examples: ` func NewRemoveBucketCommand() *cli.Command { - return &cli.Command{ + cmd := &cli.Command{ Name: "rb", HelpName: "rb", Usage: "remove bucket", @@ -50,6 +50,9 @@ func NewRemoveBucketCommand() *cli.Command { }.Run(c.Context) }, } + + cmd.BashComplete = getBashCompleteFn(cmd, true, true) + return cmd } // RemoveBucket holds bucket deletion operation flags and states. diff --git a/command/rm.go b/command/rm.go index 85d91943b..1f193b0a5 100644 --- a/command/rm.go +++ b/command/rm.go @@ -41,7 +41,7 @@ Examples: ` func NewDeleteCommand() *cli.Command { - return &cli.Command{ + cmd := &cli.Command{ Name: "rm", HelpName: "rm", Usage: "remove objects", @@ -78,6 +78,9 @@ func NewDeleteCommand() *cli.Command { }.Run(c.Context) }, } + + cmd.BashComplete = getBashCompleteFn(cmd, false, false) + return cmd } // Delete holds delete operation flags and states. diff --git a/command/select.go b/command/select.go index 1db02ef7c..99f4cd5bd 100644 --- a/command/select.go +++ b/command/select.go @@ -32,7 +32,7 @@ Examples: ` func NewSelectCommand() *cli.Command { - return &cli.Command{ + cmd := &cli.Command{ Name: "select", HelpName: "select", Usage: "run SQL queries on objects", @@ -94,6 +94,9 @@ func NewSelectCommand() *cli.Command { }.Run(c.Context) }, } + + cmd.BashComplete = getBashCompleteFn(cmd, true, false) + return cmd } // Select holds select operation flags and states. diff --git a/command/sync.go b/command/sync.go index 841766d9d..c35f27c85 100644 --- a/command/sync.go +++ b/command/sync.go @@ -77,7 +77,7 @@ func NewSyncCommandFlags() []cli.Flag { } func NewSyncCommand() *cli.Command { - return &cli.Command{ + cmd := &cli.Command{ Name: "sync", HelpName: "sync", Usage: "sync objects", @@ -97,6 +97,9 @@ func NewSyncCommand() *cli.Command { return NewSync(c).Run(c) }, } + + cmd.BashComplete = getBashCompleteFn(cmd, false, false) + return cmd } type ObjectPair struct { diff --git a/e2e/auto_complete_test.go b/e2e/auto_complete_test.go new file mode 100644 index 000000000..a0da0cee6 --- /dev/null +++ b/e2e/auto_complete_test.go @@ -0,0 +1,264 @@ +package e2e + +import ( + "testing" + + "gotest.tools/v3/icmd" +) + +func TestCompletionFlag(t *testing.T) { + flag := "--generate-bash-completion" + bucket := s3BucketFromTestName(t) + + testcases := []struct { + name string + precedingArgs []string + arg string + remoteFiles []string + expected map[int]compareFunc + shell string + }{ + { + name: "cp complete empty string", + precedingArgs: []string{"cp"}, + arg: "", + // local file completions are prepared by the shell + expected: map[int]compareFunc{}, + shell: "/bin/bash", + }, + { + name: "cat complete empty string", + precedingArgs: []string{"cat"}, + arg: "", + expected: map[int]compareFunc{ + 0: equals("s3://%s/", bucket), + }, + shell: "/bin/pwsh", + }, + { + name: "mb complete empty string", + precedingArgs: []string{"mb"}, + arg: "", + expected: map[int]compareFunc{ + 0: equals("s3://"), + }, + shell: "/bin/pwsh", + }, + { + name: "mb complete bucket", + precedingArgs: []string{"mb"}, + arg: "s3://bu", + expected: map[int]compareFunc{ + 0: equals("s3://bu"), + }, + shell: "/bin/pwsh", + }, + { + name: "rb complete empty string", + precedingArgs: []string{"rb"}, + arg: "", + expected: map[int]compareFunc{ + 0: equals("s3://%s/", bucket), + }, + shell: "/bin/pwsh", + }, + { + name: "rb should not complete keys string", + precedingArgs: []string{"rb"}, + arg: "s3://" + bucket + "/f", + expected: map[int]compareFunc{ + 0: equals("s3://%s/", bucket), + }, + remoteFiles: []string{ + "file.txt", + "fdir/child.txt", + }, + shell: "/bin/pwsh", + }, + { + name: "select complete empty string", + precedingArgs: []string{"select"}, + arg: "", + expected: map[int]compareFunc{ + 0: equals("s3://%s/", bucket), + }, + shell: "/bin/pwsh", + }, + { + name: "cp complete bucket names in pwsh", + precedingArgs: []string{"cp"}, + arg: "s3://", + expected: map[int]compareFunc{ + 0: equals("s3://%s/", bucket), + }, + shell: "/bin/pwsh", + }, + { + name: "cp complete bucket names in zsh", + precedingArgs: []string{"cp"}, + arg: "s3://", + expected: map[int]compareFunc{ + 0: equals("s3\\://%s/", bucket), + }, + shell: "/bin/zsh", + }, + { + name: "cp complete bucket names in bash", + precedingArgs: []string{"cp"}, + arg: "s3://", + expected: map[int]compareFunc{ + 0: equals("//%s/", bucket), + 1: equals("s3://%s/", bucket), + }, + shell: "/bin/bash", + }, + { + name: "cp complete bucket keys pwsh", + precedingArgs: []string{"cp"}, + arg: "s3://" + bucket + "/", + remoteFiles: []string{ + "file0.txt", + "file1.txt", + "filedir/child.txt", + "dir/child.txt", + }, + expected: map[int]compareFunc{ + 0: equals("s3://%s/dir/", bucket), + 1: equals("s3://%s/file0.txt", bucket), + 2: equals("s3://%s/file1.txt", bucket), + 3: equals("s3://%s/filedir/", bucket), + }, + shell: "/bin/pwsh", + }, + { + name: "cp complete bucket keys bash", + precedingArgs: []string{"cp"}, + arg: "s3://" + bucket + "/", + remoteFiles: []string{ + "file0.txt", + "file1.txt", + "filedir/child.txt", + "dir/child.txt", + }, + expected: map[int]compareFunc{ + 0: equals("//%s/dir/", bucket), + 1: equals("//%s/file0.txt", bucket), + 2: equals("//%s/file1.txt", bucket), + 3: equals("//%s/filedir/", bucket), + 4: equals("s3://%s/dir/", bucket), + 5: equals("s3://%s/file0.txt", bucket), + 6: equals("s3://%s/file1.txt", bucket), + 7: equals("s3://%s/filedir/", bucket), + }, + shell: "/bin/bash", + }, + { + name: "cp complete bucket keys zsh", + precedingArgs: []string{"cp"}, + arg: "s3://" + bucket + "/", + remoteFiles: []string{ + "file0.txt", + "file1.txt", + "filedir/child.txt", + "dir/child.txt", + }, + expected: map[int]compareFunc{ + 0: equals("s3\\://%s/dir/", bucket), + 1: equals("s3\\://%s/file0.txt", bucket), + 2: equals("s3\\://%s/file1.txt", bucket), + 3: equals("s3\\://%s/filedir/", bucket), + }, + shell: "/bin/zsh", + }, + { + name: "cp complete keys with colon bash", + precedingArgs: []string{"cp"}, + arg: "s3://" + bucket + "/co:lo", + remoteFiles: []string{ + "co:lon:in:key", + "co:lonized", + }, + expected: map[int]compareFunc{ + 0: equals("lon:in:key"), + 1: equals("lonized"), + 2: equals("s3://%s/co:lon:in:key", bucket), + 3: equals("s3://%s/co:lonized", bucket), + }, + shell: "/bin/bash", + }, + { + name: "cp complete keys with colon zsh", + precedingArgs: []string{"cp"}, + arg: "s3://" + bucket + "/co:lo", + remoteFiles: []string{ + "co:lon:in:key", + "co:lonized", + }, + expected: map[int]compareFunc{ + 0: equals("s3\\://%s/co\\:lon\\:in\\:key", bucket), + 1: equals("s3\\://%s/co\\:lonized", bucket), + }, + shell: "/bin/zsh", + }, + { + name: "cp complete keys with backslash", + precedingArgs: []string{"cp"}, + arg: "s3://" + bucket + "/back\\", + remoteFiles: []string{ + `back\slash`, + `backback`, + }, + expected: map[int]compareFunc{ + 0: equals("s3://%s/back\\slash", bucket), + }, + shell: "/bin/pwsh", + }, + { + name: "cp complete keys with asterisk", + precedingArgs: []string{"cp"}, + arg: "s3://" + bucket + "/as*", + remoteFiles: []string{ + "as*terisk", + "as*oburiks", + }, + expected: map[int]compareFunc{ + 0: equals("s3://%s/as*oburiks", bucket), + 1: equals("s3://%s/as*terisk", bucket), + }, + shell: "/bin/pwsh", + }, + /* + Question marks and asterisk are thought to be wildcard (special charactes) + by the s5cmd so when they're given s5cmd's behaviour changes. + + When asterisk is given s5cmd also matches the keys with literal '*' as well as + all keys that match the URL'S regexp. So the completions with '*' accidentally include the + keys that contains '*' while the shell scripts filter out those that does not have '*'s. + + On the other hand when the question mark is given then s5cmd do not list keys + if it is the last character. Because the ? represent a single character and + it is not expanded to complete remaining of the key. + */ + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + s3client, s5cmd := setup(t) + + // prepare remote bucket content + createBucket(t, s3client, bucket) + + for _, f := range tc.remoteFiles { + putFile(t, s3client, bucket, f, "content") + } + + cmd := s5cmd(append(tc.precedingArgs, tc.arg, flag)...) + res := icmd.RunCmd(cmd, withEnv("SHELL", tc.shell)) + + assertLines(t, res.Stdout(), tc.expected, sortInput(true)) + }) + } +} diff --git a/e2e/util_test.go b/e2e/util_test.go index 345aa0cc5..0c2c2aa84 100644 --- a/e2e/util_test.go +++ b/e2e/util_test.go @@ -390,6 +390,16 @@ func withWorkingDir(dir *fs.Dir) func(*icmd.Cmd) { } } +func withEnv(key, value string) func(*icmd.Cmd) { + return func(cmd *icmd.Cmd) { + if i := indexSlice(cmd.Env, key+"=", strings.HasPrefix); i > 0 { + cmd.Env[i] = key + "=" + value + } else { + cmd.Env = append(cmd.Env, key+"="+value) + } + } +} + type compareFunc func(string) error type assertOpts struct { @@ -667,3 +677,12 @@ func (l *fixedTimeSource) Advance(by time.Duration) { l.time = l.time.Add(by) } + +func indexSlice(slice []string, target string, fn func(str, target string) bool) int { + for i, str := range slice { + if fn(str, target) { + return i + } + } + return -1 +} diff --git a/go.mod b/go.mod index 6db9ffbf8..58078c665 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/igungor/gofakes3 v0.0.11 github.com/karrick/godirwalk v1.15.3 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/posener/complete v1.2.3 github.com/stretchr/testify v1.4.0 github.com/termie/go-shutil v0.0.0-20140729215957-bcacb06fecae github.com/urfave/cli/v2 v2.11.2 diff --git a/go.sum b/go.sum index 152f27312..b9bf1cdf8 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= diff --git a/vendor/github.com/posener/complete/.gitignore b/vendor/github.com/posener/complete/.gitignore deleted file mode 100644 index 293955f99..000000000 --- a/vendor/github.com/posener/complete/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.idea -coverage.txt -gocomplete/gocomplete -example/self/self diff --git a/vendor/github.com/posener/complete/.travis.yml b/vendor/github.com/posener/complete/.travis.yml deleted file mode 100644 index 6ba8d865b..000000000 --- a/vendor/github.com/posener/complete/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: go -go: - - tip - - 1.12.x - - 1.11.x - - 1.10.x - -script: - - go test -race -coverprofile=coverage.txt -covermode=atomic ./... - -after_success: - - bash <(curl -s https://codecov.io/bash) - -matrix: - allow_failures: - - go: tip \ No newline at end of file diff --git a/vendor/github.com/posener/complete/LICENSE.txt b/vendor/github.com/posener/complete/LICENSE.txt deleted file mode 100644 index 16249b4a1..000000000 --- a/vendor/github.com/posener/complete/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License - -Copyright (c) 2017 Eyal Posener - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/posener/complete/README.md b/vendor/github.com/posener/complete/README.md deleted file mode 100644 index dcc6c8932..000000000 --- a/vendor/github.com/posener/complete/README.md +++ /dev/null @@ -1,131 +0,0 @@ -# complete - -[![Build Status](https://travis-ci.org/posener/complete.svg?branch=master)](https://travis-ci.org/posener/complete) -[![codecov](https://codecov.io/gh/posener/complete/branch/master/graph/badge.svg)](https://codecov.io/gh/posener/complete) -[![golangci](https://golangci.com/badges/github.com/posener/complete.svg)](https://golangci.com/r/github.com/posener/complete) -[![GoDoc](https://godoc.org/github.com/posener/complete?status.svg)](http://godoc.org/github.com/posener/complete) -[![goreadme](https://goreadme.herokuapp.com/badge/posener/complete.svg)](https://goreadme.herokuapp.com) - -Package complete provides a tool for bash writing bash completion in go, and bash completion for the go command line. - -Writing bash completion scripts is a hard work. This package provides an easy way -to create bash completion scripts for any command, and also an easy way to install/uninstall -the completion of the command. - -#### Go Command Bash Completion - -In [./cmd/gocomplete](./cmd/gocomplete) there is an example for bash completion for the `go` command line. - -This is an example that uses the `complete` package on the `go` command - the `complete` package -can also be used to implement any completions, see #usage. - -#### Install - -1. Type in your shell: - -```go -go get -u github.com/posener/complete/gocomplete -gocomplete -install -``` - -2. Restart your shell - -Uninstall by `gocomplete -uninstall` - -#### Features - -- Complete `go` command, including sub commands and all flags. -- Complete packages names or `.go` files when necessary. -- Complete test names after `-run` flag. - -#### Complete package - -Supported shells: - -- [x] bash -- [x] zsh -- [x] fish - -#### Usage - -Assuming you have program called `run` and you want to have bash completion -for it, meaning, if you type `run` then space, then press the `Tab` key, -the shell will suggest relevant complete options. - -In that case, we will create a program called `runcomplete`, a go program, -with a `func main()` and so, that will make the completion of the `run` -program. Once the `runcomplete` will be in a binary form, we could -`runcomplete -install` and that will add to our shell all the bash completion -options for `run`. - -So here it is: - -```go -import "github.com/posener/complete" - -func main() { - - // create a Command object, that represents the command we want - // to complete. - run := complete.Command{ - - // Sub defines a list of sub commands of the program, - // this is recursive, since every command is of type command also. - Sub: complete.Commands{ - - // add a build sub command - "build": complete.Command { - - // define flags of the build sub command - Flags: complete.Flags{ - // build sub command has a flag '-cpus', which - // expects number of cpus after it. in that case - // anything could complete this flag. - "-cpus": complete.PredictAnything, - }, - }, - }, - - // define flags of the 'run' main command - Flags: complete.Flags{ - // a flag -o, which expects a file ending with .out after - // it, the tab completion will auto complete for files matching - // the given pattern. - "-o": complete.PredictFiles("*.out"), - }, - - // define global flags of the 'run' main command - // those will show up also when a sub command was entered in the - // command line - GlobalFlags: complete.Flags{ - - // a flag '-h' which does not expects anything after it - "-h": complete.PredictNothing, - }, - } - - // run the command completion, as part of the main() function. - // this triggers the autocompletion when needed. - // name must be exactly as the binary that we want to complete. - complete.New("run", run).Run() -} -``` - -#### Self completing program - -In case that the program that we want to complete is written in go we -can make it self completing. -Here is an example: [./example/self/main.go](./example/self/main.go) . - -## Sub Packages - -* [cmd](./cmd): Package cmd used for command line options for the complete tool - -* [gocomplete](./gocomplete): Package main is complete tool for the go command line - -* [match](./match): Package match contains matchers that decide if to apply completion. - - ---- - -Created by [goreadme](https://github.com/apps/goreadme) diff --git a/vendor/github.com/posener/complete/args.go b/vendor/github.com/posener/complete/args.go deleted file mode 100644 index 3340285e1..000000000 --- a/vendor/github.com/posener/complete/args.go +++ /dev/null @@ -1,114 +0,0 @@ -package complete - -import ( - "os" - "path/filepath" - "strings" - "unicode" -) - -// Args describes command line arguments -type Args struct { - // All lists of all arguments in command line (not including the command itself) - All []string - // Completed lists of all completed arguments in command line, - // If the last one is still being typed - no space after it, - // it won't appear in this list of arguments. - Completed []string - // Last argument in command line, the one being typed, if the last - // character in the command line is a space, this argument will be empty, - // otherwise this would be the last word. - Last string - // LastCompleted is the last argument that was fully typed. - // If the last character in the command line is space, this would be the - // last word, otherwise, it would be the word before that. - LastCompleted string -} - -// Directory gives the directory of the current written -// last argument if it represents a file name being written. -// in case that it is not, we fall back to the current directory. -// -// Deprecated. -func (a Args) Directory() string { - if info, err := os.Stat(a.Last); err == nil && info.IsDir() { - return fixPathForm(a.Last, a.Last) - } - dir := filepath.Dir(a.Last) - if info, err := os.Stat(dir); err != nil || !info.IsDir() { - return "./" - } - return fixPathForm(a.Last, dir) -} - -func newArgs(line string) Args { - var ( - all []string - completed []string - ) - parts := splitFields(line) - if len(parts) > 0 { - all = parts[1:] - completed = removeLast(parts[1:]) - } - return Args{ - All: all, - Completed: completed, - Last: last(parts), - LastCompleted: last(completed), - } -} - -// splitFields returns a list of fields from the given command line. -// If the last character is space, it appends an empty field in the end -// indicating that the field before it was completed. -// If the last field is of the form "a=b", it splits it to two fields: "a", "b", -// So it can be completed. -func splitFields(line string) []string { - parts := strings.Fields(line) - - // Add empty field if the last field was completed. - if len(line) > 0 && unicode.IsSpace(rune(line[len(line)-1])) { - parts = append(parts, "") - } - - // Treat the last field if it is of the form "a=b" - parts = splitLastEqual(parts) - return parts -} - -func splitLastEqual(line []string) []string { - if len(line) == 0 { - return line - } - parts := strings.Split(line[len(line)-1], "=") - return append(line[:len(line)-1], parts...) -} - -// from returns a copy of Args of all arguments after the i'th argument. -func (a Args) from(i int) Args { - if i >= len(a.All) { - i = len(a.All) - 1 - } - a.All = a.All[i+1:] - - if i >= len(a.Completed) { - i = len(a.Completed) - 1 - } - a.Completed = a.Completed[i+1:] - return a -} - -func removeLast(a []string) []string { - if len(a) > 0 { - return a[:len(a)-1] - } - return a -} - -func last(args []string) string { - if len(args) == 0 { - return "" - } - return args[len(args)-1] -} diff --git a/vendor/github.com/posener/complete/cmd/cmd.go b/vendor/github.com/posener/complete/cmd/cmd.go deleted file mode 100644 index b99fe5290..000000000 --- a/vendor/github.com/posener/complete/cmd/cmd.go +++ /dev/null @@ -1,128 +0,0 @@ -// Package cmd used for command line options for the complete tool -package cmd - -import ( - "errors" - "flag" - "fmt" - "os" - "strings" - - "github.com/posener/complete/cmd/install" -) - -// CLI for command line -type CLI struct { - Name string - InstallName string - UninstallName string - - install bool - uninstall bool - yes bool -} - -const ( - defaultInstallName = "install" - defaultUninstallName = "uninstall" -) - -// Run is used when running complete in command line mode. -// this is used when the complete is not completing words, but to -// install it or uninstall it. -func (f *CLI) Run() bool { - err := f.validate() - if err != nil { - os.Stderr.WriteString(err.Error() + "\n") - os.Exit(1) - } - - switch { - case f.install: - f.prompt() - err = install.Install(f.Name) - case f.uninstall: - f.prompt() - err = install.Uninstall(f.Name) - default: - // non of the action flags matched, - // returning false should make the real program execute - return false - } - - if err != nil { - fmt.Printf("%s failed! %s\n", f.action(), err) - os.Exit(3) - } - fmt.Println("Done!") - return true -} - -// prompt use for approval -// exit if approval was not given -func (f *CLI) prompt() { - defer fmt.Println(f.action() + "ing...") - if f.yes { - return - } - fmt.Printf("%s completion for %s? ", f.action(), f.Name) - var answer string - fmt.Scanln(&answer) - - switch strings.ToLower(answer) { - case "y", "yes": - return - default: - fmt.Println("Cancelling...") - os.Exit(1) - } -} - -// AddFlags adds the CLI flags to the flag set. -// If flags is nil, the default command line flags will be taken. -// Pass non-empty strings as installName and uninstallName to override the default -// flag names. -func (f *CLI) AddFlags(flags *flag.FlagSet) { - if flags == nil { - flags = flag.CommandLine - } - - if f.InstallName == "" { - f.InstallName = defaultInstallName - } - if f.UninstallName == "" { - f.UninstallName = defaultUninstallName - } - - if flags.Lookup(f.InstallName) == nil { - flags.BoolVar(&f.install, f.InstallName, false, - fmt.Sprintf("Install completion for %s command", f.Name)) - } - if flags.Lookup(f.UninstallName) == nil { - flags.BoolVar(&f.uninstall, f.UninstallName, false, - fmt.Sprintf("Uninstall completion for %s command", f.Name)) - } - if flags.Lookup("y") == nil { - flags.BoolVar(&f.yes, "y", false, "Don't prompt user for typing 'yes' when installing completion") - } -} - -// validate the CLI -func (f *CLI) validate() error { - if f.install && f.uninstall { - return errors.New("Install and uninstall are mutually exclusive") - } - return nil -} - -// action name according to the CLI values. -func (f *CLI) action() string { - switch { - case f.install: - return "Install" - case f.uninstall: - return "Uninstall" - default: - return "unknown" - } -} diff --git a/vendor/github.com/posener/complete/cmd/install/bash.go b/vendor/github.com/posener/complete/cmd/install/bash.go deleted file mode 100644 index 17c64de13..000000000 --- a/vendor/github.com/posener/complete/cmd/install/bash.go +++ /dev/null @@ -1,37 +0,0 @@ -package install - -import "fmt" - -// (un)install in bash -// basically adds/remove from .bashrc: -// -// complete -C -type bash struct { - rc string -} - -func (b bash) IsInstalled(cmd, bin string) bool { - completeCmd := b.cmd(cmd, bin) - return lineInFile(b.rc, completeCmd) -} - -func (b bash) Install(cmd, bin string) error { - if b.IsInstalled(cmd, bin) { - return fmt.Errorf("already installed in %s", b.rc) - } - completeCmd := b.cmd(cmd, bin) - return appendToFile(b.rc, completeCmd) -} - -func (b bash) Uninstall(cmd, bin string) error { - if !b.IsInstalled(cmd, bin) { - return fmt.Errorf("does not installed in %s", b.rc) - } - - completeCmd := b.cmd(cmd, bin) - return removeFromFile(b.rc, completeCmd) -} - -func (bash) cmd(cmd, bin string) string { - return fmt.Sprintf("complete -C %s %s", bin, cmd) -} diff --git a/vendor/github.com/posener/complete/cmd/install/fish.go b/vendor/github.com/posener/complete/cmd/install/fish.go deleted file mode 100644 index 2b64bfc83..000000000 --- a/vendor/github.com/posener/complete/cmd/install/fish.go +++ /dev/null @@ -1,69 +0,0 @@ -package install - -import ( - "bytes" - "fmt" - "os" - "path/filepath" - "text/template" -) - -// (un)install in fish - -type fish struct { - configDir string -} - -func (f fish) IsInstalled(cmd, bin string) bool { - completionFile := f.getCompletionFilePath(cmd) - if _, err := os.Stat(completionFile); err == nil { - return true - } - return false -} - -func (f fish) Install(cmd, bin string) error { - if f.IsInstalled(cmd, bin) { - return fmt.Errorf("already installed at %s", f.getCompletionFilePath(cmd)) - } - - completionFile := f.getCompletionFilePath(cmd) - completeCmd, err := f.cmd(cmd, bin) - if err != nil { - return err - } - - return createFile(completionFile, completeCmd) -} - -func (f fish) Uninstall(cmd, bin string) error { - if !f.IsInstalled(cmd, bin) { - return fmt.Errorf("does not installed in %s", f.configDir) - } - - completionFile := f.getCompletionFilePath(cmd) - return os.Remove(completionFile) -} - -func (f fish) getCompletionFilePath(cmd string) string { - return filepath.Join(f.configDir, "completions", fmt.Sprintf("%s.fish", cmd)) -} - -func (f fish) cmd(cmd, bin string) (string, error) { - var buf bytes.Buffer - params := struct{ Cmd, Bin string }{cmd, bin} - tmpl := template.Must(template.New("cmd").Parse(` -function __complete_{{.Cmd}} - set -lx COMP_LINE (commandline -cp) - test -z (commandline -ct) - and set COMP_LINE "$COMP_LINE " - {{.Bin}} -end -complete -f -c {{.Cmd}} -a "(__complete_{{.Cmd}})" -`)) - err := tmpl.Execute(&buf, params) - if err != nil { - return "", err - } - return buf.String(), nil -} diff --git a/vendor/github.com/posener/complete/cmd/install/install.go b/vendor/github.com/posener/complete/cmd/install/install.go deleted file mode 100644 index 884c23f5b..000000000 --- a/vendor/github.com/posener/complete/cmd/install/install.go +++ /dev/null @@ -1,148 +0,0 @@ -package install - -import ( - "errors" - "os" - "os/user" - "path/filepath" - "runtime" - - "github.com/hashicorp/go-multierror" -) - -type installer interface { - IsInstalled(cmd, bin string) bool - Install(cmd, bin string) error - Uninstall(cmd, bin string) error -} - -// Install complete command given: -// cmd: is the command name -func Install(cmd string) error { - is := installers() - if len(is) == 0 { - return errors.New("Did not find any shells to install") - } - bin, err := getBinaryPath() - if err != nil { - return err - } - - for _, i := range is { - errI := i.Install(cmd, bin) - if errI != nil { - err = multierror.Append(err, errI) - } - } - - return err -} - -// IsInstalled returns true if the completion -// for the given cmd is installed. -func IsInstalled(cmd string) bool { - bin, err := getBinaryPath() - if err != nil { - return false - } - - for _, i := range installers() { - installed := i.IsInstalled(cmd, bin) - if installed { - return true - } - } - - return false -} - -// Uninstall complete command given: -// cmd: is the command name -func Uninstall(cmd string) error { - is := installers() - if len(is) == 0 { - return errors.New("Did not find any shells to uninstall") - } - bin, err := getBinaryPath() - if err != nil { - return err - } - - for _, i := range is { - errI := i.Uninstall(cmd, bin) - if errI != nil { - err = multierror.Append(err, errI) - } - } - - return err -} - -func installers() (i []installer) { - // The list of bash config files candidates where it is - // possible to install the completion command. - var bashConfFiles []string - switch runtime.GOOS { - case "darwin": - bashConfFiles = []string{".bash_profile"} - default: - bashConfFiles = []string{".bashrc", ".bash_profile", ".bash_login", ".profile"} - } - for _, rc := range bashConfFiles { - if f := rcFile(rc); f != "" { - i = append(i, bash{f}) - break - } - } - if f := rcFile(".zshrc"); f != "" { - i = append(i, zsh{f}) - } - if d := fishConfigDir(); d != "" { - i = append(i, fish{d}) - } - return -} - -func fishConfigDir() string { - configDir := filepath.Join(getConfigHomePath(), "fish") - if configDir == "" { - return "" - } - if info, err := os.Stat(configDir); err != nil || !info.IsDir() { - return "" - } - return configDir -} - -func getConfigHomePath() string { - u, err := user.Current() - if err != nil { - return "" - } - - configHome := os.Getenv("XDG_CONFIG_HOME") - if configHome == "" { - return filepath.Join(u.HomeDir, ".config") - } - return configHome -} - -func getBinaryPath() (string, error) { - bin, err := os.Executable() - if err != nil { - return "", err - } - return filepath.Abs(bin) -} - -func rcFile(name string) string { - u, err := user.Current() - if err != nil { - return "" - } - path := filepath.Join(u.HomeDir, name) - if _, err := os.Stat(path); err != nil { - return "" - } - return path -} diff --git a/vendor/github.com/posener/complete/cmd/install/utils.go b/vendor/github.com/posener/complete/cmd/install/utils.go deleted file mode 100644 index d34ac8cae..000000000 --- a/vendor/github.com/posener/complete/cmd/install/utils.go +++ /dev/null @@ -1,140 +0,0 @@ -package install - -import ( - "bufio" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" -) - -func lineInFile(name string, lookFor string) bool { - f, err := os.Open(name) - if err != nil { - return false - } - defer f.Close() - r := bufio.NewReader(f) - prefix := []byte{} - for { - line, isPrefix, err := r.ReadLine() - if err == io.EOF { - return false - } - if err != nil { - return false - } - if isPrefix { - prefix = append(prefix, line...) - continue - } - line = append(prefix, line...) - if string(line) == lookFor { - return true - } - prefix = prefix[:0] - } -} - -func createFile(name string, content string) error { - // make sure file directory exists - if err := os.MkdirAll(filepath.Dir(name), 0775); err != nil { - return err - } - - // create the file - f, err := os.Create(name) - if err != nil { - return err - } - defer f.Close() - - // write file content - _, err = f.WriteString(fmt.Sprintf("%s\n", content)) - return err -} - -func appendToFile(name string, content string) error { - f, err := os.OpenFile(name, os.O_RDWR|os.O_APPEND, 0) - if err != nil { - return err - } - defer f.Close() - _, err = f.WriteString(fmt.Sprintf("\n%s\n", content)) - return err -} - -func removeFromFile(name string, content string) error { - backup := name + ".bck" - err := copyFile(name, backup) - if err != nil { - return err - } - temp, err := removeContentToTempFile(name, content) - if err != nil { - return err - } - - err = copyFile(temp, name) - if err != nil { - return err - } - - return os.Remove(backup) -} - -func removeContentToTempFile(name, content string) (string, error) { - rf, err := os.Open(name) - if err != nil { - return "", err - } - defer rf.Close() - wf, err := ioutil.TempFile("/tmp", "complete-") - if err != nil { - return "", err - } - defer wf.Close() - - r := bufio.NewReader(rf) - prefix := []byte{} - for { - line, isPrefix, err := r.ReadLine() - if err == io.EOF { - break - } - if err != nil { - return "", err - } - if isPrefix { - prefix = append(prefix, line...) - continue - } - line = append(prefix, line...) - str := string(line) - if str == content { - continue - } - _, err = wf.WriteString(str + "\n") - if err != nil { - return "", err - } - prefix = prefix[:0] - } - return wf.Name(), nil -} - -func copyFile(src string, dst string) error { - in, err := os.Open(src) - if err != nil { - return err - } - defer in.Close() - out, err := os.Create(dst) - if err != nil { - return err - } - defer out.Close() - _, err = io.Copy(out, in) - return err -} diff --git a/vendor/github.com/posener/complete/cmd/install/zsh.go b/vendor/github.com/posener/complete/cmd/install/zsh.go deleted file mode 100644 index 29950ab17..000000000 --- a/vendor/github.com/posener/complete/cmd/install/zsh.go +++ /dev/null @@ -1,44 +0,0 @@ -package install - -import "fmt" - -// (un)install in zsh -// basically adds/remove from .zshrc: -// -// autoload -U +X bashcompinit && bashcompinit" -// complete -C -type zsh struct { - rc string -} - -func (z zsh) IsInstalled(cmd, bin string) bool { - completeCmd := z.cmd(cmd, bin) - return lineInFile(z.rc, completeCmd) -} - -func (z zsh) Install(cmd, bin string) error { - if z.IsInstalled(cmd, bin) { - return fmt.Errorf("already installed in %s", z.rc) - } - - completeCmd := z.cmd(cmd, bin) - bashCompInit := "autoload -U +X bashcompinit && bashcompinit" - if !lineInFile(z.rc, bashCompInit) { - completeCmd = bashCompInit + "\n" + completeCmd - } - - return appendToFile(z.rc, completeCmd) -} - -func (z zsh) Uninstall(cmd, bin string) error { - if !z.IsInstalled(cmd, bin) { - return fmt.Errorf("does not installed in %s", z.rc) - } - - completeCmd := z.cmd(cmd, bin) - return removeFromFile(z.rc, completeCmd) -} - -func (zsh) cmd(cmd, bin string) string { - return fmt.Sprintf("complete -o nospace -C %s %s", bin, cmd) -} diff --git a/vendor/github.com/posener/complete/command.go b/vendor/github.com/posener/complete/command.go deleted file mode 100644 index 82d37d529..000000000 --- a/vendor/github.com/posener/complete/command.go +++ /dev/null @@ -1,111 +0,0 @@ -package complete - -// Command represents a command line -// It holds the data that enables auto completion of command line -// Command can also be a sub command. -type Command struct { - // Sub is map of sub commands of the current command - // The key refer to the sub command name, and the value is it's - // Command descriptive struct. - Sub Commands - - // Flags is a map of flags that the command accepts. - // The key is the flag name, and the value is it's predictions. - Flags Flags - - // GlobalFlags is a map of flags that the command accepts. - // Global flags that can appear also after a sub command. - GlobalFlags Flags - - // Args are extra arguments that the command accepts, those who are - // given without any flag before. - Args Predictor -} - -// Predict returns all possible predictions for args according to the command struct -func (c *Command) Predict(a Args) []string { - options, _ := c.predict(a) - return options -} - -// Commands is the type of Sub member, it maps a command name to a command struct -type Commands map[string]Command - -// Predict completion of sub command names names according to command line arguments -func (c Commands) Predict(a Args) (prediction []string) { - for sub := range c { - prediction = append(prediction, sub) - } - return -} - -// Flags is the type Flags of the Flags member, it maps a flag name to the flag predictions. -type Flags map[string]Predictor - -// Predict completion of flags names according to command line arguments -func (f Flags) Predict(a Args) (prediction []string) { - for flag := range f { - // If the flag starts with a hyphen, we avoid emitting the prediction - // unless the last typed arg contains a hyphen as well. - flagHyphenStart := len(flag) != 0 && flag[0] == '-' - lastHyphenStart := len(a.Last) != 0 && a.Last[0] == '-' - if flagHyphenStart && !lastHyphenStart { - continue - } - prediction = append(prediction, flag) - } - return -} - -// predict options -// only is set to true if no more options are allowed to be returned -// those are in cases of special flag that has specific completion arguments, -// and other flags or sub commands can't come after it. -func (c *Command) predict(a Args) (options []string, only bool) { - - // search sub commands for predictions first - subCommandFound := false - for i, arg := range a.Completed { - if cmd, ok := c.Sub[arg]; ok { - subCommandFound = true - - // recursive call for sub command - options, only = cmd.predict(a.from(i)) - if only { - return - } - - // We matched so stop searching. Continuing to search can accidentally - // match a subcommand with current set of commands, see issue #46. - break - } - } - - // if last completed word is a global flag that we need to complete - if predictor, ok := c.GlobalFlags[a.LastCompleted]; ok && predictor != nil { - Log("Predicting according to global flag %s", a.LastCompleted) - return predictor.Predict(a), true - } - - options = append(options, c.GlobalFlags.Predict(a)...) - - // if a sub command was entered, we won't add the parent command - // completions and we return here. - if subCommandFound { - return - } - - // if last completed word is a command flag that we need to complete - if predictor, ok := c.Flags[a.LastCompleted]; ok && predictor != nil { - Log("Predicting according to flag %s", a.LastCompleted) - return predictor.Predict(a), true - } - - options = append(options, c.Sub.Predict(a)...) - options = append(options, c.Flags.Predict(a)...) - if c.Args != nil { - options = append(options, c.Args.Predict(a)...) - } - - return -} diff --git a/vendor/github.com/posener/complete/complete.go b/vendor/github.com/posener/complete/complete.go deleted file mode 100644 index 423cbec6c..000000000 --- a/vendor/github.com/posener/complete/complete.go +++ /dev/null @@ -1,104 +0,0 @@ -package complete - -import ( - "flag" - "fmt" - "io" - "os" - "strconv" - "strings" - - "github.com/posener/complete/cmd" -) - -const ( - envLine = "COMP_LINE" - envPoint = "COMP_POINT" - envDebug = "COMP_DEBUG" -) - -// Complete structs define completion for a command with CLI options -type Complete struct { - Command Command - cmd.CLI - Out io.Writer -} - -// New creates a new complete command. -// name is the name of command we want to auto complete. -// IMPORTANT: it must be the same name - if the auto complete -// completes the 'go' command, name must be equal to "go". -// command is the struct of the command completion. -func New(name string, command Command) *Complete { - return &Complete{ - Command: command, - CLI: cmd.CLI{Name: name}, - Out: os.Stdout, - } -} - -// Run runs the completion and add installation flags beforehand. -// The flags are added to the main flag CommandLine variable. -func (c *Complete) Run() bool { - c.AddFlags(nil) - flag.Parse() - return c.Complete() -} - -// Complete a command from completion line in environment variable, -// and print out the complete options. -// returns success if the completion ran or if the cli matched -// any of the given flags, false otherwise -// For installation: it assumes that flags were added and parsed before -// it was called. -func (c *Complete) Complete() bool { - line, point, ok := getEnv() - if !ok { - // make sure flags parsed, - // in case they were not added in the main program - return c.CLI.Run() - } - - if point >= 0 && point < len(line) { - line = line[:point] - } - - Log("Completing phrase: %s", line) - a := newArgs(line) - Log("Completing last field: %s", a.Last) - options := c.Command.Predict(a) - Log("Options: %s", options) - - // filter only options that match the last argument - matches := []string{} - for _, option := range options { - if strings.HasPrefix(option, a.Last) { - matches = append(matches, option) - } - } - Log("Matches: %s", matches) - c.output(matches) - return true -} - -func getEnv() (line string, point int, ok bool) { - line = os.Getenv(envLine) - if line == "" { - return - } - point, err := strconv.Atoi(os.Getenv(envPoint)) - if err != nil { - // If failed parsing point for some reason, set it to point - // on the end of the line. - Log("Failed parsing point %s: %v", os.Getenv(envPoint), err) - point = len(line) - } - return line, point, true -} - -func (c *Complete) output(options []string) { - // stdout of program defines the complete options - for _, option := range options { - fmt.Fprintln(c.Out, option) - } -} diff --git a/vendor/github.com/posener/complete/doc.go b/vendor/github.com/posener/complete/doc.go deleted file mode 100644 index 0ae09a1b7..000000000 --- a/vendor/github.com/posener/complete/doc.go +++ /dev/null @@ -1,110 +0,0 @@ -/* -Package complete provides a tool for bash writing bash completion in go, and bash completion for the go command line. - -Writing bash completion scripts is a hard work. This package provides an easy way -to create bash completion scripts for any command, and also an easy way to install/uninstall -the completion of the command. - -Go Command Bash Completion - -In ./cmd/gocomplete there is an example for bash completion for the `go` command line. - -This is an example that uses the `complete` package on the `go` command - the `complete` package -can also be used to implement any completions, see #usage. - -Install - -1. Type in your shell: - - go get -u github.com/posener/complete/gocomplete - gocomplete -install - -2. Restart your shell - -Uninstall by `gocomplete -uninstall` - -Features - -- Complete `go` command, including sub commands and all flags. -- Complete packages names or `.go` files when necessary. -- Complete test names after `-run` flag. - -Complete package - -Supported shells: - -- [x] bash -- [x] zsh -- [x] fish - -Usage - -Assuming you have program called `run` and you want to have bash completion -for it, meaning, if you type `run` then space, then press the `Tab` key, -the shell will suggest relevant complete options. - -In that case, we will create a program called `runcomplete`, a go program, -with a `func main()` and so, that will make the completion of the `run` -program. Once the `runcomplete` will be in a binary form, we could -`runcomplete -install` and that will add to our shell all the bash completion -options for `run`. - -So here it is: - - import "github.com/posener/complete" - - func main() { - - // create a Command object, that represents the command we want - // to complete. - run := complete.Command{ - - // Sub defines a list of sub commands of the program, - // this is recursive, since every command is of type command also. - Sub: complete.Commands{ - - // add a build sub command - "build": complete.Command { - - // define flags of the build sub command - Flags: complete.Flags{ - // build sub command has a flag '-cpus', which - // expects number of cpus after it. in that case - // anything could complete this flag. - "-cpus": complete.PredictAnything, - }, - }, - }, - - // define flags of the 'run' main command - Flags: complete.Flags{ - // a flag -o, which expects a file ending with .out after - // it, the tab completion will auto complete for files matching - // the given pattern. - "-o": complete.PredictFiles("*.out"), - }, - - // define global flags of the 'run' main command - // those will show up also when a sub command was entered in the - // command line - GlobalFlags: complete.Flags{ - - // a flag '-h' which does not expects anything after it - "-h": complete.PredictNothing, - }, - } - - // run the command completion, as part of the main() function. - // this triggers the autocompletion when needed. - // name must be exactly as the binary that we want to complete. - complete.New("run", run).Run() - } - -Self completing program - -In case that the program that we want to complete is written in go we -can make it self completing. -Here is an example: ./example/self/main.go . - -*/ -package complete diff --git a/vendor/github.com/posener/complete/goreadme.json b/vendor/github.com/posener/complete/goreadme.json deleted file mode 100644 index 025ec76c9..000000000 --- a/vendor/github.com/posener/complete/goreadme.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "badges": { - "travis_ci": true, - "code_cov": true, - "golang_ci": true, - "go_doc": true, - "goreadme": true - } -} \ No newline at end of file diff --git a/vendor/github.com/posener/complete/log.go b/vendor/github.com/posener/complete/log.go deleted file mode 100644 index c3029556e..000000000 --- a/vendor/github.com/posener/complete/log.go +++ /dev/null @@ -1,22 +0,0 @@ -package complete - -import ( - "io/ioutil" - "log" - "os" -) - -// Log is used for debugging purposes -// since complete is running on tab completion, it is nice to -// have logs to the stderr (when writing your own completer) -// to write logs, set the COMP_DEBUG environment variable and -// use complete.Log in the complete program -var Log = getLogger() - -func getLogger() func(format string, args ...interface{}) { - var logfile = ioutil.Discard - if os.Getenv(envDebug) != "" { - logfile = os.Stderr - } - return log.New(logfile, "complete ", log.Flags()).Printf -} diff --git a/vendor/github.com/posener/complete/predict.go b/vendor/github.com/posener/complete/predict.go deleted file mode 100644 index 820706325..000000000 --- a/vendor/github.com/posener/complete/predict.go +++ /dev/null @@ -1,41 +0,0 @@ -package complete - -// Predictor implements a predict method, in which given -// command line arguments returns a list of options it predicts. -type Predictor interface { - Predict(Args) []string -} - -// PredictOr unions two predicate functions, so that the result predicate -// returns the union of their predication -func PredictOr(predictors ...Predictor) Predictor { - return PredictFunc(func(a Args) (prediction []string) { - for _, p := range predictors { - if p == nil { - continue - } - prediction = append(prediction, p.Predict(a)...) - } - return - }) -} - -// PredictFunc determines what terms can follow a command or a flag -// It is used for auto completion, given last - the last word in the already -// in the command line, what words can complete it. -type PredictFunc func(Args) []string - -// Predict invokes the predict function and implements the Predictor interface -func (p PredictFunc) Predict(a Args) []string { - if p == nil { - return nil - } - return p(a) -} - -// PredictNothing does not expect anything after. -var PredictNothing Predictor - -// PredictAnything expects something, but nothing particular, such as a number -// or arbitrary name. -var PredictAnything = PredictFunc(func(Args) []string { return nil }) diff --git a/vendor/github.com/posener/complete/predict_files.go b/vendor/github.com/posener/complete/predict_files.go deleted file mode 100644 index 25ae2d514..000000000 --- a/vendor/github.com/posener/complete/predict_files.go +++ /dev/null @@ -1,174 +0,0 @@ -package complete - -import ( - "io/ioutil" - "os" - "path/filepath" - "strings" -) - -// PredictDirs will search for directories in the given started to be typed -// path, if no path was started to be typed, it will complete to directories -// in the current working directory. -func PredictDirs(pattern string) Predictor { - return files(pattern, false) -} - -// PredictFiles will search for files matching the given pattern in the started to -// be typed path, if no path was started to be typed, it will complete to files that -// match the pattern in the current working directory. -// To match any file, use "*" as pattern. To match go files use "*.go", and so on. -func PredictFiles(pattern string) Predictor { - return files(pattern, true) -} - -func files(pattern string, allowFiles bool) PredictFunc { - - // search for files according to arguments, - // if only one directory has matched the result, search recursively into - // this directory to give more results. - return func(a Args) (prediction []string) { - prediction = predictFiles(a, pattern, allowFiles) - - // if the number of prediction is not 1, we either have many results or - // have no results, so we return it. - if len(prediction) != 1 { - return - } - - // only try deeper, if the one item is a directory - if stat, err := os.Stat(prediction[0]); err != nil || !stat.IsDir() { - return - } - - a.Last = prediction[0] - return predictFiles(a, pattern, allowFiles) - } -} - -func predictFiles(a Args, pattern string, allowFiles bool) []string { - if strings.HasSuffix(a.Last, "/..") { - return nil - } - - dir := directory(a.Last) - files := listFiles(dir, pattern, allowFiles) - - // add dir if match - files = append(files, dir) - - return PredictFilesSet(files).Predict(a) -} - -// directory gives the directory of the given partial path -// in case that it is not, we fall back to the current directory. -func directory(path string) string { - if info, err := os.Stat(path); err == nil && info.IsDir() { - return fixPathForm(path, path) - } - dir := filepath.Dir(path) - if info, err := os.Stat(dir); err == nil && info.IsDir() { - return fixPathForm(path, dir) - } - return "./" -} - -// PredictFilesSet predict according to file rules to a given set of file names -func PredictFilesSet(files []string) PredictFunc { - return func(a Args) (prediction []string) { - // add all matching files to prediction - for _, f := range files { - f = fixPathForm(a.Last, f) - - // test matching of file to the argument - if matchFile(f, a.Last) { - prediction = append(prediction, f) - } - } - return - } -} - -func listFiles(dir, pattern string, allowFiles bool) []string { - // set of all file names - m := map[string]bool{} - - // list files - if files, err := filepath.Glob(filepath.Join(dir, pattern)); err == nil { - for _, f := range files { - if stat, err := os.Stat(f); err != nil || stat.IsDir() || allowFiles { - m[f] = true - } - } - } - - // list directories - if dirs, err := ioutil.ReadDir(dir); err == nil { - for _, d := range dirs { - if d.IsDir() { - m[filepath.Join(dir, d.Name())] = true - } - } - } - - list := make([]string, 0, len(m)) - for k := range m { - list = append(list, k) - } - return list -} - -// MatchFile returns true if prefix can match the file -func matchFile(file, prefix string) bool { - // special case for current directory completion - if file == "./" && (prefix == "." || prefix == "") { - return true - } - if prefix == "." && strings.HasPrefix(file, ".") { - return true - } - - file = strings.TrimPrefix(file, "./") - prefix = strings.TrimPrefix(prefix, "./") - - return strings.HasPrefix(file, prefix) -} - -// fixPathForm changes a file name to a relative name -func fixPathForm(last string, file string) string { - // get wording directory for relative name - workDir, err := os.Getwd() - if err != nil { - return file - } - - abs, err := filepath.Abs(file) - if err != nil { - return file - } - - // if last is absolute, return path as absolute - if filepath.IsAbs(last) { - return fixDirPath(abs) - } - - rel, err := filepath.Rel(workDir, abs) - if err != nil { - return file - } - - // fix ./ prefix of path - if rel != "." && strings.HasPrefix(last, ".") { - rel = "./" + rel - } - - return fixDirPath(rel) -} - -func fixDirPath(path string) string { - info, err := os.Stat(path) - if err == nil && info.IsDir() && !strings.HasSuffix(path, "/") { - path += "/" - } - return path -} diff --git a/vendor/github.com/posener/complete/predict_set.go b/vendor/github.com/posener/complete/predict_set.go deleted file mode 100644 index fa4a34ae4..000000000 --- a/vendor/github.com/posener/complete/predict_set.go +++ /dev/null @@ -1,12 +0,0 @@ -package complete - -// PredictSet expects specific set of terms, given in the options argument. -func PredictSet(options ...string) Predictor { - return predictSet(options) -} - -type predictSet []string - -func (p predictSet) Predict(a Args) []string { - return p -} diff --git a/vendor/modules.txt b/vendor/modules.txt index ea39d6d87..beee4ae71 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -100,11 +100,6 @@ github.com/pkg/errors # github.com/pmezard/go-difflib v1.0.0 ## explicit github.com/pmezard/go-difflib/difflib -# github.com/posener/complete v1.2.3 -## explicit; go 1.13 -github.com/posener/complete -github.com/posener/complete/cmd -github.com/posener/complete/cmd/install # github.com/russross/blackfriday/v2 v2.1.0 ## explicit github.com/russross/blackfriday/v2