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

happy-sdk / happy / 15980077914

30 Jun 2025 05:53PM UTC coverage: 46.685% (-5.3%) from 51.943%
15980077914

push

github

mkungla
wip: gohappy cmd

Signed-off-by: Marko Kungla <marko.kungla@gmail.com>

0 of 281 new or added lines in 9 files covered. (0.0%)

2059 existing lines in 30 files now uncovered.

7943 of 17014 relevant lines covered (46.69%)

97527.29 hits per line

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

26.36
/sdk/cli/command/command.go
1
// SPDX-License-Identifier: Apache-2.0
2
//
3
// Copyright © 2024 The Happy Authors
4

5
package command
6

7
import (
8
        "errors"
9
        "fmt"
10
        "log/slog"
11
        "runtime/debug"
12
        "strings"
13
        "sync"
14

15
        "github.com/happy-sdk/happy/pkg/logging"
16
        "github.com/happy-sdk/happy/pkg/settings"
17
        "github.com/happy-sdk/happy/pkg/vars/varflag"
18
        "github.com/happy-sdk/happy/sdk/action"
19
)
20

21
var (
22
        Error                = errors.New("command")
23
        ErrFlags             = fmt.Errorf("%w flags error", Error)
24
        ErrHasNoParent       = fmt.Errorf("%w has no parent command", Error)
25
        ErrCommandNotAllowed = fmt.Errorf("%w not allowed", Error)
26
)
27

28
type Config struct {
29
        Usage            settings.String `key:"usage" mutation:"once"`
30
        HideDefaultUsage settings.Bool   `key:"hide_default_usage" default:"false"`
31
        Category         settings.String `key:"category"`
32
        Description      settings.String `key:"description"`
33
        // MinArgs Minimum argument count for command
34
        MinArgs    settings.Uint `key:"min_args" default:"0" mutation:"once"`
35
        MinArgsErr settings.String
36
        // MaxArgs Maximum argument count for command
37
        MaxArgs    settings.Uint `key:"max_args" default:"0" mutation:"once"`
38
        MaxArgsErr settings.String
39
        // SharedBeforeAction share Before action for all its subcommands
40
        SharedBeforeAction settings.Bool `key:"shared_before_action" default:"false"`
41
        // Indicates that the command should be executed immediately, without waiting for the full runtime setup.
42
        Immediate settings.Bool `key:"immediate" default:"false"`
43
        // SkipSharedBefore indicates that the BeforeAlways any shared before actions provided
44
        // by parent commands should be skipped.
45
        SkipSharedBefore settings.Bool `key:"skip_shared_before" default:"false"`
46
        // Hidden indicates that the command should be hidden from the command list.
47
        Hidden settings.Bool `key:"hidden" default:"false" mutation:"once"`
48
        // FailHidden indicates that the command should fail when hidden.
49
        // If Hide action is set, the command will fail with an error message returned by action.
50
        // If Hide action is not set, but Hidden is true, the command will fail with an error message ErrCommandNotAllowed.
51
        FailHidden settings.Bool `key:"fail_hidden" default:"false"`
52
}
53

54
func (s Config) Blueprint() (*settings.Blueprint, error) {
12✔
55

12✔
56
        b, err := settings.New(s)
12✔
57
        if err != nil {
12✔
UNCOV
58
                return nil, err
×
UNCOV
59
        }
×
60

61
        return b, nil
12✔
62
}
63

64
type Command struct {
65
        mu    sync.Mutex
66
        name  string
67
        cnf   *settings.Profile
68
        info  []string
69
        usage []string
70

71
        flags       varflag.Flags
72
        parent      *Command
73
        subCommands map[string]*Command
74

75
        beforeAction       action.WithArgs
76
        hideAction         action.Action
77
        doAction           action.WithArgs
78
        afterSuccessAction action.Action
79
        afterFailureAction action.WithPrevErr
80
        afterAlwaysAction  action.WithPrevErr
81

82
        isWrapperCommand bool
83

84
        parents []string
85

86
        catdesc map[string]string
87

88
        err error
89

90
        cnflog *logging.QueueLogger
91

92
        extraUsage []string
93
}
94

