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

command: add extensive autocompletion support #500

Merged
merged 38 commits into from
Sep 16, 2022
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f2122e3
upgrade urfave, enable urfave's auto-completion
kucukaslan Aug 12, 2022
ea557fd
remove posener/complete package
kucukaslan Aug 12, 2022
9196647
Add bucket listing as ls's default autocomplete
kucukaslan Aug 12, 2022
df05a38
ls supports bucket listing
kucukaslan Aug 12, 2022
5221856
move auto-complete logic to another method
kucukaslan Aug 17, 2022
a7b8734
complete autocompletion for ls and cat, prepare help text
kucukaslan Aug 17, 2022
8accdc9
Add auto completion support to all commands
kucukaslan Aug 17, 2022
6b86541
command/autocomplete: update bash script to test
seruman Aug 17, 2022
c671bca
improve bash completion
kucukaslan Aug 19, 2022
e72c6e6
implement the The Fix
kucukaslan Aug 24, 2022
c3bfe47
change the suggestion behaviour
kucukaslan Aug 24, 2022
24bc369
prepare suggestions depending on the shell and environment variable C…
kucukaslan Aug 25, 2022
a117df0
improve suggestions
kucukaslan Aug 25, 2022
8ac1a98
handle the leading quotation marks, URL.Match should check if filterR…
kucukaslan Aug 26, 2022
3caa60c
bash completion: Don't pass COMP_WORDBREAKS, don't add space after co…
kucukaslan Aug 26, 2022
fa36da4
Delete auto
kucukaslan Aug 26, 2022
3053f1a
print errors to stderr, prepare test template
kucukaslan Aug 26, 2022
c892c5f
add tests for autocompletion
kucukaslan Aug 31, 2022
c652664
Merge remote-tracking branch 'upstream/master' into completion-deviation
kucukaslan Aug 31, 2022
17f705d
improve suggestions
kucukaslan Aug 31, 2022
1eefa33
move auto_complete tests to another file, add zsh&bash variants for s…
kucukaslan Sep 1, 2022
0e5033b
hide the script comments from output
kucukaslan Sep 1, 2022
d2913e2
correct install-completion description
kucukaslan Sep 1, 2022
ee1e98e
Merge remote-tracking branch 'upstream/master' into completion-deviation
kucukaslan Sep 1, 2022
70d05b9
add autoocompletion tests for the commands that only accepts remote o…
kucukaslan Sep 1, 2022
6c72db5
don't print the errors encountered during autocompletion
kucukaslan Sep 5, 2022
f395b81
apply suggestions from code review
kucukaslan Sep 9, 2022
385f7a1
Merge remote-tracking branch 'upstream/master' into completion-deviation
kucukaslan Sep 12, 2022
a4e6ce7
apply suggestions from code review
kucukaslan Sep 12, 2022
7e37c98
apply suggestions from code review
kucukaslan Sep 13, 2022
f7bb79d
e2e: show expected condition of testcases explicitly
kucukaslan Sep 13, 2022
3278f1a
small bug-fix and improvements
kucukaslan Sep 13, 2022
1994ab7
add autocompletion notes to changelog and readme
kucukaslan Sep 13, 2022
578f64c
seperate the logic of constantCompleteWithDefault from urfave/clicli
kucukaslan Sep 13, 2022
5a681d9
Merge remote-tracking branch 'upstream/master' into completion-deviation
kucukaslan Sep 14, 2022
00f37c5
document tested shell versions, move os.Getenv calls to out of the au…
kucukaslan Sep 14, 2022
3a1220e
Apply suggestions from code review
kucukaslan Sep 15, 2022
20c7a01
move escapeColon into formatSuggestionForShell method
kucukaslan Sep 15, 2022
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 6 additions & 14 deletions command/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"os"
"strings"

cmpinstall "github.com/posener/complete/cmd/install"
"github.com/urfave/cli/v2"

"github.com/peak/s5cmd/log"
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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)
}
246 changes: 246 additions & 0 deletions command/auto_complete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
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
}
}
}

// it 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) {
kucukaslan marked this conversation as resolved.
Show resolved Hide resolved
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 escapeColon(suggestion)
default:
return suggestion
}
}

// replace every colon : with \:
func escapeColon(str ...interface{}) string {
return strings.ReplaceAll(fmt.Sprint(str...), ":", `\:`)
}
seruman marked this conversation as resolved.
Show resolved Hide resolved

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 charactes.
kucukaslan marked this conversation as resolved.
Show resolved Hide resolved
if strings.HasPrefix(arg, "'") {
arg = strings.TrimPrefix(arg, "'")
} else {
arg = strings.TrimPrefix(arg, "\"")
}
return arg
}
42 changes: 0 additions & 42 deletions command/autocomplete.go

This file was deleted.

Loading