• 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

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

5
// Package cli provides utilities for happy command line interfaces.
6
package cli
7

8
import (
9
        "bufio"
10
        "bytes"
11
        "errors"
12
        "fmt"
13
        "log/slog"
14
        "os"
15
        "os/exec"
16
        "strings"
17

18
        "github.com/happy-sdk/happy/pkg/logging"
19
        "github.com/happy-sdk/happy/sdk/session"
20
)
21

22
var (
23
        ErrCommandInvalid = errors.New("invalid command definition")
24
        ErrCommandArgs    = errors.New("command arguments error")
25
        ErrCommandFlags   = errors.New("command flags error")
26
        ErrPanic          = errors.New("there was panic, check logs for more info")
27
)
28

29
// AskForConfirmation gets (y/Y)es or (n/N)o from cli input.
UNCOV
30
func AskForConfirmation(q string) bool {
×
UNCOV
31
        var response string
×
UNCOV
32
        _, _ = fmt.Fprintln(os.Stdout, q, "(y/Y)es or (n/N)o?")
×
UNCOV
33

×
UNCOV
34
        if _, err := fmt.Scanln(&response); err != nil {
×
UNCOV
35
                return false
×
UNCOV
36
        }
×
37

UNCOV
38
        switch strings.ToLower(response) {
×
UNCOV
39
        case "y", "Y", "yes":
×
UNCOV
40
                return true
×
UNCOV
41
        case "n", "N", "no":
×
UNCOV
42
                return false
×
UNCOV
43
        default:
×
UNCOV
44
                _, _ = fmt.Fprintln(
×
UNCOV
45
                        os.Stdout,
×
UNCOV
46
                        "I'm sorry but I didn't get what you meant, please type (y/Y)es or (n/N)o and then press enter:")
×
UNCOV
47

×
UNCOV
48
                return AskForConfirmation(q)
×
49
        }
50
}
51

52
func AskForInput(q string) string {
×
53
        _, _ = fmt.Fprintln(os.Stdout, q)
×
54
        reader := bufio.NewReader(os.Stdin)
×
UNCOV
55
        response, err := reader.ReadString('\n')
×
56
        if err != nil {
×
UNCOV
57
                return ""
×
UNCOV
58
        }
×
UNCOV
59
        return strings.TrimSpace(response)
×
60
}
61

62
// Exec wraps ExecRaw to return output as string.
63
func Exec(sess *session.Context, cmd *exec.Cmd) (string, error) {
×
64
        out, err := ExecRaw(sess, cmd)
×
65
        return string(bytes.TrimSpace(out)), err
×
66
}
×
67

68
// ExecRaw wraps and executes provided command and returns its
69
// CombinedOutput. It ensures that -x flag is taken into account and
70
// Command is Session Context aware.
71
func ExecRaw(sess *session.Context, cmd *exec.Cmd) ([]byte, error) {
×
72
        return execCommandRaw(sess, cmd)
×
73
}
×
74

75
// Run wraps and executes provided command and writes
76
// its Stdout and Stderr. It ensures that -x flag is taken
77
// into account and Command is Session Context aware.
78
func Run(sess *session.Context, cmd *exec.Cmd) error {
×
UNCOV
79
        return run(sess, cmd)
×
UNCOV
80
}
×
81