95
func New(name string, cnf Config) *Command {
12✔
96
        c := &Command{
12✔
97
                name:    name,
12✔
98
                catdesc: make(map[string]string),
12✔
99
                cnflog:  logging.NewQueueLogger(),
12✔
100
        }
12✔
101

12✔
102
        if err := c.configure(&cnf); err != nil {
12✔
UNCOV
103
                c.error(fmt.Errorf("%w: %s", Error, err.Error()))
×
104
                return c.toInvalid()
×
105
        }
×
106

107
        maxArgs := c.cnf.Get("max_args").Value().Int()
12✔
108

12✔
109
        flags, err := varflag.NewFlagSet(c.name, maxArgs)
12✔
110
        if err != nil {
12✔
UNCOV
111
                if errors.Is(err, varflag.ErrInvalidFlagSetName) {
×
UNCOV
112
                        c.error(fmt.Errorf("%w: invalid command name %q", Error, c.name))
×
UNCOV
113
                } else {
×
UNCOV
114
                        c.error(fmt.Errorf("%w: failed to create FlagSet: %v", Error, err))
×
UNCOV
115
                }
×
116
                return c.toInvalid()
×
117
        }
118

119
        c.flags = flags
12✔
120

12✔
121
        return c
12✔
122
}
123

124
func (c *Command) DescribeCategory(cat, desc string) *Command {
×
125
        if !c.tryLock("DescribeCategory") {
×
UNCOV
126
                return c
×
UNCOV
127
        }
×
128
        defer c.mu.Unlock()
×
129
        c.catdesc[strings.ToLower(cat)] = desc
×
130
        return c
×
131
}
132

133
func (c *Command) WithFlags(ffns ...varflag.FlagCreateFunc) *Command {
×
134
        for _, fn := range ffns {
×
135
                c.withFlag(fn)
×
136
        }
×
137
        return c
×
138
}
139

140
func (c *Command) AddInfo(paragraph string) *Command {
×
141
        if !c.tryLock("AddInfo") {
×
142
                return c
×
143
        }
×
144
        defer c.mu.Unlock()
×
145

×
146
        c.info = append(c.info, paragraph)
×
147
        return c
×
148
}
149

150
func (c *Command) WithSubCommands(cmds ...*Command) *Command {
9✔
151
        for _, cmd := range cmds {
9✔
UNCOV
152
                c.withSubCommand(cmd)
×
UNCOV
153
        }
×
154
        return c
9✔
155
}
156

UNCOV
157
func (c *Command) AddUsage(usage string) {
×
UNCOV
158
        if !c.tryLock("Usage") {
×
159
                return
×
160
        }
×
UNCOV
161
        defer c.mu.Unlock()
×
UNCOV
162
        c.extraUsage = append(c.extraUsage, usage)
×
163
}
164

165
func (c *Command) Hide(a action.Action) *Command {
×
UNCOV
166
        if !c.tryLock("Hide") {
×
UNCOV
167
                return c
×
UNCOV
168
        }
×
UNCOV
169
        defer c.mu.Unlock()
×
170

×
171
        if c.hideAction != nil {
×
172
                c.error(fmt.Errorf("%w: attempt to override Hide action for %s", Error, c.name))
×
173
                return c
×
174
        }
×
175
        c.hideAction = a
×
176
        return c
×
177
}
178
func (c *Command) Before(a action.WithArgs) *Command {
3✔
179
        if !c.tryLock("Before") {
3✔
180
                return c
×
UNCOV
181
        }
×
182
        defer c.mu.Unlock()
3✔
183

3✔
184
        if c.beforeAction != nil {
3✔
185
                c.error(fmt.Errorf("%w: attempt to override Before action for %s", Error, c.name))
×
186
                return c
×
UNCOV
187
        }
×
188
        c.beforeAction = a
3✔
189
        return c
3✔
190
}
191

