• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rsteube / carapace / 5810451053

pending completion
5810451053

Pull #892

github

rsteube
split: support redirects
Pull Request #892: split: support redirects

13 of 13 new or added lines in 1 file covered. (100.0%)

5 existing lines in 1 file now uncovered.

2638 of 3962 relevant lines covered (66.58%)

1.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.71
/action.go
1
package carapace
2

3
import (
4
        "fmt"
5
        "os"
6
        "regexp"
7
        "runtime"
8
        "strings"
9
        "time"
10

11
        shlex "github.com/rsteube/carapace-shlex"
12
        "github.com/rsteube/carapace/internal/cache"
13
        "github.com/rsteube/carapace/internal/common"
14
        pkgcache "github.com/rsteube/carapace/pkg/cache"
15
        "github.com/rsteube/carapace/pkg/style"
16
        pkgtraverse "github.com/rsteube/carapace/pkg/traverse"
17
)
18

19
// Action indicates how to complete a flag or positional argument.
20
type Action struct {
21
        meta      common.Meta
22
        rawValues common.RawValues
23
        callback  CompletionCallback
24
}
25

26
// ActionMap maps Actions to an identifier.
27
type ActionMap map[string]Action
28

29
// CompletionCallback is executed during completion of associated flag or positional argument.
30
type CompletionCallback func(c Context) Action
31

32
// Cache cashes values of a CompletionCallback for given duration and keys.
33
func (a Action) Cache(timeout time.Duration, keys ...pkgcache.Key) Action {
2✔
34
        if a.callback != nil { // only relevant for callback actions
4✔
35
                cachedCallback := a.callback
2✔
36
                _, file, line, _ := runtime.Caller(1) // generate uid from wherever Cache() was called
2✔
37
                a.callback = func(c Context) Action {
4✔
38
                        cacheFile, err := cache.File(file, line, keys...)
2✔
39
                        if err != nil {
2✔
40
                                return cachedCallback(c)
×
41
                        }
×
42

43
                        if cached, err := cache.Load(cacheFile, timeout); err == nil {
4✔
44
                                return Action{meta: cached.Meta, rawValues: cached.Values}
2✔
45
                        }
2✔
46

47
                        invokedAction := (Action{callback: cachedCallback}).Invoke(c)
2✔
48
                        if invokedAction.meta.Messages.IsEmpty() {
4✔
49
                                if cacheFile, err := cache.File(file, line, keys...); err == nil { // regenerate as cache keys might have changed due to invocation
4✔
50
                                        _ = cache.Write(cacheFile, invokedAction.export())
2✔
51
                                }
2✔
52
                        }
53
                        return invokedAction.ToA()
2✔
54
                }
55
        }
56
        return a
2✔
57
}
58

59
// Chdir changes the current working directory to the named directory for the duration of invocation.
60
func (a Action) Chdir(dir string) Action {
2✔
61
        return ActionCallback(func(c Context) Action {
4✔
62
                abs, err := c.Abs(dir)
2✔
63
                if err != nil {
2✔
64
                        return ActionMessage(err.Error())
×
65
                }
×
66
                if info, err := os.Stat(abs); err != nil {
3✔
67
                        return ActionMessage(err.Error())
1✔
68
                } else if !info.IsDir() {
4✔
69
                        return ActionMessage("not a directory: %v", abs)
1✔
70
                }
1✔
71
                c.Dir = abs
2✔
72
                return a.Invoke(c).ToA()
2✔
73
        })
74
}
75

76
// ChdirF is like Chdir but uses a function.
77
func (a Action) ChdirF(f func(tc pkgtraverse.Context) (string, error)) Action {
2✔
78
        return ActionCallback(func(c Context) Action {
2✔
79
                newDir, err := f(c)
×
80
                if err != nil {
×
81
                        return ActionMessage(err.Error())
×
82
                }
×
83
                return a.Chdir(newDir)
×
84
        })
85
}
86

