-
Notifications
You must be signed in to change notification settings - Fork 255
/
Copy pathauto_complete.go
241 lines (209 loc) · 7.24 KB
/
auto_complete.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
package command
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/peak/s5cmd/v2/storage"
"github.com/peak/s5cmd/v2/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
}