192
func (c *Command) Do(action action.WithArgs) *Command {
9✔
193
        if !c.tryLock("Do") {
9✔
UNCOV
194
                return c
×
UNCOV
195
        }
×
196
        defer c.mu.Unlock()
9✔
197
        if c.doAction != nil {
9✔
198
                c.error(fmt.Errorf("%w: attempt to override Before action for %s", Error, c.name))
×
199
                return c
×
UNCOV
200
        }
×
201
        c.doAction = action
9✔
202
        return c
9✔
203
}
204

205
func (c *Command) AfterSuccess(a action.Action) *Command {
3✔
206
        if !c.tryLock("AfterSuccess") {
3✔
UNCOV
207
                return c
×
UNCOV
208
        }
×
209
        defer c.mu.Unlock()
3✔
210
        if c.afterSuccessAction != nil {
3✔
211
                c.error(fmt.Errorf("%w: attempt to override AfterSuccess action for %s", Error, c.name))
×
212
                return c
×
213
        }
×
214
        c.afterSuccessAction = a
3✔
215
        return c
3✔
216
}
217

UNCOV
218
func (c *Command) AfterFailure(a action.WithPrevErr) *Command {
×
UNCOV
219
        if !c.tryLock("AfterFailure") {
×
UNCOV
220
                return c
×
221
        }
×
222
        defer c.mu.Unlock()
×
UNCOV
223
        if c.afterFailureAction != nil {
×
UNCOV
224
                c.error(fmt.Errorf("%w: attempt to override AfterFailure action for %s", Error, c.name))
×
225
                return c
×
226
        }
×
227
        c.afterFailureAction = a
×
UNCOV
228
        return c
×
229
}
230

231
func (c *Command) AfterAlways(a action.WithPrevErr) *Command {
3✔
232
        if !c.tryLock("AfterAlways") {
3✔
UNCOV
233
                return c
×
UNCOV
234
        }
×
235
        defer c.mu.Unlock()
3✔
236
        if c.afterAlwaysAction != nil {
3✔
UNCOV
237
                c.error(fmt.Errorf("%w: attempt to override AfterAlways action for %s", Error, c.name))
×
UNCOV
238
                return c
×
UNCOV
239
        }
×
240
        c.afterAlwaysAction = a
3✔
241
        return c
3✔
242
}
243

UNCOV
244
func (c *Command) withSubCommand(cmd *Command) *Command {
×
UNCOV
245
        if !c.tryLock("WithSubCommand") {
×
UNCOV
246
                return c
×
247
        }
×
248
        defer c.mu.Unlock()
×
249
        if cmd == nil {
×
UNCOV
250
                return c
×
UNCOV
251
        }
×
252

253
        if cmd.err != nil {
×
UNCOV
254
                c.error(cmd.err)
×
UNCOV
255
                return c
×
UNCOV
256
        }
×
UNCOV
257
        if c.subCommands == nil {
×
UNCOV
258
                c.subCommands = make(map[string]*Command)
×
259
        }
×
260
        if err := c.flags.AddSet(cmd.flags); err != nil {
×
UNCOV
261
                c.error(fmt.Errorf(
×
UNCOV
262
                        "%w: failed to attach subcommand %s flags to %s",
×
UNCOV
263
                        Error,
×
UNCOV
264
                        cmd.name,
×
UNCOV
265
                        c.name,
×
UNCOV
266
                ))
×
UNCOV
267
                return c
×
UNCOV
268
        }
×
UNCOV
269
        cmd.parent = c
×
UNCOV
270

×
UNCOV
271
        c.subCommands[cmd.name] = cmd
×
UNCOV
272
        return c
×
273
}
274

UNCOV
275
func (c *Command) Err() error {
×
276
        c.mu.Lock()
×
277
        defer c.mu.Unlock()
×
UNCOV
278
        if c.err != nil {
×
UNCOV
279
                return c.err
×
280
        }
×
281
        for _, scmd := range c.subCommands {
×
UNCOV
282
                if err := scmd.Err(); err != nil {
×
UNCOV
283
                        return err
×
284
                }
×
285
        }
286
        return nil
×
287
}
288

