@@ -11,6 +11,7 @@ import (
11
11
"os"
12
12
"path"
13
13
"path/filepath"
14
+ "regexp"
14
15
"runtime"
15
16
"runtime/debug"
16
17
"strings"
@@ -100,54 +101,107 @@ func generic(method string, addr string, args []string) {
100
101
MakeRequestAndFormat (req )
101
102
}
102
103
103
- func completeCurrentConfig (cmd * cobra.Command , args []string , toComplete string ) ([]string , cobra.ShellCompDirective ) {
104
- possible := []string {}
105
- if currentConfig != nil {
106
- for _ , cmd := range Root .Commands () {
107
- if cmd .Use == currentConfig .name {
108
- api , _ := Load (currentConfig .Base , cmd )
109
- for _ , op := range api .Operations {
110
- template := op .URITemplate
111
- if strings .HasPrefix (toComplete , currentConfig .name ) {
112
- template = strings .Replace (template , currentConfig .Base , currentConfig .name , 1 )
113
- }
114
- possible = append (possible , template )
115
- }
104
+ // templateVarRegex used to find/replace variables `/{foo}/bar/{baz}` in a
105
+ // template string.
106
+ var templateVarRegex = regexp .MustCompile (`\{.*?\}` )
107
+
108
+ // matchTemplate will see if a given URL matches a URL template, and if so,
109
+ // returns the template with the variable parts replaced by the matched part.
110
+ // If no match, returns the original template. Example:
111
+ // Input URL: https://example.com/items/foo
112
+ // Input tpl: https://example.com/items/{item-id}/tags/{tag-id}
113
+ // Output : https://example.com/items/foo/tags/{tag-id}
114
+ func matchTemplate (url , template string ) string {
115
+ urlParts := strings .Split (url , "/" )
116
+ tplParts := strings .Split (template , "/" )
117
+ for i , urlPart := range urlParts {
118
+ if len (tplParts ) < i + 1 {
119
+ break
120
+ }
121
+
122
+ tplPart := tplParts [i ]
123
+
124
+ if strings .Contains (tplPart , "{" ) {
125
+ matcher := regexp .MustCompile (templateVarRegex .ReplaceAllString (tplPart , ".*" ))
126
+ if matcher .MatchString (urlPart ) && urlPart != "" {
127
+ tplParts [i ] = urlPart
116
128
}
129
+ } else if urlPart == tplPart {
130
+ // This is an exact path match.
131
+ continue
117
132
}
118
- return possible , cobra .ShellCompDirectiveNoFileComp
133
+
134
+ // Give up, not a match!
135
+ break
119
136
}
120
- return []string {}, cobra .ShellCompDirectiveDefault
137
+
138
+ return strings .Join (tplParts , "/" )
121
139
}
122
140
123
- func completeGenericCmd ( cmd * cobra. Command , args [] string , toComplete string ) ([] string , cobra. ShellCompDirective ) {
124
- possible , directive := completeCurrentConfig ( cmd , args , toComplete )
125
- if directive != cobra . ShellCompDirectiveDefault {
126
- return possible , directive
127
- }
141
+ // completeCurrentConfig generates possible completions based on the currently
142
+ // selected API configuration's known operation URL templates. Takes into
143
+ // account short-names as well as the full URL.
144
+ func completeCurrentConfig ( cmd * cobra. Command , args [] string , toComplete string , method string ) ([] string , cobra. ShellCompDirective ) {
145
+ possible := [] string { }
128
146
if currentConfig != nil {
129
147
for _ , cmd := range Root .Commands () {
130
148
if cmd .Use == currentConfig .name {
149
+ // This is the matching command. Load the URL and check each operation.
131
150
api , _ := Load (currentConfig .Base , cmd )
132
151
for _ , op := range api .Operations {
133
- template := op .URITemplate
152
+ if op .Method != method {
153
+ // We only care about operations which match the currently selected
154
+ // HTTP method, otherwise it makes no sense to show it as an
155
+ // option since it couldn't possibly work.
156
+ continue
157
+ }
158
+
159
+ // Handle short-name, missing https:// prefix.
160
+ fixed := fixAddress (toComplete )
161
+
162
+ // Modify the template to fill in matched variables.
163
+ template := matchTemplate (fixed , op .URITemplate )
134
164
if strings .HasPrefix (toComplete , currentConfig .name ) {
165
+ // We were using a short-name, convert back to it! This is
166
+ // friendlier than forcing the full URL on the user.
135
167
template = strings .Replace (template , currentConfig .Base , currentConfig .name , 1 )
168
+ } else if ! strings .HasPrefix (toComplete , "https://" ) {
169
+ // Handle missing prefix.
170
+ template = strings .TrimPrefix (template , "https://" )
171
+ }
172
+ if strings .HasPrefix (template , toComplete ) || strings .HasPrefix (template , fixed ) {
173
+ if op .Short != "" {
174
+ // Cobra supports descriptions for each completion, so if
175
+ // available we add it here.s
176
+ template += "\t " + op .Short
177
+ }
178
+ possible = append (possible , template )
136
179
}
137
- possible = append (possible , template )
138
180
}
139
181
}
140
182
}
141
183
return possible , cobra .ShellCompDirectiveNoFileComp
142
184
}
185
+ return []string {}, cobra .ShellCompDirectiveDefault
186
+ }
143
187
144
- if len (args ) == 0 {
145
- for name := range configs {
146
- possible = append (possible , name )
188
+ // completeGenericCmd shows possible completions for generic commands, for
189
+ // example get/post/put/patch/delete/etc.
190
+ func completeGenericCmd (method string , showAPIs bool ) func (cmd * cobra.Command , args []string , toComplete string ) ([]string , cobra.ShellCompDirective ) {
191
+ return func (cmd * cobra.Command , args []string , toComplete string ) ([]string , cobra.ShellCompDirective ) {
192
+ possible , directive := completeCurrentConfig (cmd , args , toComplete , method )
193
+ if directive != cobra .ShellCompDirectiveDefault {
194
+ return possible , directive
147
195
}
148
- }
149
196
150
- return possible , cobra .ShellCompDirectiveNoFileComp | cobra .ShellCompDirectiveNoSpace
197
+ if showAPIs && len (args ) == 0 {
198
+ for name := range configs {
199
+ possible = append (possible , name )
200
+ }
201
+ }
202
+
203
+ return possible , cobra .ShellCompDirectiveNoFileComp | cobra .ShellCompDirectiveNoSpace
204
+ }
151
205
}
152
206
153
207
// Init will set up the CLI.
@@ -204,7 +258,7 @@ func Init(name string, version string) {
204
258
# Specify verb, header, and body shorthand
205
259
$ %s post :8888/users -H authorization:abc123 name: Kari, role: admin` , name , name ),
206
260
Args : cobra .MinimumNArgs (1 ),
207
- ValidArgsFunction : completeCurrentConfig ,
261
+ ValidArgsFunction : completeGenericCmd ( http . MethodGet , false ) ,
208
262
PersistentPreRun : func (cmd * cobra.Command , args []string ) {
209
263
settings := viper .AllSettings ()
210
264
LogDebug ("Configuration: %v" , settings )
@@ -223,7 +277,7 @@ func Init(name string, version string) {
223
277
Short : "Head a URI" ,
224
278
Long : "Perform an HTTP HEAD on the given URI" ,
225
279
Args : cobra .MinimumNArgs (1 ),
226
- ValidArgsFunction : completeGenericCmd ,
280
+ ValidArgsFunction : completeGenericCmd ( http . MethodHead , true ) ,
227
281
Run : func (cmd * cobra.Command , args []string ) {
228
282
generic (http .MethodHead , args [0 ], args [1 :])
229
283
},
@@ -235,7 +289,7 @@ func Init(name string, version string) {
235
289
Short : "Options a URI" ,
236
290
Long : "Perform an HTTP OPTIONS on the given URI" ,
237
291
Args : cobra .MinimumNArgs (1 ),
238
- ValidArgsFunction : completeGenericCmd ,
292
+ ValidArgsFunction : completeGenericCmd ( http . MethodOptions , true ) ,
239
293
Run : func (cmd * cobra.Command , args []string ) {
240
294
generic (http .MethodOptions , args [0 ], args [1 :])
241
295
},
@@ -247,7 +301,7 @@ func Init(name string, version string) {
247
301
Short : "Get a URI" ,
248
302
Long : "Perform an HTTP GET on the given URI" ,
249
303
Args : cobra .MinimumNArgs (1 ),
250
- ValidArgsFunction : completeGenericCmd ,
304
+ ValidArgsFunction : completeGenericCmd ( http . MethodGet , true ) ,
251
305
Run : func (cmd * cobra.Command , args []string ) {
252
306
generic (http .MethodGet , args [0 ], args [1 :])
253
307
},
@@ -259,7 +313,7 @@ func Init(name string, version string) {
259
313
Short : "Post a URI" ,
260
314
Long : "Perform an HTTP POST on the given URI" ,
261
315
Args : cobra .MinimumNArgs (1 ),
262
- ValidArgsFunction : completeGenericCmd ,
316
+ ValidArgsFunction : completeGenericCmd ( http . MethodPost , true ) ,
263
317
Run : func (cmd * cobra.Command , args []string ) {
264
318
generic (http .MethodPost , args [0 ], args [1 :])
265
319
},
@@ -271,7 +325,7 @@ func Init(name string, version string) {
271
325
Short : "Put a URI" ,
272
326
Long : "Perform an HTTP PUT on the given URI" ,
273
327
Args : cobra .MinimumNArgs (1 ),
274
- ValidArgsFunction : completeGenericCmd ,
328
+ ValidArgsFunction : completeGenericCmd ( http . MethodPut , true ) ,
275
329
Run : func (cmd * cobra.Command , args []string ) {
276
330
generic (http .MethodPut , args [0 ], args [1 :])
277
331
},
@@ -283,7 +337,7 @@ func Init(name string, version string) {
283
337
Short : "Patch a URI" ,
284
338
Long : "Perform an HTTP PATCH on the given URI" ,
285
339
Args : cobra .MinimumNArgs (1 ),
286
- ValidArgsFunction : completeGenericCmd ,
340
+ ValidArgsFunction : completeGenericCmd ( http . MethodPatch , true ) ,
287
341
Run : func (cmd * cobra.Command , args []string ) {
288
342
generic (http .MethodPatch , args [0 ], args [1 :])
289
343
},
@@ -295,7 +349,7 @@ func Init(name string, version string) {
295
349
Short : "Delete a URI" ,
296
350
Long : "Perform an HTTP DELETE on the given URI" ,
297
351
Args : cobra .MinimumNArgs (1 ),
298
- ValidArgsFunction : completeGenericCmd ,
352
+ ValidArgsFunction : completeGenericCmd ( http . MethodDelete , true ) ,
299
353
Run : func (cmd * cobra.Command , args []string ) {
300
354
generic (http .MethodDelete , args [0 ], args [1 :])
301
355
},
@@ -310,7 +364,7 @@ func Init(name string, version string) {
310
364
Short : "Edit a resource by URI" ,
311
365
Long : "Convenience function which combines a GET, edit, and PUT operation into one command" ,
312
366
Args : cobra .MinimumNArgs (1 ),
313
- ValidArgsFunction : completeGenericCmd ,
367
+ ValidArgsFunction : completeGenericCmd ( http . MethodGet , true ) ,
314
368
Run : func (cmd * cobra.Command , args []string ) {
315
369
switch * editFormat {
316
370
case "json" :
@@ -340,7 +394,7 @@ func Init(name string, version string) {
340
394
# Example usage with curl
341
395
$ curl https://my-apiexample.com/ -H "Authorization: $(%s auth-header my-api)"` , name , name , name ),
342
396
Args : cobra .ExactArgs (1 ),
343
- ValidArgsFunction : completeGenericCmd ,
397
+ ValidArgsFunction : completeGenericCmd ( http . MethodGet , true ) ,
344
398
RunE : func (cmd * cobra.Command , args []string ) error {
345
399
addr := fixAddress (args [0 ])
346
400
name , config := findAPI (addr )
@@ -376,7 +430,7 @@ func Init(name string, version string) {
376
430
Short : "Get cert info" ,
377
431
Long : "Get TLS certificate information including expiration date" ,
378
432
Args : cobra .ExactArgs (1 ),
379
- ValidArgsFunction : completeGenericCmd ,
433
+ ValidArgsFunction : completeGenericCmd ( http . MethodGet , true ) ,
380
434
Run : func (cmd * cobra.Command , args []string ) {
381
435
u , err := url .Parse (fixAddress (args [0 ]))
382
436
if err != nil {
@@ -428,7 +482,7 @@ Not after (expires): %s (%s)
428
482
Short : "Get link relations from the given URI, with optional filtering" ,
429
483
Long : "Returns a list of resolved references to the link relations after making an HTTP GET request to the given URI. Additional arguments filter down the set of returned relationship names." ,
430
484
Args : cobra .MinimumNArgs (1 ),
431
- ValidArgsFunction : completeGenericCmd ,
485
+ ValidArgsFunction : completeGenericCmd ( http . MethodGet , true ) ,
432
486
Run : func (cmd * cobra.Command , args []string ) {
433
487
req , _ := http .NewRequest (http .MethodGet , fixAddress (args [0 ]), nil )
434
488
resp , err := GetParsedResponse (req )
0 commit comments