87
// Filter filters given values.
88
//
89
//        carapace.ActionValues("A", "B", "C").Filter("B") // ["A", "C"]
90
func (a Action) Filter(values ...string) Action {
2✔
91
        return ActionCallback(func(c Context) Action {
3✔
92
                return a.Invoke(c).Filter(values...).ToA()
1✔
93
        })
1✔
94
}
95

96
// FilterArgs filters Context.Args
97
func (a Action) FilterArgs() Action {
2✔
98
        return ActionCallback(func(c Context) Action {
3✔
99
                return a.Filter(c.Args...)
1✔
100
        })
1✔
101
}
102

103
// FilterArgs filters Context.Parts
104
func (a Action) FilterParts() Action {
1✔
105
        return ActionCallback(func(c Context) Action {
2✔
106
                return a.Filter(c.Parts...)
1✔
107
        })
1✔
108
}
109

110
// Invoke executes the callback of an action if it exists (supports nesting).
111
func (a Action) Invoke(c Context) InvokedAction {
2✔
112
        if c.Args == nil {
4✔
113
                c.Args = []string{}
2✔
114
        }
2✔
115
        if c.Env == nil {
3✔
116
                c.Env = []string{}
1✔
117
        }
1✔
118
        if c.Parts == nil {
4✔
119
                c.Parts = []string{}
2✔
120
        }
2✔
121

122
        if a.rawValues == nil && a.callback != nil {
4✔
123
                result := a.callback(c).Invoke(c)
2✔
124
                result.meta.Merge(a.meta)
2✔
125
                return result
2✔
126
        }
2✔
127
        return InvokedAction{a}
2✔
128
}
129

130
// List wraps the Action in an ActionMultiParts with given divider.
131
func (a Action) List(divider string) Action {
2✔
132
        return ActionMultiParts(divider, func(c Context) Action {
2✔
133
                return a.Invoke(c).ToA().NoSpace()
×
134
        })
×
135
}
136

137
// MultiParts splits values of an Action by given dividers and completes each segment separately.
138
func (a Action) MultiParts(dividers ...string) Action {
2✔
139
        return ActionCallback(func(c Context) Action {
4✔
140
                return a.Invoke(c).ToMultiPartsA(dividers...)
2✔
141
        })
2✔
142
}
143

144
// MultiPartsP is like MultiParts but with placeholders
145
func (a Action) MultiPartsP(delimiter string, pattern string, f func(placeholder string, matches map[string]string) Action) Action {
2✔
146
        // TODO add delimiter as suffix for proper nospace handling (some values/placeholders might appear with and without suffix)
2✔
147
        return ActionCallback(func(c Context) Action {
3✔
148
                invoked := a.Invoke(c)
1✔
149

1✔
150
                return ActionMultiParts(delimiter, func(c Context) Action {
2✔
151
                        rPlaceholder := regexp.MustCompile(pattern)
1✔
152
                        matchedData := make(map[string]string)
1✔
153
                        matchedSegments := make(map[string]common.RawValue)
1✔
154
                        staticMatches := make(map[int]bool)
1✔
155

1✔
156
                path:
1✔
157
                        for index, value := range invoked.rawValues {
2✔
158
                                segments := strings.Split(value.Value, delimiter)
1✔
159
                        segment:
1✔
160
                                for index, segment := range segments {
2✔
161
                                        if index > len(c.Parts)-1 {
2✔
162
                                                break segment
1✔
163
                                        } else {
1✔
164
                                                if segment != c.Parts[index] {
2✔
165
                                                        if !rPlaceholder.MatchString(segment) {
2✔
166
                                                                continue path // skip this path as it doesn't match and is not a placeholder
1✔
167
                                                        } else {
1✔
168
                                                                matchedData[segment] = c.Parts[index] // store entered data for placeholder (overwrite if duplicate)
1✔
169
                                                        }
1✔
170
                                                } else {
1✔
171
                                                        staticMatches[index] = true // static segment matches so placeholders should be ignored for this index
1✔
172
                                                }
1✔
173
                                        }
174
                                }
175

176
                                if len(segments) < len(c.Parts)+1 {
2✔
177
                                        continue path // skip path as it is shorter than what was entered (must be after staticMatches being set)
1✔
178
                                }
179

180
                                for key := range staticMatches {
2✔
181
                                        if segments[key] != c.Parts[key] {
1✔
182
                                                continue path // skip this path as it has a placeholder where a static segment was matched
×
183
                                        }
184
                                }
185

186
                                // store segment as path matched so far and this is currently being completed
187
                                if len(segments) == (len(c.Parts) + 1) {
2✔
188
                                        matchedSegments[segments[len(c.Parts)]] = invoked.rawValues[index]
1✔
189
                                } else {
2✔
190
                                        if segment := segments[len(c.Parts)]; rPlaceholder.MatchString(segment) {
2✔
191
                                                matchedSegments[segment] = common.RawValue{}
1✔
192
                                        } else {
2✔
193
                                                matchedSegments[segment+delimiter] = common.RawValue{}
1✔
194
                                        }
1✔
195
                                }
196
                        }
197

198
                        actions := make([]Action, 0, len(matchedSegments))
1✔
199
                        for key, value := range matchedSegments {
2✔
200
                                if rPlaceholder.MatchString(key) {
2✔
201
                                        actions = append(actions, f(key, matchedData))
1✔
202
                                } else {
2✔
203
                                        actions = append(actions, ActionStyledValuesDescribed(key, value.Description, value.Style)) // TODO tag,..
1✔
204
                                }
1✔
205
                        }
206

207
                        // TODO verify
208
                        a := Batch(actions...).ToA().NoSpace()
1✔
209
                        a.meta.Merge(invoked.meta)
1✔
210
                        return a
1✔
211
                })
212
        })
213
}
214