UNCOV
289
func (c *Command) withFlag(ffn varflag.FlagCreateFunc) *Command {
×
UNCOV
290
        if !c.tryLock("WithFlag") {
×
291
                return c
×
292
        }
×
293
        defer c.mu.Unlock()
×
294

×
295
        f, cerr := ffn()
×
296
        if cerr != nil {
×
297
                c.error(fmt.Errorf("%w: %s", ErrFlags, cerr.Error()))
×
298
                return c
×
UNCOV
299
        }
×
300

UNCOV
301
        if err := c.flags.Add(f); err != nil {
×
UNCOV
302
                c.error(fmt.Errorf("%w: %s", ErrFlags, err.Error()))
×
UNCOV
303
        }
×
UNCOV
304
        return c
×
305
}
306

307
func (c *Command) tryLock(method string) bool {
36✔
308
        if !c.mu.TryLock() {
36✔
309
                c.cnflog.BUG(
×
310
                        "command configuration failed",
×
311
                        slog.String("command", c.name),
×
312
                        slog.String("method", method),
×
313
                )
×
314
                return false
×
UNCOV
315
        }
×
316
        return true
36✔
317
}
318

319
// configure is called in New so defering outside of lock is ok.
320
func (c *Command) configure(s *Config) error {
12✔
321
        defer func() {
24✔
322
                if c.cnf == nil {
12✔
323
                        // set empty profile when settings have failed to load
×
324
                        c.cnf = &settings.Profile{}
×
325
                }
×
326
        }()
327

328
        c.mu.Lock()
12✔
329

12✔
330
        bp, err := s.Blueprint()
12✔
331
        if err != nil {
12✔
UNCOV
332
                return err
×
UNCOV
333
        }
×
334

335
        schema, err := bp.Schema("cmd", "v1")
12✔
336
        if err != nil {
12✔
337
                return err
×
UNCOV
338
        }
×
339
        c.cnf, err = schema.Profile("cli", nil)
12✔
340
        if err != nil {
12✔
UNCOV
341
                return err
×
UNCOV
342
        }
×
343

344
        if minargs := c.cnf.Get("min_args").Value().Int(); minargs > c.cnf.Get("max_args").Value().Int() {
12✔
345
                if err := c.cnf.Set("max_args", minargs); err != nil {
×
UNCOV
346
                        return err
×
UNCOV
347
                }
×
348
        }
349

350
        // we dont defer unlock here because we want to keep it locked for tryLock on error.
351
        c.mu.Unlock()
12✔
352
        return nil
12✔
353
}
354

