forked from rliebz/tusk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcompletion.go
240 lines (199 loc) · 5.27 KB
/
completion.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
package appcli
import (
"fmt"
"io"
"os"
"strings"
"github.com/rliebz/tusk/runner"
"github.com/urfave/cli"
)
// completionFlag is the flag passed when performing shell completions.
var completionFlag = "--" + cli.BashCompletionFlag.GetName()
// IsCompleting returns whether tab-completion is currently occurring.
func IsCompleting(args []string) bool {
return args[len(args)-1] == completionFlag
}
// context represents the subset of *cli.Context required for flag completion.
type context interface {
// IsSet checks if a flag was already set, meaning we no longer need to
// complete it.
IsSet(string) bool
// NArg is the number of non-flag arguments. This is used to determine if a
// sub command is being called.
NArg() int
}
// createDefaultComplete prints the completion metadata for the top-level app.
// The metadata includes the completion type followed by a list of options.
// The available completion types are "normal" and "file". Normal will return
// tasks and flags, while file allows completion engines to use system files.
func createDefaultComplete(w io.Writer, app *cli.App) func(c *cli.Context) {
return func(c *cli.Context) {
defaultComplete(w, c, app)
}
}
func defaultComplete(w io.Writer, c context, app *cli.App) {
// If there's an arg, but we're not using command-completion, it's a user
// error. There's nothing to complete.
if c.NArg() > 0 {
return
}
trailingArg := os.Args[len(os.Args)-2]
if isCompletingFlagArg(app.Flags, trailingArg) {
fmt.Fprintln(w, "file")
return
}
fmt.Fprintln(w, "normal")
for i := range app.Commands {
printCommand(w, &app.Commands[i])
}
for _, flag := range app.Flags {
printFlag(w, c, flag)
}
}
// createCommandComplete prints the completion metadata for a cli command.
// The metadata includes the completion type followed by a list of options.
// The available completion types are "normal" and "file". Normal will return
// task-specific flags, while file allows completion engines to use system files.
func createCommandComplete(
w io.Writer,
command *cli.Command,
cfg *runner.Config,
) func(c *cli.Context) {
return func(c *cli.Context) {
commandComplete(w, c, command, cfg)
}
}
func commandComplete(w io.Writer, c context, command *cli.Command, cfg *runner.Config) {
t := cfg.Tasks[command.Name]
trailingArg := os.Args[len(os.Args)-2]
if isCompletingFlagArg(command.Flags, trailingArg) {
printCompletingFlagArg(w, t, cfg, trailingArg)
return
}
if c.NArg()+1 <= len(t.Args) {
fmt.Fprintln(w, "task-args")
arg := t.Args[c.NArg()]
for _, value := range arg.ValuesAllowed {
fmt.Fprintln(w, value)
}
} else {
fmt.Fprintln(w, "task-no-args")
}
for _, flag := range command.Flags {
printFlag(w, c, flag)
}
}
func printCompletingFlagArg(w io.Writer, t *runner.Task, cfg *runner.Config, trailingArg string) {
options, err := runner.FindAllOptions(t, cfg)
if err != nil {
return
}
opt, ok := getOptionFlag(trailingArg, options)
if !ok {
return
}
if len(opt.ValuesAllowed) > 0 {
fmt.Fprintln(w, "value")
for _, value := range opt.ValuesAllowed {
fmt.Fprintln(w, value)
}
return
}
// Default to file completion
fmt.Fprintln(w, "file")
}
func getOptionFlag(flag string, options []*runner.Option) (*runner.Option, bool) {
flagName := getFlagName(flag)
for _, opt := range options {
if flagName == opt.Name || flagName == opt.Short {
return opt, true
}
}
return nil, false
}
func printCommand(w io.Writer, command *cli.Command) {
if command.Hidden {
return
}
name := strings.ReplaceAll(command.Name, ":", `\:`)
if command.Usage == "" {
fmt.Fprintln(w, name)
return
}
fmt.Fprintf(
w,
"%s:%s\n",
name,
strings.ReplaceAll(command.Usage, "\n", ""),
)
}
func printFlag(w io.Writer, c context, flag cli.Flag) {
values := strings.Split(flag.GetName(), ", ")
for _, value := range values {
if len(value) == 1 || c.IsSet(value) {
continue
}
name := strings.ReplaceAll(value, ":", `\:`)
desc := getDescription(flag)
if desc == "" {
fmt.Fprintln(w, "--"+name)
return
}
fmt.Fprintf(
w,
"--%s:%s\n",
name,
strings.ReplaceAll(desc, "\n", ""),
)
}
}
func getDescription(flag cli.Flag) string {
return strings.SplitN(flag.String(), "\t", 2)[1]
}
func removeCompletionArg(args []string) []string {
var output []string
for _, arg := range args {
if arg != completionFlag {
output = append(output, arg)
}
}
return output
}
// isCompletingFlagArg returns if the trailing arg is an incomplete flag.
func isCompletingFlagArg(flags []cli.Flag, arg string) bool {
if !strings.HasPrefix(arg, "-") {
return false
}
name := getFlagName(arg)
short := !strings.HasPrefix(arg, "--")
for _, flag := range flags {
switch flag.(type) {
case cli.BoolFlag, cli.BoolTFlag:
continue
}
if flagMatchesName(flag, name, short) {
return true
}
}
return false
}
func flagMatchesName(flag cli.Flag, name string, short bool) bool {
for _, candidate := range strings.Split(flag.GetName(), ", ") {
if len(candidate) == 1 && !short {
continue
}
if name == candidate {
return true
}
}
return false
}
func getFlagName(flag string) string {
if strings.HasPrefix(flag, "--") {
return flag[2:]
}
return flag[len(flag)-1:]
}
func isFlagArgumentError(err error) bool {
return strings.HasPrefix(err.Error(), "flag needs an argument")
}