215
// NoSpace disables space suffix for given characters (or all if none are given).
216
func (a Action) NoSpace(suffixes ...rune) Action {
2✔
217
        return ActionCallback(func(c Context) Action {
4✔
218
                if len(suffixes) == 0 {
4✔
219
                        a.meta.Nospace.Add('*')
2✔
220
                }
2✔
221
                a.meta.Nospace.Add(suffixes...)
2✔
222
                return a
2✔
223
        })
224
}
225

226
// Prefix adds a prefix to values (only the ones inserted, not the display values).
227
//
228
//        carapace.ActionValues("melon", "drop", "fall").Prefix("water")
229
func (a Action) Prefix(prefix string) Action {
2✔
230
        return ActionCallback(func(c Context) Action {
4✔
231
                switch {
2✔
232
                case strings.HasPrefix(c.Value, prefix):
2✔
233
                        c.Value = strings.TrimPrefix(c.Value, prefix)
2✔
234
                case strings.HasPrefix(prefix, c.Value):
2✔
235
                        c.Value = ""
2✔
236
                default:
×
237
                        return ActionValues()
×
238
                }
239
                return a.Invoke(c).Prefix(prefix).ToA()
2✔
240
        })
241
}
242

243
// Retain retains given values.
244
//
245
//        carapace.ActionValues("A", "B", "C").Retain("A", "C") // ["A", "C"]
246
func (a Action) Retain(values ...string) Action {
2✔
247
        return ActionCallback(func(c Context) Action {
3✔
248
                return a.Invoke(c).Retain(values...).ToA()
1✔
249
        })
1✔
250
}
251

252
// Shift shifts positional arguments left `n` times.
253
func (a Action) Shift(n int) Action {
2✔
254
        return ActionCallback(func(c Context) Action {
3✔
255
                switch {
1✔
256
                case n < 0:
×
257
                        return ActionMessage("invalid argument [ActionShift]: %v", n)
×
258
                case len(c.Args) < n:
×
259
                        c.Args = []string{}
×
260
                default:
1✔
261
                        c.Args = c.Args[n:]
1✔
262
                }
263
                return a.Invoke(c).ToA()
1✔
264
        })
265
}
266

267
// Split splits `Context.Value` lexicographically and replaces `Context.Args` with the tokens.
268
func (a Action) Split() Action {
2✔
269
        return a.split(false)
2✔
270
}
2✔
271