355
// Verify veifies command,  flags and the sub commands
356
//   - verify that commands are valid and have atleast Do function
357
//   - verify that subcommand do not shadow flags of any parent command
358
func (c *Command) verify() error {
9✔
359
        if !c.tryLock("verify") {
9✔
360
                return fmt.Errorf("%w: failed to obtain lock to verify command (%s)", Error, c.name)
×
361
        }
×
362
        defer c.mu.Unlock()
9✔
363

9✔
364
        if c.err != nil {
9✔
365
                return c.err
×
366
        }
×
367

368
        var usage []string
9✔
369
        usage = append(usage, c.parents...)
9✔
370
        usage = append(usage, c.name)
9✔
371
        if c.flags.Len() > 0 {
9✔
UNCOV
372
                usage = append(usage, "[flags]")
×
UNCOV
373
        }
×
374
        if c.subCommands != nil {
9✔
UNCOV
375
                usage = append(usage, "[subcommand]")
×
UNCOV
376
        }
×
377
        c.usage = append(c.usage, strings.Join(usage, " "))
9✔
378

9✔
379
        if c.flags.AcceptsArgs() {
9✔
380
                var withargs []string
×
UNCOV
381
                withargs = append(withargs, c.parents...)
×
UNCOV
382
                withargs = append(withargs, c.name)
×
UNCOV
383
                withargs = append(withargs, "[args...]")
×
384
                withargs = append(withargs, fmt.Sprintf(
×
385
                        " // min %d max %d",
×
UNCOV
386
                        c.cnf.Get("min_args").Value().Int(),
×
UNCOV
387
                        c.cnf.Get("max_args").Value().Int(),
×
UNCOV
388
                ))
×
UNCOV
389
                c.usage = append(c.usage, strings.Join(withargs, " "))
×
UNCOV
390
        }
×
391

392
        defineUsage := c.cnf.Get("usage").String()
9✔
393
        if defineUsage != "" {
9✔
UNCOV
394
                var usage []string
×
UNCOV
395
                usage = append(usage, c.parents...)
×
UNCOV
396
                usage = append(usage, c.name)
×
UNCOV
397
                usage = append(usage, defineUsage)
×
UNCOV
398
                if c.cnf.Get("hide_default_usage").Value().Bool() {
×
UNCOV
399
                        c.usage = []string{strings.Join(usage, " ")}
×
UNCOV
400
                } else {
×
UNCOV
401
                        c.usage = append(c.usage, strings.Join(usage, " "))
×
UNCOV
402
                }
×
403
        }
404

405
        if len(c.extraUsage) > 0 {
9✔
UNCOV
406
                for _, u := range c.extraUsage {
×
UNCOV
407
                        var usage []string
×
UNCOV
408
                        usage = append(usage, c.parents...)
×
UNCOV
409
                        usage = append(usage, c.name)
×
UNCOV
410
                        usage = append(usage, u)
×
UNCOV
411
                        c.usage = append(c.usage, strings.Join(usage, " "))
×
UNCOV
412
                }
×
413
        }
414

415
        if c.err != nil {
9✔
UNCOV
416
                return c.err
×
UNCOV
417
        }
×
418

419
        if c.doAction == nil {
9✔
UNCOV
420
                if !c.isWrapperCommand {
×
UNCOV
421
                        c.isWrapperCommand = len(c.subCommands) > 0
×
UNCOV
422
                }
×
423

UNCOV
424
                if c.subCommands != nil {
×
UNCOV
425
                        goto SubCommands
×
UNCOV
426
                } else {
×
UNCOV
427
                        return fmt.Errorf("%w: command (%s) must have Do action or atleeast one subcommand", Error, c.name)
×
UNCOV
428
                }
×
429
        }
430

431
SubCommands:
432
        if c.subCommands != nil {
9✔
UNCOV
433
                for _, cmd := range c.subCommands {
×
UNCOV
434
                        // Add subcommand loogs to parent command log queue
×
UNCOV
435
                        if err := c.cnflog.ConsumeQueue(cmd.cnflog); err != nil {
×
436
                                return err
×
437
                        }
×
UNCOV
438
                        cmd.parents = append(c.parents, c.name)
×
UNCOV
439
                        if err := cmd.verify(); err != nil {
×
UNCOV
440
                                return err
×
UNCOV
441
                        }
×
442
                }
443
        }
444
        return nil
9✔
445
}
446

447
func (c *Command) getActiveCommand() (*Command, error) {
9✔
448
        subtree := c.flags.GetActiveSets()
9✔
449

9✔
450
        // Skip self
9✔
451
        for _, subset := range subtree {
18✔
452
                cmd, exists := c.getSubCommand(subset.Name())
9✔
453
                if exists {
9✔
UNCOV
454
                        return cmd.getActiveCommand()
×
UNCOV
455
                }
×
456
        }
457

458
        args := c.flags.Args()
9✔
459
        if !c.flags.AcceptsArgs() && len(args) > 0 {
9✔
460
                return nil, fmt.Errorf("%w: unknown subcommand: %s for %s", Error, args[0].String(), c.name)
×
461
        }
×
462

463
        return c, nil
9✔
464
}
465

466
func (c *Command) getSubCommand(name string) (cmd *Command, exists bool) {
9✔
467
        if cmd, exists := c.subCommands[name]; exists {
9✔
UNCOV
468
                return cmd, exists
×
469
        }
×
470
        return
9✔
471
}
472