82
func run(sess *session.Context, cmd *exec.Cmd) error {
×
83
        sess.Log().Debug("exec: ", slog.String("cmd", cmd.String()))
×
84

×
85
        if sess.Get("app.main.exec.x").Bool() {
×
86
                sess.Log().LogDepth(4, logging.LevelAlways, cmd.String())
×
87
        }
×
88

89
        scmd := exec.CommandContext(sess, cmd.Path, cmd.Args[1:]...) //nolint: gosec
×
UNCOV
90
        scmd.Env = cmd.Env
×
UNCOV
91
        scmd.Dir = cmd.Dir
×
UNCOV
92
        scmd.Stdin = cmd.Stdin
×
93
        scmd.Stdout = cmd.Stdout
×
94
        scmd.Stderr = cmd.Stderr
×
95
        scmd.ExtraFiles = cmd.ExtraFiles
×
96
        cmd = scmd
×
UNCOV
97

×
UNCOV
98
        stderr, err := cmd.StderrPipe()
×
UNCOV
99
        if err != nil {
×
UNCOV
100
                return err
×
101
        }
×
102

103
        stdout, err := cmd.StdoutPipe()
×
UNCOV
104
        if err != nil {
×
UNCOV
105
                return err
×
UNCOV
106
        }
×
107

108
        stdopipe := bufio.NewScanner(stdout)
×
109
        go func() {
×
110
                for stdopipe.Scan() {
×
UNCOV
111
                        _, _ = fmt.Fprintln(os.Stdout, stdopipe.Text())
×
112
                }
×
113
        }()
114
        stdepipe := bufio.NewScanner(stderr)
×
115
        go func() {
×
116
                for stdepipe.Scan() {
×
117
                        _, _ = fmt.Fprintln(os.Stderr, stdepipe.Text())
×
UNCOV
118
                }
×
119
        }()
120

121
        if err := cmd.Start(); err != nil {
×
122
                return err
×
123
        }
×
124

125
        if err := cmd.Wait(); err != nil {
×
126
                //nolint: forbidigo
×
127
                fmt.Println("")
×
128
                var ee *exec.ExitError
×
129
                if errors.As(err, &ee) {
×
130
                        fmt.Println(string(ee.Stderr))
×
131
                        sess.Log().Error(ee.Error())
×
UNCOV
132
                }
×
133

134
                return err
×
135
        }
136
        sess.Log().Debug(cmd.String(), slog.Int("exit", 0))
×
UNCOV
137
        return nil
×
138
}
139

140
func execCommandRaw(sess *session.Context, cmd *exec.Cmd) ([]byte, error) {
×
141
        sess.Log().Debug("exec: ", slog.String("cmd", cmd.String()))
×
142

×
UNCOV
143
        if sess.Get("app.main.exec.x").Bool() {
×
144
                sess.Log().LogDepth(4, logging.LevelAlways, cmd.String())
×
145
        }
×
146

147
        scmd := exec.CommandContext(sess, cmd.Path, cmd.Args[1:]...) //nolint: gosec
×
148
        scmd.Env = cmd.Env
×
UNCOV
149
        scmd.Dir = cmd.Dir
×
UNCOV
150

×
151
        // Create buffers to capture stdout and stderr separately
×
152
        var stdoutBuf, stderrBuf bytes.Buffer
×
153

×
UNCOV
154
        // Always redirect stdout and stderr to our buffers, ignoring any previously set values
×
155
        // This ensures we don't write to the original stdout/stderr
×
156
        scmd.Stdout = &stdoutBuf
×
157
        scmd.Stderr = &stderrBuf
×
158

×
159
        scmd.ExtraFiles = cmd.ExtraFiles
×
160
        cmd = scmd
×
161

×
162
        // Execute command
×
UNCOV
163
        err := cmd.Run()
×
164

×
UNCOV
165
        // Get stdout content
×
166
        stdoutBytes := stdoutBuf.Bytes()
×
167

×
UNCOV
168
        // If no error, just return the stdout
×
UNCOV
169
        if err == nil {
×
170
                return stdoutBytes, nil
×
171
        }
×
172

173
        // On error, clean stderr by replacing newlines with spaces
174
        cleanedStderr := strings.ReplaceAll(stderrBuf.String(), "\n", " ")
×
175

×
UNCOV
176
        var ee *exec.ExitError
×
177
        if errors.As(err, &ee) {
×
178
                // Create new error with original error and cleaned stderr appended
×
179
                enhancedErr := fmt.Errorf("%w: %s", err, cleanedStderr)
×
180
                return stdoutBytes, enhancedErr
×
181
        }
×
182

183
        // For other types of errors, also append cleaned stderr
184
        enhancedErr := fmt.Errorf("%w: %s", err, cleanedStderr)
×
185
        return stdoutBytes, enhancedErr
×
186
}
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