272
// SplitP is like Split but supports pipelines.
273
func (a Action) SplitP() Action {
2✔
274
        return a.split(true)
2✔
275
}
2✔
276

277
func (a Action) split(pipelines bool) Action {
2✔
278
        return ActionCallback(func(c Context) Action {
3✔
279
                tokens, err := shlex.Split(c.Value)
1✔
280
                if err != nil {
1✔
281
                        return ActionMessage(err.Error())
×
282
                }
×
283

284
                if pipelines {
2✔
285
                        tokens = tokens.CurrentPipeline()
1✔
286
                }
1✔
287

288
                originalValue := c.Value
1✔
289

1✔
290
                LOG.Printf("tokens: %#v", tokens)
1✔
291
                context := NewContext(tokens.FilterRedirects().Words().Strings()...)
1✔
292
                prefix := originalValue[:tokens.Words().CurrentToken().Index]
1✔
293
                c.Args = context.Args
1✔
294
                c.Parts = []string{}
1✔
295
                c.Value = context.Value
1✔
296

1✔
297
                if pipelines { // support redirects
2✔
298
                        if len(tokens) > 1 && tokens[len(tokens)-2].WordbreakType.IsRedirect() {
2✔
299
                                prefix = originalValue[:tokens.CurrentToken().Index]
1✔
300
                                c.Value = tokens.CurrentToken().Value
1✔
301
                                LOG.Printf("completing files for redirect arg %#v", tokens.Words().CurrentToken().Value)
1✔
302
                                a = ActionFiles()
1✔
303
                        }
1✔
304
                }
305

306
                invoked := a.Invoke(c)
1✔
307
                for index, value := range invoked.rawValues {
2✔
308
                        if !invoked.meta.Nospace.Matches(value.Value) || strings.Contains(value.Value, " ") { // TODO special characters
2✔
309
                                switch tokens.CurrentToken().State {
1✔
310
                                case shlex.QUOTING_ESCAPING_STATE:
1✔
311
                                        invoked.rawValues[index].Value = fmt.Sprintf(`"%v"`, strings.Replace(value.Value, `"`, `\"`, -1))
1✔
312
                                case shlex.QUOTING_STATE:
1✔
313
                                        invoked.rawValues[index].Value = fmt.Sprintf(`'%v'`, strings.Replace(value.Value, `'`, `'"'"'`, -1))
1✔
314
                                default:
1✔
315
                                        invoked.rawValues[index].Value = strings.Replace(value.Value, ` `, `\ `, -1)
1✔
316
                                }
317
                        }
318
                        if !invoked.meta.Nospace.Matches(value.Value) {
2✔
319
                                invoked.rawValues[index].Value += " "
1✔
320
                        }
1✔
321
                }
322
                return invoked.Prefix(prefix).ToA().NoSpace()
1✔
323
        })
324
}
325

326
// Style sets the style.
327
//
328
//        ActionValues("yes").Style(style.Green)
329
//        ActionValues("no").Style(style.Red)
330
func (a Action) Style(s string) Action {
2✔
331
        return a.StyleF(func(_ string, _ style.Context) string {
4✔
332
                return s
2✔
333
        })
2✔
334
}
335

336
// Style sets the style using a function.
337
//
338
//        ActionValues("dir/", "test.txt").StyleF(style.ForPathExt)
339
//        ActionValues("true", "false").StyleF(style.ForKeyword)
340
func (a Action) StyleF(f func(s string, sc style.Context) string) Action {
2✔
341
        return ActionCallback(func(c Context) Action {
4✔
342
                invoked := a.Invoke(c)
2✔
343
                for index, v := range invoked.rawValues {
4✔
344
                        invoked.rawValues[index].Style = f(v.Value, c)
2✔
345
                }
2✔
346
                return invoked.ToA()
2✔
347
        })
348
}
349