473
func (c *Command) getGlobalFlags() varflag.Flags {
9✔
474
        if c.parent == nil {
18✔
475
                return c.flags
9✔
476
        }
9✔
UNCOV
477
        return c.parent.getGlobalFlags()
×
478
}
479

UNCOV
480
func (c *Command) getSharedFlags() (varflag.Flags, error) {
×
UNCOV
481
        // ignore global flags
×
482
        if c.parent == nil || c.parent.parent == nil {
×
483
                flags, err := varflag.NewFlagSet("x-"+c.name+"-noparent", 0)
×
UNCOV
484
                if err != nil {
×
UNCOV
485
                        return nil, err
×
UNCOV
486
                }
×
UNCOV
487
                return flags, ErrHasNoParent
×
488
        }
489

UNCOV
490
        flags := c.parent.getFlags()
×
UNCOV
491
        if flags == nil {
×
UNCOV
492
                flags, _ = varflag.NewFlagSet(c.parent.name, 0)
×
UNCOV
493
        }
×
UNCOV
494
        parentFlags, err := c.parent.getSharedFlags()
×
UNCOV
495
        if err != nil && !errors.Is(err, ErrHasNoParent) {
×
496
                return nil, err
×
497
        }
×
498

UNCOV
499
        if parentFlags != nil {
×
UNCOV
500
                for _, flag := range parentFlags.Flags() {
×
UNCOV
501
                        if err := flags.Add(flag); err != nil {
×
UNCOV
502
                                return nil, err
×
UNCOV
503
                        }
×
504
                }
505
        }
506

UNCOV
507
        return flags, nil
×
508
}
509

510
func (c *Command) getFlags() varflag.Flags {
×
511
        c.mu.Lock()
×
512
        defer c.mu.Unlock()
×
513

×
514
        return c.flags
×
515
}
×
516

UNCOV
517
func (c *Command) toInvalid() *Command {
×
518
        c.mu.Lock()
×
519
        defer c.mu.Unlock()
×
520

×
521
        defer func() {
×
522
                stackTrace := debug.Stack()
×
523
                fmt.Println("HAPPY COMMAND")
×
524
                fmt.Println("err: ", c.err.Error())
×
525
                fmt.Println(string(stackTrace))
×
UNCOV
526
        }()
×
527

528
        // Ensure that the error field is set.
529
        if c.err == nil {
×
530
                c.error(fmt.Errorf("%w: command marked invalid", Error))
×
531
        }
×
532

533
        // Clear all actions to avoid any execution.
UNCOV
534
        c.beforeAction = nil
×
535
        c.doAction = nil
×
UNCOV
536
        c.afterSuccessAction = nil
×
UNCOV
537
        c.afterFailureAction = nil
×
538
        c.afterAlwaysAction = nil
×
539

×
540
        // Remove any subcommands.
×
541
        for _, subCommand := range c.subCommands {
×
542
                subCommand.toInvalid()
×
543
        }
×
UNCOV
544
        c.subCommands = nil
×
545

×
546
        // If flags is still nil, assign a dummy flag set to avoid nil dereference later.
×
547
        if c.flags == nil {
×
548
                // Use a dummy flag set. We assume that this call will succeed for a command marked as invalid.
×
549
                if dummy, err := varflag.NewFlagSet("invalid", 0); err == nil {
×
UNCOV
550
                        c.flags = dummy
×
UNCOV
551
                } else {
×
UNCOV
552
                        // If even this fails, log the error.
×
UNCOV
553
                        c.error(fmt.Errorf("failed to create dummy flag set for invalid command %q",
×
UNCOV
554
                                err.Error()))
×
UNCOV
555
                }
×
556
        }
557

UNCOV
558
        return c
×
559
}
560

UNCOV
561
func (c *Command) error(err error) {
×
UNCOV
562
        if c.cnflog != nil {
×
UNCOV
563
                c.cnflog.Error(err.Error())
×
UNCOV
564
        }
×
UNCOV
565
        c.err = err
×
566
}
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

© 2026 Coveralls, Inc