350
// Style sets the style using a reference.
351
//
352
//        ActionValues("value").StyleR(&style.Carapace.Value)
353
//        ActionValues("description").StyleR(&style.Carapace.Value)
354
func (a Action) StyleR(s *string) Action {
2✔
355
        return ActionCallback(func(c Context) Action {
3✔
356
                if s != nil {
2✔
357
                        return a.Style(*s)
1✔
358
                }
1✔
UNCOV
359
                return a
×
360
        })
361
}
362

363
// Suffix adds a suffx to values (only the ones inserted, not the display values).
364
//
365
//        carapace.ActionValues("apple", "melon", "orange").Suffix("juice")
366
func (a Action) Suffix(suffix string) Action {
2✔
367
        return ActionCallback(func(c Context) Action {
4✔
368
                return a.Invoke(c).Suffix(suffix).ToA()
2✔
369
        })
2✔
370
}
371

372
// Suppress suppresses specific error messages using regular expressions.
373
func (a Action) Suppress(expr ...string) Action {
2✔
374
        return ActionCallback(func(c Context) Action {
3✔
375
                invoked := a.Invoke(c)
1✔
376
                if err := invoked.meta.Messages.Suppress(expr...); err != nil {
1✔
UNCOV
377
                        return ActionMessage(err.Error())
×
UNCOV
378
                }
×
379
                return invoked.ToA()
1✔
380
        })
381
}
382

383
// Tag sets the tag.
384
//
385
//        ActionValues("192.168.1.1", "127.0.0.1").Tag("interfaces").
386
func (a Action) Tag(tag string) Action {
2✔
387
        return a.TagF(func(value string) string {
4✔
388
                return tag
2✔
389
        })
2✔
390
}
391

392
// Tag sets the tag using a function.
393
//
394
//        ActionValues("192.168.1.1", "127.0.0.1").TagF(func(value string) string {
395
//                return "interfaces"
396
//        })
397
func (a Action) TagF(f func(s string) string) Action {
2✔
398
        return ActionCallback(func(c Context) Action {
4✔
399
                invoked := a.Invoke(c)
2✔
400
                for index, v := range invoked.rawValues {
4✔
401
                        invoked.rawValues[index].Tag = f(v.Value)
2✔
402
                }
2✔
403
                return invoked.ToA()
2✔
404
        })
405
}
406

407
// Timeout sets the maximum duration an Action may take to invoke.
408
//
409
//        carapace.ActionCallback(func(c carapace.Context) carapace.Action {
410
//                time.Sleep(2*time.Second)
411
//                return carapace.ActionValues("done")
412
//        }).Timeout(1*time.Second, carapace.ActionMessage("timeout exceeded"))
413
func (a Action) Timeout(d time.Duration, alternative Action) Action {
2✔
414
        return ActionCallback(func(c Context) Action {
3✔
415
                currentChannel := make(chan string, 1)
1✔
416

1✔
417
                var result InvokedAction
1✔
418
                go func() {
2✔
419
                        result = a.Invoke(c)
1✔
420
                        currentChannel <- ""
1✔
421
                }()
1✔
422

423
                select {
1✔
UNCOV
424
                case <-currentChannel:
×
425
                case <-time.After(d):
1✔
426
                        return alternative
1✔
427
                }
UNCOV
428
                return result.ToA()
×
429
        })
430
}
431

432
// UniqueList wraps the Action in an ActionMultiParts with given divider.
433
func (a Action) UniqueList(divider string) Action {
2✔
434
        return ActionMultiParts(divider, func(c Context) Action {
3✔
435
                return a.FilterParts().NoSpace()
1✔
436
        })
1✔
437
}
438

439
// Usage sets the usage.
440
func (a Action) Usage(usage string, args ...interface{}) Action {
2✔
441
        return a.UsageF(func() string {
4✔
442
                return fmt.Sprintf(usage, args...)
2✔
443
        })
2✔
444
}
445

446
// Usage sets the usage using a function.
447
func (a Action) UsageF(f func() string) Action {
2✔
448
        return ActionCallback(func(c Context) Action {
4✔
449
                if usage := f(); usage != "" {
4✔
450
                        a.meta.Usage = usage
2✔
451
                }
2✔
452
                return a
2✔
453
        })
454
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc